From 7503ba97e0d84910050b517b7401b0db800b2dfb Mon Sep 17 00:00:00 2001 From: Omar Abdulaziz Date: Tue, 25 Jun 2024 15:11:53 +0300 Subject: [PATCH 01/20] init grid-compose --- go.work | 3 +- grid-compose/README.md | 22 +++++++ grid-compose/cmd/down.go | 28 +++++++++ grid-compose/cmd/root.go | 84 +++++++++++++++++++++++++++ grid-compose/cmd/up.go | 78 +++++++++++++++++++++++++ grid-compose/cmd/version.go | 17 ++++++ grid-compose/example/full_example.yml | 54 +++++++++++++++++ grid-compose/example/single_vm.yml | 12 ++++ grid-compose/go.mod | 16 +++++ grid-compose/go.sum | 25 ++++++++ grid-compose/internal/utils.go | 19 ++++++ grid-compose/main.go | 7 +++ grid-compose/pkg/types/types.go | 70 ++++++++++++++++++++++ 13 files changed, 434 insertions(+), 1 deletion(-) create mode 100644 grid-compose/README.md create mode 100644 grid-compose/cmd/down.go create mode 100644 grid-compose/cmd/root.go create mode 100644 grid-compose/cmd/up.go create mode 100644 grid-compose/cmd/version.go create mode 100644 grid-compose/example/full_example.yml create mode 100644 grid-compose/example/single_vm.yml create mode 100644 grid-compose/go.mod create mode 100644 grid-compose/go.sum create mode 100644 grid-compose/internal/utils.go create mode 100644 grid-compose/main.go create mode 100644 grid-compose/pkg/types/types.go diff --git a/go.work b/go.work index bece8b7cf..981dea955 100644 --- a/go.work +++ b/go.work @@ -1,4 +1,4 @@ -go 1.21 +go 1.21.0 use ( ./activation-service @@ -12,4 +12,5 @@ use ( ./tfrobot ./tools/relay-cache-warmer ./user-contracts-mon + ./grid-compose ) diff --git a/grid-compose/README.md b/grid-compose/README.md new file mode 100644 index 000000000..b82e8fb76 --- /dev/null +++ b/grid-compose/README.md @@ -0,0 +1,22 @@ +# Grid-Compose + +is a tool for running multi-vm applications on TFGrid defined using a Yaml formatted file. + +## Usage + +`REQUIRED` EnvVars: + +- `MNEMONIC`: your secret words +- `NETWORK`: one of (dev, qa, test, main) + +```bash +grid-compose [OPTIONS] [COMMAND] + +OPTIONS: + -f path to yaml file, default is ./grid-compose.yaml + +COMMANDS: + - version: shows the project version + - up: deploy the app + - down: cancel all deployments +``` diff --git a/grid-compose/cmd/down.go b/grid-compose/cmd/down.go new file mode 100644 index 000000000..a01807dae --- /dev/null +++ b/grid-compose/cmd/down.go @@ -0,0 +1,28 @@ +package cmd + +import ( + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal" +) + +var downCmd = &cobra.Command{ + Use: "down", + Short: "cancel your project on the grid", + Run: func(cmd *cobra.Command, args []string) { + if err := down(); err != nil { + log.Fatal().Err(err).Send() + } + }, +} + +func down() error { + for key := range app.Specs.Services { + projectName := internal.GetProjectName(key, app.Client.TwinID) + log.Info().Str("projectName", projectName).Msg("canceling deployments") + if err := app.Client.CancelByProjectName(projectName); err != nil { + return err + } + } + return nil +} diff --git a/grid-compose/cmd/root.go b/grid-compose/cmd/root.go new file mode 100644 index 000000000..6ca742dba --- /dev/null +++ b/grid-compose/cmd/root.go @@ -0,0 +1,84 @@ +package cmd + +import ( + "fmt" + "io" + "os" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" + "github.com/threefoldtech/tfgrid-sdk-go/grid-client/deployer" + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/pkg/types" + "gopkg.in/yaml.v2" +) + +var ( + app App + configFile string + network string + mnemonic string +) + +var rootCmd = &cobra.Command{ + Use: "grid-compose", + Short: "Grid-Compose is a tool for running multi-vm applications on TFGrid defined using a Yaml formatted file.", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + var err error + app, err = NewApp(network, mnemonic, configFile) + if err != nil { + log.Fatal().Err(err).Send() + } + }, +} + +func init() { + network = os.Getenv("NETWORK") + mnemonic = os.Getenv("MNEMONIC") + rootCmd.PersistentFlags().StringVarP(&configFile, "file", "f", "./grid-compose.yaml", "the grid-compose configuration file") + + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + + rootCmd.AddCommand(versionCmd) + rootCmd.AddCommand(upCmd) + rootCmd.AddCommand(downCmd) +} + +type App struct { + Client deployer.TFPluginClient + Specs types.Specs +} + +func NewApp(net, mne, filePath string) (App, error) { + file, err := os.Open(filePath) + if err != nil { + return App{}, fmt.Errorf("failed to open file: %w", err) + } + defer file.Close() + + content, err := io.ReadAll(file) + if err != nil { + return App{}, fmt.Errorf("failed to read file: %w", err) + } + + var specs types.Specs + if err := yaml.Unmarshal(content, &specs); err != nil { + return App{}, fmt.Errorf("failed to parse file: %w", err) + } + + client, err := deployer.NewTFPluginClient(mne, deployer.WithNetwork(net)) + if err != nil { + return App{}, fmt.Errorf("failed to load grid client: %w", err) + } + + return App{ + Specs: specs, + Client: client, + }, nil +} + +func Execute() { + if err := rootCmd.Execute(); err != nil { + log.Fatal().Err(err).Send() + } +} diff --git a/grid-compose/cmd/up.go b/grid-compose/cmd/up.go new file mode 100644 index 000000000..43da461b3 --- /dev/null +++ b/grid-compose/cmd/up.go @@ -0,0 +1,78 @@ +package cmd + +import ( + "context" + "fmt" + "net" + + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" + "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal" + "github.com/threefoldtech/zos/pkg/gridtypes" +) + +var upCmd = &cobra.Command{ + Use: "up", + Short: "deploy application on the grid", + Run: func(cmd *cobra.Command, args []string) { + if err := up(cmd.Context()); err != nil { + log.Fatal().Err(err).Send() + } + }, +} + +func up(ctx context.Context) error { + results := make(map[string]workloads.VM) + for key, val := range app.Specs.Services { + projectName := internal.GetProjectName(key, app.Client.TwinID) + + networkName := key + "net" + log.Info().Str("projectName", projectName).Str("workloadName", networkName).Msg("deploying network") + net := workloads.ZNet{ + Name: networkName, + Nodes: []uint32{uint32(val.NodeID)}, + IPRange: gridtypes.NewIPNet(net.IPNet{ + IP: net.IPv4(10, 20, 0, 0), + Mask: net.CIDRMask(16, 32), + }), + SolutionType: projectName, + } + if err := app.Client.NetworkDeployer.Deploy(ctx, &net); err != nil { + return err + } + + log.Info().Str("projectName", projectName).Str("workloadName", key).Msg("deploying vm") + vm := workloads.VM{ + Name: key, + Flist: val.Flist, + Entrypoint: val.Entrypoint, + EnvVars: val.Environment, + CPU: int(val.Resources.CPU), + Memory: int(val.Resources.Memory), + Planetary: true, + NetworkName: net.Name, + } + dl := workloads.NewDeployment(vm.Name, uint32(val.NodeID), projectName, nil, net.Name, nil, nil, []workloads.VM{vm}, nil) + if err := app.Client.DeploymentDeployer.Deploy(ctx, &dl); err != nil { + log.Error().Err(err).Msg("reverting deployed network") + if err := app.Client.NetworkDeployer.Cancel(ctx, &net); err != nil { + return err + } + return err + } + + res, err := app.Client.State.LoadVMFromGrid(ctx, uint32(val.NodeID), vm.Name, dl.Name) + if err != nil { + return err + } + + results[vm.Name] = res + } + + for key, val := range results { + fmt.Printf("%s vm addresses:\n", key) + fmt.Println("\t", internal.GetVmAddresses(val)) + } + return nil +} diff --git a/grid-compose/cmd/version.go b/grid-compose/cmd/version.go new file mode 100644 index 000000000..ec5ceb709 --- /dev/null +++ b/grid-compose/cmd/version.go @@ -0,0 +1,17 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +var version = "v0.0.1" + +var versionCmd = &cobra.Command{ + Use: "version", + Short: "get current version number", + Run: func(cmd *cobra.Command, args []string) { + fmt.Println(version) + }, +} diff --git a/grid-compose/example/full_example.yml b/grid-compose/example/full_example.yml new file mode 100644 index 000000000..41ea7ff5c --- /dev/null +++ b/grid-compose/example/full_example.yml @@ -0,0 +1,54 @@ +# not all features here are implemented yet +version: '1.0.0' + +networks: + net1: + type: 'wg' + net2: + type: 'myc' + net3: + type: 'ygg' + net4: + type: 'ip4' + net5: + type: 'ip6' + +services: + web: + flist: 'https://hub.grid.tf/tf-official-apps/nginx-latest.flist' + environment: + - ENV_VAR_NAME=value + volumes: + - web-data:/data + networks: + - net1 + resources: + cpu: 2 + memory: 2 + depends_on: + - database + database: + flist: 'https://hub.grid.tf/tf-official-apps/postgresql-latest.flist' + environment: + - POSTGRES_DB=postgres + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=password + volumes: + - db-data:/var/lib/postgresql/data + networks: + - net1 + - net2 + - net3 + resources: + cpu: 4 + memoryGB: 2 + healthcheck: + test: ['CMD', 'curl' , 'http://{myceliumip}'] + interval: 1m30s + timeout: 10s + retries: 3 + +storage: + web-data: + type: 'zmount' + size: 10GB \ No newline at end of file diff --git a/grid-compose/example/single_vm.yml b/grid-compose/example/single_vm.yml new file mode 100644 index 000000000..d0e9eec2b --- /dev/null +++ b/grid-compose/example/single_vm.yml @@ -0,0 +1,12 @@ +version: '1.0.0' + +services: + new_single_vm: + flist: 'https://hub.grid.tf/omarabdulaziz.3bot/ubuntu-jammy.flist' + entrypoint: '/sbin/init' + environment: + - SSH_KEY=... + resources: + cpu: 1 + memory: 512 + node_id: 14 \ No newline at end of file diff --git a/grid-compose/go.mod b/grid-compose/go.mod new file mode 100644 index 000000000..7a49569cd --- /dev/null +++ b/grid-compose/go.mod @@ -0,0 +1,16 @@ +module github.com/threefoldtech/tfgrid-sdk-go/grid-compose + +go 1.21.0 + +require ( + github.com/rs/zerolog v1.33.0 + github.com/spf13/cobra v1.8.0 +) + +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/sys v0.12.0 // indirect +) diff --git a/grid-compose/go.sum b/grid-compose/go.sum new file mode 100644 index 000000000..affd0fa4e --- /dev/null +++ b/grid-compose/go.sum @@ -0,0 +1,25 @@ +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/grid-compose/internal/utils.go b/grid-compose/internal/utils.go new file mode 100644 index 000000000..f5c527ca7 --- /dev/null +++ b/grid-compose/internal/utils.go @@ -0,0 +1,19 @@ +package internal + +import ( + "fmt" + + "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" +) + +func GetProjectName(key string, twinId uint32) string { + return fmt.Sprintf("compose/%v/%v", twinId, key) +} + +func GetVmAddresses(vm workloads.VM) string { + var res string + + res += fmt.Sprintf("ygg: %v", vm.PlanetaryIP) + + return res +} diff --git a/grid-compose/main.go b/grid-compose/main.go new file mode 100644 index 000000000..3a493a75e --- /dev/null +++ b/grid-compose/main.go @@ -0,0 +1,7 @@ +package main + +import "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/cmd" + +func main() { + cmd.Execute() +} diff --git a/grid-compose/pkg/types/types.go b/grid-compose/pkg/types/types.go new file mode 100644 index 000000000..809696283 --- /dev/null +++ b/grid-compose/pkg/types/types.go @@ -0,0 +1,70 @@ +package types + +import ( + "fmt" + "strings" + + "gopkg.in/yaml.v3" +) + +type Specs struct { + Version string `yaml:"version"` + Networks map[string]Network `yaml:"networks"` + Services map[string]Service `yaml:"services"` + Storage map[string]Storage `yaml:"storage"` +} + +type Network struct { + Type string `yaml:"type"` +} + +type Storage struct { + Type string `yaml:"type"` + Size string `yaml:"size"` +} + +type Service struct { + Flist string `yaml:"flist"` + Entrypoint string `yaml:"entrypoint,omitempty"` + Environment KVMap `yaml:"environment"` + Resources Resources `yaml:"resources"` + NodeID uint `yaml:"node_id"` + Volumes []string `yaml:"volumes"` + Networks []string `yaml:"networks"` + HealthCheck *HealthCheck `yaml:"healthcheck,omitempty"` + DependsOn []string `yaml:"depends_on,omitempty"` +} + +type HealthCheck struct { + Test []string `yaml:"test"` + Interval string `yaml:"interval"` + Timeout string `yaml:"timeout"` + Retries int `yaml:"retries"` +} + +type Resources struct { + CPU uint `yaml:"cpu"` + Memory uint `yaml:"memory"` + SSD uint `yaml:"ssd"` + HDD uint `yaml:"hdd"` +} + +type KVMap map[string]string + +func (m *KVMap) UnmarshalYAML(value *yaml.Node) error { + var raw []string + if err := value.Decode(&raw); err != nil { + return err + } + + *m = make(map[string]string) + for _, ele := range raw { + kv := strings.SplitN(ele, "=", 2) + if len(kv) != 2 { + return fmt.Errorf("invalid kvmap format: %s", ele) + } + (*m)[kv[0]] = kv[1] + } + + return nil +} From 84cbd108e1776de3048328b2bc45b44a7b88d701 Mon Sep 17 00:00:00 2001 From: Omar Abdulaziz Date: Tue, 25 Jun 2024 16:37:00 +0300 Subject: [PATCH 02/20] add network support --- grid-compose/cmd/up.go | 21 +++++++++++++++++++-- grid-compose/example/single_vm.yml | 4 +++- grid-compose/internal/utils.go | 24 +++++++++++++++++++++++- 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/grid-compose/cmd/up.go b/grid-compose/cmd/up.go index 43da461b3..0452be32c 100644 --- a/grid-compose/cmd/up.go +++ b/grid-compose/cmd/up.go @@ -50,9 +50,26 @@ func up(ctx context.Context) error { EnvVars: val.Environment, CPU: int(val.Resources.CPU), Memory: int(val.Resources.Memory), - Planetary: true, NetworkName: net.Name, } + + for _, net := range val.Networks { + switch net { + case "publicIp4": + vm.PublicIP = true + case "publicIp6": + vm.PublicIP6 = true + case "yggdrasil": + vm.Planetary = true + case "mycelium": + seed, err := internal.GetRandomMyceliumIPSeed() + if err != nil { + return fmt.Errorf("failed to get mycelium seed: %w", err) + } + vm.MyceliumIPSeed = seed + } + } + dl := workloads.NewDeployment(vm.Name, uint32(val.NodeID), projectName, nil, net.Name, nil, nil, []workloads.VM{vm}, nil) if err := app.Client.DeploymentDeployer.Deploy(ctx, &dl); err != nil { log.Error().Err(err).Msg("reverting deployed network") @@ -72,7 +89,7 @@ func up(ctx context.Context) error { for key, val := range results { fmt.Printf("%s vm addresses:\n", key) - fmt.Println("\t", internal.GetVmAddresses(val)) + fmt.Println(internal.GetVmAddresses(val)) } return nil } diff --git a/grid-compose/example/single_vm.yml b/grid-compose/example/single_vm.yml index d0e9eec2b..b89da2621 100644 --- a/grid-compose/example/single_vm.yml +++ b/grid-compose/example/single_vm.yml @@ -9,4 +9,6 @@ services: resources: cpu: 1 memory: 512 - node_id: 14 \ No newline at end of file + node_id: 14 + networks: + - yggdrasil \ No newline at end of file diff --git a/grid-compose/internal/utils.go b/grid-compose/internal/utils.go index f5c527ca7..bf42de648 100644 --- a/grid-compose/internal/utils.go +++ b/grid-compose/internal/utils.go @@ -1,9 +1,11 @@ package internal import ( + "crypto/rand" "fmt" "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" + "github.com/threefoldtech/zos/pkg/gridtypes/zos" ) func GetProjectName(key string, twinId uint32) string { @@ -13,7 +15,27 @@ func GetProjectName(key string, twinId uint32) string { func GetVmAddresses(vm workloads.VM) string { var res string - res += fmt.Sprintf("ygg: %v", vm.PlanetaryIP) + if vm.IP != "" { + res += fmt.Sprintf("\twireguard: %v\n", vm.IP) + } + if vm.Planetary { + res += fmt.Sprintf("\tyggdrasil: %v\n", vm.PlanetaryIP) + } + if vm.PublicIP { + res += fmt.Sprintf("\tpublicIp4: %v\n", vm.ComputedIP) + } + if vm.PublicIP6 { + res += fmt.Sprintf("\tpublicIp6: %v\n", vm.ComputedIP6) + } + if len(vm.MyceliumIPSeed) != 0 { + res += fmt.Sprintf("\tmycelium: %v\n", vm.MyceliumIP) + } return res } + +func GetRandomMyceliumIPSeed() ([]byte, error) { + key := make([]byte, zos.MyceliumIPSeedLen) + _, err := rand.Read(key) + return key, err +} From 7f0d28be21af1827cd3a5b97cc4352eb0325fd86 Mon Sep 17 00:00:00 2001 From: Omar Abdulaziz Date: Wed, 26 Jun 2024 09:52:37 +0300 Subject: [PATCH 03/20] use yaml.v3 to fix the custom unmarshal --- grid-compose/cmd/root.go | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/grid-compose/cmd/root.go b/grid-compose/cmd/root.go index 6ca742dba..6bd259242 100644 --- a/grid-compose/cmd/root.go +++ b/grid-compose/cmd/root.go @@ -10,7 +10,7 @@ import ( "github.com/spf13/cobra" "github.com/threefoldtech/tfgrid-sdk-go/grid-client/deployer" "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/pkg/types" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" ) var ( @@ -41,6 +41,7 @@ func init() { rootCmd.AddCommand(versionCmd) rootCmd.AddCommand(upCmd) + rootCmd.AddCommand(psCmd) rootCmd.AddCommand(downCmd) } @@ -50,31 +51,40 @@ type App struct { } func NewApp(net, mne, filePath string) (App, error) { + specs, err := LoadSpecsFromFile(filePath) + if err != nil { + return App{}, fmt.Errorf("failed to load specs from file: %w", err) + } + + client, err := deployer.NewTFPluginClient(mne, deployer.WithNetwork(net)) + if err != nil { + return App{}, fmt.Errorf("failed to load grid client: %w", err) + } + + return App{ + Specs: specs, + Client: client, + }, nil +} + +func LoadSpecsFromFile(filePath string) (types.Specs, error) { file, err := os.Open(filePath) if err != nil { - return App{}, fmt.Errorf("failed to open file: %w", err) + return types.Specs{}, fmt.Errorf("failed to open file: %w", err) } defer file.Close() content, err := io.ReadAll(file) if err != nil { - return App{}, fmt.Errorf("failed to read file: %w", err) + return types.Specs{}, fmt.Errorf("failed to read file: %w", err) } var specs types.Specs if err := yaml.Unmarshal(content, &specs); err != nil { - return App{}, fmt.Errorf("failed to parse file: %w", err) + return types.Specs{}, fmt.Errorf("failed to parse file: %w", err) } - client, err := deployer.NewTFPluginClient(mne, deployer.WithNetwork(net)) - if err != nil { - return App{}, fmt.Errorf("failed to load grid client: %w", err) - } - - return App{ - Specs: specs, - Client: client, - }, nil + return specs, nil } func Execute() { From 9e708656b640f853b486830199e94a8ba0c04dd2 Mon Sep 17 00:00:00 2001 From: eyad-hussein Date: Wed, 24 Jul 2024 17:32:25 +0300 Subject: [PATCH 04/20] feat: implement init logic for ps command --- grid-compose/.gitignore | 2 + grid-compose/Makefile | 6 ++ grid-compose/cmd/ps.go | 123 ++++++++++++++++++++++++++ grid-compose/example/full_example.yml | 4 +- grid-compose/example/single_vm.yml | 2 +- grid-compose/grid-compose.yaml | 11 +++ 6 files changed, 145 insertions(+), 3 deletions(-) create mode 100644 grid-compose/.gitignore create mode 100644 grid-compose/Makefile create mode 100644 grid-compose/cmd/ps.go create mode 100644 grid-compose/grid-compose.yaml diff --git a/grid-compose/.gitignore b/grid-compose/.gitignore new file mode 100644 index 000000000..bfd319054 --- /dev/null +++ b/grid-compose/.gitignore @@ -0,0 +1,2 @@ +bin/* +.pre-commit-config.yaml diff --git a/grid-compose/Makefile b/grid-compose/Makefile new file mode 100644 index 000000000..b02f246f5 --- /dev/null +++ b/grid-compose/Makefile @@ -0,0 +1,6 @@ +BIN_PATH = bin/main + +.PHONY: build + +build: + @go build -o $(BIN_PATH) ./main.go diff --git a/grid-compose/cmd/ps.go b/grid-compose/cmd/ps.go new file mode 100644 index 000000000..196f008c9 --- /dev/null +++ b/grid-compose/cmd/ps.go @@ -0,0 +1,123 @@ +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "strconv" + "strings" + + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" + "github.com/threefoldtech/tfgrid-sdk-go/grid-client/deployer" + "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal" + "github.com/threefoldtech/tfgrid-sdk-go/grid-proxy/pkg/types" +) + +var psCmd = &cobra.Command{ + Use: "ps", + Short: "list containers", + Run: func(cmd *cobra.Command, args []string) { + if err := ps(cmd.Context()); err != nil { + log.Fatal().Err(err).Send() + } + }, +} + +func ps(ctx context.Context) error { + twindId := uint64(app.Client.TwinID) + filters := types.ContractFilter{ + TwinID: &twindId, + } + limits := types.Limit{ + Size: 100, + } + cache := make(map[string]bool, 0) + + contracts, _, err := app.Client.GridProxyClient.Contracts(ctx, filters, limits) + + if err != nil { + return err + } + + for _, contract := range contracts { + if contract.Type != "node" || contract.State == "Deleted" { + continue + } + + details, err := workloads.ParseDeploymentData(contract.Details.(types.NodeContractDetails).DeploymentData) + if err != nil { + return err + } + + if strings.Split(details.ProjectName, "/")[0] != "compose" || cache[details.ProjectName] { + continue + } + + res, err := GetVM(ctx, app.Client, details.Name) + if err != nil { + return err + } + + s, err := json.MarshalIndent(res, "", "\t") + if err != nil { + log.Fatal().Err(err).Send() + } + + log.Info().Msg("vm:\n" + string(s)) + cache[details.ProjectName] = true + } + + return nil +} + +// GetVM gets a vm with its project name +func GetVM(ctx context.Context, t deployer.TFPluginClient, name string) (workloads.Deployment, error) { + name, _ = strings.CutSuffix(name, "net") + projectName := internal.GetProjectName(name, app.Client.TwinID) + + // try to get contracts with the old project name format "" + contracts, err := t.ContractsGetter.ListContractsOfProjectName(projectName, true) + if err != nil { + return workloads.Deployment{}, err + } + + if len(contracts.NodeContracts) == 0 { + // if could not find any contracts try to get contracts with the new project name format "vm/" + projectName = fmt.Sprintf("vm/%s", name) + contracts, err = t.ContractsGetter.ListContractsOfProjectName(projectName, true) + if err != nil { + return workloads.Deployment{}, err + } + + if len(contracts.NodeContracts) == 0 { + return workloads.Deployment{}, fmt.Errorf("couldn't find any contracts with name %s", name) + } + } + + var nodeID uint32 + + for _, contract := range contracts.NodeContracts { + contractID, err := strconv.ParseUint(contract.ContractID, 10, 64) + if err != nil { + return workloads.Deployment{}, err + } + + nodeID = contract.NodeID + checkIfExistAndAppend(t, nodeID, contractID) + + } + + return t.State.LoadDeploymentFromGrid(ctx, nodeID, name) +} + +func checkIfExistAndAppend(t deployer.TFPluginClient, node uint32, contractID uint64) { + for _, n := range t.State.CurrentNodeDeployments[node] { + if n == contractID { + return + } + } + + t.State.CurrentNodeDeployments[node] = append(t.State.CurrentNodeDeployments[node], contractID) +} diff --git a/grid-compose/example/full_example.yml b/grid-compose/example/full_example.yml index 41ea7ff5c..cd60dc40d 100644 --- a/grid-compose/example/full_example.yml +++ b/grid-compose/example/full_example.yml @@ -43,7 +43,7 @@ services: cpu: 4 memoryGB: 2 healthcheck: - test: ['CMD', 'curl' , 'http://{myceliumip}'] + test: ['CMD', 'curl', 'http://{myceliumip}'] interval: 1m30s timeout: 10s retries: 3 @@ -51,4 +51,4 @@ services: storage: web-data: type: 'zmount' - size: 10GB \ No newline at end of file + size: 10GB diff --git a/grid-compose/example/single_vm.yml b/grid-compose/example/single_vm.yml index b89da2621..66d2e8cbc 100644 --- a/grid-compose/example/single_vm.yml +++ b/grid-compose/example/single_vm.yml @@ -11,4 +11,4 @@ services: memory: 512 node_id: 14 networks: - - yggdrasil \ No newline at end of file + - yggdrasil diff --git a/grid-compose/grid-compose.yaml b/grid-compose/grid-compose.yaml new file mode 100644 index 000000000..aed957484 --- /dev/null +++ b/grid-compose/grid-compose.yaml @@ -0,0 +1,11 @@ +version: '1.0.0' +services: + web: + flist: 'https://hub.grid.tf/tf-official-apps/base:latest.flist' + networks: ['publicip4'] + node_id: 14 + entrypoint: '/sbin/zinit init' + resources: + cpu: 2 + memory: 1024 + disk: 4096 From 7d3cff00d2238c0e5c03df4a959813c3dcee458d Mon Sep 17 00:00:00 2001 From: eyad-hussein Date: Sat, 3 Aug 2024 14:58:07 +0300 Subject: [PATCH 05/20] enhance: implement logic to parse env vars and disks for up command --- grid-compose/Makefile | 3 + grid-compose/cmd/down.go | 14 +- grid-compose/cmd/ps.go | 109 +-------- grid-compose/cmd/root.go | 68 +----- grid-compose/cmd/up.go | 85 +------ grid-compose/example/full_example.yml | 8 +- grid-compose/grid-compose.yaml | 49 +++- grid-compose/internal/app.go | 230 ++++++++++++++++++ grid-compose/internal/config/config.go | 60 +++++ grid-compose/internal/config/errors.go | 13 + grid-compose/internal/mycelium_seed.go | 13 + grid-compose/internal/validation.go | 7 + .../internal/{utils.go => vmaddresses.go} | 12 - grid-compose/pkg/{types => }/types.go | 38 +-- 14 files changed, 395 insertions(+), 314 deletions(-) create mode 100644 grid-compose/internal/app.go create mode 100644 grid-compose/internal/config/config.go create mode 100644 grid-compose/internal/config/errors.go create mode 100644 grid-compose/internal/mycelium_seed.go create mode 100644 grid-compose/internal/validation.go rename grid-compose/internal/{utils.go => vmaddresses.go} (64%) rename grid-compose/pkg/{types => }/types.go (54%) diff --git a/grid-compose/Makefile b/grid-compose/Makefile index b02f246f5..2d73eaa8c 100644 --- a/grid-compose/Makefile +++ b/grid-compose/Makefile @@ -4,3 +4,6 @@ BIN_PATH = bin/main build: @go build -o $(BIN_PATH) ./main.go + +run: + @go run main.go up -f ./grid-compose.yaml diff --git a/grid-compose/cmd/down.go b/grid-compose/cmd/down.go index a01807dae..08c3c36e2 100644 --- a/grid-compose/cmd/down.go +++ b/grid-compose/cmd/down.go @@ -3,26 +3,14 @@ package cmd import ( "github.com/rs/zerolog/log" "github.com/spf13/cobra" - "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal" ) var downCmd = &cobra.Command{ Use: "down", Short: "cancel your project on the grid", Run: func(cmd *cobra.Command, args []string) { - if err := down(); err != nil { + if err := app.Down(); err != nil { log.Fatal().Err(err).Send() } }, } - -func down() error { - for key := range app.Specs.Services { - projectName := internal.GetProjectName(key, app.Client.TwinID) - log.Info().Str("projectName", projectName).Msg("canceling deployments") - if err := app.Client.CancelByProjectName(projectName); err != nil { - return err - } - } - return nil -} diff --git a/grid-compose/cmd/ps.go b/grid-compose/cmd/ps.go index 196f008c9..8f8f6cbaf 100644 --- a/grid-compose/cmd/ps.go +++ b/grid-compose/cmd/ps.go @@ -1,123 +1,16 @@ package cmd import ( - "context" - "encoding/json" - "fmt" - "strconv" - "strings" - "github.com/rs/zerolog/log" "github.com/spf13/cobra" - "github.com/threefoldtech/tfgrid-sdk-go/grid-client/deployer" - "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" - "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal" - "github.com/threefoldtech/tfgrid-sdk-go/grid-proxy/pkg/types" ) var psCmd = &cobra.Command{ Use: "ps", Short: "list containers", Run: func(cmd *cobra.Command, args []string) { - if err := ps(cmd.Context()); err != nil { + if err := app.Ps(cmd.Context()); err != nil { log.Fatal().Err(err).Send() } }, } - -func ps(ctx context.Context) error { - twindId := uint64(app.Client.TwinID) - filters := types.ContractFilter{ - TwinID: &twindId, - } - limits := types.Limit{ - Size: 100, - } - cache := make(map[string]bool, 0) - - contracts, _, err := app.Client.GridProxyClient.Contracts(ctx, filters, limits) - - if err != nil { - return err - } - - for _, contract := range contracts { - if contract.Type != "node" || contract.State == "Deleted" { - continue - } - - details, err := workloads.ParseDeploymentData(contract.Details.(types.NodeContractDetails).DeploymentData) - if err != nil { - return err - } - - if strings.Split(details.ProjectName, "/")[0] != "compose" || cache[details.ProjectName] { - continue - } - - res, err := GetVM(ctx, app.Client, details.Name) - if err != nil { - return err - } - - s, err := json.MarshalIndent(res, "", "\t") - if err != nil { - log.Fatal().Err(err).Send() - } - - log.Info().Msg("vm:\n" + string(s)) - cache[details.ProjectName] = true - } - - return nil -} - -// GetVM gets a vm with its project name -func GetVM(ctx context.Context, t deployer.TFPluginClient, name string) (workloads.Deployment, error) { - name, _ = strings.CutSuffix(name, "net") - projectName := internal.GetProjectName(name, app.Client.TwinID) - - // try to get contracts with the old project name format "" - contracts, err := t.ContractsGetter.ListContractsOfProjectName(projectName, true) - if err != nil { - return workloads.Deployment{}, err - } - - if len(contracts.NodeContracts) == 0 { - // if could not find any contracts try to get contracts with the new project name format "vm/" - projectName = fmt.Sprintf("vm/%s", name) - contracts, err = t.ContractsGetter.ListContractsOfProjectName(projectName, true) - if err != nil { - return workloads.Deployment{}, err - } - - if len(contracts.NodeContracts) == 0 { - return workloads.Deployment{}, fmt.Errorf("couldn't find any contracts with name %s", name) - } - } - - var nodeID uint32 - - for _, contract := range contracts.NodeContracts { - contractID, err := strconv.ParseUint(contract.ContractID, 10, 64) - if err != nil { - return workloads.Deployment{}, err - } - - nodeID = contract.NodeID - checkIfExistAndAppend(t, nodeID, contractID) - - } - - return t.State.LoadDeploymentFromGrid(ctx, nodeID, name) -} - -func checkIfExistAndAppend(t deployer.TFPluginClient, node uint32, contractID uint64) { - for _, n := range t.State.CurrentNodeDeployments[node] { - if n == contractID { - return - } - } - - t.State.CurrentNodeDeployments[node] = append(t.State.CurrentNodeDeployments[node], contractID) -} diff --git a/grid-compose/cmd/root.go b/grid-compose/cmd/root.go index 6bd259242..9319880c3 100644 --- a/grid-compose/cmd/root.go +++ b/grid-compose/cmd/root.go @@ -1,31 +1,33 @@ package cmd import ( - "fmt" - "io" "os" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/spf13/cobra" - "github.com/threefoldtech/tfgrid-sdk-go/grid-client/deployer" - "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/pkg/types" - "gopkg.in/yaml.v3" + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal" ) var ( - app App - configFile string + app *internal.App + configPath string network string mnemonic string ) +func Execute() { + if err := rootCmd.Execute(); err != nil { + log.Fatal().Err(err).Send() + } +} + var rootCmd = &cobra.Command{ Use: "grid-compose", Short: "Grid-Compose is a tool for running multi-vm applications on TFGrid defined using a Yaml formatted file.", PersistentPreRun: func(cmd *cobra.Command, args []string) { var err error - app, err = NewApp(network, mnemonic, configFile) + app, err = internal.NewApp(network, mnemonic, configPath) if err != nil { log.Fatal().Err(err).Send() } @@ -35,7 +37,7 @@ var rootCmd = &cobra.Command{ func init() { network = os.Getenv("NETWORK") mnemonic = os.Getenv("MNEMONIC") - rootCmd.PersistentFlags().StringVarP(&configFile, "file", "f", "./grid-compose.yaml", "the grid-compose configuration file") + rootCmd.PersistentFlags().StringVarP(&configPath, "file", "f", "./grid-compose.yaml", "the grid-compose configuration file") log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) @@ -44,51 +46,3 @@ func init() { rootCmd.AddCommand(psCmd) rootCmd.AddCommand(downCmd) } - -type App struct { - Client deployer.TFPluginClient - Specs types.Specs -} - -func NewApp(net, mne, filePath string) (App, error) { - specs, err := LoadSpecsFromFile(filePath) - if err != nil { - return App{}, fmt.Errorf("failed to load specs from file: %w", err) - } - - client, err := deployer.NewTFPluginClient(mne, deployer.WithNetwork(net)) - if err != nil { - return App{}, fmt.Errorf("failed to load grid client: %w", err) - } - - return App{ - Specs: specs, - Client: client, - }, nil -} - -func LoadSpecsFromFile(filePath string) (types.Specs, error) { - file, err := os.Open(filePath) - if err != nil { - return types.Specs{}, fmt.Errorf("failed to open file: %w", err) - } - defer file.Close() - - content, err := io.ReadAll(file) - if err != nil { - return types.Specs{}, fmt.Errorf("failed to read file: %w", err) - } - - var specs types.Specs - if err := yaml.Unmarshal(content, &specs); err != nil { - return types.Specs{}, fmt.Errorf("failed to parse file: %w", err) - } - - return specs, nil -} - -func Execute() { - if err := rootCmd.Execute(); err != nil { - log.Fatal().Err(err).Send() - } -} diff --git a/grid-compose/cmd/up.go b/grid-compose/cmd/up.go index 0452be32c..4d42aa108 100644 --- a/grid-compose/cmd/up.go +++ b/grid-compose/cmd/up.go @@ -1,95 +1,20 @@ package cmd import ( - "context" - "fmt" - "net" - "github.com/rs/zerolog/log" "github.com/spf13/cobra" - "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" - "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal" - "github.com/threefoldtech/zos/pkg/gridtypes" ) +// load config from file + validate +// parse environment variables +// deploy networks + volumes +// deploy services var upCmd = &cobra.Command{ Use: "up", Short: "deploy application on the grid", Run: func(cmd *cobra.Command, args []string) { - if err := up(cmd.Context()); err != nil { + if err := app.Up(cmd.Context()); err != nil { log.Fatal().Err(err).Send() } }, } - -func up(ctx context.Context) error { - results := make(map[string]workloads.VM) - for key, val := range app.Specs.Services { - projectName := internal.GetProjectName(key, app.Client.TwinID) - - networkName := key + "net" - log.Info().Str("projectName", projectName).Str("workloadName", networkName).Msg("deploying network") - net := workloads.ZNet{ - Name: networkName, - Nodes: []uint32{uint32(val.NodeID)}, - IPRange: gridtypes.NewIPNet(net.IPNet{ - IP: net.IPv4(10, 20, 0, 0), - Mask: net.CIDRMask(16, 32), - }), - SolutionType: projectName, - } - if err := app.Client.NetworkDeployer.Deploy(ctx, &net); err != nil { - return err - } - - log.Info().Str("projectName", projectName).Str("workloadName", key).Msg("deploying vm") - vm := workloads.VM{ - Name: key, - Flist: val.Flist, - Entrypoint: val.Entrypoint, - EnvVars: val.Environment, - CPU: int(val.Resources.CPU), - Memory: int(val.Resources.Memory), - NetworkName: net.Name, - } - - for _, net := range val.Networks { - switch net { - case "publicIp4": - vm.PublicIP = true - case "publicIp6": - vm.PublicIP6 = true - case "yggdrasil": - vm.Planetary = true - case "mycelium": - seed, err := internal.GetRandomMyceliumIPSeed() - if err != nil { - return fmt.Errorf("failed to get mycelium seed: %w", err) - } - vm.MyceliumIPSeed = seed - } - } - - dl := workloads.NewDeployment(vm.Name, uint32(val.NodeID), projectName, nil, net.Name, nil, nil, []workloads.VM{vm}, nil) - if err := app.Client.DeploymentDeployer.Deploy(ctx, &dl); err != nil { - log.Error().Err(err).Msg("reverting deployed network") - if err := app.Client.NetworkDeployer.Cancel(ctx, &net); err != nil { - return err - } - return err - } - - res, err := app.Client.State.LoadVMFromGrid(ctx, uint32(val.NodeID), vm.Name, dl.Name) - if err != nil { - return err - } - - results[vm.Name] = res - } - - for key, val := range results { - fmt.Printf("%s vm addresses:\n", key) - fmt.Println(internal.GetVmAddresses(val)) - } - return nil -} diff --git a/grid-compose/example/full_example.yml b/grid-compose/example/full_example.yml index cd60dc40d..f4fd1fcce 100644 --- a/grid-compose/example/full_example.yml +++ b/grid-compose/example/full_example.yml @@ -1,6 +1,7 @@ # not all features here are implemented yet version: '1.0.0' +# cannot figure out how to run mycelium always error deployment not configured networks: net1: type: 'wg' @@ -41,14 +42,17 @@ services: - net3 resources: cpu: 4 - memoryGB: 2 + memory: 2 healthcheck: test: ['CMD', 'curl', 'http://{myceliumip}'] interval: 1m30s timeout: 10s retries: 3 - +# ask omar about the types zmount and all that storage: web-data: type: 'zmount' size: 10GB + db-data: + type: 'zmount' + size: 10GB diff --git a/grid-compose/grid-compose.yaml b/grid-compose/grid-compose.yaml index aed957484..cd435187e 100644 --- a/grid-compose/grid-compose.yaml +++ b/grid-compose/grid-compose.yaml @@ -1,11 +1,48 @@ version: '1.0.0' + +networks: + net1: + type: 'wg' + net2: + type: 'myc' + net3: + type: 'ygg' + net4: + type: 'ip4' + net5: + type: 'ip6' + services: - web: - flist: 'https://hub.grid.tf/tf-official-apps/base:latest.flist' - networks: ['publicip4'] + web5: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + networks: + - net3 + node_id: 144 + entrypoint: '/sbin/zinit init' + volumes: + - webdata:/data + resources: + cpu: 1 + memory: 2048 + disk: 25600 + database: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + networks: + - net3 + - net1 + volumes: + - dbdata:/var/lib/postgresql/data node_id: 14 entrypoint: '/sbin/zinit init' resources: - cpu: 2 - memory: 1024 - disk: 4096 + cpu: 1 + memory: 2048 + disk: 25600 + +storage: + webdata: + type: 'zmount' + size: 10GB + dbdata: + type: 'zmount' + size: 10GB diff --git a/grid-compose/internal/app.go b/grid-compose/internal/app.go new file mode 100644 index 000000000..ba0696459 --- /dev/null +++ b/grid-compose/internal/app.go @@ -0,0 +1,230 @@ +package internal + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net" + "os" + "strconv" + "strings" + + "github.com/rs/zerolog/log" + "github.com/threefoldtech/tfgrid-sdk-go/grid-client/deployer" + "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/config" + "github.com/threefoldtech/tfgrid-sdk-go/grid-proxy/pkg/types" + "github.com/threefoldtech/zos/pkg/gridtypes" + "gopkg.in/yaml.v2" +) + +type App struct { + Client *deployer.TFPluginClient + Config *config.Config +} + +func NewApp(net, mnemonic, configPath string) (*App, error) { + configFile, err := os.Open(configPath) + if err != nil { + return nil, err + } + + defer configFile.Close() + + config, err := loadConfigFromReader(configFile) + if err != nil { + return nil, fmt.Errorf("failed to load config from file: %w", err) + } + + if err := config.ValidateConfig(); err != nil { + return nil, fmt.Errorf("failed to validate config %w", err) + } + + client, err := deployer.NewTFPluginClient(mnemonic, deployer.WithNetwork(net)) + if err != nil { + return nil, fmt.Errorf("failed to load grid client: %w", err) + } + + return &App{ + Config: config, + Client: &client, + }, nil +} + +func (a *App) Up(ctx context.Context) error { + deployments := make(map[string]*workloads.Deployment, 0) + + for key, val := range a.Config.Services { + projectName := a.getProjectName(key, a.Client.TwinID) + networkName := key + "net" + + network := workloads.ZNet{ + Name: networkName, + Nodes: []uint32{uint32(val.NodeID)}, + IPRange: gridtypes.NewIPNet(net.IPNet{ + IP: net.IPv4(10, 20, 0, 0), + Mask: net.CIDRMask(16, 32), + }), + SolutionType: projectName, + } + + vm := workloads.VM{ + Name: key, + Flist: val.Flist, + Entrypoint: val.Entrypoint, + CPU: int(val.Resources.CPU), + Memory: int(val.Resources.Memory), + NetworkName: networkName, + } + + env := make(map[string]string, 0) + + for _, envVar := range val.Environment { + split := strings.Split(envVar, "=") + env[split[0]] = split[1] + } + + vm.EnvVars = env + + var mounts []workloads.Mount + var disks []workloads.Disk + for _, volume := range val.Volumes { + split := strings.Split(volume, ":") + + storage := a.Config.Storage[split[0]] + size, _ := strconv.Atoi(strings.TrimSuffix(storage.Size, "GB")) + disk := workloads.Disk{ + Name: split[0], + SizeGB: size, + } + + disks = append(disks, disk) + + mounts = append(mounts, workloads.Mount{ + DiskName: disk.Name, + MountPoint: split[1], + }) + } + vm.Mounts = mounts + + for _, net := range val.Networks { + switch a.Config.Networks[net].Type { + case "wg": + network.AddWGAccess = true + case "ip4": + vm.PublicIP = true + case "ip6": + vm.PublicIP6 = true + case "ygg": + vm.Planetary = true + case "myc": + seed, err := getRandomMyceliumIPSeed() + if err != nil { + return fmt.Errorf("failed to get mycelium seed: %w", err) + } + vm.MyceliumIPSeed = seed + } + } + if err := a.Client.NetworkDeployer.Deploy(context.Background(), &network); err != nil { + return err + } + + dl := workloads.NewDeployment(vm.Name, uint32(val.NodeID), projectName, nil, networkName, disks, nil, []workloads.VM{vm}, nil) + if err := a.Client.DeploymentDeployer.Deploy(context.Background(), &dl); err != nil { + if err := a.Client.NetworkDeployer.Cancel(context.Background(), &network); err != nil { + return err + } + return err + } + + deployments[dl.Name] = &dl + } + + for name, dl := range deployments { + vmState, err := a.Client.State.LoadVMFromGrid(ctx, uint32(dl.NodeID), name, name) + if err != nil { + return fmt.Errorf("%w vm %s", err, name) + } + + log.Info().Str("ip", vmState.IP).Str("vm name", name).Msg("vm deployed") + } + + return nil +} + +func (a *App) Ps(ctx context.Context) error { + twindId := uint64(a.Client.TwinID) + filters := types.ContractFilter{ + TwinID: &twindId, + } + limits := types.Limit{ + Size: 100, + } + cache := make(map[string]bool, 0) + + contracts, _, err := a.Client.GridProxyClient.Contracts(ctx, filters, limits) + + if err != nil { + return err + } + + for _, contract := range contracts { + if contract.Type != "node" || contract.State == "Deleted" { + continue + } + + details, err := workloads.ParseDeploymentData(contract.Details.(types.NodeContractDetails).DeploymentData) + if err != nil { + return err + } + + if strings.Split(details.ProjectName, "/")[0] != "compose" || cache[details.ProjectName] { + continue + } + + res, err := GetVM(ctx, a.Client, details.Name) + if err != nil { + return err + } + + s, err := json.MarshalIndent(res, "", "\t") + if err != nil { + log.Fatal().Err(err).Send() + } + + log.Info().Msg("vm:\n" + string(s)) + cache[details.ProjectName] = true + } + + return nil +} + +func (a *App) Down() error { + for key := range a.Config.Services { + projectName := a.getProjectName(key, a.Client.TwinID) + log.Info().Str("projectName", projectName).Msg("canceling deployments") + if err := a.Client.CancelByProjectName(projectName); err != nil { + return err + } + } + return nil +} + +func (a *App) getProjectName(key string, twinId uint32) string { + return fmt.Sprintf("compose/%v/%v", twinId, key) +} + +func loadConfigFromReader(configFile io.Reader) (*config.Config, error) { + content, err := io.ReadAll(configFile) + if err != nil { + return &config.Config{}, fmt.Errorf("failed to read file: %w", err) + } + + var config config.Config + if err := yaml.Unmarshal(content, &config); err != nil { + return &config, fmt.Errorf("failed to parse file: %w", err) + } + + return &config, nil +} diff --git a/grid-compose/internal/config/config.go b/grid-compose/internal/config/config.go new file mode 100644 index 000000000..92373ffff --- /dev/null +++ b/grid-compose/internal/config/config.go @@ -0,0 +1,60 @@ +package config + +import ( + "fmt" + + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/pkg" +) + +type Config struct { + Version string `yaml:"version"` + Networks map[string]pkg.Network `yaml:"networks"` + Services map[string]pkg.Service `yaml:"services"` + Storage map[string]pkg.Storage `yaml:"storage"` +} + +func NewConfig() *Config { + return &Config{ + Networks: make(map[string]pkg.Network), + Services: make(map[string]pkg.Service), + Storage: make(map[string]pkg.Storage), + } +} + +func (c *Config) ValidateConfig() (err error) { + if c.Version == "" { + return ErrVersionNotSet + } + + for name, network := range c.Networks { + if network.Type == "" { + return fmt.Errorf("%w for network %s", ErrNetworkTypeNotSet, name) + } + } + + for name, service := range c.Services { + if service.Flist == "" { + return fmt.Errorf("%w for service %s", ErrServiceFlistNotSet, name) + } + + if service.Resources.CPU == 0 { + return fmt.Errorf("%w for service %s", ErrServiceCPUResourceNotSet, name) + } + + if service.Resources.Memory == 0 { + return fmt.Errorf("%w for service %s", ErrServiceMemoryResourceNotSet, name) + } + } + + for name, storage := range c.Storage { + if storage.Type == "" { + return fmt.Errorf("%w for storage %s", ErrStorageTypeNotSet, name) + } + + if storage.Size == "" { + return fmt.Errorf("%w for storage %s", ErrStorageSizeNotSet, name) + } + } + + return nil +} diff --git a/grid-compose/internal/config/errors.go b/grid-compose/internal/config/errors.go new file mode 100644 index 000000000..e03236c0a --- /dev/null +++ b/grid-compose/internal/config/errors.go @@ -0,0 +1,13 @@ +package config + +import "errors" + +var ( + ErrVersionNotSet = errors.New("version not set") + ErrNetworkTypeNotSet = errors.New("network type not set") + ErrServiceFlistNotSet = errors.New("service flist not set") + ErrServiceCPUResourceNotSet = errors.New("service cpu resource not set") + ErrServiceMemoryResourceNotSet = errors.New("service memory resource not set") + ErrStorageTypeNotSet = errors.New("storage type not set") + ErrStorageSizeNotSet = errors.New("storage size not set") +) diff --git a/grid-compose/internal/mycelium_seed.go b/grid-compose/internal/mycelium_seed.go new file mode 100644 index 000000000..2677e5d76 --- /dev/null +++ b/grid-compose/internal/mycelium_seed.go @@ -0,0 +1,13 @@ +package internal + +import ( + "crypto/rand" + + "github.com/threefoldtech/zos/pkg/gridtypes/zos" +) + +func getRandomMyceliumIPSeed() ([]byte, error) { + key := make([]byte, zos.MyceliumIPSeedLen) + _, err := rand.Read(key) + return key, err +} diff --git a/grid-compose/internal/validation.go b/grid-compose/internal/validation.go new file mode 100644 index 000000000..945fd3c36 --- /dev/null +++ b/grid-compose/internal/validation.go @@ -0,0 +1,7 @@ +package internal + +import "github.com/cosmos/go-bip39" + +func ValidateMnemonics(mnemonics string) bool { + return bip39.IsMnemonicValid(mnemonics) +} diff --git a/grid-compose/internal/utils.go b/grid-compose/internal/vmaddresses.go similarity index 64% rename from grid-compose/internal/utils.go rename to grid-compose/internal/vmaddresses.go index bf42de648..8a5f720d3 100644 --- a/grid-compose/internal/utils.go +++ b/grid-compose/internal/vmaddresses.go @@ -1,17 +1,11 @@ package internal import ( - "crypto/rand" "fmt" "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" - "github.com/threefoldtech/zos/pkg/gridtypes/zos" ) -func GetProjectName(key string, twinId uint32) string { - return fmt.Sprintf("compose/%v/%v", twinId, key) -} - func GetVmAddresses(vm workloads.VM) string { var res string @@ -33,9 +27,3 @@ func GetVmAddresses(vm workloads.VM) string { return res } - -func GetRandomMyceliumIPSeed() ([]byte, error) { - key := make([]byte, zos.MyceliumIPSeedLen) - _, err := rand.Read(key) - return key, err -} diff --git a/grid-compose/pkg/types/types.go b/grid-compose/pkg/types.go similarity index 54% rename from grid-compose/pkg/types/types.go rename to grid-compose/pkg/types.go index 809696283..c71e88b20 100644 --- a/grid-compose/pkg/types/types.go +++ b/grid-compose/pkg/types.go @@ -1,18 +1,4 @@ -package types - -import ( - "fmt" - "strings" - - "gopkg.in/yaml.v3" -) - -type Specs struct { - Version string `yaml:"version"` - Networks map[string]Network `yaml:"networks"` - Services map[string]Service `yaml:"services"` - Storage map[string]Storage `yaml:"storage"` -} +package pkg type Network struct { Type string `yaml:"type"` @@ -26,7 +12,7 @@ type Storage struct { type Service struct { Flist string `yaml:"flist"` Entrypoint string `yaml:"entrypoint,omitempty"` - Environment KVMap `yaml:"environment"` + Environment []string `yaml:"environment"` Resources Resources `yaml:"resources"` NodeID uint `yaml:"node_id"` Volumes []string `yaml:"volumes"` @@ -48,23 +34,3 @@ type Resources struct { SSD uint `yaml:"ssd"` HDD uint `yaml:"hdd"` } - -type KVMap map[string]string - -func (m *KVMap) UnmarshalYAML(value *yaml.Node) error { - var raw []string - if err := value.Decode(&raw); err != nil { - return err - } - - *m = make(map[string]string) - for _, ele := range raw { - kv := strings.SplitN(ele, "=", 2) - if len(kv) != 2 { - return fmt.Errorf("invalid kvmap format: %s", ele) - } - (*m)[kv[0]] = kv[1] - } - - return nil -} From 905394b2d529c935358b63d7dca1cb9fe0091b28 Mon Sep 17 00:00:00 2001 From: eyad-hussein Date: Sat, 3 Aug 2024 17:05:21 +0300 Subject: [PATCH 06/20] refactor: refactor ps command implementation --- grid-compose/internal/app.go | 73 ++++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 32 deletions(-) diff --git a/grid-compose/internal/app.go b/grid-compose/internal/app.go index ba0696459..adc5762d9 100644 --- a/grid-compose/internal/app.go +++ b/grid-compose/internal/app.go @@ -14,7 +14,6 @@ import ( "github.com/threefoldtech/tfgrid-sdk-go/grid-client/deployer" "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/config" - "github.com/threefoldtech/tfgrid-sdk-go/grid-proxy/pkg/types" "github.com/threefoldtech/zos/pkg/gridtypes" "gopkg.in/yaml.v2" ) @@ -154,49 +153,27 @@ func (a *App) Up(ctx context.Context) error { } func (a *App) Ps(ctx context.Context) error { - twindId := uint64(a.Client.TwinID) - filters := types.ContractFilter{ - TwinID: &twindId, - } - limits := types.Limit{ - Size: 100, - } - cache := make(map[string]bool, 0) - - contracts, _, err := a.Client.GridProxyClient.Contracts(ctx, filters, limits) - - if err != nil { - return err - } - - for _, contract := range contracts { - if contract.Type != "node" || contract.State == "Deleted" { - continue - } - - details, err := workloads.ParseDeploymentData(contract.Details.(types.NodeContractDetails).DeploymentData) + // need to add the option for verbose and format the output + for key, val := range a.Config.Services { + err := a.loadCurrentNodeDeplyments(a.getProjectName(key, a.Client.TwinID)) if err != nil { return err } - - if strings.Split(details.ProjectName, "/")[0] != "compose" || cache[details.ProjectName] { - continue - } - - res, err := GetVM(ctx, a.Client, details.Name) + wl, dl, err := a.Client.State.GetWorkloadInDeployment(ctx, uint32(val.NodeID), key, key) if err != nil { return err } - s, err := json.MarshalIndent(res, "", "\t") + log.Info().Str("deployment", string(wl.Name)).Msg("deployment") + + s, err := json.MarshalIndent(dl, "", "\t") if err != nil { log.Fatal().Err(err).Send() } - log.Info().Msg("vm:\n" + string(s)) - cache[details.ProjectName] = true - } + log.Info().Msg(string(s)) + } return nil } @@ -212,9 +189,41 @@ func (a *App) Down() error { } func (a *App) getProjectName(key string, twinId uint32) string { + key = strings.TrimSuffix(key, "net") return fmt.Sprintf("compose/%v/%v", twinId, key) } +func (a *App) loadCurrentNodeDeplyments(projectName string) error { + contracts, err := a.Client.ContractsGetter.ListContractsOfProjectName(projectName, true) + if err != nil { + return err + } + + var nodeID uint32 + + for _, contract := range contracts.NodeContracts { + contractID, err := strconv.ParseUint(contract.ContractID, 10, 64) + if err != nil { + return err + } + + nodeID = contract.NodeID + a.checkIfExistAndAppend(nodeID, contractID) + } + + return nil +} + +func (a *App) checkIfExistAndAppend(node uint32, contractID uint64) { + for _, n := range a.Client.State.CurrentNodeDeployments[node] { + if n == contractID { + return + } + } + + a.Client.State.CurrentNodeDeployments[node] = append(a.Client.State.CurrentNodeDeployments[node], contractID) +} + func loadConfigFromReader(configFile io.Reader) (*config.Config, error) { content, err := io.ReadAll(configFile) if err != nil { From 40e67a29b5af9d61d736ed479ec7632ede1b37b5 Mon Sep 17 00:00:00 2001 From: eyad-hussein Date: Sun, 4 Aug 2024 19:50:39 +0300 Subject: [PATCH 07/20] refactor: refactor up and ps commands --- grid-compose/.gitignore | 1 + grid-compose/cmd/ps.go | 4 +- grid-compose/cmd/root.go | 2 + grid-compose/example/full_example_v2.yml | 58 +++++++++ grid-compose/grid-compose.yaml | 2 + grid-compose/internal/app.go | 153 ++++++++++++----------- grid-compose/internal/config/config.go | 15 +++ grid-compose/internal/utils.go | 73 +++++++++++ grid-compose/internal/vmaddresses.go | 17 +-- grid-compose/pkg/types.go | 31 ++++- 10 files changed, 272 insertions(+), 84 deletions(-) create mode 100644 grid-compose/example/full_example_v2.yml create mode 100644 grid-compose/internal/utils.go diff --git a/grid-compose/.gitignore b/grid-compose/.gitignore index bfd319054..d65a64c45 100644 --- a/grid-compose/.gitignore +++ b/grid-compose/.gitignore @@ -1,2 +1,3 @@ bin/* .pre-commit-config.yaml +out diff --git a/grid-compose/cmd/ps.go b/grid-compose/cmd/ps.go index 8f8f6cbaf..6d673da30 100644 --- a/grid-compose/cmd/ps.go +++ b/grid-compose/cmd/ps.go @@ -9,7 +9,9 @@ var psCmd = &cobra.Command{ Use: "ps", Short: "list containers", Run: func(cmd *cobra.Command, args []string) { - if err := app.Ps(cmd.Context()); err != nil { + flags := cmd.Flags() + + if err := app.Ps(cmd.Context(), flags); err != nil { log.Fatal().Err(err).Send() } }, diff --git a/grid-compose/cmd/root.go b/grid-compose/cmd/root.go index 9319880c3..0a7fc375a 100644 --- a/grid-compose/cmd/root.go +++ b/grid-compose/cmd/root.go @@ -38,6 +38,8 @@ func init() { network = os.Getenv("NETWORK") mnemonic = os.Getenv("MNEMONIC") rootCmd.PersistentFlags().StringVarP(&configPath, "file", "f", "./grid-compose.yaml", "the grid-compose configuration file") + psCmd.PersistentFlags().BoolP("verbose", "v", false, "all information about deployed services") + psCmd.PersistentFlags().StringP("output", "o", "", "output result to a file") log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) diff --git a/grid-compose/example/full_example_v2.yml b/grid-compose/example/full_example_v2.yml new file mode 100644 index 000000000..f4fd1fcce --- /dev/null +++ b/grid-compose/example/full_example_v2.yml @@ -0,0 +1,58 @@ +# not all features here are implemented yet +version: '1.0.0' + +# cannot figure out how to run mycelium always error deployment not configured +networks: + net1: + type: 'wg' + net2: + type: 'myc' + net3: + type: 'ygg' + net4: + type: 'ip4' + net5: + type: 'ip6' + +services: + web: + flist: 'https://hub.grid.tf/tf-official-apps/nginx-latest.flist' + environment: + - ENV_VAR_NAME=value + volumes: + - web-data:/data + networks: + - net1 + resources: + cpu: 2 + memory: 2 + depends_on: + - database + database: + flist: 'https://hub.grid.tf/tf-official-apps/postgresql-latest.flist' + environment: + - POSTGRES_DB=postgres + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=password + volumes: + - db-data:/var/lib/postgresql/data + networks: + - net1 + - net2 + - net3 + resources: + cpu: 4 + memory: 2 + healthcheck: + test: ['CMD', 'curl', 'http://{myceliumip}'] + interval: 1m30s + timeout: 10s + retries: 3 +# ask omar about the types zmount and all that +storage: + web-data: + type: 'zmount' + size: 10GB + db-data: + type: 'zmount' + size: 10GB diff --git a/grid-compose/grid-compose.yaml b/grid-compose/grid-compose.yaml index cd435187e..7594400ba 100644 --- a/grid-compose/grid-compose.yaml +++ b/grid-compose/grid-compose.yaml @@ -17,6 +17,8 @@ services: flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' networks: - net3 + - net4 + - net5 node_id: 144 entrypoint: '/sbin/zinit init' volumes: diff --git a/grid-compose/internal/app.go b/grid-compose/internal/app.go index adc5762d9..333ebfd44 100644 --- a/grid-compose/internal/app.go +++ b/grid-compose/internal/app.go @@ -4,18 +4,18 @@ import ( "context" "encoding/json" "fmt" - "io" "net" "os" "strconv" "strings" "github.com/rs/zerolog/log" + "github.com/spf13/pflag" "github.com/threefoldtech/tfgrid-sdk-go/grid-client/deployer" "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/config" + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/pkg" "github.com/threefoldtech/zos/pkg/gridtypes" - "gopkg.in/yaml.v2" ) type App struct { @@ -31,9 +31,10 @@ func NewApp(net, mnemonic, configPath string) (*App, error) { defer configFile.Close() - config, err := loadConfigFromReader(configFile) + config := config.NewConfig() + err = config.LoadConfigFromReader(configFile) if err != nil { - return nil, fmt.Errorf("failed to load config from file: %w", err) + return nil, fmt.Errorf("failed to load config from file %w", err) } if err := config.ValidateConfig(); err != nil { @@ -42,7 +43,7 @@ func NewApp(net, mnemonic, configPath string) (*App, error) { client, err := deployer.NewTFPluginClient(mnemonic, deployer.WithNetwork(net)) if err != nil { - return nil, fmt.Errorf("failed to load grid client: %w", err) + return nil, fmt.Errorf("failed to load grid client %w", err) } return &App{ @@ -77,54 +78,17 @@ func (a *App) Up(ctx context.Context) error { NetworkName: networkName, } - env := make(map[string]string, 0) + assignEnvs(&vm, val.Environment) - for _, envVar := range val.Environment { - split := strings.Split(envVar, "=") - env[split[0]] = split[1] + disks, err := assignMounts(&vm, val.Volumes, a.Config.Storage) + if err != nil { + return fmt.Errorf("failed to assign mounts %w", err) } - vm.EnvVars = env - - var mounts []workloads.Mount - var disks []workloads.Disk - for _, volume := range val.Volumes { - split := strings.Split(volume, ":") - - storage := a.Config.Storage[split[0]] - size, _ := strconv.Atoi(strings.TrimSuffix(storage.Size, "GB")) - disk := workloads.Disk{ - Name: split[0], - SizeGB: size, - } - - disks = append(disks, disk) - - mounts = append(mounts, workloads.Mount{ - DiskName: disk.Name, - MountPoint: split[1], - }) - } - vm.Mounts = mounts - - for _, net := range val.Networks { - switch a.Config.Networks[net].Type { - case "wg": - network.AddWGAccess = true - case "ip4": - vm.PublicIP = true - case "ip6": - vm.PublicIP6 = true - case "ygg": - vm.Planetary = true - case "myc": - seed, err := getRandomMyceliumIPSeed() - if err != nil { - return fmt.Errorf("failed to get mycelium seed: %w", err) - } - vm.MyceliumIPSeed = seed - } + if err := assignNetworks(&vm, val.Networks, a.Config.Networks, &network); err != nil { + return fmt.Errorf("failed to assign networks %w", err) } + if err := a.Client.NetworkDeployer.Deploy(context.Background(), &network); err != nil { return err } @@ -152,31 +116,92 @@ func (a *App) Up(ctx context.Context) error { return nil } -func (a *App) Ps(ctx context.Context) error { - // need to add the option for verbose and format the output +func (a *App) Ps(ctx context.Context, flags *pflag.FlagSet) error { + verbose, outputFile, err := parsePsFlags(flags) + if err != nil { + return err + } + + var output strings.Builder + if !verbose { + output.WriteString(fmt.Sprintf("%-15s | %-15s | %-15s | %-10s | %s\n", "Name", "Network", "Storage", "State", "IP Address")) + output.WriteString(strings.Repeat("-", 79) + "\n") + } for key, val := range a.Config.Services { - err := a.loadCurrentNodeDeplyments(a.getProjectName(key, a.Client.TwinID)) - if err != nil { + if err := a.loadCurrentNodeDeplyments(a.getProjectName(key, a.Client.TwinID)); err != nil { return err } + wl, dl, err := a.Client.State.GetWorkloadInDeployment(ctx, uint32(val.NodeID), key, key) if err != nil { return err } - log.Info().Str("deployment", string(wl.Name)).Msg("deployment") + vm, err := workloads.NewVMFromWorkload(&wl, &dl) + if err != nil { + return err + } + + addresses := getVmAddresses(vm) - s, err := json.MarshalIndent(dl, "", "\t") + s, err := json.MarshalIndent(dl, "", " ") if err != nil { - log.Fatal().Err(err).Send() + return err } - log.Info().Msg(string(s)) + if verbose { + if outputFile != "" { + output.WriteString(fmt.Sprintf("\"%s\": %s,\n", key, string(s))) + } else { + output.WriteString(fmt.Sprintf("deplyment: %s\n%s\n\n\n", key, string(s))) + } + } else { + var wl gridtypes.Workload + + for _, workload := range dl.Workloads { + if workload.Type == "zmachine" { + wl = workload + break + } + } + + var wlData pkg.WorkloadData + err = json.Unmarshal(wl.Data, &wlData) + if err != nil { + return err + } + + output.WriteString(fmt.Sprintf("%-15s | %-15s | %-15s | %-10s | %s \n", wl.Name, wlData.Network.Interfaces[0].Network, wlData.Mounts[0].Name, wl.Result.State, addresses)) + } + } + if outputFile != "" { + if err := os.WriteFile(outputFile, []byte(fmt.Sprintf("{\n%s\n}", strings.TrimSuffix(output.String(), ",\n"))), 0644); err != nil { + return err + } + return nil + } else { + // for better formatting + println("\n" + output.String()) } + return nil } +func parsePsFlags(flags *pflag.FlagSet) (bool, string, error) { + verbose, err := flags.GetBool("verbose") + if err != nil { + return verbose, "", err + } + + outputFile, err := flags.GetString("output") + if err != nil { + return verbose, outputFile, err + } + + return verbose, outputFile, nil +} + func (a *App) Down() error { for key := range a.Config.Services { projectName := a.getProjectName(key, a.Client.TwinID) @@ -223,17 +248,3 @@ func (a *App) checkIfExistAndAppend(node uint32, contractID uint64) { a.Client.State.CurrentNodeDeployments[node] = append(a.Client.State.CurrentNodeDeployments[node], contractID) } - -func loadConfigFromReader(configFile io.Reader) (*config.Config, error) { - content, err := io.ReadAll(configFile) - if err != nil { - return &config.Config{}, fmt.Errorf("failed to read file: %w", err) - } - - var config config.Config - if err := yaml.Unmarshal(content, &config); err != nil { - return &config, fmt.Errorf("failed to parse file: %w", err) - } - - return &config, nil -} diff --git a/grid-compose/internal/config/config.go b/grid-compose/internal/config/config.go index 92373ffff..188e0fccf 100644 --- a/grid-compose/internal/config/config.go +++ b/grid-compose/internal/config/config.go @@ -2,8 +2,10 @@ package config import ( "fmt" + "io" "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/pkg" + "gopkg.in/yaml.v2" ) type Config struct { @@ -58,3 +60,16 @@ func (c *Config) ValidateConfig() (err error) { return nil } + +func (c *Config) LoadConfigFromReader(configFile io.Reader) error { + content, err := io.ReadAll(configFile) + if err != nil { + return fmt.Errorf("failed to read file %w", err) + } + + if err := yaml.Unmarshal(content, &c); err != nil { + return fmt.Errorf("failed to parse file %w", err) + } + + return nil +} diff --git a/grid-compose/internal/utils.go b/grid-compose/internal/utils.go new file mode 100644 index 000000000..cba70065f --- /dev/null +++ b/grid-compose/internal/utils.go @@ -0,0 +1,73 @@ +package internal + +import ( + "fmt" + "strconv" + "strings" + + "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/pkg" +) + +func assignEnvs(vm *workloads.VM, envs []string) { + env := make(map[string]string, 0) + for _, envVar := range envs { + keyValuePair := strings.Split(envVar, "=") + env[keyValuePair[0]] = keyValuePair[1] + } + + vm.EnvVars = env +} + +func assignMounts(vm *workloads.VM, volumns []string, storage map[string]pkg.Storage) ([]workloads.Disk, error) { + var disks []workloads.Disk + mounts := make([]workloads.Mount, 0) + for _, volume := range volumns { + pair := strings.Split(volume, ":") + + storage := storage[pair[0]] + size, err := strconv.Atoi(strings.TrimSuffix(storage.Size, "GB")) + + if err != nil { + return nil, err + } + + disk := workloads.Disk{ + Name: pair[0], + SizeGB: size, + } + + disks = append(disks, disk) + + mounts = append(mounts, workloads.Mount{ + DiskName: disk.Name, + MountPoint: pair[1], + }) + } + vm.Mounts = mounts + + return disks, nil +} + +func assignNetworks(vm *workloads.VM, networks []string, networksConfig map[string]pkg.Network, network *workloads.ZNet) error { + for _, net := range networks { + switch networksConfig[net].Type { + case "wg": + network.AddWGAccess = true + case "ip4": + vm.PublicIP = true + case "ip6": + vm.PublicIP6 = true + case "ygg": + vm.Planetary = true + case "myc": + seed, err := getRandomMyceliumIPSeed() + if err != nil { + return fmt.Errorf("failed to get mycelium seed %w", err) + } + vm.MyceliumIPSeed = seed + } + } + + return nil +} diff --git a/grid-compose/internal/vmaddresses.go b/grid-compose/internal/vmaddresses.go index 8a5f720d3..0159e7b9f 100644 --- a/grid-compose/internal/vmaddresses.go +++ b/grid-compose/internal/vmaddresses.go @@ -2,28 +2,29 @@ package internal import ( "fmt" + "strings" "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" ) -func GetVmAddresses(vm workloads.VM) string { - var res string +func getVmAddresses(vm workloads.VM) string { + var addresses strings.Builder if vm.IP != "" { - res += fmt.Sprintf("\twireguard: %v\n", vm.IP) + addresses.WriteString(fmt.Sprintf("wireguard: %v, ", vm.IP)) } if vm.Planetary { - res += fmt.Sprintf("\tyggdrasil: %v\n", vm.PlanetaryIP) + addresses.WriteString(fmt.Sprintf("yggdrasil: %v, ", vm.PlanetaryIP)) } if vm.PublicIP { - res += fmt.Sprintf("\tpublicIp4: %v\n", vm.ComputedIP) + addresses.WriteString(fmt.Sprintf("publicIp4: %v, ", vm.ComputedIP)) } if vm.PublicIP6 { - res += fmt.Sprintf("\tpublicIp6: %v\n", vm.ComputedIP6) + addresses.WriteString(fmt.Sprintf("publicIp6: %v, ", vm.ComputedIP6)) } if len(vm.MyceliumIPSeed) != 0 { - res += fmt.Sprintf("\tmycelium: %v\n", vm.MyceliumIP) + addresses.WriteString(fmt.Sprintf("mycelium: %v, ", vm.MyceliumIP)) } - return res + return strings.TrimSuffix(addresses.String(), ", ") } diff --git a/grid-compose/pkg/types.go b/grid-compose/pkg/types.go index c71e88b20..9a4671a44 100644 --- a/grid-compose/pkg/types.go +++ b/grid-compose/pkg/types.go @@ -29,8 +29,31 @@ type HealthCheck struct { } type Resources struct { - CPU uint `yaml:"cpu"` - Memory uint `yaml:"memory"` - SSD uint `yaml:"ssd"` - HDD uint `yaml:"hdd"` + CPU uint `yaml:"cpu" json:"cpu"` + Memory uint `yaml:"memory" json:"memory"` + SSD uint `yaml:"ssd" json:"ssd"` + HDD uint `yaml:"hdd" json:"hdd"` +} + +type WorkloadData struct { + Flist string `json:"flist"` + Network Net `json:"network"` + ComputeCapacity Resources `json:"compute_capacity"` + Size int `json:"size"` + Mounts []struct { + Name string `json:"name"` + MountPoint string `json:"mountpoint"` + } `json:"mounts"` + Entrypoint string `json:"entrypoint"` + Env map[string]string `json:"env"` + Corex bool `json:"corex"` +} + +type Net struct { + PublicIP string `json:"public_ip"` + Planetary bool `json:"planetary"` + Interfaces []struct { + Network string `json:"network"` + IP string `json:"ip"` + } } From 9fcedd204996655eaa34b6c4d4d9e69964ad7e3a Mon Sep 17 00:00:00 2001 From: eyad-hussein Date: Mon, 5 Aug 2024 00:45:53 +0300 Subject: [PATCH 08/20] feat: implement deployment of two or more services on same network --- grid-compose/Makefile | 2 +- grid-compose/example/full_example_v2.yml | 79 +++++++------- grid-compose/example/full_example_v3.yml | 50 +++++++++ grid-compose/internal/app.go | 128 ++++++++++++++++------- grid-compose/internal/config/config.go | 25 ++--- grid-compose/internal/utils.go | 79 ++++++++++++-- grid-compose/pkg/types.go | 65 +++++++++--- 7 files changed, 309 insertions(+), 119 deletions(-) create mode 100644 grid-compose/example/full_example_v3.yml diff --git a/grid-compose/Makefile b/grid-compose/Makefile index 2d73eaa8c..b083ae919 100644 --- a/grid-compose/Makefile +++ b/grid-compose/Makefile @@ -1,6 +1,6 @@ BIN_PATH = bin/main -.PHONY: build +.PHONY: build run build: @go build -o $(BIN_PATH) ./main.go diff --git a/grid-compose/example/full_example_v2.yml b/grid-compose/example/full_example_v2.yml index f4fd1fcce..5154c7cfc 100644 --- a/grid-compose/example/full_example_v2.yml +++ b/grid-compose/example/full_example_v2.yml @@ -1,58 +1,57 @@ -# not all features here are implemented yet version: '1.0.0' -# cannot figure out how to run mycelium always error deployment not configured networks: net1: - type: 'wg' - net2: - type: 'myc' - net3: - type: 'ygg' - net4: - type: 'ip4' - net5: - type: 'ip6' + name: 'nettyy' + nodes: + - 11 + - 14 + - 144 + range: + ip: + type: ip4 + ip: 10.20.0.0 + mask: + type: cidr + mask: 16/32 + wg: true +project_name: 'example_project' services: - web: - flist: 'https://hub.grid.tf/tf-official-apps/nginx-latest.flist' - environment: - - ENV_VAR_NAME=value - volumes: - - web-data:/data + web5: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' networks: - net1 + network_types: + - ip4 + - ygg + node_id: 11 + entrypoint: '/sbin/zinit init' + volumes: + - webdata:/data resources: - cpu: 2 - memory: 2 - depends_on: - - database + cpu: 1 + memory: 2048 + disk: 25600 database: - flist: 'https://hub.grid.tf/tf-official-apps/postgresql-latest.flist' - environment: - - POSTGRES_DB=postgres - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=password - volumes: - - db-data:/var/lib/postgresql/data + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' networks: - net1 - - net2 - - net3 + network_types: + - ygg + volumes: + - dbdata:/var/lib/postgresql/data + node_id: 11 + entrypoint: '/sbin/zinit init' resources: - cpu: 4 - memory: 2 - healthcheck: - test: ['CMD', 'curl', 'http://{myceliumip}'] - interval: 1m30s - timeout: 10s - retries: 3 -# ask omar about the types zmount and all that + cpu: 1 + memory: 2048 + disk: 25600 + storage: - web-data: + webdata: type: 'zmount' size: 10GB - db-data: + dbdata: type: 'zmount' size: 10GB diff --git a/grid-compose/example/full_example_v3.yml b/grid-compose/example/full_example_v3.yml new file mode 100644 index 000000000..7594400ba --- /dev/null +++ b/grid-compose/example/full_example_v3.yml @@ -0,0 +1,50 @@ +version: '1.0.0' + +networks: + net1: + type: 'wg' + net2: + type: 'myc' + net3: + type: 'ygg' + net4: + type: 'ip4' + net5: + type: 'ip6' + +services: + web5: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + networks: + - net3 + - net4 + - net5 + node_id: 144 + entrypoint: '/sbin/zinit init' + volumes: + - webdata:/data + resources: + cpu: 1 + memory: 2048 + disk: 25600 + database: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + networks: + - net3 + - net1 + volumes: + - dbdata:/var/lib/postgresql/data + node_id: 14 + entrypoint: '/sbin/zinit init' + resources: + cpu: 1 + memory: 2048 + disk: 25600 + +storage: + webdata: + type: 'zmount' + size: 10GB + dbdata: + type: 'zmount' + size: 10GB diff --git a/grid-compose/internal/app.go b/grid-compose/internal/app.go index 333ebfd44..a91daaee9 100644 --- a/grid-compose/internal/app.go +++ b/grid-compose/internal/app.go @@ -13,8 +13,9 @@ import ( "github.com/spf13/pflag" "github.com/threefoldtech/tfgrid-sdk-go/grid-client/deployer" "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/config" - "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/pkg" + types "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/pkg" "github.com/threefoldtech/zos/pkg/gridtypes" ) @@ -53,21 +54,35 @@ func NewApp(net, mnemonic, configPath string) (*App, error) { } func (a *App) Up(ctx context.Context) error { - deployments := make(map[string]*workloads.Deployment, 0) + // deployments := make(map[string]*workloads.Deployment, 0) + networks := generateNetworks(a.Config.Networks) + networkDeploymentsMap := make(map[string]types.DeploymentData, 0) for key, val := range a.Config.Services { - projectName := a.getProjectName(key, a.Client.TwinID) - networkName := key + "net" - - network := workloads.ZNet{ - Name: networkName, - Nodes: []uint32{uint32(val.NodeID)}, - IPRange: gridtypes.NewIPNet(net.IPNet{ - IP: net.IPv4(10, 20, 0, 0), - Mask: net.CIDRMask(16, 32), - }), - SolutionType: projectName, + // network := workloads.ZNet{ + // Name: networkName, + // Nodes: []uint32{val.NodeID}, + // IPRange: gridtypes.NewIPNet(net.IPNet{ + // IP: net.IPv4(10, 20, 0, 0), + // Mask: net.CIDRMask(16, 32), + // }), + // SolutionType: projectName, + // } + + var network *workloads.ZNet + if val.Networks == nil || len(val.Networks) == 0 { + network = &workloads.ZNet{ + Name: key + "net", + Nodes: []uint32{val.NodeID}, + IPRange: gridtypes.NewIPNet(net.IPNet{ + IP: net.IPv4(10, 20, 0, 0), + Mask: net.CIDRMask(16, 32), + }), + } + } else { + network = networks[val.Networks[0]] } + network.SolutionType = a.Config.ProjectName vm := workloads.VM{ Name: key, @@ -75,7 +90,7 @@ func (a *App) Up(ctx context.Context) error { Entrypoint: val.Entrypoint, CPU: int(val.Resources.CPU), Memory: int(val.Resources.Memory), - NetworkName: networkName, + NetworkName: network.Name, } assignEnvs(&vm, val.Environment) @@ -85,37 +100,66 @@ func (a *App) Up(ctx context.Context) error { return fmt.Errorf("failed to assign mounts %w", err) } - if err := assignNetworks(&vm, val.Networks, a.Config.Networks, &network); err != nil { + if err := assignNetworksTypes(&vm, val.NetworkTypes); err != nil { return fmt.Errorf("failed to assign networks %w", err) } - if err := a.Client.NetworkDeployer.Deploy(context.Background(), &network); err != nil { - return err + // if err := a.Client.NetworkDeployer.Deploy(context.Background(), &network); err != nil { + // return err + // } + + // dl := workloads.NewDeployment(vm.Name, uint32(val.NodeID), projectName, nil, network.Name, disks, nil, nil, nil) + deploymentData := networkDeploymentsMap[network.Name] + + deploymentData.Vms = append(deploymentData.Vms, vm) + deploymentData.Disks = append(deploymentData.Disks, disks...) + if !checkIfNodeIDExist(val.NodeID, deploymentData.NodeIDs) { + deploymentData.NodeIDs = append(deploymentData.NodeIDs, val.NodeID) } + networkDeploymentsMap[network.Name] = deploymentData + } - dl := workloads.NewDeployment(vm.Name, uint32(val.NodeID), projectName, nil, networkName, disks, nil, []workloads.VM{vm}, nil) - if err := a.Client.DeploymentDeployer.Deploy(context.Background(), &dl); err != nil { - if err := a.Client.NetworkDeployer.Cancel(context.Background(), &network); err != nil { - return err - } + log.Info().Str("status", "started").Msg("deploying networks...") + for _, val := range networks { + if err := a.Client.NetworkDeployer.Deploy(ctx, val); err != nil { return err } - - deployments[dl.Name] = &dl } + log.Info().Str("status", "done").Msg("networks deployed successfully") + + for key, val := range networkDeploymentsMap { + for _, nodeID := range val.NodeIDs { + dlName := a.getDeploymentName() + log.Info().Str("deployment", dlName).Str("services", fmt.Sprintf("%v", val.Vms)).Msg("deploying...") + + dl := workloads.NewDeployment(dlName, nodeID, a.Config.ProjectName, nil, key, val.Disks, nil, val.Vms, nil) + if err := a.Client.DeploymentDeployer.Deploy(ctx, &dl); err != nil { + for _, val := range networks { + if err := a.Client.NetworkDeployer.Cancel(ctx, val); err != nil { + return err + } + } + return err + } - for name, dl := range deployments { - vmState, err := a.Client.State.LoadVMFromGrid(ctx, uint32(dl.NodeID), name, name) - if err != nil { - return fmt.Errorf("%w vm %s", err, name) + log.Info().Str("deployment", dlName).Msg("deployed successfully") } - - log.Info().Str("ip", vmState.IP).Str("vm name", name).Msg("vm deployed") } + log.Info().Msg("all services deployed successfully") return nil } +func checkIfNodeIDExist(nodeID uint32, nodes []uint32) bool { + for _, node := range nodes { + if node == nodeID { + return true + } + } + + return false +} + func (a *App) Ps(ctx context.Context, flags *pflag.FlagSet) error { verbose, outputFile, err := parsePsFlags(flags) if err != nil { @@ -128,7 +172,7 @@ func (a *App) Ps(ctx context.Context, flags *pflag.FlagSet) error { output.WriteString(strings.Repeat("-", 79) + "\n") } for key, val := range a.Config.Services { - if err := a.loadCurrentNodeDeplyments(a.getProjectName(key, a.Client.TwinID)); err != nil { + if err := a.loadCurrentNodeDeplyments(a.Config.ProjectName); err != nil { return err } @@ -165,7 +209,7 @@ func (a *App) Ps(ctx context.Context, flags *pflag.FlagSet) error { } } - var wlData pkg.WorkloadData + var wlData types.WorkloadData err = json.Unmarshal(wl.Data, &wlData) if err != nil { return err @@ -203,19 +247,23 @@ func parsePsFlags(flags *pflag.FlagSet) (bool, string, error) { } func (a *App) Down() error { - for key := range a.Config.Services { - projectName := a.getProjectName(key, a.Client.TwinID) - log.Info().Str("projectName", projectName).Msg("canceling deployments") - if err := a.Client.CancelByProjectName(projectName); err != nil { - return err - } + + projectName := a.Config.ProjectName + log.Info().Str("projectName", projectName).Msg("canceling deployments") + if err := a.Client.CancelByProjectName(projectName); err != nil { + return err } + return nil } -func (a *App) getProjectName(key string, twinId uint32) string { +func (a *App) getProjectName(key string) string { key = strings.TrimSuffix(key, "net") - return fmt.Sprintf("compose/%v/%v", twinId, key) + return fmt.Sprintf("compose/%v/%v", a.Client.TwinID, key) +} + +func (a *App) getDeploymentName() string { + return fmt.Sprintf("dl_%v_%v", a.Client.TwinID, generateRandString(5)) } func (a *App) loadCurrentNodeDeplyments(projectName string) error { diff --git a/grid-compose/internal/config/config.go b/grid-compose/internal/config/config.go index 188e0fccf..7c7bc0596 100644 --- a/grid-compose/internal/config/config.go +++ b/grid-compose/internal/config/config.go @@ -4,22 +4,23 @@ import ( "fmt" "io" - "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/pkg" - "gopkg.in/yaml.v2" + types "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/pkg" + "gopkg.in/yaml.v3" ) type Config struct { - Version string `yaml:"version"` - Networks map[string]pkg.Network `yaml:"networks"` - Services map[string]pkg.Service `yaml:"services"` - Storage map[string]pkg.Storage `yaml:"storage"` + Version string `yaml:"version"` + Networks map[string]types.Network `yaml:"networks"` + Services map[string]types.Service `yaml:"services"` + Storage map[string]types.Storage `yaml:"storage"` + ProjectName string `yaml:"project_name"` } func NewConfig() *Config { return &Config{ - Networks: make(map[string]pkg.Network), - Services: make(map[string]pkg.Service), - Storage: make(map[string]pkg.Storage), + Networks: make(map[string]types.Network), + Services: make(map[string]types.Service), + Storage: make(map[string]types.Storage), } } @@ -28,12 +29,6 @@ func (c *Config) ValidateConfig() (err error) { return ErrVersionNotSet } - for name, network := range c.Networks { - if network.Type == "" { - return fmt.Errorf("%w for network %s", ErrNetworkTypeNotSet, name) - } - } - for name, service := range c.Services { if service.Flist == "" { return fmt.Errorf("%w for service %s", ErrServiceFlistNotSet, name) diff --git a/grid-compose/internal/utils.go b/grid-compose/internal/utils.go index cba70065f..00f8542c9 100644 --- a/grid-compose/internal/utils.go +++ b/grid-compose/internal/utils.go @@ -2,11 +2,14 @@ package internal import ( "fmt" + mrand "math/rand" + "net" "strconv" "strings" "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" - "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/pkg" + types "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/pkg" + "github.com/threefoldtech/zos/pkg/gridtypes" ) func assignEnvs(vm *workloads.VM, envs []string) { @@ -19,7 +22,7 @@ func assignEnvs(vm *workloads.VM, envs []string) { vm.EnvVars = env } -func assignMounts(vm *workloads.VM, volumns []string, storage map[string]pkg.Storage) ([]workloads.Disk, error) { +func assignMounts(vm *workloads.VM, volumns []string, storage map[string]types.Storage) ([]workloads.Disk, error) { var disks []workloads.Disk mounts := make([]workloads.Mount, 0) for _, volume := range volumns { @@ -49,11 +52,9 @@ func assignMounts(vm *workloads.VM, volumns []string, storage map[string]pkg.Sto return disks, nil } -func assignNetworks(vm *workloads.VM, networks []string, networksConfig map[string]pkg.Network, network *workloads.ZNet) error { - for _, net := range networks { - switch networksConfig[net].Type { - case "wg": - network.AddWGAccess = true +func assignNetworksTypes(vm *workloads.VM, networksTypes []string) error { + for _, networkType := range networksTypes { + switch networkType { case "ip4": vm.PublicIP = true case "ip6": @@ -71,3 +72,67 @@ func assignNetworks(vm *workloads.VM, networks []string, networksConfig map[stri return nil } + +func generateIPNet(ip types.IP, mask types.IPMask) net.IPNet { + var ipNet net.IPNet + + switch ip.Type { + case "ipv4": + ipSplit := strings.Split(ip.IP, ".") + byte1, _ := strconv.Atoi(ipSplit[0]) + byte2, _ := strconv.Atoi(ipSplit[1]) + byte3, _ := strconv.Atoi(ipSplit[2]) + byte4, _ := strconv.Atoi(ipSplit[3]) + + ipNet.IP = net.IPv4(byte(byte1), byte(byte2), byte(byte3), byte(byte4)) + default: + return ipNet + } + + var maskIP net.IPMask + + switch mask.Type { + case "cidr": + maskSplit := strings.Split(mask.Mask, "/") + maskOnes, _ := strconv.Atoi(maskSplit[0]) + maskBits, _ := strconv.Atoi(maskSplit[1]) + maskIP = net.CIDRMask(maskOnes, maskBits) + default: + return ipNet + } + + ipNet.Mask = maskIP + + return ipNet +} + +func generateNetworks(networks map[string]types.Network) map[string]*workloads.ZNet { + zNets := make(map[string]*workloads.ZNet, 0) + + for key, network := range networks { + zNet := workloads.ZNet{ + Name: network.Name, + Description: network.Description, + Nodes: network.Nodes, + IPRange: gridtypes.NewIPNet(net.IPNet{ + IP: net.IPv4(10, 20, 0, 0), + Mask: net.CIDRMask(16, 32), + }), + AddWGAccess: network.AddWGAccess, + MyceliumKeys: network.MyceliumKeys, + } + + zNets[key] = &zNet + } + + return zNets +} + +func generateRandString(n int) string { + const letters = "abcdefghijklmnopqrstuvwxyz123456789" + b := make([]byte, n) + for i := range b { + b[i] = letters[mrand.Intn(len(letters))] + } + return string(b) +} diff --git a/grid-compose/pkg/types.go b/grid-compose/pkg/types.go index 9a4671a44..1cb40357f 100644 --- a/grid-compose/pkg/types.go +++ b/grid-compose/pkg/types.go @@ -1,6 +1,8 @@ -package pkg +package types -type Network struct { +import "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" + +type NetworkTypes struct { Type string `yaml:"type"` } @@ -10,15 +12,16 @@ type Storage struct { } type Service struct { - Flist string `yaml:"flist"` - Entrypoint string `yaml:"entrypoint,omitempty"` - Environment []string `yaml:"environment"` - Resources Resources `yaml:"resources"` - NodeID uint `yaml:"node_id"` - Volumes []string `yaml:"volumes"` - Networks []string `yaml:"networks"` - HealthCheck *HealthCheck `yaml:"healthcheck,omitempty"` - DependsOn []string `yaml:"depends_on,omitempty"` + Flist string `yaml:"flist"` + Entrypoint string `yaml:"entrypoint,omitempty"` + Environment []string `yaml:"environment"` + Resources Resources `yaml:"resources"` + NodeID uint32 `yaml:"node_id"` + Volumes []string `yaml:"volumes"` + NetworkTypes []string `yaml:"network_types"` + Networks []string `yaml:"networks"` + HealthCheck *HealthCheck `yaml:"healthcheck,omitempty"` + DependsOn []string `yaml:"depends_on,omitempty"` } type HealthCheck struct { @@ -36,10 +39,10 @@ type Resources struct { } type WorkloadData struct { - Flist string `json:"flist"` - Network Net `json:"network"` - ComputeCapacity Resources `json:"compute_capacity"` - Size int `json:"size"` + Flist string `json:"flist"` + Network WorkloadDataNetwork `json:"network"` + ComputeCapacity Resources `json:"compute_capacity"` + Size int `json:"size"` Mounts []struct { Name string `json:"name"` MountPoint string `json:"mountpoint"` @@ -49,7 +52,7 @@ type WorkloadData struct { Corex bool `json:"corex"` } -type Net struct { +type WorkloadDataNetwork struct { PublicIP string `json:"public_ip"` Planetary bool `json:"planetary"` Interfaces []struct { @@ -57,3 +60,33 @@ type Net struct { IP string `json:"ip"` } } + +type Network struct { + Name string `yaml:"name"` + Description string `yaml:"description"` + Nodes []uint32 `yaml:"nodes"` + IPRange IPNet `yaml:"range"` + AddWGAccess bool `yaml:"wg"` + MyceliumKeys map[uint32][]byte `yaml:"mycelium_keys"` +} + +type IPNet struct { + IP IP `yaml:"ip"` + Mask IPMask `yaml:"mask"` +} + +type IP struct { + Type string `yaml:"type"` + IP string `yaml:"ip"` +} + +type IPMask struct { + Type string `yaml:"type"` + Mask string `yaml:"mask"` +} + +type DeploymentData struct { + Vms []workloads.VM + Disks []workloads.Disk + NodeIDs []uint32 +} From 09d55e21a4426f78e9401c383e5c06402783bf32 Mon Sep 17 00:00:00 2001 From: eyad-hussein Date: Tue, 6 Aug 2024 17:01:24 +0300 Subject: [PATCH 09/20] exmple: add more example files to cover test cases --- grid-compose/example copy/full_example.yml | 74 ++++++ .../multiple_services_diff_network_1.yml | 74 ++++++ .../multiple_services_diff_network_2.yml | 86 +++++++ .../multiple_services_same_network_1.yml | 63 +++++ .../multiple_services_same_network_2.yml | 62 +++++ .../multiple_services_same_network_3.yml | 61 +++++ .../example copy/single_service_1.yml | 10 + .../example copy/single_service_2.yml | 11 + .../two_services_same_network_1.yml} | 44 ++-- .../two_services_same_network_2.yml | 49 ++++ .../two_services_same_network_3.yml} | 46 ++-- grid-compose/example/full_example.yml | 100 ++++---- .../multiple_services_diff_network_1.yml | 70 ++++++ .../multiple_services_diff_network_2.yml | 84 +++++++ .../multiple_services_diff_network_3.yml | 82 +++++++ grid-compose/example/single_service_1.yml | 10 + grid-compose/example/single_service_2.yml | 11 + grid-compose/example/single_vm.yml | 14 -- .../example/two_services_same_network_1.yml | 49 ++++ .../example/two_services_same_network_2.yml | 36 +++ .../example/two_services_same_network_3.yml | 35 +++ grid-compose/internal/app.go | 218 ++++++++++-------- grid-compose/internal/config/config.go | 37 ++- grid-compose/{pkg => internal}/types.go | 26 ++- grid-compose/internal/utils.go | 53 +---- 25 files changed, 1136 insertions(+), 269 deletions(-) create mode 100644 grid-compose/example copy/full_example.yml create mode 100644 grid-compose/example copy/multiple_services_diff_network_1.yml create mode 100644 grid-compose/example copy/multiple_services_diff_network_2.yml create mode 100644 grid-compose/example copy/multiple_services_same_network_1.yml create mode 100644 grid-compose/example copy/multiple_services_same_network_2.yml create mode 100644 grid-compose/example copy/multiple_services_same_network_3.yml create mode 100644 grid-compose/example copy/single_service_1.yml create mode 100644 grid-compose/example copy/single_service_2.yml rename grid-compose/{example/full_example_v2.yml => example copy/two_services_same_network_1.yml} (57%) create mode 100644 grid-compose/example copy/two_services_same_network_2.yml rename grid-compose/{example/full_example_v3.yml => example copy/two_services_same_network_3.yml} (53%) create mode 100644 grid-compose/example/multiple_services_diff_network_1.yml create mode 100644 grid-compose/example/multiple_services_diff_network_2.yml create mode 100644 grid-compose/example/multiple_services_diff_network_3.yml create mode 100644 grid-compose/example/single_service_1.yml create mode 100644 grid-compose/example/single_service_2.yml delete mode 100644 grid-compose/example/single_vm.yml create mode 100644 grid-compose/example/two_services_same_network_1.yml create mode 100644 grid-compose/example/two_services_same_network_2.yml create mode 100644 grid-compose/example/two_services_same_network_3.yml rename grid-compose/{pkg => internal}/types.go (83%) diff --git a/grid-compose/example copy/full_example.yml b/grid-compose/example copy/full_example.yml new file mode 100644 index 000000000..596668be2 --- /dev/null +++ b/grid-compose/example copy/full_example.yml @@ -0,0 +1,74 @@ +version: '1.0.0' + +networks: + net1: + name: 'miaminet' + nodes: + - 14 + - 144 + range: + ip: + type: ipv4 + ip: 10.20.0.0 + mask: + type: cidr + mask: 16/32 + wg: true + net2: + name: 'cartoonnetwork' + node_id: 144 + range: + ip: + type: ipv4 + ip: 10.20.0.0 + mask: + type: cidr + mask: 16/32 + +services: + server: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + - ygg + entrypoint: '/sbin/zinit init' + volumes: + - webdata + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + network: net1 + node_id: 14 + frontend: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + entrypoint: '/sbin/zinit init' + volumes: + - dbdata + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + network: net1 + database: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + entrypoint: '/sbin/zinit init' + volumes: + - dbdata + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + network: net2 + +volumes: + webdata: + mountpoint: '/data' + size: 10GB + dbdata: + mountpoint: '/var/lib/postgresql/data' + size: 10GB diff --git a/grid-compose/example copy/multiple_services_diff_network_1.yml b/grid-compose/example copy/multiple_services_diff_network_1.yml new file mode 100644 index 000000000..b7f1d0146 --- /dev/null +++ b/grid-compose/example copy/multiple_services_diff_network_1.yml @@ -0,0 +1,74 @@ +version: '1.0.0' + +networks: + net1: + name: 'miaminet' + nodes: + - 14 + - 144 + range: + ip: + type: ipv4 + ip: 10.20.0.0 + mask: + type: cidr + mask: 16/32 + wg: true + net2: + name: 'cartoonnetwork' + node_id: 144 + range: + ip: + type: ipv4 + ip: 10.20.0.0 + mask: + type: cidr + mask: 16/32 + +services: + server: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + - ygg + entrypoint: '/sbin/zinit init' + volumes: + - webdata + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + network: net2 + frontend: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + entrypoint: '/sbin/zinit init' + volumes: + - dbdata + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + network: net1 + node_id: 144 + database: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + entrypoint: '/sbin/zinit init' + volumes: + - dbdata + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + network: net2 + +volumes: + webdata: + mountpoint: '/data' + size: 10GB + dbdata: + mountpoint: '/var/lib/postgresql/data' + size: 10GB diff --git a/grid-compose/example copy/multiple_services_diff_network_2.yml b/grid-compose/example copy/multiple_services_diff_network_2.yml new file mode 100644 index 000000000..c3b6f8eb6 --- /dev/null +++ b/grid-compose/example copy/multiple_services_diff_network_2.yml @@ -0,0 +1,86 @@ +version: '1.0.0' + +networks: + net1: + name: 'miaminet' + nodes: + - 14 + - 144 + range: + ip: + type: ipv4 + ip: 10.20.0.0 + mask: + type: cidr + mask: 16/32 + wg: true + net2: + name: 'cartoonnetwork' + node_id: 144 + range: + ip: + type: ipv4 + ip: 10.20.0.0 + mask: + type: cidr + mask: 16/32 + +services: + server: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + - ygg + entrypoint: '/sbin/zinit init' + volumes: + - webdata + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + network: net2 + server2: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + - ygg + entrypoint: '/sbin/zinit init' + volumes: + - webdata + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + network: net1 + frontend: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + entrypoint: '/sbin/zinit init' + volumes: + - dbdata + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + network: net1 + database: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + entrypoint: '/sbin/zinit init' + volumes: + - dbdata + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + network: net2 + +volumes: + webdata: + mountpoint: '/data' + size: 10GB + dbdata: + mountpoint: '/var/lib/postgresql/data' + size: 10GB diff --git a/grid-compose/example copy/multiple_services_same_network_1.yml b/grid-compose/example copy/multiple_services_same_network_1.yml new file mode 100644 index 000000000..aef546753 --- /dev/null +++ b/grid-compose/example copy/multiple_services_same_network_1.yml @@ -0,0 +1,63 @@ +version: '1.0.0' + +networks: + net1: + name: 'miaminet' + range: + ip: + type: ipv4 + ip: 10.20.0.0 + mask: + type: cidr + mask: 16/32 + wg: true + +services: + server: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + - ygg + entrypoint: '/sbin/zinit init' + volumes: + - webdata + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + network: net1 + node_id: 14 + frontend: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + entrypoint: '/sbin/zinit init' + volumes: + - dbdata + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + network: net1 + node_id: 14 + database: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + entrypoint: '/sbin/zinit init' + volumes: + - dbdata + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + network: net1 + node_id: 14 + +volumes: + webdata: + mountpoint: '/data' + size: 10GB + dbdata: + mountpoint: '/var/lib/postgresql/data' + size: 10GB diff --git a/grid-compose/example copy/multiple_services_same_network_2.yml b/grid-compose/example copy/multiple_services_same_network_2.yml new file mode 100644 index 000000000..fe1016b90 --- /dev/null +++ b/grid-compose/example copy/multiple_services_same_network_2.yml @@ -0,0 +1,62 @@ +version: '1.0.0' + +networks: + net1: + name: 'miaminet' + range: + ip: + type: ipv4 + ip: 10.20.0.0 + mask: + type: cidr + mask: 16/32 + wg: true + +services: + server: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + - ygg + entrypoint: '/sbin/zinit init' + volumes: + - webdata + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + network: net1 + node_id: 14 + frontend: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + entrypoint: '/sbin/zinit init' + volumes: + - dbdata + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + network: net1 + node_id: 14 + database: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + entrypoint: '/sbin/zinit init' + volumes: + - dbdata + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + network: net1 + +volumes: + webdata: + mountpoint: '/data' + size: 10GB + dbdata: + mountpoint: '/var/lib/postgresql/data' + size: 10GB diff --git a/grid-compose/example copy/multiple_services_same_network_3.yml b/grid-compose/example copy/multiple_services_same_network_3.yml new file mode 100644 index 000000000..8ac46fcd4 --- /dev/null +++ b/grid-compose/example copy/multiple_services_same_network_3.yml @@ -0,0 +1,61 @@ +version: '1.0.0' + +networks: + net1: + name: 'miaminet' + node_id: 14 + range: + ip: + type: ipv4 + ip: 10.20.0.0 + mask: + type: cidr + mask: 16/32 + wg: true + +services: + server: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + - ygg + entrypoint: '/sbin/zinit init' + volumes: + - webdata + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + network: net1 + frontend: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + entrypoint: '/sbin/zinit init' + volumes: + - dbdata + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + network: net1 + database: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + entrypoint: '/sbin/zinit init' + volumes: + - dbdata + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + network: net1 + +volumes: + webdata: + mountpoint: '/data' + size: 10GB + dbdata: + mountpoint: '/var/lib/postgresql/data' + size: 10GB diff --git a/grid-compose/example copy/single_service_1.yml b/grid-compose/example copy/single_service_1.yml new file mode 100644 index 000000000..fdc031cce --- /dev/null +++ b/grid-compose/example copy/single_service_1.yml @@ -0,0 +1,10 @@ +version: '1.0.0' + +services: + server: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + entrypoint: '/sbin/zinit init' + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 diff --git a/grid-compose/example copy/single_service_2.yml b/grid-compose/example copy/single_service_2.yml new file mode 100644 index 000000000..7bba8e257 --- /dev/null +++ b/grid-compose/example copy/single_service_2.yml @@ -0,0 +1,11 @@ +version: '1.0.0' + +services: + server: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + entrypoint: '/sbin/zinit init' + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + node_id: 14 diff --git a/grid-compose/example/full_example_v2.yml b/grid-compose/example copy/two_services_same_network_1.yml similarity index 57% rename from grid-compose/example/full_example_v2.yml rename to grid-compose/example copy/two_services_same_network_1.yml index 5154c7cfc..77da823f4 100644 --- a/grid-compose/example/full_example_v2.yml +++ b/grid-compose/example copy/two_services_same_network_1.yml @@ -2,56 +2,48 @@ version: '1.0.0' networks: net1: - name: 'nettyy' - nodes: - - 11 - - 14 - - 144 + name: 'miaminet' + node_id: 14 range: ip: - type: ip4 + type: ipv4 ip: 10.20.0.0 mask: type: cidr mask: 16/32 wg: true -project_name: 'example_project' services: - web5: + server: flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - networks: - - net1 - network_types: - - ip4 + ip_types: + - ipv4 - ygg - node_id: 11 entrypoint: '/sbin/zinit init' volumes: - - webdata:/data + - webdata resources: cpu: 1 memory: 2048 - disk: 25600 + rootfs: 25600 + network: net1 database: flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - networks: - - net1 - network_types: - - ygg - volumes: - - dbdata:/var/lib/postgresql/data - node_id: 11 + ip_types: + - ipv4 entrypoint: '/sbin/zinit init' + volumes: + - dbdata resources: cpu: 1 memory: 2048 - disk: 25600 + rootfs: 25600 + network: net1 -storage: +volumes: webdata: - type: 'zmount' + mountpoint: '/data' size: 10GB dbdata: - type: 'zmount' + mountpoint: '/var/lib/postgresql/data' size: 10GB diff --git a/grid-compose/example copy/two_services_same_network_2.yml b/grid-compose/example copy/two_services_same_network_2.yml new file mode 100644 index 000000000..4fddb8f9b --- /dev/null +++ b/grid-compose/example copy/two_services_same_network_2.yml @@ -0,0 +1,49 @@ +version: '1.0.0' + +networks: + net1: + name: 'miaminet' + range: + ip: + type: ipv4 + ip: 10.20.0.0 + mask: + type: cidr + mask: 16/32 + wg: true + +services: + server: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + - ygg + entrypoint: '/sbin/zinit init' + volumes: + - webdata + node_id: 14 + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + network: net1 + database: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + entrypoint: '/sbin/zinit init' + volumes: + - dbdata + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + network: net1 + +volumes: + webdata: + mountpoint: '/data' + size: 10GB + dbdata: + mountpoint: '/var/lib/postgresql/data' + size: 10GB diff --git a/grid-compose/example/full_example_v3.yml b/grid-compose/example copy/two_services_same_network_3.yml similarity index 53% rename from grid-compose/example/full_example_v3.yml rename to grid-compose/example copy/two_services_same_network_3.yml index 7594400ba..dd437249e 100644 --- a/grid-compose/example/full_example_v3.yml +++ b/grid-compose/example copy/two_services_same_network_3.yml @@ -1,50 +1,36 @@ version: '1.0.0' -networks: - net1: - type: 'wg' - net2: - type: 'myc' - net3: - type: 'ygg' - net4: - type: 'ip4' - net5: - type: 'ip6' - services: - web5: + server: flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - networks: - - net3 - - net4 - - net5 - node_id: 144 + ip_types: + - ipv4 + - ygg entrypoint: '/sbin/zinit init' volumes: - - webdata:/data + - webdata + node_id: 14 resources: cpu: 1 memory: 2048 - disk: 25600 + rootfs: 25600 + database: flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - networks: - - net3 - - net1 - volumes: - - dbdata:/var/lib/postgresql/data - node_id: 14 + ip_types: + - ipv4 entrypoint: '/sbin/zinit init' + volumes: + - dbdata resources: cpu: 1 memory: 2048 - disk: 25600 + rootfs: 25600 -storage: +volumes: webdata: - type: 'zmount' + mountpoint: '/data' size: 10GB dbdata: - type: 'zmount' + mountpoint: '/var/lib/postgresql/data' size: 10GB diff --git a/grid-compose/example/full_example.yml b/grid-compose/example/full_example.yml index f4fd1fcce..a815a1450 100644 --- a/grid-compose/example/full_example.yml +++ b/grid-compose/example/full_example.yml @@ -1,58 +1,70 @@ -# not all features here are implemented yet version: '1.0.0' -# cannot figure out how to run mycelium always error deployment not configured networks: net1: - type: 'wg' + name: 'miaminet' + range: + ip: + type: ipv4 + ip: 10.20.0.0 + mask: + type: cidr + mask: 16/32 + wg: true net2: - type: 'myc' - net3: - type: 'ygg' - net4: - type: 'ip4' - net5: - type: 'ip6' + name: 'cartoonnetwork' + range: + ip: + type: ipv4 + ip: 10.20.0.0 + mask: + type: cidr + mask: 16/32 services: - web: - flist: 'https://hub.grid.tf/tf-official-apps/nginx-latest.flist' - environment: - - ENV_VAR_NAME=value + server: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + - ygg + entrypoint: '/sbin/zinit init' volumes: - - web-data:/data - networks: - - net1 + - webdata resources: - cpu: 2 - memory: 2 - depends_on: - - database + cpu: 1 + memory: 2048 + rootfs: 25600 + network: net1 + node_id: 14 + frontend: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + entrypoint: '/sbin/zinit init' + volumes: + - dbdata + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + network: net1 database: - flist: 'https://hub.grid.tf/tf-official-apps/postgresql-latest.flist' - environment: - - POSTGRES_DB=postgres - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=password + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + entrypoint: '/sbin/zinit init' volumes: - - db-data:/var/lib/postgresql/data - networks: - - net1 - - net2 - - net3 + - dbdata resources: - cpu: 4 - memory: 2 - healthcheck: - test: ['CMD', 'curl', 'http://{myceliumip}'] - interval: 1m30s - timeout: 10s - retries: 3 -# ask omar about the types zmount and all that -storage: - web-data: - type: 'zmount' + cpu: 1 + memory: 2048 + rootfs: 25600 + network: net2 + +volumes: + webdata: + mountpoint: '/data' size: 10GB - db-data: - type: 'zmount' + dbdata: + mountpoint: '/var/lib/postgresql/data' size: 10GB diff --git a/grid-compose/example/multiple_services_diff_network_1.yml b/grid-compose/example/multiple_services_diff_network_1.yml new file mode 100644 index 000000000..54293dbdf --- /dev/null +++ b/grid-compose/example/multiple_services_diff_network_1.yml @@ -0,0 +1,70 @@ +version: '1.0.0' + +networks: + net1: + name: 'miaminet' + range: + ip: + type: ipv4 + ip: 10.20.0.0 + mask: + type: cidr + mask: 16/32 + wg: true + net2: + name: 'cartoonnetwork' + range: + ip: + type: ipv4 + ip: 10.20.0.0 + mask: + type: cidr + mask: 16/32 + +services: + server: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + - ygg + entrypoint: '/sbin/zinit init' + volumes: + - webdata + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + network: net2 + frontend: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + entrypoint: '/sbin/zinit init' + volumes: + - dbdata + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + network: net1 + node_id: 144 + database: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + entrypoint: '/sbin/zinit init' + volumes: + - dbdata + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + network: net2 + +volumes: + webdata: + mountpoint: '/data' + size: 10GB + dbdata: + mountpoint: '/var/lib/postgresql/data' + size: 10GB diff --git a/grid-compose/example/multiple_services_diff_network_2.yml b/grid-compose/example/multiple_services_diff_network_2.yml new file mode 100644 index 000000000..ca35be864 --- /dev/null +++ b/grid-compose/example/multiple_services_diff_network_2.yml @@ -0,0 +1,84 @@ +version: '1.0.0' + +networks: + net1: + name: 'miaminet' + range: + ip: + type: ipv4 + ip: 10.20.0.0 + mask: + type: cidr + mask: 16/32 + wg: true + net2: + name: 'cartoonnetwork' + range: + ip: + type: ipv4 + ip: 10.20.0.0 + mask: + type: cidr + mask: 16/32 + +services: + server: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + - ygg + entrypoint: '/sbin/zinit init' + volumes: + - webdata + node_id: 144 + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + network: net2 + server2: + node_id: 14 + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + - ygg + entrypoint: '/sbin/zinit init' + volumes: + - webdata + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + network: net1 + frontend: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + entrypoint: '/sbin/zinit init' + volumes: + - dbdata + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + network: net1 + database: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + entrypoint: '/sbin/zinit init' + volumes: + - dbdata + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + network: net2 + +volumes: + webdata: + mountpoint: '/data' + size: 10GB + dbdata: + mountpoint: '/var/lib/postgresql/data' + size: 10GB diff --git a/grid-compose/example/multiple_services_diff_network_3.yml b/grid-compose/example/multiple_services_diff_network_3.yml new file mode 100644 index 000000000..055e4723c --- /dev/null +++ b/grid-compose/example/multiple_services_diff_network_3.yml @@ -0,0 +1,82 @@ +version: '1.0.0' + +networks: + net1: + name: 'miaminet' + range: + ip: + type: ipv4 + ip: 10.20.0.0 + mask: + type: cidr + mask: 16/32 + wg: true + net2: + name: 'cartoonnetwork' + range: + ip: + type: ipv4 + ip: 10.20.0.0 + mask: + type: cidr + mask: 16/32 + +services: + server: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + - ygg + entrypoint: '/sbin/zinit init' + volumes: + - webdata + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + network: net2 + server2: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + - ygg + entrypoint: '/sbin/zinit init' + volumes: + - webdata + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + network: net1 + frontend: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + entrypoint: '/sbin/zinit init' + volumes: + - dbdata + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + network: net1 + database: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + entrypoint: '/sbin/zinit init' + volumes: + - dbdata + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + network: net2 + +volumes: + webdata: + mountpoint: '/data' + size: 10GB + dbdata: + mountpoint: '/var/lib/postgresql/data' + size: 10GB diff --git a/grid-compose/example/single_service_1.yml b/grid-compose/example/single_service_1.yml new file mode 100644 index 000000000..fdc031cce --- /dev/null +++ b/grid-compose/example/single_service_1.yml @@ -0,0 +1,10 @@ +version: '1.0.0' + +services: + server: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + entrypoint: '/sbin/zinit init' + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 diff --git a/grid-compose/example/single_service_2.yml b/grid-compose/example/single_service_2.yml new file mode 100644 index 000000000..7bba8e257 --- /dev/null +++ b/grid-compose/example/single_service_2.yml @@ -0,0 +1,11 @@ +version: '1.0.0' + +services: + server: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + entrypoint: '/sbin/zinit init' + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + node_id: 14 diff --git a/grid-compose/example/single_vm.yml b/grid-compose/example/single_vm.yml deleted file mode 100644 index 66d2e8cbc..000000000 --- a/grid-compose/example/single_vm.yml +++ /dev/null @@ -1,14 +0,0 @@ -version: '1.0.0' - -services: - new_single_vm: - flist: 'https://hub.grid.tf/omarabdulaziz.3bot/ubuntu-jammy.flist' - entrypoint: '/sbin/init' - environment: - - SSH_KEY=... - resources: - cpu: 1 - memory: 512 - node_id: 14 - networks: - - yggdrasil diff --git a/grid-compose/example/two_services_same_network_1.yml b/grid-compose/example/two_services_same_network_1.yml new file mode 100644 index 000000000..4fddb8f9b --- /dev/null +++ b/grid-compose/example/two_services_same_network_1.yml @@ -0,0 +1,49 @@ +version: '1.0.0' + +networks: + net1: + name: 'miaminet' + range: + ip: + type: ipv4 + ip: 10.20.0.0 + mask: + type: cidr + mask: 16/32 + wg: true + +services: + server: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + - ygg + entrypoint: '/sbin/zinit init' + volumes: + - webdata + node_id: 14 + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + network: net1 + database: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + entrypoint: '/sbin/zinit init' + volumes: + - dbdata + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + network: net1 + +volumes: + webdata: + mountpoint: '/data' + size: 10GB + dbdata: + mountpoint: '/var/lib/postgresql/data' + size: 10GB diff --git a/grid-compose/example/two_services_same_network_2.yml b/grid-compose/example/two_services_same_network_2.yml new file mode 100644 index 000000000..dd437249e --- /dev/null +++ b/grid-compose/example/two_services_same_network_2.yml @@ -0,0 +1,36 @@ +version: '1.0.0' + +services: + server: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + - ygg + entrypoint: '/sbin/zinit init' + volumes: + - webdata + node_id: 14 + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + + database: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + entrypoint: '/sbin/zinit init' + volumes: + - dbdata + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + +volumes: + webdata: + mountpoint: '/data' + size: 10GB + dbdata: + mountpoint: '/var/lib/postgresql/data' + size: 10GB diff --git a/grid-compose/example/two_services_same_network_3.yml b/grid-compose/example/two_services_same_network_3.yml new file mode 100644 index 000000000..58e70850e --- /dev/null +++ b/grid-compose/example/two_services_same_network_3.yml @@ -0,0 +1,35 @@ +version: '1.0.0' + +services: + server: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + - ygg + entrypoint: '/sbin/zinit init' + volumes: + - webdata + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + + database: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + entrypoint: '/sbin/zinit init' + volumes: + - dbdata + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + +volumes: + webdata: + mountpoint: '/data' + size: 10GB + dbdata: + mountpoint: '/var/lib/postgresql/data' + size: 10GB diff --git a/grid-compose/internal/app.go b/grid-compose/internal/app.go index a91daaee9..2f7941496 100644 --- a/grid-compose/internal/app.go +++ b/grid-compose/internal/app.go @@ -54,35 +54,28 @@ func NewApp(net, mnemonic, configPath string) (*App, error) { } func (a *App) Up(ctx context.Context) error { - // deployments := make(map[string]*workloads.Deployment, 0) - networks := generateNetworks(a.Config.Networks) - networkDeploymentsMap := make(map[string]types.DeploymentData, 0) + networks := a.generateNetworks() + deployments := a.generateInitDeployments() for key, val := range a.Config.Services { - // network := workloads.ZNet{ - // Name: networkName, - // Nodes: []uint32{val.NodeID}, - // IPRange: gridtypes.NewIPNet(net.IPNet{ - // IP: net.IPv4(10, 20, 0, 0), - // Mask: net.CIDRMask(16, 32), - // }), - // SolutionType: projectName, - // } + deployment := deployments[val.DeployTo] var network *workloads.ZNet - if val.Networks == nil || len(val.Networks) == 0 { + if deployment.Name == "" { network = &workloads.ZNet{ - Name: key + "net", - Nodes: []uint32{val.NodeID}, + Name: deployment.Name + "net", + Nodes: []uint32{deployment.NodeID}, IPRange: gridtypes.NewIPNet(net.IPNet{ IP: net.IPv4(10, 20, 0, 0), Mask: net.CIDRMask(16, 32), }), } + deployment.NetworkName = network.Name } else { - network = networks[val.Networks[0]] + network = networks[a.Config.Deployments[val.DeployTo].Network.Name] } - network.SolutionType = a.Config.ProjectName + + network.SolutionType = a.getProjectName(deployment.Name) vm := workloads.VM{ Name: key, @@ -91,6 +84,7 @@ func (a *App) Up(ctx context.Context) error { CPU: int(val.Resources.CPU), Memory: int(val.Resources.Memory), NetworkName: network.Name, + RootfsSize: int(val.Resources.Rootfs), } assignEnvs(&vm, val.Environment) @@ -104,19 +98,8 @@ func (a *App) Up(ctx context.Context) error { return fmt.Errorf("failed to assign networks %w", err) } - // if err := a.Client.NetworkDeployer.Deploy(context.Background(), &network); err != nil { - // return err - // } - - // dl := workloads.NewDeployment(vm.Name, uint32(val.NodeID), projectName, nil, network.Name, disks, nil, nil, nil) - deploymentData := networkDeploymentsMap[network.Name] - - deploymentData.Vms = append(deploymentData.Vms, vm) - deploymentData.Disks = append(deploymentData.Disks, disks...) - if !checkIfNodeIDExist(val.NodeID, deploymentData.NodeIDs) { - deploymentData.NodeIDs = append(deploymentData.NodeIDs, val.NodeID) - } - networkDeploymentsMap[network.Name] = deploymentData + deployment.Vms = append(deployment.Vms, vm) + deployment.Disks = append(deployment.Disks, disks...) } log.Info().Str("status", "started").Msg("deploying networks...") @@ -127,39 +110,24 @@ func (a *App) Up(ctx context.Context) error { } log.Info().Str("status", "done").Msg("networks deployed successfully") - for key, val := range networkDeploymentsMap { - for _, nodeID := range val.NodeIDs { - dlName := a.getDeploymentName() - log.Info().Str("deployment", dlName).Str("services", fmt.Sprintf("%v", val.Vms)).Msg("deploying...") - - dl := workloads.NewDeployment(dlName, nodeID, a.Config.ProjectName, nil, key, val.Disks, nil, val.Vms, nil) - if err := a.Client.DeploymentDeployer.Deploy(ctx, &dl); err != nil { - for _, val := range networks { - if err := a.Client.NetworkDeployer.Cancel(ctx, val); err != nil { - return err - } + for key, val := range deployments { + log.Info().Str("deployment", key).Msg("deploying...") + + if err := a.Client.DeploymentDeployer.Deploy(ctx, val); err != nil { + for _, val := range networks { + if err := a.Client.NetworkDeployer.Cancel(ctx, val); err != nil { + return err } - return err } - - log.Info().Str("deployment", dlName).Msg("deployed successfully") + return err } + log.Info().Str("deployment", key).Msg("deployed successfully") } log.Info().Msg("all services deployed successfully") return nil } -func checkIfNodeIDExist(nodeID uint32, nodes []uint32) bool { - for _, node := range nodes { - if node == nodeID { - return true - } - } - - return false -} - func (a *App) Ps(ctx context.Context, flags *pflag.FlagSet) error { verbose, outputFile, err := parsePsFlags(flags) if err != nil { @@ -168,44 +136,61 @@ func (a *App) Ps(ctx context.Context, flags *pflag.FlagSet) error { var output strings.Builder if !verbose { - output.WriteString(fmt.Sprintf("%-15s | %-15s | %-15s | %-10s | %s\n", "Name", "Network", "Storage", "State", "IP Address")) + output.WriteString(fmt.Sprintf("%-15s | %-15s | %-15s | %-15s | %-10s | %s\n", "Deployment Name", "Network", "Service Name", "Storage", "State", "IP Address")) output.WriteString(strings.Repeat("-", 79) + "\n") } - for key, val := range a.Config.Services { - if err := a.loadCurrentNodeDeplyments(a.Config.ProjectName); err != nil { - return err - } - wl, dl, err := a.Client.State.GetWorkloadInDeployment(ctx, uint32(val.NodeID), key, key) - if err != nil { - return err + outputMap := make(map[string]struct { + Deployment types.Deployment + Workloads []struct { + Workload gridtypes.Workload + WorkloadData types.WorkloadData + Addresses string } + }) - vm, err := workloads.NewVMFromWorkload(&wl, &dl) - if err != nil { + for _, deployment := range a.Config.Deployments { + if err := a.loadCurrentNodeDeployments(a.getProjectName(deployment.Name)); err != nil { return err } - addresses := getVmAddresses(vm) - - s, err := json.MarshalIndent(dl, "", " ") - if err != nil { - return err + outputMap[deployment.Name] = struct { + Deployment types.Deployment + Workloads []struct { + Workload gridtypes.Workload + WorkloadData types.WorkloadData + Addresses string + } + }{ + Deployment: deployment, + Workloads: []struct { + Workload gridtypes.Workload + WorkloadData types.WorkloadData + Addresses string + }{}, } - if verbose { - if outputFile != "" { - output.WriteString(fmt.Sprintf("\"%s\": %s,\n", key, string(s))) - } else { - output.WriteString(fmt.Sprintf("deplyment: %s\n%s\n\n\n", key, string(s))) + for _, workloadName := range deployment.Workloads { + wlStruct := struct { + Workload gridtypes.Workload + WorkloadData types.WorkloadData + Addresses string + }{ + Workload: gridtypes.Workload{}, + WorkloadData: types.WorkloadData{}, + } + + wl, dl, err := a.Client.State.GetWorkloadInDeployment(ctx, deployment.NodeID, workloadName, deployment.Name) + if err != nil { + return err } - } else { - var wl gridtypes.Workload - for _, workload := range dl.Workloads { - if workload.Type == "zmachine" { - wl = workload - break + wlStruct.Workload = wl + + if wl.Type == "zmachine" { + err = json.Unmarshal(wl.Data, &wlStruct.WorkloadData) + if err != nil { + return err } } @@ -215,7 +200,27 @@ func (a *App) Ps(ctx context.Context, flags *pflag.FlagSet) error { return err } - output.WriteString(fmt.Sprintf("%-15s | %-15s | %-15s | %-10s | %s \n", wl.Name, wlData.Network.Interfaces[0].Network, wlData.Mounts[0].Name, wl.Result.State, addresses)) + vm, err := workloads.NewVMFromWorkload(&wl, &dl) + if err != nil { + return err + } + + addresses := getVmAddresses(vm) + + wlStruct.Addresses = addresses + + deploymentEntry := outputMap[deployment.Name] + deploymentEntry.Workloads = append(deploymentEntry.Workloads, wlStruct) + outputMap[deployment.Name] = deploymentEntry + } + } + + for key, val := range outputMap { + fmt.Printf("%+v\n", val) + output.WriteString(fmt.Sprintf("%-15s | %-15s | ", key, a.Config.Networks[val.Deployment.Network.Name].Name)) + + for _, wl := range val.Workloads { + output.WriteString(fmt.Sprintf("%-15s | %-15s | %-10s | %s \n", wl.Workload.Name, wl.WorkloadData.Mounts[0].Name, wl.Workload.Result.State, wl.Addresses)) } } @@ -247,26 +252,55 @@ func parsePsFlags(flags *pflag.FlagSet) (bool, string, error) { } func (a *App) Down() error { + for _, val := range a.Config.Deployments { + projectName := a.getProjectName(val.Name) + log.Info().Str("projectName", projectName).Msg("canceling deployments") + if err := a.Client.CancelByProjectName(projectName); err != nil { + return err + } + } + return nil +} - projectName := a.Config.ProjectName - log.Info().Str("projectName", projectName).Msg("canceling deployments") - if err := a.Client.CancelByProjectName(projectName); err != nil { - return err +func (a *App) generateNetworks() map[string]*workloads.ZNet { + zNets := make(map[string]*workloads.ZNet, 0) + + for key, network := range a.Config.Networks { + zNet := workloads.ZNet{ + Name: network.Name, + Description: network.Description, + Nodes: network.Nodes, + IPRange: gridtypes.NewIPNet(generateIPNet(network.IPRange.IP, network.IPRange.Mask)), + AddWGAccess: network.AddWGAccess, + MyceliumKeys: network.MyceliumKeys, + } + + zNets[key] = &zNet } - return nil + return zNets } -func (a *App) getProjectName(key string) string { - key = strings.TrimSuffix(key, "net") - return fmt.Sprintf("compose/%v/%v", a.Client.TwinID, key) +func (a *App) generateInitDeployments() map[string]*workloads.Deployment { + workloadsDeployments := make(map[string]*workloads.Deployment, 0) + + for key, deployment := range a.Config.Deployments { + var networkName string + if deployment.Network != nil { + networkName = a.Config.Networks[deployment.Network.Name].Name + } + workloadsDeployment := workloads.NewDeployment(deployment.Name, deployment.NodeID, a.getProjectName(deployment.Name), nil, networkName, make([]workloads.Disk, 0), nil, make([]workloads.VM, 0), nil) + workloadsDeployments[key] = &workloadsDeployment + } + + return workloadsDeployments } -func (a *App) getDeploymentName() string { - return fmt.Sprintf("dl_%v_%v", a.Client.TwinID, generateRandString(5)) +func (a *App) getProjectName(key string) string { + return fmt.Sprintf("compose/%v/%v", a.Client.TwinID, key) } -func (a *App) loadCurrentNodeDeplyments(projectName string) error { +func (a *App) loadCurrentNodeDeployments(projectName string) error { contracts, err := a.Client.ContractsGetter.ListContractsOfProjectName(projectName, true) if err != nil { return err diff --git a/grid-compose/internal/config/config.go b/grid-compose/internal/config/config.go index 7c7bc0596..364ecafb0 100644 --- a/grid-compose/internal/config/config.go +++ b/grid-compose/internal/config/config.go @@ -9,11 +9,11 @@ import ( ) type Config struct { - Version string `yaml:"version"` - Networks map[string]types.Network `yaml:"networks"` - Services map[string]types.Service `yaml:"services"` - Storage map[string]types.Storage `yaml:"storage"` - ProjectName string `yaml:"project_name"` + Version string `yaml:"version"` + Networks map[string]types.Network `yaml:"networks"` + Services map[string]types.Service `yaml:"services"` + Storage map[string]types.Storage `yaml:"storage"` + Deployments map[string]types.Deployment `yaml:"deployments"` } func NewConfig() *Config { @@ -44,10 +44,6 @@ func (c *Config) ValidateConfig() (err error) { } for name, storage := range c.Storage { - if storage.Type == "" { - return fmt.Errorf("%w for storage %s", ErrStorageTypeNotSet, name) - } - if storage.Size == "" { return fmt.Errorf("%w for storage %s", ErrStorageSizeNotSet, name) } @@ -62,9 +58,30 @@ func (c *Config) LoadConfigFromReader(configFile io.Reader) error { return fmt.Errorf("failed to read file %w", err) } - if err := yaml.Unmarshal(content, &c); err != nil { + if err := c.UnmarshalYAML(content); err != nil { return fmt.Errorf("failed to parse file %w", err) } return nil } + +func (c *Config) UnmarshalYAML(content []byte) error { + if err := yaml.Unmarshal(content, c); err != nil { + return err + } + + for serviceName, service := range c.Services { + deployTo := service.DeployTo + + if deployment, exists := c.Deployments[deployTo]; exists { + if deployment.Workloads == nil { + deployment.Workloads = make([]string, 0) + } + deployment.Workloads = append(deployment.Workloads, serviceName) + + c.Deployments[deployTo] = deployment + } + } + + return nil +} diff --git a/grid-compose/pkg/types.go b/grid-compose/internal/types.go similarity index 83% rename from grid-compose/pkg/types.go rename to grid-compose/internal/types.go index 1cb40357f..ca4a75178 100644 --- a/grid-compose/pkg/types.go +++ b/grid-compose/internal/types.go @@ -1,4 +1,4 @@ -package types +package internal import "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" @@ -7,7 +7,6 @@ type NetworkTypes struct { } type Storage struct { - Type string `yaml:"type"` Size string `yaml:"size"` } @@ -16,33 +15,41 @@ type Service struct { Entrypoint string `yaml:"entrypoint,omitempty"` Environment []string `yaml:"environment"` Resources Resources `yaml:"resources"` - NodeID uint32 `yaml:"node_id"` Volumes []string `yaml:"volumes"` NetworkTypes []string `yaml:"network_types"` Networks []string `yaml:"networks"` HealthCheck *HealthCheck `yaml:"healthcheck,omitempty"` DependsOn []string `yaml:"depends_on,omitempty"` + DeployTo string `yaml:"deploy_to,omitempty"` } +type Deployment struct { + Name string `yaml:"name"` + NodeID uint32 `yaml:"node_id"` + Network *struct { + Name string `yaml:"name"` + } `yaml:"network"` + + Workloads []string +} type HealthCheck struct { Test []string `yaml:"test"` Interval string `yaml:"interval"` Timeout string `yaml:"timeout"` - Retries int `yaml:"retries"` + Retries uint `yaml:"retries"` } type Resources struct { CPU uint `yaml:"cpu" json:"cpu"` Memory uint `yaml:"memory" json:"memory"` - SSD uint `yaml:"ssd" json:"ssd"` - HDD uint `yaml:"hdd" json:"hdd"` + Rootfs uint `yaml:"ssd" json:"ssd"` } type WorkloadData struct { Flist string `json:"flist"` Network WorkloadDataNetwork `json:"network"` ComputeCapacity Resources `json:"compute_capacity"` - Size int `json:"size"` + Size uint `json:"size"` Mounts []struct { Name string `json:"name"` MountPoint string `json:"mountpoint"` @@ -86,7 +93,6 @@ type IPMask struct { } type DeploymentData struct { - Vms []workloads.VM - Disks []workloads.Disk - NodeIDs []uint32 + Vms []workloads.VM + Disks []workloads.Disk } diff --git a/grid-compose/internal/utils.go b/grid-compose/internal/utils.go index 00f8542c9..b03a7e69c 100644 --- a/grid-compose/internal/utils.go +++ b/grid-compose/internal/utils.go @@ -2,14 +2,12 @@ package internal import ( "fmt" - mrand "math/rand" "net" "strconv" "strings" "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" types "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/pkg" - "github.com/threefoldtech/zos/pkg/gridtypes" ) func assignEnvs(vm *workloads.VM, envs []string) { @@ -77,12 +75,12 @@ func generateIPNet(ip types.IP, mask types.IPMask) net.IPNet { var ipNet net.IPNet switch ip.Type { - case "ipv4": + case "ip4": ipSplit := strings.Split(ip.IP, ".") - byte1, _ := strconv.Atoi(ipSplit[0]) - byte2, _ := strconv.Atoi(ipSplit[1]) - byte3, _ := strconv.Atoi(ipSplit[2]) - byte4, _ := strconv.Atoi(ipSplit[3]) + byte1, _ := strconv.ParseUint(ipSplit[0], 10, 8) + byte2, _ := strconv.ParseUint(ipSplit[1], 10, 8) + byte3, _ := strconv.ParseUint(ipSplit[2], 10, 8) + byte4, _ := strconv.ParseUint(ipSplit[3], 10, 8) ipNet.IP = net.IPv4(byte(byte1), byte(byte2), byte(byte3), byte(byte4)) default: @@ -94,45 +92,14 @@ func generateIPNet(ip types.IP, mask types.IPMask) net.IPNet { switch mask.Type { case "cidr": maskSplit := strings.Split(mask.Mask, "/") - maskOnes, _ := strconv.Atoi(maskSplit[0]) - maskBits, _ := strconv.Atoi(maskSplit[1]) - maskIP = net.CIDRMask(maskOnes, maskBits) + maskOnes, _ := strconv.ParseInt(maskSplit[0], 10, 8) + maskBits, _ := strconv.ParseInt(maskSplit[1], 10, 8) + + maskIP = net.CIDRMask(int(maskOnes), int(maskBits)) + ipNet.Mask = maskIP default: return ipNet } - ipNet.Mask = maskIP - return ipNet } - -func generateNetworks(networks map[string]types.Network) map[string]*workloads.ZNet { - zNets := make(map[string]*workloads.ZNet, 0) - - for key, network := range networks { - zNet := workloads.ZNet{ - Name: network.Name, - Description: network.Description, - Nodes: network.Nodes, - IPRange: gridtypes.NewIPNet(net.IPNet{ - IP: net.IPv4(10, 20, 0, 0), - Mask: net.CIDRMask(16, 32), - }), - AddWGAccess: network.AddWGAccess, - MyceliumKeys: network.MyceliumKeys, - } - - zNets[key] = &zNet - } - - return zNets -} - -func generateRandString(n int) string { - const letters = "abcdefghijklmnopqrstuvwxyz123456789" - b := make([]byte, n) - for i := range b { - b[i] = letters[mrand.Intn(len(letters))] - } - return string(b) -} From 3b095a7cafac183c42c3c49476a4fe5c2061781b Mon Sep 17 00:00:00 2001 From: eyad-hussein Date: Tue, 13 Aug 2024 05:11:16 +0300 Subject: [PATCH 10/20] refactor: refactor logic for all commands to comply with cases and constraints --- grid-compose/.gitignore | 1 + grid-compose/Makefile | 24 +- grid-compose/README.md | 16 + grid-compose/cmd/down.go | 4 + grid-compose/cmd/ps.go | 8 +- grid-compose/cmd/root.go | 8 +- grid-compose/cmd/up.go | 8 +- grid-compose/cmd/version.go | 13 +- grid-compose/docs/cases.md | 0 grid-compose/docs/down.md | 0 grid-compose/docs/ps.md | 0 grid-compose/docs/up.md | 0 grid-compose/docs/version.md | 0 grid-compose/example copy/full_example.yml | 74 ---- .../multiple_services_diff_network_1.yml | 74 ---- .../multiple_services_diff_network_2.yml | 86 ----- .../multiple_services_same_network_1.yml | 63 ---- .../multiple_services_same_network_2.yml | 62 ---- .../multiple_services_same_network_3.yml | 61 ---- .../example copy/single_service_1.yml | 10 - .../example copy/single_service_2.yml | 11 - .../two_services_same_network_1.yml | 49 --- .../two_services_same_network_2.yml | 49 --- .../two_services_same_network_3.yml | 36 -- grid-compose/example/full_example.yml | 70 ---- .../example/two_services_same_network_1.yml | 2 +- grid-compose/internal/app.go | 328 ++++++++++-------- grid-compose/internal/config/config.go | 83 +++-- grid-compose/internal/config/errors.go | 13 - grid-compose/internal/types.go | 98 ------ grid-compose/internal/types/types.go | 65 ++++ grid-compose/internal/utils.go | 105 ------ .../internal/{ => utils}/mycelium_seed.go | 4 +- grid-compose/internal/utils/network.go | 53 +++ grid-compose/internal/utils/validation.go | 20 ++ grid-compose/internal/utils/vm.go | 93 +++++ grid-compose/internal/validation.go | 7 - grid-compose/internal/vmaddresses.go | 30 -- 38 files changed, 543 insertions(+), 1085 deletions(-) create mode 100644 grid-compose/docs/cases.md create mode 100644 grid-compose/docs/down.md create mode 100644 grid-compose/docs/ps.md create mode 100644 grid-compose/docs/up.md create mode 100644 grid-compose/docs/version.md delete mode 100644 grid-compose/example copy/full_example.yml delete mode 100644 grid-compose/example copy/multiple_services_diff_network_1.yml delete mode 100644 grid-compose/example copy/multiple_services_diff_network_2.yml delete mode 100644 grid-compose/example copy/multiple_services_same_network_1.yml delete mode 100644 grid-compose/example copy/multiple_services_same_network_2.yml delete mode 100644 grid-compose/example copy/multiple_services_same_network_3.yml delete mode 100644 grid-compose/example copy/single_service_1.yml delete mode 100644 grid-compose/example copy/single_service_2.yml delete mode 100644 grid-compose/example copy/two_services_same_network_1.yml delete mode 100644 grid-compose/example copy/two_services_same_network_2.yml delete mode 100644 grid-compose/example copy/two_services_same_network_3.yml delete mode 100644 grid-compose/example/full_example.yml delete mode 100644 grid-compose/internal/config/errors.go delete mode 100644 grid-compose/internal/types.go create mode 100644 grid-compose/internal/types/types.go delete mode 100644 grid-compose/internal/utils.go rename grid-compose/internal/{ => utils}/mycelium_seed.go (71%) create mode 100644 grid-compose/internal/utils/network.go create mode 100644 grid-compose/internal/utils/validation.go create mode 100644 grid-compose/internal/utils/vm.go delete mode 100644 grid-compose/internal/validation.go delete mode 100644 grid-compose/internal/vmaddresses.go diff --git a/grid-compose/.gitignore b/grid-compose/.gitignore index d65a64c45..e1ed12ba8 100644 --- a/grid-compose/.gitignore +++ b/grid-compose/.gitignore @@ -1,3 +1,4 @@ bin/* .pre-commit-config.yaml out +full_example.yml diff --git a/grid-compose/Makefile b/grid-compose/Makefile index b083ae919..5704400b7 100644 --- a/grid-compose/Makefile +++ b/grid-compose/Makefile @@ -1,9 +1,21 @@ -BIN_PATH = bin/main +test: + @echo "Running Tests" + go test -v ./... -.PHONY: build run +clean: + rm ./bin -rf -build: - @go build -o $(BIN_PATH) ./main.go +getverifiers: + @echo "Installing golangci-lint" && go install github.com/golangci/golangci-lint/cmd/golangci-lint + go mod tidy + +lint: + @echo "Running $@" + golangci-lint run -c ../.golangci.yml -run: - @go run main.go up -f ./grid-compose.yaml +build: + @echo "Running $@" + @go build -ldflags=\ + "-X 'github.com/threefoldtech/tfgrid-sdk-go/grid-compose/cmd.commit=$(shell git rev-parse HEAD)'\ + -X 'github.com/threefoldtech/tfgrid-sdk-go/grid-compose/cmd.version=$(shell git tag --sort=-version:refname | head -n 1)'"\ + -o bin/grid-compose main.go diff --git a/grid-compose/README.md b/grid-compose/README.md index b82e8fb76..78ba49758 100644 --- a/grid-compose/README.md +++ b/grid-compose/README.md @@ -19,4 +19,20 @@ COMMANDS: - version: shows the project version - up: deploy the app - down: cancel all deployments + - ps: list deployments on the grid ``` + +Run: + +```bash +make build +``` + +Then: + +```bash +./bin/grid-compose [COMMAND] +``` + +For example: +./bin/grid-compose ps -f example/multiple_services_diff_network_3.yml diff --git a/grid-compose/cmd/down.go b/grid-compose/cmd/down.go index 08c3c36e2..06b50916f 100644 --- a/grid-compose/cmd/down.go +++ b/grid-compose/cmd/down.go @@ -14,3 +14,7 @@ var downCmd = &cobra.Command{ } }, } + +func init() { + rootCmd.AddCommand(downCmd) +} diff --git a/grid-compose/cmd/ps.go b/grid-compose/cmd/ps.go index 6d673da30..04ebdc1e1 100644 --- a/grid-compose/cmd/ps.go +++ b/grid-compose/cmd/ps.go @@ -7,7 +7,7 @@ import ( var psCmd = &cobra.Command{ Use: "ps", - Short: "list containers", + Short: "list deployments on the grid", Run: func(cmd *cobra.Command, args []string) { flags := cmd.Flags() @@ -16,3 +16,9 @@ var psCmd = &cobra.Command{ } }, } + +func init() { + psCmd.PersistentFlags().BoolP("verbose", "v", false, "all information about deployed services") + psCmd.PersistentFlags().StringP("output", "o", "", "output result to a file") + rootCmd.AddCommand(psCmd) +} diff --git a/grid-compose/cmd/root.go b/grid-compose/cmd/root.go index 0a7fc375a..c55e2b259 100644 --- a/grid-compose/cmd/root.go +++ b/grid-compose/cmd/root.go @@ -22,6 +22,7 @@ func Execute() { } } +// TODO: Validate command line arguments var rootCmd = &cobra.Command{ Use: "grid-compose", Short: "Grid-Compose is a tool for running multi-vm applications on TFGrid defined using a Yaml formatted file.", @@ -38,13 +39,6 @@ func init() { network = os.Getenv("NETWORK") mnemonic = os.Getenv("MNEMONIC") rootCmd.PersistentFlags().StringVarP(&configPath, "file", "f", "./grid-compose.yaml", "the grid-compose configuration file") - psCmd.PersistentFlags().BoolP("verbose", "v", false, "all information about deployed services") - psCmd.PersistentFlags().StringP("output", "o", "", "output result to a file") log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) - - rootCmd.AddCommand(versionCmd) - rootCmd.AddCommand(upCmd) - rootCmd.AddCommand(psCmd) - rootCmd.AddCommand(downCmd) } diff --git a/grid-compose/cmd/up.go b/grid-compose/cmd/up.go index 4d42aa108..b29882c5b 100644 --- a/grid-compose/cmd/up.go +++ b/grid-compose/cmd/up.go @@ -5,10 +5,6 @@ import ( "github.com/spf13/cobra" ) -// load config from file + validate -// parse environment variables -// deploy networks + volumes -// deploy services var upCmd = &cobra.Command{ Use: "up", Short: "deploy application on the grid", @@ -18,3 +14,7 @@ var upCmd = &cobra.Command{ } }, } + +func init() { + rootCmd.AddCommand(upCmd) +} diff --git a/grid-compose/cmd/version.go b/grid-compose/cmd/version.go index ec5ceb709..af884636b 100644 --- a/grid-compose/cmd/version.go +++ b/grid-compose/cmd/version.go @@ -1,3 +1,4 @@ +// Package cmd for parsing command line arguments package cmd import ( @@ -6,12 +7,20 @@ import ( "github.com/spf13/cobra" ) -var version = "v0.0.1" +// set at build time +var commit string +var version string +// versionCmd represents the version command var versionCmd = &cobra.Command{ Use: "version", - Short: "get current version number", + Short: "Get latest build tag", Run: func(cmd *cobra.Command, args []string) { fmt.Println(version) + fmt.Println(commit) }, } + +func init() { + rootCmd.AddCommand(versionCmd) +} diff --git a/grid-compose/docs/cases.md b/grid-compose/docs/cases.md new file mode 100644 index 000000000..e69de29bb diff --git a/grid-compose/docs/down.md b/grid-compose/docs/down.md new file mode 100644 index 000000000..e69de29bb diff --git a/grid-compose/docs/ps.md b/grid-compose/docs/ps.md new file mode 100644 index 000000000..e69de29bb diff --git a/grid-compose/docs/up.md b/grid-compose/docs/up.md new file mode 100644 index 000000000..e69de29bb diff --git a/grid-compose/docs/version.md b/grid-compose/docs/version.md new file mode 100644 index 000000000..e69de29bb diff --git a/grid-compose/example copy/full_example.yml b/grid-compose/example copy/full_example.yml deleted file mode 100644 index 596668be2..000000000 --- a/grid-compose/example copy/full_example.yml +++ /dev/null @@ -1,74 +0,0 @@ -version: '1.0.0' - -networks: - net1: - name: 'miaminet' - nodes: - - 14 - - 144 - range: - ip: - type: ipv4 - ip: 10.20.0.0 - mask: - type: cidr - mask: 16/32 - wg: true - net2: - name: 'cartoonnetwork' - node_id: 144 - range: - ip: - type: ipv4 - ip: 10.20.0.0 - mask: - type: cidr - mask: 16/32 - -services: - server: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 - - ygg - entrypoint: '/sbin/zinit init' - volumes: - - webdata - resources: - cpu: 1 - memory: 2048 - rootfs: 25600 - network: net1 - node_id: 14 - frontend: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 - entrypoint: '/sbin/zinit init' - volumes: - - dbdata - resources: - cpu: 1 - memory: 2048 - rootfs: 25600 - network: net1 - database: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 - entrypoint: '/sbin/zinit init' - volumes: - - dbdata - resources: - cpu: 1 - memory: 2048 - rootfs: 25600 - network: net2 - -volumes: - webdata: - mountpoint: '/data' - size: 10GB - dbdata: - mountpoint: '/var/lib/postgresql/data' - size: 10GB diff --git a/grid-compose/example copy/multiple_services_diff_network_1.yml b/grid-compose/example copy/multiple_services_diff_network_1.yml deleted file mode 100644 index b7f1d0146..000000000 --- a/grid-compose/example copy/multiple_services_diff_network_1.yml +++ /dev/null @@ -1,74 +0,0 @@ -version: '1.0.0' - -networks: - net1: - name: 'miaminet' - nodes: - - 14 - - 144 - range: - ip: - type: ipv4 - ip: 10.20.0.0 - mask: - type: cidr - mask: 16/32 - wg: true - net2: - name: 'cartoonnetwork' - node_id: 144 - range: - ip: - type: ipv4 - ip: 10.20.0.0 - mask: - type: cidr - mask: 16/32 - -services: - server: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 - - ygg - entrypoint: '/sbin/zinit init' - volumes: - - webdata - resources: - cpu: 1 - memory: 2048 - rootfs: 25600 - network: net2 - frontend: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 - entrypoint: '/sbin/zinit init' - volumes: - - dbdata - resources: - cpu: 1 - memory: 2048 - rootfs: 25600 - network: net1 - node_id: 144 - database: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 - entrypoint: '/sbin/zinit init' - volumes: - - dbdata - resources: - cpu: 1 - memory: 2048 - rootfs: 25600 - network: net2 - -volumes: - webdata: - mountpoint: '/data' - size: 10GB - dbdata: - mountpoint: '/var/lib/postgresql/data' - size: 10GB diff --git a/grid-compose/example copy/multiple_services_diff_network_2.yml b/grid-compose/example copy/multiple_services_diff_network_2.yml deleted file mode 100644 index c3b6f8eb6..000000000 --- a/grid-compose/example copy/multiple_services_diff_network_2.yml +++ /dev/null @@ -1,86 +0,0 @@ -version: '1.0.0' - -networks: - net1: - name: 'miaminet' - nodes: - - 14 - - 144 - range: - ip: - type: ipv4 - ip: 10.20.0.0 - mask: - type: cidr - mask: 16/32 - wg: true - net2: - name: 'cartoonnetwork' - node_id: 144 - range: - ip: - type: ipv4 - ip: 10.20.0.0 - mask: - type: cidr - mask: 16/32 - -services: - server: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 - - ygg - entrypoint: '/sbin/zinit init' - volumes: - - webdata - resources: - cpu: 1 - memory: 2048 - rootfs: 25600 - network: net2 - server2: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 - - ygg - entrypoint: '/sbin/zinit init' - volumes: - - webdata - resources: - cpu: 1 - memory: 2048 - rootfs: 25600 - network: net1 - frontend: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 - entrypoint: '/sbin/zinit init' - volumes: - - dbdata - resources: - cpu: 1 - memory: 2048 - rootfs: 25600 - network: net1 - database: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 - entrypoint: '/sbin/zinit init' - volumes: - - dbdata - resources: - cpu: 1 - memory: 2048 - rootfs: 25600 - network: net2 - -volumes: - webdata: - mountpoint: '/data' - size: 10GB - dbdata: - mountpoint: '/var/lib/postgresql/data' - size: 10GB diff --git a/grid-compose/example copy/multiple_services_same_network_1.yml b/grid-compose/example copy/multiple_services_same_network_1.yml deleted file mode 100644 index aef546753..000000000 --- a/grid-compose/example copy/multiple_services_same_network_1.yml +++ /dev/null @@ -1,63 +0,0 @@ -version: '1.0.0' - -networks: - net1: - name: 'miaminet' - range: - ip: - type: ipv4 - ip: 10.20.0.0 - mask: - type: cidr - mask: 16/32 - wg: true - -services: - server: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 - - ygg - entrypoint: '/sbin/zinit init' - volumes: - - webdata - resources: - cpu: 1 - memory: 2048 - rootfs: 25600 - network: net1 - node_id: 14 - frontend: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 - entrypoint: '/sbin/zinit init' - volumes: - - dbdata - resources: - cpu: 1 - memory: 2048 - rootfs: 25600 - network: net1 - node_id: 14 - database: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 - entrypoint: '/sbin/zinit init' - volumes: - - dbdata - resources: - cpu: 1 - memory: 2048 - rootfs: 25600 - network: net1 - node_id: 14 - -volumes: - webdata: - mountpoint: '/data' - size: 10GB - dbdata: - mountpoint: '/var/lib/postgresql/data' - size: 10GB diff --git a/grid-compose/example copy/multiple_services_same_network_2.yml b/grid-compose/example copy/multiple_services_same_network_2.yml deleted file mode 100644 index fe1016b90..000000000 --- a/grid-compose/example copy/multiple_services_same_network_2.yml +++ /dev/null @@ -1,62 +0,0 @@ -version: '1.0.0' - -networks: - net1: - name: 'miaminet' - range: - ip: - type: ipv4 - ip: 10.20.0.0 - mask: - type: cidr - mask: 16/32 - wg: true - -services: - server: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 - - ygg - entrypoint: '/sbin/zinit init' - volumes: - - webdata - resources: - cpu: 1 - memory: 2048 - rootfs: 25600 - network: net1 - node_id: 14 - frontend: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 - entrypoint: '/sbin/zinit init' - volumes: - - dbdata - resources: - cpu: 1 - memory: 2048 - rootfs: 25600 - network: net1 - node_id: 14 - database: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 - entrypoint: '/sbin/zinit init' - volumes: - - dbdata - resources: - cpu: 1 - memory: 2048 - rootfs: 25600 - network: net1 - -volumes: - webdata: - mountpoint: '/data' - size: 10GB - dbdata: - mountpoint: '/var/lib/postgresql/data' - size: 10GB diff --git a/grid-compose/example copy/multiple_services_same_network_3.yml b/grid-compose/example copy/multiple_services_same_network_3.yml deleted file mode 100644 index 8ac46fcd4..000000000 --- a/grid-compose/example copy/multiple_services_same_network_3.yml +++ /dev/null @@ -1,61 +0,0 @@ -version: '1.0.0' - -networks: - net1: - name: 'miaminet' - node_id: 14 - range: - ip: - type: ipv4 - ip: 10.20.0.0 - mask: - type: cidr - mask: 16/32 - wg: true - -services: - server: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 - - ygg - entrypoint: '/sbin/zinit init' - volumes: - - webdata - resources: - cpu: 1 - memory: 2048 - rootfs: 25600 - network: net1 - frontend: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 - entrypoint: '/sbin/zinit init' - volumes: - - dbdata - resources: - cpu: 1 - memory: 2048 - rootfs: 25600 - network: net1 - database: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 - entrypoint: '/sbin/zinit init' - volumes: - - dbdata - resources: - cpu: 1 - memory: 2048 - rootfs: 25600 - network: net1 - -volumes: - webdata: - mountpoint: '/data' - size: 10GB - dbdata: - mountpoint: '/var/lib/postgresql/data' - size: 10GB diff --git a/grid-compose/example copy/single_service_1.yml b/grid-compose/example copy/single_service_1.yml deleted file mode 100644 index fdc031cce..000000000 --- a/grid-compose/example copy/single_service_1.yml +++ /dev/null @@ -1,10 +0,0 @@ -version: '1.0.0' - -services: - server: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - entrypoint: '/sbin/zinit init' - resources: - cpu: 1 - memory: 2048 - rootfs: 25600 diff --git a/grid-compose/example copy/single_service_2.yml b/grid-compose/example copy/single_service_2.yml deleted file mode 100644 index 7bba8e257..000000000 --- a/grid-compose/example copy/single_service_2.yml +++ /dev/null @@ -1,11 +0,0 @@ -version: '1.0.0' - -services: - server: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - entrypoint: '/sbin/zinit init' - resources: - cpu: 1 - memory: 2048 - rootfs: 25600 - node_id: 14 diff --git a/grid-compose/example copy/two_services_same_network_1.yml b/grid-compose/example copy/two_services_same_network_1.yml deleted file mode 100644 index 77da823f4..000000000 --- a/grid-compose/example copy/two_services_same_network_1.yml +++ /dev/null @@ -1,49 +0,0 @@ -version: '1.0.0' - -networks: - net1: - name: 'miaminet' - node_id: 14 - range: - ip: - type: ipv4 - ip: 10.20.0.0 - mask: - type: cidr - mask: 16/32 - wg: true - -services: - server: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 - - ygg - entrypoint: '/sbin/zinit init' - volumes: - - webdata - resources: - cpu: 1 - memory: 2048 - rootfs: 25600 - network: net1 - database: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 - entrypoint: '/sbin/zinit init' - volumes: - - dbdata - resources: - cpu: 1 - memory: 2048 - rootfs: 25600 - network: net1 - -volumes: - webdata: - mountpoint: '/data' - size: 10GB - dbdata: - mountpoint: '/var/lib/postgresql/data' - size: 10GB diff --git a/grid-compose/example copy/two_services_same_network_2.yml b/grid-compose/example copy/two_services_same_network_2.yml deleted file mode 100644 index 4fddb8f9b..000000000 --- a/grid-compose/example copy/two_services_same_network_2.yml +++ /dev/null @@ -1,49 +0,0 @@ -version: '1.0.0' - -networks: - net1: - name: 'miaminet' - range: - ip: - type: ipv4 - ip: 10.20.0.0 - mask: - type: cidr - mask: 16/32 - wg: true - -services: - server: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 - - ygg - entrypoint: '/sbin/zinit init' - volumes: - - webdata - node_id: 14 - resources: - cpu: 1 - memory: 2048 - rootfs: 25600 - network: net1 - database: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 - entrypoint: '/sbin/zinit init' - volumes: - - dbdata - resources: - cpu: 1 - memory: 2048 - rootfs: 25600 - network: net1 - -volumes: - webdata: - mountpoint: '/data' - size: 10GB - dbdata: - mountpoint: '/var/lib/postgresql/data' - size: 10GB diff --git a/grid-compose/example copy/two_services_same_network_3.yml b/grid-compose/example copy/two_services_same_network_3.yml deleted file mode 100644 index dd437249e..000000000 --- a/grid-compose/example copy/two_services_same_network_3.yml +++ /dev/null @@ -1,36 +0,0 @@ -version: '1.0.0' - -services: - server: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 - - ygg - entrypoint: '/sbin/zinit init' - volumes: - - webdata - node_id: 14 - resources: - cpu: 1 - memory: 2048 - rootfs: 25600 - - database: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 - entrypoint: '/sbin/zinit init' - volumes: - - dbdata - resources: - cpu: 1 - memory: 2048 - rootfs: 25600 - -volumes: - webdata: - mountpoint: '/data' - size: 10GB - dbdata: - mountpoint: '/var/lib/postgresql/data' - size: 10GB diff --git a/grid-compose/example/full_example.yml b/grid-compose/example/full_example.yml deleted file mode 100644 index a815a1450..000000000 --- a/grid-compose/example/full_example.yml +++ /dev/null @@ -1,70 +0,0 @@ -version: '1.0.0' - -networks: - net1: - name: 'miaminet' - range: - ip: - type: ipv4 - ip: 10.20.0.0 - mask: - type: cidr - mask: 16/32 - wg: true - net2: - name: 'cartoonnetwork' - range: - ip: - type: ipv4 - ip: 10.20.0.0 - mask: - type: cidr - mask: 16/32 - -services: - server: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 - - ygg - entrypoint: '/sbin/zinit init' - volumes: - - webdata - resources: - cpu: 1 - memory: 2048 - rootfs: 25600 - network: net1 - node_id: 14 - frontend: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 - entrypoint: '/sbin/zinit init' - volumes: - - dbdata - resources: - cpu: 1 - memory: 2048 - rootfs: 25600 - network: net1 - database: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 - entrypoint: '/sbin/zinit init' - volumes: - - dbdata - resources: - cpu: 1 - memory: 2048 - rootfs: 25600 - network: net2 - -volumes: - webdata: - mountpoint: '/data' - size: 10GB - dbdata: - mountpoint: '/var/lib/postgresql/data' - size: 10GB diff --git a/grid-compose/example/two_services_same_network_1.yml b/grid-compose/example/two_services_same_network_1.yml index 4fddb8f9b..df5aea784 100644 --- a/grid-compose/example/two_services_same_network_1.yml +++ b/grid-compose/example/two_services_same_network_1.yml @@ -21,7 +21,7 @@ services: entrypoint: '/sbin/zinit init' volumes: - webdata - node_id: 14 + node_id: 144 resources: cpu: 1 memory: 2048 diff --git a/grid-compose/internal/app.go b/grid-compose/internal/app.go index 2f7941496..a923588fd 100644 --- a/grid-compose/internal/app.go +++ b/grid-compose/internal/app.go @@ -13,18 +13,25 @@ import ( "github.com/spf13/pflag" "github.com/threefoldtech/tfgrid-sdk-go/grid-client/deployer" "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" + "github.com/threefoldtech/tfgrid-sdk-go/grid-proxy/pkg/types" + "github.com/threefoldtech/zos/pkg/gridtypes" "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/config" - types "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/pkg" - "github.com/threefoldtech/zos/pkg/gridtypes" + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/utils" ) +// App is the main application struct that holds the client and the config data type App struct { Client *deployer.TFPluginClient Config *config.Config } +// NewApp creates a new instance of the application func NewApp(net, mnemonic, configPath string) (*App, error) { + if !utils.ValidateCredentials(mnemonic, net) { + return nil, fmt.Errorf("invalid mnemonic or network") + } + configFile, err := os.Open(configPath) if err != nil { return nil, err @@ -53,81 +60,99 @@ func NewApp(net, mnemonic, configPath string) (*App, error) { }, nil } +// Up deploys the services described in the config file func (a *App) Up(ctx context.Context) error { - networks := a.generateNetworks() - deployments := a.generateInitDeployments() - - for key, val := range a.Config.Services { - deployment := deployments[val.DeployTo] + err := a.generateMissingNodes(ctx) + if err != nil { + return err + } - var network *workloads.ZNet - if deployment.Name == "" { - network = &workloads.ZNet{ - Name: deployment.Name + "net", - Nodes: []uint32{deployment.NodeID}, - IPRange: gridtypes.NewIPNet(net.IPNet{ - IP: net.IPv4(10, 20, 0, 0), - Mask: net.CIDRMask(16, 32), - }), - } - deployment.NetworkName = network.Name - } else { - network = networks[a.Config.Deployments[val.DeployTo].Network.Name] + networks := a.generateNetworks() + dls := []*workloads.Deployment{} + + for networkName, deploymentData := range a.Config.DeploymentData { + network := networks[networkName] + projectName := a.getProjectName(networkName) + + network.SolutionType = projectName + network.Nodes = []uint32{deploymentData.NodeID} + dl := &workloads.Deployment{ + Name: a.getDeploymentName(networkName), + NodeID: deploymentData.NodeID, + SolutionType: projectName, + NetworkName: network.Name, } - network.SolutionType = a.getProjectName(deployment.Name) + for _, service := range deploymentData.Services { + vm := workloads.VM{ + Name: service.Name, + Flist: service.Flist, + Entrypoint: service.Entrypoint, + CPU: int(service.Resources.CPU), + Memory: int(service.Resources.Memory), + RootfsSize: int(service.Resources.Rootfs), + NetworkName: network.Name, + } - vm := workloads.VM{ - Name: key, - Flist: val.Flist, - Entrypoint: val.Entrypoint, - CPU: int(val.Resources.CPU), - Memory: int(val.Resources.Memory), - NetworkName: network.Name, - RootfsSize: int(val.Resources.Rootfs), - } + utils.AssignEnvs(&vm, service.Environment) - assignEnvs(&vm, val.Environment) + disks, err := utils.AssignMounts(&vm, service.Volumes, a.Config.Volumes) + if err != nil { + return fmt.Errorf("failed to assign mounts %w", err) + } - disks, err := assignMounts(&vm, val.Volumes, a.Config.Storage) - if err != nil { - return fmt.Errorf("failed to assign mounts %w", err) - } + if err := utils.AssignNetworksTypes(&vm, service.IPTypes); err != nil { + return fmt.Errorf("failed to assign networks %w", err) + } - if err := assignNetworksTypes(&vm, val.NetworkTypes); err != nil { - return fmt.Errorf("failed to assign networks %w", err) + dl.Vms = append(dl.Vms, vm) + dl.Disks = append(dl.Disks, disks...) } - deployment.Vms = append(deployment.Vms, vm) - deployment.Disks = append(deployment.Disks, disks...) + dls = append(dls, dl) } log.Info().Str("status", "started").Msg("deploying networks...") - for _, val := range networks { - if err := a.Client.NetworkDeployer.Deploy(ctx, val); err != nil { + + for _, network := range networks { + if err := a.Client.NetworkDeployer.Deploy(ctx, network); err != nil { return err } } log.Info().Str("status", "done").Msg("networks deployed successfully") - for key, val := range deployments { - log.Info().Str("deployment", key).Msg("deploying...") + deployed := make([]*workloads.Deployment, 0) - if err := a.Client.DeploymentDeployer.Deploy(ctx, val); err != nil { - for _, val := range networks { - if err := a.Client.NetworkDeployer.Cancel(ctx, val); err != nil { + for _, dl := range dls { + log.Info().Str("deployment", dl.Name).Msg("deploying...") + + if err := a.Client.DeploymentDeployer.Deploy(ctx, dl); err != nil { + log.Info().Msg("an error occurred while deploying the deployment, canceling all deployments") + + for _, network := range networks { + if err := a.Client.NetworkDeployer.Cancel(ctx, network); err != nil { + return err + } + } + + for _, deployment := range deployed { + if err := a.Client.DeploymentDeployer.Cancel(ctx, deployment); err != nil { return err } } + log.Info().Msg("all deployments canceled successfully") return err } - log.Info().Str("deployment", key).Msg("deployed successfully") + log.Info().Str("deployment", dl.Name).Msg("deployed successfully") + + deployed = append(deployed, dl) } log.Info().Msg("all services deployed successfully") return nil } +// Ps lists the deployed services func (a *App) Ps(ctx context.Context, flags *pflag.FlagSet) error { verbose, outputFile, err := parsePsFlags(flags) if err != nil { @@ -135,105 +160,79 @@ func (a *App) Ps(ctx context.Context, flags *pflag.FlagSet) error { } var output strings.Builder + outputMap := make(map[string]gridtypes.Deployment) + if !verbose { - output.WriteString(fmt.Sprintf("%-15s | %-15s | %-15s | %-15s | %-10s | %s\n", "Deployment Name", "Network", "Service Name", "Storage", "State", "IP Address")) - output.WriteString(strings.Repeat("-", 79) + "\n") + output.WriteString(fmt.Sprintf("%-15s | %-15s | %-15s | %-15s | %-10s | %s\n", "Deployment Name", "Network", "Services", "Storage", "State", "IP Address")) + output.WriteString(strings.Repeat("-", 100) + "\n") } - outputMap := make(map[string]struct { - Deployment types.Deployment - Workloads []struct { - Workload gridtypes.Workload - WorkloadData types.WorkloadData - Addresses string - } - }) + for networkName, deploymentData := range a.Config.DeploymentData { + projectName := a.getProjectName(networkName) - for _, deployment := range a.Config.Deployments { - if err := a.loadCurrentNodeDeployments(a.getProjectName(deployment.Name)); err != nil { + if err := a.loadCurrentNodeDeployments(projectName); err != nil { return err } - outputMap[deployment.Name] = struct { - Deployment types.Deployment - Workloads []struct { - Workload gridtypes.Workload - WorkloadData types.WorkloadData - Addresses string - } - }{ - Deployment: deployment, - Workloads: []struct { - Workload gridtypes.Workload - WorkloadData types.WorkloadData - Addresses string - }{}, + contracts, err := a.Client.ContractsGetter.ListContractsOfProjectName(projectName) + if err != nil { + return err } - for _, workloadName := range deployment.Workloads { - wlStruct := struct { - Workload gridtypes.Workload - WorkloadData types.WorkloadData - Addresses string - }{ - Workload: gridtypes.Workload{}, - WorkloadData: types.WorkloadData{}, - } - - wl, dl, err := a.Client.State.GetWorkloadInDeployment(ctx, deployment.NodeID, workloadName, deployment.Name) + for _, contract := range contracts.NodeContracts { + contractDlData, err := workloads.ParseDeploymentData(contract.DeploymentData) if err != nil { return err } - wlStruct.Workload = wl + if contractDlData.Type == "network" { + continue + } - if wl.Type == "zmachine" { - err = json.Unmarshal(wl.Data, &wlStruct.WorkloadData) + dlAdded := false + for _, service := range deploymentData.Services { + wl, dl, err := a.Client.State.GetWorkloadInDeployment(ctx, contract.NodeID, service.Name, contractDlData.Name) if err != nil { return err } - } - var wlData types.WorkloadData - err = json.Unmarshal(wl.Data, &wlData) - if err != nil { - return err - } + vm, err := workloads.NewVMFromWorkload(&wl, &dl) + if err != nil { + return err + } - vm, err := workloads.NewVMFromWorkload(&wl, &dl) - if err != nil { - return err + if !verbose { + if !dlAdded { + output.WriteString(fmt.Sprintf("%-15s | %-15s | %-15s | %-15s | %-10s | %s\n", contractDlData.Name, vm.NetworkName, vm.Name, vm.Mounts[0].DiskName, wl.Result.State, utils.GetVmAddresses(vm))) + dlAdded = true + } else { + output.WriteString(fmt.Sprintf("%-15s | %-15s | %-15s | %-15s | %-10s | %s\n", strings.Repeat("-", 15), strings.Repeat("-", 15), vm.Name, vm.Mounts[0].DiskName, wl.Result.State, utils.GetVmAddresses(vm))) + } + } + outputMap[contractDlData.Name] = dl } - - addresses := getVmAddresses(vm) - - wlStruct.Addresses = addresses - - deploymentEntry := outputMap[deployment.Name] - deploymentEntry.Workloads = append(deploymentEntry.Workloads, wlStruct) - outputMap[deployment.Name] = deploymentEntry } } - for key, val := range outputMap { - fmt.Printf("%+v\n", val) - output.WriteString(fmt.Sprintf("%-15s | %-15s | ", key, a.Config.Networks[val.Deployment.Network.Name].Name)) - - for _, wl := range val.Workloads { - output.WriteString(fmt.Sprintf("%-15s | %-15s | %-10s | %s \n", wl.Workload.Name, wl.WorkloadData.Mounts[0].Name, wl.Workload.Result.State, wl.Addresses)) + if verbose { + out, err := json.MarshalIndent(outputMap, "", " ") + if err != nil { + return err + } + if outputFile == "" { + fmt.Println(string(out)) + return nil } - } - if outputFile != "" { - if err := os.WriteFile(outputFile, []byte(fmt.Sprintf("{\n%s\n}", strings.TrimSuffix(output.String(), ",\n"))), 0644); err != nil { + if err := os.WriteFile(outputFile, out, 0644); err != nil { return err } + return nil - } else { - // for better formatting - println("\n" + output.String()) } + // print for better formatting + fmt.Printf("\n%s\n", output.String()) return nil } @@ -251,9 +250,10 @@ func parsePsFlags(flags *pflag.FlagSet) (bool, string, error) { return verbose, outputFile, nil } +// Down cancels all the deployments func (a *App) Down() error { - for _, val := range a.Config.Deployments { - projectName := a.getProjectName(val.Name) + for networkName := range a.Config.DeploymentData { + projectName := a.getProjectName(networkName) log.Info().Str("projectName", projectName).Msg("canceling deployments") if err := a.Client.CancelByProjectName(projectName); err != nil { return err @@ -264,42 +264,40 @@ func (a *App) Down() error { func (a *App) generateNetworks() map[string]*workloads.ZNet { zNets := make(map[string]*workloads.ZNet, 0) - - for key, network := range a.Config.Networks { - zNet := workloads.ZNet{ - Name: network.Name, - Description: network.Description, - Nodes: network.Nodes, - IPRange: gridtypes.NewIPNet(generateIPNet(network.IPRange.IP, network.IPRange.Mask)), - AddWGAccess: network.AddWGAccess, - MyceliumKeys: network.MyceliumKeys, + defNetName := utils.GenerateDefaultNetworkName(a.Config.Services) + for networkName := range a.Config.DeploymentData { + if networkName == defNetName { + zNets[networkName] = &workloads.ZNet{ + Name: networkName, + IPRange: gridtypes.NewIPNet(net.IPNet{ + IP: net.IPv4(10, 20, 0, 0), + Mask: net.CIDRMask(16, 32), + }), + AddWGAccess: false, + } + } else { + network := a.Config.Networks[networkName] + zNets[networkName] = &workloads.ZNet{ + Name: network.Name, + Description: network.Description, + IPRange: gridtypes.NewIPNet(utils.GenerateIPNet(network.IPRange.IP, network.IPRange.Mask)), + AddWGAccess: network.AddWGAccess, + MyceliumKeys: network.MyceliumKeys, + } } - - zNets[key] = &zNet } return zNets } -func (a *App) generateInitDeployments() map[string]*workloads.Deployment { - workloadsDeployments := make(map[string]*workloads.Deployment, 0) - - for key, deployment := range a.Config.Deployments { - var networkName string - if deployment.Network != nil { - networkName = a.Config.Networks[deployment.Network.Name].Name - } - workloadsDeployment := workloads.NewDeployment(deployment.Name, deployment.NodeID, a.getProjectName(deployment.Name), nil, networkName, make([]workloads.Disk, 0), nil, make([]workloads.VM, 0), nil) - workloadsDeployments[key] = &workloadsDeployment - } - - return workloadsDeployments -} - func (a *App) getProjectName(key string) string { return fmt.Sprintf("compose/%v/%v", a.Client.TwinID, key) } +func (a *App) getDeploymentName(key string) string { + return fmt.Sprintf("dl_%v", key) +} + func (a *App) loadCurrentNodeDeployments(projectName string) error { contracts, err := a.Client.ContractsGetter.ListContractsOfProjectName(projectName, true) if err != nil { @@ -330,3 +328,45 @@ func (a *App) checkIfExistAndAppend(node uint32, contractID uint64) { a.Client.State.CurrentNodeDeployments[node] = append(a.Client.State.CurrentNodeDeployments[node], contractID) } + +// TODO: Calculate total MRU and SRU while populating the deployment data +func (a *App) generateMissingNodes(ctx context.Context) error { + for _, deploymentData := range a.Config.DeploymentData { + if deploymentData.NodeID != 0 { + continue + } + + // freeCRU is not in NodeFilter? + var freeMRU, freeSRU uint64 + + for _, service := range deploymentData.Services { + freeMRU += service.Resources.Memory + freeSRU += service.Resources.Rootfs + } + + filter := types.NodeFilter{ + Status: []string{"up"}, + FreeSRU: &freeSRU, + FreeMRU: &freeMRU, + } + + nodes, _, err := a.Client.GridProxyClient.Nodes(ctx, filter, types.Limit{}) + if err != nil { + return err + } + + if len(nodes) == 0 || (len(nodes) == 1 && nodes[0].NodeID == 1) { + return fmt.Errorf("no available nodes") + } + + // TODO: still need to agree on logic to select the node + for _, node := range nodes { + if node.NodeID != 1 { + deploymentData.NodeID = uint32(node.NodeID) + break + } + } + } + + return nil +} diff --git a/grid-compose/internal/config/config.go b/grid-compose/internal/config/config.go index 364ecafb0..174893cfd 100644 --- a/grid-compose/internal/config/config.go +++ b/grid-compose/internal/config/config.go @@ -1,29 +1,51 @@ package config import ( + "errors" "fmt" "io" - types "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/pkg" + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/types" + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/utils" + "gopkg.in/yaml.v3" ) +var ( + ErrVersionNotSet = errors.New("version not set") + ErrNetworkTypeNotSet = errors.New("network type not set") + ErrServiceFlistNotSet = errors.New("service flist not set") + ErrServiceCPUResourceNotSet = errors.New("service cpu resource not set") + ErrServiceMemoryResourceNotSet = errors.New("service memory resource not set") + ErrStorageTypeNotSet = errors.New("storage type not set") + ErrStorageSizeNotSet = errors.New("storage size not set") +) + +// Config represents the configuration file content type Config struct { - Version string `yaml:"version"` - Networks map[string]types.Network `yaml:"networks"` - Services map[string]types.Service `yaml:"services"` - Storage map[string]types.Storage `yaml:"storage"` - Deployments map[string]types.Deployment `yaml:"deployments"` + Version string `yaml:"version"` + Networks map[string]types.Network `yaml:"networks"` + Services map[string]types.Service `yaml:"services"` + Volumes map[string]types.Volume `yaml:"volumes"` + + // Constructed map from config file content to be used to generate deployments + DeploymentData map[string]*struct { + Services []*types.Service + NodeID uint32 + } } +// NewConfig creates a new instance of the configuration func NewConfig() *Config { return &Config{ Networks: make(map[string]types.Network), Services: make(map[string]types.Service), - Storage: make(map[string]types.Storage), + Volumes: make(map[string]types.Volume), } } +// ValidateConfig validates the configuration file content +// TODO: Create more validation rules func (c *Config) ValidateConfig() (err error) { if c.Version == "" { return ErrVersionNotSet @@ -43,15 +65,10 @@ func (c *Config) ValidateConfig() (err error) { } } - for name, storage := range c.Storage { - if storage.Size == "" { - return fmt.Errorf("%w for storage %s", ErrStorageSizeNotSet, name) - } - } - return nil } +// LoadConfigFromReader loads the configuration file content from a reader func (c *Config) LoadConfigFromReader(configFile io.Reader) error { content, err := io.ReadAll(configFile) if err != nil { @@ -59,28 +76,54 @@ func (c *Config) LoadConfigFromReader(configFile io.Reader) error { } if err := c.UnmarshalYAML(content); err != nil { - return fmt.Errorf("failed to parse file %w", err) + return err } return nil } +// UnmarshalYAML unmarshals the configuration file content and populates the DeploymentData map func (c *Config) UnmarshalYAML(content []byte) error { if err := yaml.Unmarshal(content, c); err != nil { return err } + defaultNetName := utils.GenerateDefaultNetworkName(c.Services) + c.DeploymentData = make(map[string]*struct { + Services []*types.Service + NodeID uint32 + }) + for serviceName, service := range c.Services { - deployTo := service.DeployTo + svc := service + var netName string + if svc.Network == "" { + netName = defaultNetName + } else { + netName = svc.Network + } - if deployment, exists := c.Deployments[deployTo]; exists { - if deployment.Workloads == nil { - deployment.Workloads = make([]string, 0) + if _, ok := c.DeploymentData[netName]; !ok { + c.DeploymentData[netName] = &struct { + Services []*types.Service + NodeID uint32 + }{ + Services: make([]*types.Service, 0), + NodeID: svc.NodeID, } - deployment.Workloads = append(deployment.Workloads, serviceName) + } - c.Deployments[deployTo] = deployment + if c.DeploymentData[netName].NodeID == 0 && svc.NodeID != 0 { + c.DeploymentData[netName].NodeID = svc.NodeID } + + if svc.NodeID != 0 && svc.NodeID != c.DeploymentData[netName].NodeID { + return fmt.Errorf("service name %s node_id %d should be the same for all or some or left blank for services in the same network", serviceName, svc.NodeID) + } + + svc.Name = serviceName + + c.DeploymentData[netName].Services = append(c.DeploymentData[netName].Services, &svc) } return nil diff --git a/grid-compose/internal/config/errors.go b/grid-compose/internal/config/errors.go deleted file mode 100644 index e03236c0a..000000000 --- a/grid-compose/internal/config/errors.go +++ /dev/null @@ -1,13 +0,0 @@ -package config - -import "errors" - -var ( - ErrVersionNotSet = errors.New("version not set") - ErrNetworkTypeNotSet = errors.New("network type not set") - ErrServiceFlistNotSet = errors.New("service flist not set") - ErrServiceCPUResourceNotSet = errors.New("service cpu resource not set") - ErrServiceMemoryResourceNotSet = errors.New("service memory resource not set") - ErrStorageTypeNotSet = errors.New("storage type not set") - ErrStorageSizeNotSet = errors.New("storage size not set") -) diff --git a/grid-compose/internal/types.go b/grid-compose/internal/types.go deleted file mode 100644 index ca4a75178..000000000 --- a/grid-compose/internal/types.go +++ /dev/null @@ -1,98 +0,0 @@ -package internal - -import "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" - -type NetworkTypes struct { - Type string `yaml:"type"` -} - -type Storage struct { - Size string `yaml:"size"` -} - -type Service struct { - Flist string `yaml:"flist"` - Entrypoint string `yaml:"entrypoint,omitempty"` - Environment []string `yaml:"environment"` - Resources Resources `yaml:"resources"` - Volumes []string `yaml:"volumes"` - NetworkTypes []string `yaml:"network_types"` - Networks []string `yaml:"networks"` - HealthCheck *HealthCheck `yaml:"healthcheck,omitempty"` - DependsOn []string `yaml:"depends_on,omitempty"` - DeployTo string `yaml:"deploy_to,omitempty"` -} - -type Deployment struct { - Name string `yaml:"name"` - NodeID uint32 `yaml:"node_id"` - Network *struct { - Name string `yaml:"name"` - } `yaml:"network"` - - Workloads []string -} -type HealthCheck struct { - Test []string `yaml:"test"` - Interval string `yaml:"interval"` - Timeout string `yaml:"timeout"` - Retries uint `yaml:"retries"` -} - -type Resources struct { - CPU uint `yaml:"cpu" json:"cpu"` - Memory uint `yaml:"memory" json:"memory"` - Rootfs uint `yaml:"ssd" json:"ssd"` -} - -type WorkloadData struct { - Flist string `json:"flist"` - Network WorkloadDataNetwork `json:"network"` - ComputeCapacity Resources `json:"compute_capacity"` - Size uint `json:"size"` - Mounts []struct { - Name string `json:"name"` - MountPoint string `json:"mountpoint"` - } `json:"mounts"` - Entrypoint string `json:"entrypoint"` - Env map[string]string `json:"env"` - Corex bool `json:"corex"` -} - -type WorkloadDataNetwork struct { - PublicIP string `json:"public_ip"` - Planetary bool `json:"planetary"` - Interfaces []struct { - Network string `json:"network"` - IP string `json:"ip"` - } -} - -type Network struct { - Name string `yaml:"name"` - Description string `yaml:"description"` - Nodes []uint32 `yaml:"nodes"` - IPRange IPNet `yaml:"range"` - AddWGAccess bool `yaml:"wg"` - MyceliumKeys map[uint32][]byte `yaml:"mycelium_keys"` -} - -type IPNet struct { - IP IP `yaml:"ip"` - Mask IPMask `yaml:"mask"` -} - -type IP struct { - Type string `yaml:"type"` - IP string `yaml:"ip"` -} - -type IPMask struct { - Type string `yaml:"type"` - Mask string `yaml:"mask"` -} - -type DeploymentData struct { - Vms []workloads.VM - Disks []workloads.Disk -} diff --git a/grid-compose/internal/types/types.go b/grid-compose/internal/types/types.go new file mode 100644 index 000000000..b42fb1f02 --- /dev/null +++ b/grid-compose/internal/types/types.go @@ -0,0 +1,65 @@ +package types + +// Service represents a service in the deployment +type Service struct { + Flist string `yaml:"flist"` + Entrypoint string `yaml:"entrypoint,omitempty"` + Environment []string `yaml:"environment"` + Resources Resources `yaml:"resources"` + Volumes []string `yaml:"volumes"` + NodeID uint32 `yaml:"node_id"` + IPTypes []string `yaml:"ip_types"` + Network string `yaml:"network"` + HealthCheck *HealthCheck `yaml:"healthcheck,omitempty"` + DependsOn []string `yaml:"depends_on,omitempty"` + + Name string +} + +// Resources represents the resources required by the service +type Resources struct { + CPU uint64 `yaml:"cpu"` + Memory uint64 `yaml:"memory"` + Rootfs uint64 `yaml:"rootfs"` +} + +// HealthCheck represents the health check configuration for the service +type HealthCheck struct { + Test []string `yaml:"test"` + Interval string `yaml:"interval"` + Timeout string `yaml:"timeout"` + Retries uint `yaml:"retries"` +} + +// Volume represents a volume in the deployment +type Volume struct { + MountPoint string `yaml:"mountpoint"` + Size string `yaml:"size"` +} + +// Network represents the network configuration +type Network struct { + Name string `yaml:"name"` + Description string `yaml:"description"` + IPRange IPNet `yaml:"range"` + AddWGAccess bool `yaml:"wg"` + MyceliumKeys map[uint32][]byte `yaml:"mycelium_keys"` +} + +// IPNet represents the IP and mask of a network +type IPNet struct { + IP IP `yaml:"ip"` + Mask IPMask `yaml:"mask"` +} + +// IP represents the IP of a network +type IP struct { + Type string `yaml:"type"` + IP string `yaml:"ip"` +} + +// IPMask represents the mask of a network +type IPMask struct { + Type string `yaml:"type"` + Mask string `yaml:"mask"` +} diff --git a/grid-compose/internal/utils.go b/grid-compose/internal/utils.go deleted file mode 100644 index b03a7e69c..000000000 --- a/grid-compose/internal/utils.go +++ /dev/null @@ -1,105 +0,0 @@ -package internal - -import ( - "fmt" - "net" - "strconv" - "strings" - - "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" - types "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/pkg" -) - -func assignEnvs(vm *workloads.VM, envs []string) { - env := make(map[string]string, 0) - for _, envVar := range envs { - keyValuePair := strings.Split(envVar, "=") - env[keyValuePair[0]] = keyValuePair[1] - } - - vm.EnvVars = env -} - -func assignMounts(vm *workloads.VM, volumns []string, storage map[string]types.Storage) ([]workloads.Disk, error) { - var disks []workloads.Disk - mounts := make([]workloads.Mount, 0) - for _, volume := range volumns { - pair := strings.Split(volume, ":") - - storage := storage[pair[0]] - size, err := strconv.Atoi(strings.TrimSuffix(storage.Size, "GB")) - - if err != nil { - return nil, err - } - - disk := workloads.Disk{ - Name: pair[0], - SizeGB: size, - } - - disks = append(disks, disk) - - mounts = append(mounts, workloads.Mount{ - DiskName: disk.Name, - MountPoint: pair[1], - }) - } - vm.Mounts = mounts - - return disks, nil -} - -func assignNetworksTypes(vm *workloads.VM, networksTypes []string) error { - for _, networkType := range networksTypes { - switch networkType { - case "ip4": - vm.PublicIP = true - case "ip6": - vm.PublicIP6 = true - case "ygg": - vm.Planetary = true - case "myc": - seed, err := getRandomMyceliumIPSeed() - if err != nil { - return fmt.Errorf("failed to get mycelium seed %w", err) - } - vm.MyceliumIPSeed = seed - } - } - - return nil -} - -func generateIPNet(ip types.IP, mask types.IPMask) net.IPNet { - var ipNet net.IPNet - - switch ip.Type { - case "ip4": - ipSplit := strings.Split(ip.IP, ".") - byte1, _ := strconv.ParseUint(ipSplit[0], 10, 8) - byte2, _ := strconv.ParseUint(ipSplit[1], 10, 8) - byte3, _ := strconv.ParseUint(ipSplit[2], 10, 8) - byte4, _ := strconv.ParseUint(ipSplit[3], 10, 8) - - ipNet.IP = net.IPv4(byte(byte1), byte(byte2), byte(byte3), byte(byte4)) - default: - return ipNet - } - - var maskIP net.IPMask - - switch mask.Type { - case "cidr": - maskSplit := strings.Split(mask.Mask, "/") - maskOnes, _ := strconv.ParseInt(maskSplit[0], 10, 8) - maskBits, _ := strconv.ParseInt(maskSplit[1], 10, 8) - - maskIP = net.CIDRMask(int(maskOnes), int(maskBits)) - ipNet.Mask = maskIP - default: - return ipNet - } - - return ipNet -} diff --git a/grid-compose/internal/mycelium_seed.go b/grid-compose/internal/utils/mycelium_seed.go similarity index 71% rename from grid-compose/internal/mycelium_seed.go rename to grid-compose/internal/utils/mycelium_seed.go index 2677e5d76..2e5a04dbd 100644 --- a/grid-compose/internal/mycelium_seed.go +++ b/grid-compose/internal/utils/mycelium_seed.go @@ -1,4 +1,4 @@ -package internal +package utils import ( "crypto/rand" @@ -6,7 +6,7 @@ import ( "github.com/threefoldtech/zos/pkg/gridtypes/zos" ) -func getRandomMyceliumIPSeed() ([]byte, error) { +func GetRandomMyceliumIPSeed() ([]byte, error) { key := make([]byte, zos.MyceliumIPSeedLen) _, err := rand.Read(key) return key, err diff --git a/grid-compose/internal/utils/network.go b/grid-compose/internal/utils/network.go new file mode 100644 index 000000000..19d9abd0d --- /dev/null +++ b/grid-compose/internal/utils/network.go @@ -0,0 +1,53 @@ +package utils + +import ( + "fmt" + "net" + "strconv" + "strings" + + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/types" +) + +func GenerateDefaultNetworkName(services map[string]types.Service) string { + var defaultNetName string + + for serviceName := range services { + defaultNetName += serviceName[:2] + } + + return fmt.Sprintf("net_%s", defaultNetName) +} + +func GenerateIPNet(ip types.IP, mask types.IPMask) net.IPNet { + var ipNet net.IPNet + + switch ip.Type { + case "ipv4": + ipSplit := strings.Split(ip.IP, ".") + byte1, _ := strconv.ParseUint(ipSplit[0], 10, 8) + byte2, _ := strconv.ParseUint(ipSplit[1], 10, 8) + byte3, _ := strconv.ParseUint(ipSplit[2], 10, 8) + byte4, _ := strconv.ParseUint(ipSplit[3], 10, 8) + + ipNet.IP = net.IPv4(byte(byte1), byte(byte2), byte(byte3), byte(byte4)) + default: + return ipNet + } + + var maskIP net.IPMask + + switch mask.Type { + case "cidr": + maskSplit := strings.Split(mask.Mask, "/") + maskOnes, _ := strconv.ParseInt(maskSplit[0], 10, 8) + maskBits, _ := strconv.ParseInt(maskSplit[1], 10, 8) + + maskIP = net.CIDRMask(int(maskOnes), int(maskBits)) + ipNet.Mask = maskIP + default: + return ipNet + } + + return ipNet +} diff --git a/grid-compose/internal/utils/validation.go b/grid-compose/internal/utils/validation.go new file mode 100644 index 000000000..908f6baf5 --- /dev/null +++ b/grid-compose/internal/utils/validation.go @@ -0,0 +1,20 @@ +package utils + +import "github.com/cosmos/go-bip39" + +func ValidateCredentials(mnemonics, network string) bool { + return validateMnemonics(mnemonics) && validateNetwork(network) +} + +func validateMnemonics(mnemonics string) bool { + return bip39.IsMnemonicValid(mnemonics) +} + +func validateNetwork(network string) bool { + switch network { + case "test", "dev", "main", "qa": + return true + default: + return false + } +} diff --git a/grid-compose/internal/utils/vm.go b/grid-compose/internal/utils/vm.go new file mode 100644 index 000000000..748aa6fac --- /dev/null +++ b/grid-compose/internal/utils/vm.go @@ -0,0 +1,93 @@ +package utils + +import ( + "fmt" + "strconv" + "strings" + + "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/types" +) + +func AssignEnvs(vm *workloads.VM, envs []string) { + env := make(map[string]string, 0) + for _, envVar := range envs { + keyValuePair := strings.Split(envVar, "=") + env[keyValuePair[0]] = keyValuePair[1] + } + + vm.EnvVars = env +} + +// TODO: Create a parser to parse the size given to each field in service +func AssignMounts(vm *workloads.VM, serviceVolumes []string, volumes map[string]types.Volume) ([]workloads.Disk, error) { + var disks []workloads.Disk + mounts := make([]workloads.Mount, 0) + for _, volumeName := range serviceVolumes { + volume := volumes[volumeName] + + size, err := strconv.Atoi(strings.TrimSuffix(volume.Size, "GB")) + + if err != nil { + return nil, err + } + + disk := workloads.Disk{ + Name: volumeName, + SizeGB: size, + } + + disks = append(disks, disk) + + mounts = append(mounts, workloads.Mount{ + DiskName: disk.Name, + MountPoint: volume.MountPoint, + }) + } + vm.Mounts = mounts + + return disks, nil +} + +func AssignNetworksTypes(vm *workloads.VM, ipTypes []string) error { + for _, ipType := range ipTypes { + switch ipType { + case "ipv4": + vm.PublicIP = true + case "ipv6": + vm.PublicIP6 = true + case "ygg": + vm.Planetary = true + case "myc": + seed, err := GetRandomMyceliumIPSeed() + if err != nil { + return fmt.Errorf("failed to get mycelium seed %w", err) + } + vm.MyceliumIPSeed = seed + } + } + + return nil +} + +func GetVmAddresses(vm workloads.VM) string { + var addresses strings.Builder + + if vm.IP != "" { + addresses.WriteString(fmt.Sprintf("wireguard: %v, ", vm.IP)) + } + if vm.Planetary { + addresses.WriteString(fmt.Sprintf("yggdrasil: %v, ", vm.PlanetaryIP)) + } + if vm.PublicIP { + addresses.WriteString(fmt.Sprintf("publicIp4: %v, ", vm.ComputedIP)) + } + if vm.PublicIP6 { + addresses.WriteString(fmt.Sprintf("publicIp6: %v, ", vm.ComputedIP6)) + } + if len(vm.MyceliumIPSeed) != 0 { + addresses.WriteString(fmt.Sprintf("mycelium: %v, ", vm.MyceliumIP)) + } + + return strings.TrimSuffix(addresses.String(), ", ") +} diff --git a/grid-compose/internal/validation.go b/grid-compose/internal/validation.go deleted file mode 100644 index 945fd3c36..000000000 --- a/grid-compose/internal/validation.go +++ /dev/null @@ -1,7 +0,0 @@ -package internal - -import "github.com/cosmos/go-bip39" - -func ValidateMnemonics(mnemonics string) bool { - return bip39.IsMnemonicValid(mnemonics) -} diff --git a/grid-compose/internal/vmaddresses.go b/grid-compose/internal/vmaddresses.go deleted file mode 100644 index 0159e7b9f..000000000 --- a/grid-compose/internal/vmaddresses.go +++ /dev/null @@ -1,30 +0,0 @@ -package internal - -import ( - "fmt" - "strings" - - "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" -) - -func getVmAddresses(vm workloads.VM) string { - var addresses strings.Builder - - if vm.IP != "" { - addresses.WriteString(fmt.Sprintf("wireguard: %v, ", vm.IP)) - } - if vm.Planetary { - addresses.WriteString(fmt.Sprintf("yggdrasil: %v, ", vm.PlanetaryIP)) - } - if vm.PublicIP { - addresses.WriteString(fmt.Sprintf("publicIp4: %v, ", vm.ComputedIP)) - } - if vm.PublicIP6 { - addresses.WriteString(fmt.Sprintf("publicIp6: %v, ", vm.ComputedIP6)) - } - if len(vm.MyceliumIPSeed) != 0 { - addresses.WriteString(fmt.Sprintf("mycelium: %v, ", vm.MyceliumIP)) - } - - return strings.TrimSuffix(addresses.String(), ", ") -} From 9e227e1320a859370150d019efe42a201e0d908d Mon Sep 17 00:00:00 2001 From: eyad-hussein Date: Tue, 13 Aug 2024 05:16:42 +0300 Subject: [PATCH 11/20] docs: edit readme file --- grid-compose/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/grid-compose/README.md b/grid-compose/README.md index 78ba49758..91d7b2fdd 100644 --- a/grid-compose/README.md +++ b/grid-compose/README.md @@ -35,4 +35,7 @@ Then: ``` For example: + +```bash ./bin/grid-compose ps -f example/multiple_services_diff_network_3.yml +``` From b0f3b2263bf5bf542713a6f5ecae72712a88695d Mon Sep 17 00:00:00 2001 From: eyad-hussein Date: Tue, 13 Aug 2024 20:24:52 +0300 Subject: [PATCH 12/20] docs: add supported cases for deployment, update readme --- grid-compose/README.md | 54 +++++++++++++++- grid-compose/docs/cases.md | 64 +++++++++++++++++++ grid-compose/docs/down.md | 0 grid-compose/docs/ps.md | 0 grid-compose/docs/up.md | 0 grid-compose/docs/version.md | 0 .../multiple_services_diff_network_1.yml | 0 .../multiple_services_diff_network_2.yml | 0 .../multiple_services_diff_network_3.yml | 0 .../single_service_1.yml | 0 .../single_service_2.yml | 0 grid-compose/examples/single_service_3.yml | 24 +++++++ .../two_services_same_network_1.yml | 0 .../two_services_same_network_2.yml | 0 .../two_services_same_network_3.yml | 0 15 files changed, 140 insertions(+), 2 deletions(-) delete mode 100644 grid-compose/docs/down.md delete mode 100644 grid-compose/docs/ps.md delete mode 100644 grid-compose/docs/up.md delete mode 100644 grid-compose/docs/version.md rename grid-compose/{example => examples}/multiple_services_diff_network_1.yml (100%) rename grid-compose/{example => examples}/multiple_services_diff_network_2.yml (100%) rename grid-compose/{example => examples}/multiple_services_diff_network_3.yml (100%) rename grid-compose/{example => examples}/single_service_1.yml (100%) rename grid-compose/{example => examples}/single_service_2.yml (100%) create mode 100644 grid-compose/examples/single_service_3.yml rename grid-compose/{example => examples}/two_services_same_network_1.yml (100%) rename grid-compose/{example => examples}/two_services_same_network_2.yml (100%) rename grid-compose/{example => examples}/two_services_same_network_3.yml (100%) diff --git a/grid-compose/README.md b/grid-compose/README.md index 91d7b2fdd..6c285c284 100644 --- a/grid-compose/README.md +++ b/grid-compose/README.md @@ -13,13 +13,23 @@ is a tool for running multi-vm applications on TFGrid defined using a Yaml forma grid-compose [OPTIONS] [COMMAND] OPTIONS: - -f path to yaml file, default is ./grid-compose.yaml + -f, --file: path to yaml file, default is ./grid-compose.yaml COMMANDS: - version: shows the project version - up: deploy the app - down: cancel all deployments - ps: list deployments on the grid + OPTIONS: + - -v, --verbose: show full details of each deployment + - -o, --output: redirects the output to a file given its path +``` + +Export env vars using: + +```bash +export MNEMONIC=your_mnemonics +export NETWORK=working_network ``` Run: @@ -28,7 +38,7 @@ Run: make build ``` -Then: +To use any of the commands, run: ```bash ./bin/grid-compose [COMMAND] @@ -39,3 +49,43 @@ For example: ```bash ./bin/grid-compose ps -f example/multiple_services_diff_network_3.yml ``` + +## Usage For Each Command + +### up + +The up command deploys the services defined in the yaml file to the grid. + +Refer to the [cases](docs/cases.md) for more information on the cases supported. + +Refer to examples in the [examples](examples) directory to have a look at different possible configurations. + +```bash +./bin/grid-compose up [OPTIONS] +``` + +OPTIONS: + +- `-f, --file`: path to the yaml file, default is `./grid-compose.yaml` + +### down + +```bash +./bin/grid-compose down [OPTIONS] +``` + +OPTIONS: + +- `-f, --file`: path to the yaml file, default is `./grid-compose.yaml` + +### ps + +```bash +./bin/grid-compose ps [FLAGS] [OPTIONS] +``` + +OPTIONS: + +- `-f, --file`: path to the yaml file, default is `./grid-compose.yaml` +- `-v, --verbose`: show full details of each deployment +- `-o, --output`: redirects the output to a file given its path(in json format) diff --git a/grid-compose/docs/cases.md b/grid-compose/docs/cases.md index e69de29bb..12e178342 100644 --- a/grid-compose/docs/cases.md +++ b/grid-compose/docs/cases.md @@ -0,0 +1,64 @@ +These are most if not all the cases supported by the grid compose cli when deploying one or more service to the grid. + +## Single Service + +### Case 1 - Node ID Not Given + No Assigned Network + +This is probably the simplest case there is. + +- Filter the nodes based on the resources given to the service and choose a random one. +- Generate a default network and assign it to the deployment. + +Refer to example [single_service_1.yml](../examples/single_service_1.yml) + +### Case 2 - Node ID Given + No Assigned Network + +- Simply use the the node id given to deploy the service. + - (**CURRENT BEHAVIOR**) Return an error if node is not available + - (**TODO BEHAVIOR**) If the available resources in the given node are less than what the service needs, filter the nodes and check if there is one that can be used. + - If there is a node available, prompt the user if they would like to use it instead. +- Generate a default network and assign it to the deployment. + +Refer to example [single_service_2.yml](../examples/single_service_2.yml) + +### Case 3 - Assigned Network + +- Either use the assigned node id or filter the nodes for an available node if no node id given. +- Use the network assigned to the service when deploying. + +Refer to example [single_service_3.yml](../examples/single_service_3.yml) + +## Multiple Services + +Dealing with multiple services will depend on the networks assigned to each service. In a nutshell, it is assumed that **if two services are assigned the same network they are going to be in the same deployment, which means in the same node,** so failing to stick to this assumption will yield errors and no service will be deployed. + +### Same Network/No Network + +This is a common general case: Laying down services a user needs to deploy in the same node using a defined network or not defining any network at all. + +#### Case 1 - Node ID Given + +Essentially what is required is that at least one service is assigned a node id and automatically all the other services assigned to the same network will be deployed using this node. + +It is also possible to assign a node id to some of the services or even all of them, but keep in mind that **if two services running on the same network and each one is assigned a different node id, this will cause an error and nothing will be deployed.** + +#### Case 2 - No Node ID Given + +This is a more common case, the user mostly probably will not care to provide any node ids. In that case: + +- The node id will be filtered based on the total resources for the services provided. + +
+If all the services are assigned a network, then all of them will be deployed using that network. + +If no networks are defined, then all the services will use the **default generated network**. + +Refer to examples + +- [two_services_same_network_1.yml](../examples/two_services_same_network_1.yml) +- [two_services_same_network_2.yml](../examples/two_services_same_network_2.yml) +- [two_services_same_network_3.yml](../examples/two_services_same_network_3.yml) + +### Different Networks + +Simple divide the services into groups having the same network(given or generated) and deal with each group using the approached described in the previous [section](#same-networkno-network). diff --git a/grid-compose/docs/down.md b/grid-compose/docs/down.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/grid-compose/docs/ps.md b/grid-compose/docs/ps.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/grid-compose/docs/up.md b/grid-compose/docs/up.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/grid-compose/docs/version.md b/grid-compose/docs/version.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/grid-compose/example/multiple_services_diff_network_1.yml b/grid-compose/examples/multiple_services_diff_network_1.yml similarity index 100% rename from grid-compose/example/multiple_services_diff_network_1.yml rename to grid-compose/examples/multiple_services_diff_network_1.yml diff --git a/grid-compose/example/multiple_services_diff_network_2.yml b/grid-compose/examples/multiple_services_diff_network_2.yml similarity index 100% rename from grid-compose/example/multiple_services_diff_network_2.yml rename to grid-compose/examples/multiple_services_diff_network_2.yml diff --git a/grid-compose/example/multiple_services_diff_network_3.yml b/grid-compose/examples/multiple_services_diff_network_3.yml similarity index 100% rename from grid-compose/example/multiple_services_diff_network_3.yml rename to grid-compose/examples/multiple_services_diff_network_3.yml diff --git a/grid-compose/example/single_service_1.yml b/grid-compose/examples/single_service_1.yml similarity index 100% rename from grid-compose/example/single_service_1.yml rename to grid-compose/examples/single_service_1.yml diff --git a/grid-compose/example/single_service_2.yml b/grid-compose/examples/single_service_2.yml similarity index 100% rename from grid-compose/example/single_service_2.yml rename to grid-compose/examples/single_service_2.yml diff --git a/grid-compose/examples/single_service_3.yml b/grid-compose/examples/single_service_3.yml new file mode 100644 index 000000000..548635e87 --- /dev/null +++ b/grid-compose/examples/single_service_3.yml @@ -0,0 +1,24 @@ +version: '1.0.0' + +networks: + net1: + name: 'miaminet' + range: + ip: + type: ipv4 + ip: 10.20.0.0 + mask: + type: cidr + mask: 16/32 + wg: true + +services: + server: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + entrypoint: '/sbin/zinit init' + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + node_id: 14 + network: net1 diff --git a/grid-compose/example/two_services_same_network_1.yml b/grid-compose/examples/two_services_same_network_1.yml similarity index 100% rename from grid-compose/example/two_services_same_network_1.yml rename to grid-compose/examples/two_services_same_network_1.yml diff --git a/grid-compose/example/two_services_same_network_2.yml b/grid-compose/examples/two_services_same_network_2.yml similarity index 100% rename from grid-compose/example/two_services_same_network_2.yml rename to grid-compose/examples/two_services_same_network_2.yml diff --git a/grid-compose/example/two_services_same_network_3.yml b/grid-compose/examples/two_services_same_network_3.yml similarity index 100% rename from grid-compose/example/two_services_same_network_3.yml rename to grid-compose/examples/two_services_same_network_3.yml From 4bf7aaf735eba7f693a073cba6911037b02416a5 Mon Sep 17 00:00:00 2001 From: eyad-hussein Date: Sat, 17 Aug 2024 09:23:04 +0300 Subject: [PATCH 13/20] feat: add support for depends_on optoin(dependency resolution) refactor: refactor folder structure and add more examples --- grid-compose/.gitignore | 1 + grid-compose/cmd/down.go | 6 +- grid-compose/cmd/ps.go | 15 +- grid-compose/cmd/root.go | 34 +- grid-compose/cmd/up.go | 6 +- grid-compose/cmd/version.go | 10 +- .../examples/dependency/diff_networks.yml | 73 ++++ .../dependency/multiple_dependencies.yml | 63 +++ .../examples/dependency/same_network.yml | 50 +++ .../multiple_services_diff_network_1.yml | 6 +- .../multiple_services_diff_network_2.yml | 16 +- .../multiple_services_diff_network_3.yml | 14 +- grid-compose/examples/single_service_2.yml | 4 +- grid-compose/examples/single_service_3.yml | 4 +- .../examples/two_services_same_network_2.yml | 6 +- .../examples/two_services_same_network_3.yml | 7 +- grid-compose/internal/app.go | 372 ------------------ grid-compose/internal/app/app.go | 108 +++++ grid-compose/internal/app/down.go | 22 ++ grid-compose/internal/app/ps.go | 121 ++++++ grid-compose/internal/app/up.go | 273 +++++++++++++ grid-compose/internal/app/version.go | 1 + grid-compose/internal/config/config.go | 90 ++--- grid-compose/internal/convert/convert.go | 100 +++++ .../internal/{utils => generator}/network.go | 2 +- grid-compose/internal/types/common.go | 11 + grid-compose/internal/types/dependency.go | 76 ++++ grid-compose/internal/types/network.go | 28 ++ .../internal/types/{types.go => service.go} | 29 -- grid-compose/internal/utils/mycelium_seed.go | 13 - grid-compose/internal/utils/validation.go | 20 - grid-compose/internal/utils/vm.go | 93 ----- 32 files changed, 1008 insertions(+), 666 deletions(-) create mode 100644 grid-compose/examples/dependency/diff_networks.yml create mode 100644 grid-compose/examples/dependency/multiple_dependencies.yml create mode 100644 grid-compose/examples/dependency/same_network.yml delete mode 100644 grid-compose/internal/app.go create mode 100644 grid-compose/internal/app/app.go create mode 100644 grid-compose/internal/app/down.go create mode 100644 grid-compose/internal/app/ps.go create mode 100644 grid-compose/internal/app/up.go create mode 100644 grid-compose/internal/app/version.go create mode 100644 grid-compose/internal/convert/convert.go rename grid-compose/internal/{utils => generator}/network.go (98%) create mode 100644 grid-compose/internal/types/common.go create mode 100644 grid-compose/internal/types/dependency.go create mode 100644 grid-compose/internal/types/network.go rename grid-compose/internal/types/{types.go => service.go} (62%) delete mode 100644 grid-compose/internal/utils/mycelium_seed.go delete mode 100644 grid-compose/internal/utils/validation.go delete mode 100644 grid-compose/internal/utils/vm.go diff --git a/grid-compose/.gitignore b/grid-compose/.gitignore index e1ed12ba8..e30629ae3 100644 --- a/grid-compose/.gitignore +++ b/grid-compose/.gitignore @@ -2,3 +2,4 @@ bin/* .pre-commit-config.yaml out full_example.yml +invalid diff --git a/grid-compose/cmd/down.go b/grid-compose/cmd/down.go index 06b50916f..a04cec5a8 100644 --- a/grid-compose/cmd/down.go +++ b/grid-compose/cmd/down.go @@ -3,18 +3,16 @@ package cmd import ( "github.com/rs/zerolog/log" "github.com/spf13/cobra" + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/app" ) var downCmd = &cobra.Command{ Use: "down", Short: "cancel your project on the grid", Run: func(cmd *cobra.Command, args []string) { + app := cmd.Context().Value("app").(*app.App) if err := app.Down(); err != nil { log.Fatal().Err(err).Send() } }, } - -func init() { - rootCmd.AddCommand(downCmd) -} diff --git a/grid-compose/cmd/ps.go b/grid-compose/cmd/ps.go index 04ebdc1e1..f677f6ccc 100644 --- a/grid-compose/cmd/ps.go +++ b/grid-compose/cmd/ps.go @@ -3,22 +3,21 @@ package cmd import ( "github.com/rs/zerolog/log" "github.com/spf13/cobra" + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/app" ) var psCmd = &cobra.Command{ Use: "ps", Short: "list deployments on the grid", Run: func(cmd *cobra.Command, args []string) { - flags := cmd.Flags() + verbose, err := cmd.Flags().GetBool("verbose") + if err != nil { + log.Fatal().Err(err).Send() + } - if err := app.Ps(cmd.Context(), flags); err != nil { + app := cmd.Context().Value("app").(*app.App) + if err := app.Ps(cmd.Context(), verbose); err != nil { log.Fatal().Err(err).Send() } }, } - -func init() { - psCmd.PersistentFlags().BoolP("verbose", "v", false, "all information about deployed services") - psCmd.PersistentFlags().StringP("output", "o", "", "output result to a file") - rootCmd.AddCommand(psCmd) -} diff --git a/grid-compose/cmd/root.go b/grid-compose/cmd/root.go index c55e2b259..8b1d7ffbe 100644 --- a/grid-compose/cmd/root.go +++ b/grid-compose/cmd/root.go @@ -1,19 +1,13 @@ package cmd import ( + "context" "os" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/spf13/cobra" - "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal" -) - -var ( - app *internal.App - configPath string - network string - mnemonic string + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/app" ) func Execute() { @@ -22,23 +16,35 @@ func Execute() { } } -// TODO: Validate command line arguments +// TODO: validate command line arguments var rootCmd = &cobra.Command{ Use: "grid-compose", Short: "Grid-Compose is a tool for running multi-vm applications on TFGrid defined using a Yaml formatted file.", PersistentPreRun: func(cmd *cobra.Command, args []string) { - var err error - app, err = internal.NewApp(network, mnemonic, configPath) + network := os.Getenv("NETWORK") + mnemonic := os.Getenv("MNEMONIC") + configPath, _ := cmd.Flags().GetString("file") + + app, err := app.NewApp(network, mnemonic, configPath) + if err != nil { log.Fatal().Err(err).Send() } + + ctx := context.WithValue(cmd.Context(), "app", app) + cmd.SetContext(ctx) }, } func init() { - network = os.Getenv("NETWORK") - mnemonic = os.Getenv("MNEMONIC") - rootCmd.PersistentFlags().StringVarP(&configPath, "file", "f", "./grid-compose.yaml", "the grid-compose configuration file") + rootCmd.PersistentFlags().StringP("file", "f", "./grid-compose.yaml", "the grid-compose configuration file") + + rootCmd.AddCommand(downCmd) + rootCmd.AddCommand(upCmd) + rootCmd.AddCommand(versionCmd) + + psCmd.PersistentFlags().BoolP("verbose", "v", false, "all information about deployed services") + rootCmd.AddCommand(psCmd) log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) } diff --git a/grid-compose/cmd/up.go b/grid-compose/cmd/up.go index b29882c5b..8030f43bb 100644 --- a/grid-compose/cmd/up.go +++ b/grid-compose/cmd/up.go @@ -3,18 +3,16 @@ package cmd import ( "github.com/rs/zerolog/log" "github.com/spf13/cobra" + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/app" ) var upCmd = &cobra.Command{ Use: "up", Short: "deploy application on the grid", Run: func(cmd *cobra.Command, args []string) { + app := cmd.Context().Value("app").(*app.App) if err := app.Up(cmd.Context()); err != nil { log.Fatal().Err(err).Send() } }, } - -func init() { - rootCmd.AddCommand(upCmd) -} diff --git a/grid-compose/cmd/version.go b/grid-compose/cmd/version.go index af884636b..ebe132cf0 100644 --- a/grid-compose/cmd/version.go +++ b/grid-compose/cmd/version.go @@ -2,7 +2,7 @@ package cmd import ( - "fmt" + "log" "github.com/spf13/cobra" ) @@ -16,11 +16,7 @@ var versionCmd = &cobra.Command{ Use: "version", Short: "Get latest build tag", Run: func(cmd *cobra.Command, args []string) { - fmt.Println(version) - fmt.Println(commit) + log.Println(version) + log.Println(commit) }, } - -func init() { - rootCmd.AddCommand(versionCmd) -} diff --git a/grid-compose/examples/dependency/diff_networks.yml b/grid-compose/examples/dependency/diff_networks.yml new file mode 100644 index 000000000..a4d1bb639 --- /dev/null +++ b/grid-compose/examples/dependency/diff_networks.yml @@ -0,0 +1,73 @@ +version: '1.0.0' + +networks: + net1: + name: 'miaminet' + range: + ip: + type: ipv4 + ip: 10.20.0.0 + mask: + type: cidr + mask: 16/32 + wg: true + net2: + name: 'cartoonnetwork' + range: + ip: + type: ipv4 + ip: 10.20.0.0 + mask: + type: cidr + mask: 16/32 + +services: + server: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + - ygg + entrypoint: '/sbin/zinit init' + volumes: + - webdata + resources: + cpu: 1 + memory: 2048 + rootfs: 2048 + network: net2 + depends_on: + - frontend + frontend: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + entrypoint: '/sbin/zinit init' + volumes: + - dbdata + resources: + cpu: 1 + memory: 2048 + rootfs: 2048 + network: net1 + node_id: 144 + depends_on: + - database + database: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + entrypoint: '/sbin/zinit init' + volumes: + - dbdata + resources: + cpu: 1 + memory: 2048 + rootfs: 2048 + network: net2 + node_id: 144 + +volumes: + webdata: + mountpoint: '/data' + size: 10GB + dbdata: + mountpoint: '/var/lib/postgresql/data' + size: 10GB diff --git a/grid-compose/examples/dependency/multiple_dependencies.yml b/grid-compose/examples/dependency/multiple_dependencies.yml new file mode 100644 index 000000000..779e81b05 --- /dev/null +++ b/grid-compose/examples/dependency/multiple_dependencies.yml @@ -0,0 +1,63 @@ +version: '1.0.0' + +networks: + net2: + name: 'cartoonnetwork' + range: + ip: + type: ipv4 + ip: 10.20.0.0 + mask: + type: cidr + mask: 16/32 + +services: + server: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + - ygg + entrypoint: '/sbin/zinit init' + volumes: + - webdata + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + network: net2 + frontend: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + entrypoint: '/sbin/zinit init' + volumes: + - dbdata + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + network: net2 + node_id: 144 + depends_on: + - server + - database + database: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + entrypoint: '/sbin/zinit init' + volumes: + - dbdata + resources: + cpu: 1 + memory: 2048 + rootfs: 25600 + network: net2 + +volumes: + webdata: + mountpoint: '/data' + size: 10GB + dbdata: + mountpoint: '/var/lib/postgresql/data' + size: 10GB diff --git a/grid-compose/examples/dependency/same_network.yml b/grid-compose/examples/dependency/same_network.yml new file mode 100644 index 000000000..c529cb270 --- /dev/null +++ b/grid-compose/examples/dependency/same_network.yml @@ -0,0 +1,50 @@ +version: '1.0.0' + +services: + server: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + - ygg + entrypoint: '/sbin/zinit init' + volumes: + - webdata + resources: + cpu: 1 + memory: 2048 + rootfs: 2048 + depends_on: + - database + frontend: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + entrypoint: '/sbin/zinit init' + volumes: + - dbdata + resources: + cpu: 1 + memory: 2048 + rootfs: 2048 + node_id: 144 + depends_on: + - server + database: + flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + ip_types: + - ipv4 + entrypoint: '/sbin/zinit init' + volumes: + - dbdata + resources: + cpu: 1 + memory: 2048 + rootfs: 2048 + +volumes: + webdata: + mountpoint: '/data' + size: 10GB + dbdata: + mountpoint: '/var/lib/postgresql/data' + size: 10GB diff --git a/grid-compose/examples/multiple_services_diff_network_1.yml b/grid-compose/examples/multiple_services_diff_network_1.yml index 54293dbdf..6a8848161 100644 --- a/grid-compose/examples/multiple_services_diff_network_1.yml +++ b/grid-compose/examples/multiple_services_diff_network_1.yml @@ -33,7 +33,7 @@ services: resources: cpu: 1 memory: 2048 - rootfs: 25600 + rootfs: 2048 network: net2 frontend: flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' @@ -45,7 +45,7 @@ services: resources: cpu: 1 memory: 2048 - rootfs: 25600 + rootfs: 2048 network: net1 node_id: 144 database: @@ -58,7 +58,7 @@ services: resources: cpu: 1 memory: 2048 - rootfs: 25600 + rootfs: 2048 network: net2 volumes: diff --git a/grid-compose/examples/multiple_services_diff_network_2.yml b/grid-compose/examples/multiple_services_diff_network_2.yml index ca35be864..cda999749 100644 --- a/grid-compose/examples/multiple_services_diff_network_2.yml +++ b/grid-compose/examples/multiple_services_diff_network_2.yml @@ -24,9 +24,6 @@ networks: services: server: flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 - - ygg entrypoint: '/sbin/zinit init' volumes: - webdata @@ -34,21 +31,18 @@ services: resources: cpu: 1 memory: 2048 - rootfs: 25600 + rootfs: 2048 network: net2 server2: - node_id: 14 + node_id: 11 flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 - - ygg entrypoint: '/sbin/zinit init' volumes: - webdata resources: cpu: 1 memory: 2048 - rootfs: 25600 + rootfs: 2048 network: net1 frontend: flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' @@ -60,7 +54,7 @@ services: resources: cpu: 1 memory: 2048 - rootfs: 25600 + rootfs: 2048 network: net1 database: flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' @@ -72,7 +66,7 @@ services: resources: cpu: 1 memory: 2048 - rootfs: 25600 + rootfs: 2048 network: net2 volumes: diff --git a/grid-compose/examples/multiple_services_diff_network_3.yml b/grid-compose/examples/multiple_services_diff_network_3.yml index 055e4723c..5d503ef99 100644 --- a/grid-compose/examples/multiple_services_diff_network_3.yml +++ b/grid-compose/examples/multiple_services_diff_network_3.yml @@ -24,29 +24,23 @@ networks: services: server: flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 - - ygg entrypoint: '/sbin/zinit init' volumes: - webdata resources: cpu: 1 memory: 2048 - rootfs: 25600 + rootfs: 2048 network: net2 server2: flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 - - ygg entrypoint: '/sbin/zinit init' volumes: - webdata resources: cpu: 1 memory: 2048 - rootfs: 25600 + rootfs: 2048 network: net1 frontend: flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' @@ -58,7 +52,7 @@ services: resources: cpu: 1 memory: 2048 - rootfs: 25600 + rootfs: 2048 network: net1 database: flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' @@ -70,7 +64,7 @@ services: resources: cpu: 1 memory: 2048 - rootfs: 25600 + rootfs: 2048 network: net2 volumes: diff --git a/grid-compose/examples/single_service_2.yml b/grid-compose/examples/single_service_2.yml index 7bba8e257..d984bf0fa 100644 --- a/grid-compose/examples/single_service_2.yml +++ b/grid-compose/examples/single_service_2.yml @@ -7,5 +7,5 @@ services: resources: cpu: 1 memory: 2048 - rootfs: 25600 - node_id: 14 + rootfs: 2048 + node_id: 144 diff --git a/grid-compose/examples/single_service_3.yml b/grid-compose/examples/single_service_3.yml index 548635e87..8775065a5 100644 --- a/grid-compose/examples/single_service_3.yml +++ b/grid-compose/examples/single_service_3.yml @@ -19,6 +19,6 @@ services: resources: cpu: 1 memory: 2048 - rootfs: 25600 - node_id: 14 + rootfs: 2048 + node_id: 144 network: net1 diff --git a/grid-compose/examples/two_services_same_network_2.yml b/grid-compose/examples/two_services_same_network_2.yml index dd437249e..f3549b68f 100644 --- a/grid-compose/examples/two_services_same_network_2.yml +++ b/grid-compose/examples/two_services_same_network_2.yml @@ -9,11 +9,11 @@ services: entrypoint: '/sbin/zinit init' volumes: - webdata - node_id: 14 + node_id: 144 resources: cpu: 1 memory: 2048 - rootfs: 25600 + rootfs: 2048 database: flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' @@ -25,7 +25,7 @@ services: resources: cpu: 1 memory: 2048 - rootfs: 25600 + rootfs: 2048 volumes: webdata: diff --git a/grid-compose/examples/two_services_same_network_3.yml b/grid-compose/examples/two_services_same_network_3.yml index 58e70850e..1a5630a85 100644 --- a/grid-compose/examples/two_services_same_network_3.yml +++ b/grid-compose/examples/two_services_same_network_3.yml @@ -3,16 +3,13 @@ version: '1.0.0' services: server: flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 - - ygg entrypoint: '/sbin/zinit init' volumes: - webdata resources: cpu: 1 memory: 2048 - rootfs: 25600 + rootfs: 2048 database: flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' @@ -24,7 +21,7 @@ services: resources: cpu: 1 memory: 2048 - rootfs: 25600 + rootfs: 2048 volumes: webdata: diff --git a/grid-compose/internal/app.go b/grid-compose/internal/app.go deleted file mode 100644 index a923588fd..000000000 --- a/grid-compose/internal/app.go +++ /dev/null @@ -1,372 +0,0 @@ -package internal - -import ( - "context" - "encoding/json" - "fmt" - "net" - "os" - "strconv" - "strings" - - "github.com/rs/zerolog/log" - "github.com/spf13/pflag" - "github.com/threefoldtech/tfgrid-sdk-go/grid-client/deployer" - "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" - "github.com/threefoldtech/tfgrid-sdk-go/grid-proxy/pkg/types" - "github.com/threefoldtech/zos/pkg/gridtypes" - - "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/config" - "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/utils" -) - -// App is the main application struct that holds the client and the config data -type App struct { - Client *deployer.TFPluginClient - Config *config.Config -} - -// NewApp creates a new instance of the application -func NewApp(net, mnemonic, configPath string) (*App, error) { - if !utils.ValidateCredentials(mnemonic, net) { - return nil, fmt.Errorf("invalid mnemonic or network") - } - - configFile, err := os.Open(configPath) - if err != nil { - return nil, err - } - - defer configFile.Close() - - config := config.NewConfig() - err = config.LoadConfigFromReader(configFile) - if err != nil { - return nil, fmt.Errorf("failed to load config from file %w", err) - } - - if err := config.ValidateConfig(); err != nil { - return nil, fmt.Errorf("failed to validate config %w", err) - } - - client, err := deployer.NewTFPluginClient(mnemonic, deployer.WithNetwork(net)) - if err != nil { - return nil, fmt.Errorf("failed to load grid client %w", err) - } - - return &App{ - Config: config, - Client: &client, - }, nil -} - -// Up deploys the services described in the config file -func (a *App) Up(ctx context.Context) error { - err := a.generateMissingNodes(ctx) - if err != nil { - return err - } - - networks := a.generateNetworks() - dls := []*workloads.Deployment{} - - for networkName, deploymentData := range a.Config.DeploymentData { - network := networks[networkName] - projectName := a.getProjectName(networkName) - - network.SolutionType = projectName - network.Nodes = []uint32{deploymentData.NodeID} - dl := &workloads.Deployment{ - Name: a.getDeploymentName(networkName), - NodeID: deploymentData.NodeID, - SolutionType: projectName, - NetworkName: network.Name, - } - - for _, service := range deploymentData.Services { - vm := workloads.VM{ - Name: service.Name, - Flist: service.Flist, - Entrypoint: service.Entrypoint, - CPU: int(service.Resources.CPU), - Memory: int(service.Resources.Memory), - RootfsSize: int(service.Resources.Rootfs), - NetworkName: network.Name, - } - - utils.AssignEnvs(&vm, service.Environment) - - disks, err := utils.AssignMounts(&vm, service.Volumes, a.Config.Volumes) - if err != nil { - return fmt.Errorf("failed to assign mounts %w", err) - } - - if err := utils.AssignNetworksTypes(&vm, service.IPTypes); err != nil { - return fmt.Errorf("failed to assign networks %w", err) - } - - dl.Vms = append(dl.Vms, vm) - dl.Disks = append(dl.Disks, disks...) - } - - dls = append(dls, dl) - } - - log.Info().Str("status", "started").Msg("deploying networks...") - - for _, network := range networks { - if err := a.Client.NetworkDeployer.Deploy(ctx, network); err != nil { - return err - } - } - log.Info().Str("status", "done").Msg("networks deployed successfully") - - deployed := make([]*workloads.Deployment, 0) - - for _, dl := range dls { - log.Info().Str("deployment", dl.Name).Msg("deploying...") - - if err := a.Client.DeploymentDeployer.Deploy(ctx, dl); err != nil { - log.Info().Msg("an error occurred while deploying the deployment, canceling all deployments") - - for _, network := range networks { - if err := a.Client.NetworkDeployer.Cancel(ctx, network); err != nil { - return err - } - } - - for _, deployment := range deployed { - if err := a.Client.DeploymentDeployer.Cancel(ctx, deployment); err != nil { - return err - } - } - log.Info().Msg("all deployments canceled successfully") - return err - } - log.Info().Str("deployment", dl.Name).Msg("deployed successfully") - - deployed = append(deployed, dl) - } - - log.Info().Msg("all services deployed successfully") - return nil -} - -// Ps lists the deployed services -func (a *App) Ps(ctx context.Context, flags *pflag.FlagSet) error { - verbose, outputFile, err := parsePsFlags(flags) - if err != nil { - return err - } - - var output strings.Builder - outputMap := make(map[string]gridtypes.Deployment) - - if !verbose { - output.WriteString(fmt.Sprintf("%-15s | %-15s | %-15s | %-15s | %-10s | %s\n", "Deployment Name", "Network", "Services", "Storage", "State", "IP Address")) - output.WriteString(strings.Repeat("-", 100) + "\n") - } - - for networkName, deploymentData := range a.Config.DeploymentData { - projectName := a.getProjectName(networkName) - - if err := a.loadCurrentNodeDeployments(projectName); err != nil { - return err - } - - contracts, err := a.Client.ContractsGetter.ListContractsOfProjectName(projectName) - if err != nil { - return err - } - - for _, contract := range contracts.NodeContracts { - contractDlData, err := workloads.ParseDeploymentData(contract.DeploymentData) - if err != nil { - return err - } - - if contractDlData.Type == "network" { - continue - } - - dlAdded := false - for _, service := range deploymentData.Services { - wl, dl, err := a.Client.State.GetWorkloadInDeployment(ctx, contract.NodeID, service.Name, contractDlData.Name) - if err != nil { - return err - } - - vm, err := workloads.NewVMFromWorkload(&wl, &dl) - if err != nil { - return err - } - - if !verbose { - if !dlAdded { - output.WriteString(fmt.Sprintf("%-15s | %-15s | %-15s | %-15s | %-10s | %s\n", contractDlData.Name, vm.NetworkName, vm.Name, vm.Mounts[0].DiskName, wl.Result.State, utils.GetVmAddresses(vm))) - dlAdded = true - } else { - output.WriteString(fmt.Sprintf("%-15s | %-15s | %-15s | %-15s | %-10s | %s\n", strings.Repeat("-", 15), strings.Repeat("-", 15), vm.Name, vm.Mounts[0].DiskName, wl.Result.State, utils.GetVmAddresses(vm))) - } - } - outputMap[contractDlData.Name] = dl - } - } - } - - if verbose { - out, err := json.MarshalIndent(outputMap, "", " ") - if err != nil { - return err - } - if outputFile == "" { - fmt.Println(string(out)) - return nil - } - - if err := os.WriteFile(outputFile, out, 0644); err != nil { - return err - } - - return nil - } - - // print for better formatting - fmt.Printf("\n%s\n", output.String()) - return nil -} - -func parsePsFlags(flags *pflag.FlagSet) (bool, string, error) { - verbose, err := flags.GetBool("verbose") - if err != nil { - return verbose, "", err - } - - outputFile, err := flags.GetString("output") - if err != nil { - return verbose, outputFile, err - } - - return verbose, outputFile, nil -} - -// Down cancels all the deployments -func (a *App) Down() error { - for networkName := range a.Config.DeploymentData { - projectName := a.getProjectName(networkName) - log.Info().Str("projectName", projectName).Msg("canceling deployments") - if err := a.Client.CancelByProjectName(projectName); err != nil { - return err - } - } - return nil -} - -func (a *App) generateNetworks() map[string]*workloads.ZNet { - zNets := make(map[string]*workloads.ZNet, 0) - defNetName := utils.GenerateDefaultNetworkName(a.Config.Services) - for networkName := range a.Config.DeploymentData { - if networkName == defNetName { - zNets[networkName] = &workloads.ZNet{ - Name: networkName, - IPRange: gridtypes.NewIPNet(net.IPNet{ - IP: net.IPv4(10, 20, 0, 0), - Mask: net.CIDRMask(16, 32), - }), - AddWGAccess: false, - } - } else { - network := a.Config.Networks[networkName] - zNets[networkName] = &workloads.ZNet{ - Name: network.Name, - Description: network.Description, - IPRange: gridtypes.NewIPNet(utils.GenerateIPNet(network.IPRange.IP, network.IPRange.Mask)), - AddWGAccess: network.AddWGAccess, - MyceliumKeys: network.MyceliumKeys, - } - } - } - - return zNets -} - -func (a *App) getProjectName(key string) string { - return fmt.Sprintf("compose/%v/%v", a.Client.TwinID, key) -} - -func (a *App) getDeploymentName(key string) string { - return fmt.Sprintf("dl_%v", key) -} - -func (a *App) loadCurrentNodeDeployments(projectName string) error { - contracts, err := a.Client.ContractsGetter.ListContractsOfProjectName(projectName, true) - if err != nil { - return err - } - - var nodeID uint32 - - for _, contract := range contracts.NodeContracts { - contractID, err := strconv.ParseUint(contract.ContractID, 10, 64) - if err != nil { - return err - } - - nodeID = contract.NodeID - a.checkIfExistAndAppend(nodeID, contractID) - } - - return nil -} - -func (a *App) checkIfExistAndAppend(node uint32, contractID uint64) { - for _, n := range a.Client.State.CurrentNodeDeployments[node] { - if n == contractID { - return - } - } - - a.Client.State.CurrentNodeDeployments[node] = append(a.Client.State.CurrentNodeDeployments[node], contractID) -} - -// TODO: Calculate total MRU and SRU while populating the deployment data -func (a *App) generateMissingNodes(ctx context.Context) error { - for _, deploymentData := range a.Config.DeploymentData { - if deploymentData.NodeID != 0 { - continue - } - - // freeCRU is not in NodeFilter? - var freeMRU, freeSRU uint64 - - for _, service := range deploymentData.Services { - freeMRU += service.Resources.Memory - freeSRU += service.Resources.Rootfs - } - - filter := types.NodeFilter{ - Status: []string{"up"}, - FreeSRU: &freeSRU, - FreeMRU: &freeMRU, - } - - nodes, _, err := a.Client.GridProxyClient.Nodes(ctx, filter, types.Limit{}) - if err != nil { - return err - } - - if len(nodes) == 0 || (len(nodes) == 1 && nodes[0].NodeID == 1) { - return fmt.Errorf("no available nodes") - } - - // TODO: still need to agree on logic to select the node - for _, node := range nodes { - if node.NodeID != 1 { - deploymentData.NodeID = uint32(node.NodeID) - break - } - } - } - - return nil -} diff --git a/grid-compose/internal/app/app.go b/grid-compose/internal/app/app.go new file mode 100644 index 000000000..e468f86c0 --- /dev/null +++ b/grid-compose/internal/app/app.go @@ -0,0 +1,108 @@ +package app + +import ( + "fmt" + "os" + "strconv" + + "github.com/cosmos/go-bip39" + "github.com/threefoldtech/tfgrid-sdk-go/grid-client/deployer" + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/config" +) + +// App is the main application struct that holds the client and the config data +type App struct { + Client *deployer.TFPluginClient + Config *config.Config +} + +// NewApp creates a new instance of the application +func NewApp(net, mnemonic, configPath string) (*App, error) { + if !validateCredentials(mnemonic, net) { + return nil, fmt.Errorf("invalid mnemonic or network") + } + + configFile, err := os.Open(configPath) + if err != nil { + return nil, err + } + + defer configFile.Close() + + config := config.NewConfig() + err = config.LoadConfigFromReader(configFile) + if err != nil { + return nil, fmt.Errorf("failed to load config from file %w", err) + } + + if err := config.ValidateConfig(); err != nil { + return nil, fmt.Errorf("failed to validate config %w", err) + } + + client, err := deployer.NewTFPluginClient(mnemonic, deployer.WithNetwork(net)) + if err != nil { + return nil, fmt.Errorf("failed to load grid client %w", err) + } + + return &App{ + Config: config, + Client: &client, + }, nil +} + +func (a *App) getProjectName(key string) string { + return fmt.Sprintf("compose/%v/%v", a.Client.TwinID, key) +} + +func (a *App) getDeploymentName(key string) string { + return fmt.Sprintf("dl_%v", key) +} + +func (a *App) loadCurrentNodeDeployments(projectName string) error { + contracts, err := a.Client.ContractsGetter.ListContractsOfProjectName(projectName, true) + if err != nil { + return err + } + + var nodeID uint32 + + for _, contract := range contracts.NodeContracts { + contractID, err := strconv.ParseUint(contract.ContractID, 10, 64) + if err != nil { + return err + } + + nodeID = contract.NodeID + a.checkIfExistAndAppend(nodeID, contractID) + } + + return nil +} + +func (a *App) checkIfExistAndAppend(node uint32, contractID uint64) { + for _, n := range a.Client.State.CurrentNodeDeployments[node] { + if n == contractID { + return + } + } + + a.Client.State.CurrentNodeDeployments[node] = append(a.Client.State.CurrentNodeDeployments[node], contractID) +} + +// validateCredentials validates the mnemonic and network +func validateCredentials(mnemonics, network string) bool { + return validateMnemonics(mnemonics) && validateNetwork(network) +} + +func validateMnemonics(mnemonics string) bool { + return bip39.IsMnemonicValid(mnemonics) +} + +func validateNetwork(network string) bool { + switch network { + case "test", "dev", "main", "qa": + return true + default: + return false + } +} diff --git a/grid-compose/internal/app/down.go b/grid-compose/internal/app/down.go new file mode 100644 index 000000000..1cebbbcb4 --- /dev/null +++ b/grid-compose/internal/app/down.go @@ -0,0 +1,22 @@ +package app + +import ( + "github.com/rs/zerolog/log" + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/generator" + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/types" +) + +// Down cancels all the deployments +func (a *App) Down() error { + if a.Config.Networks == nil { + a.Config.Networks[generator.GenerateDefaultNetworkName(a.Config.Services)] = types.Network{} + } + for networkName := range a.Config.Networks { + projectName := a.getProjectName(networkName) + log.Info().Str("projectName", projectName).Msg("canceling deployments") + if err := a.Client.CancelByProjectName(projectName); err != nil { + return err + } + } + return nil +} diff --git a/grid-compose/internal/app/ps.go b/grid-compose/internal/app/ps.go new file mode 100644 index 000000000..fda437960 --- /dev/null +++ b/grid-compose/internal/app/ps.go @@ -0,0 +1,121 @@ +package app + +import ( + "context" + "encoding/json" + "fmt" + "strconv" + "strings" + + "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/generator" + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/types" + "github.com/threefoldtech/zos/pkg/gridtypes" +) + +// Ps lists the deployed services +func (a *App) Ps(ctx context.Context, verbose bool) error { + + var output strings.Builder + outputMap := make(map[string]gridtypes.Deployment) + + if !verbose { + output.WriteString(fmt.Sprintf("%-15s | %-15s | %-15s | %-15s | %-15s | %-10s | %s\n", "Deployment Name", "Node ID", "Network", "Services", "Storage", "State", "IP Address")) + output.WriteString(strings.Repeat("-", 100) + "\n") + } + + if a.Config.Networks == nil { + a.Config.Networks[generator.GenerateDefaultNetworkName(a.Config.Services)] = types.Network{} + } + for networkName := range a.Config.Networks { + projectName := a.getProjectName(networkName) + + if err := a.loadCurrentNodeDeployments(projectName); err != nil { + return err + } + + contracts, err := a.Client.ContractsGetter.ListContractsOfProjectName(projectName) + if err != nil { + return err + } + + for _, contract := range contracts.NodeContracts { + nodeClient, err := a.Client.State.NcPool.GetNodeClient(a.Client.SubstrateConn, contract.NodeID) + + if err != nil { + return err + } + + contId, _ := strconv.ParseUint(contract.ContractID, 10, 64) + dl, err := nodeClient.DeploymentGet(ctx, contId) + if err != nil { + return err + } + + for _, wl := range dl.Workloads { + if wl.Type.String() != "zmachine" { + continue + } + + vm, err := workloads.NewVMFromWorkload(&wl, &dl) + + if err != nil { + return err + } + output.WriteString(fmt.Sprintf("%-15s | %-15d | %-15s | %-15s | %-15s | %-10s | %s\n", a.getDeploymentName(wl.Name.String()), contract.NodeID, vm.NetworkName, vm.Name, vm.Mounts[0].DiskName, wl.Result.State, getVmAddresses(vm))) + } + + dlData, err := workloads.ParseDeploymentData(dl.Metadata) + if err != nil { + return err + } + + if verbose { + outputMap[dlData.Name] = dl + } + + // if !verbose { + // output.WriteString(fmt.Sprintf("%-15s | %-15s | %-15s | %-15s | %-15s | %-10s | %s\n", contractDlData.Name, contract.NodeID, vm.NetworkName, vm.Name, vm.Mounts[0].DiskName, wl.Result.State, getVmAddresses(vm))) + // // output.WriteString(fmt.Sprintf("%-15s | %-15s | %-15s | %-15s | %-10s | %s\n", strings.Repeat("-", 15), strings.Repeat("-", 15), vm.Name, vm.Mounts[0].DiskName, wl.Result.State, getVmAddresses(vm))) + // } + // } + + } + } + + if verbose { + out, err := json.MarshalIndent(outputMap, "", " ") + if err != nil { + return err + } + + fmt.Println(string(out)) + return nil + } + + // print for better formatting + fmt.Printf("\n%s\n", output.String()) + return nil +} + +func getVmAddresses(vm workloads.VM) string { + var addresses strings.Builder + + if vm.IP != "" { + addresses.WriteString(fmt.Sprintf("wireguard: %v, ", vm.IP)) + } + if vm.Planetary { + addresses.WriteString(fmt.Sprintf("yggdrasil: %v, ", vm.PlanetaryIP)) + } + if vm.PublicIP { + addresses.WriteString(fmt.Sprintf("publicIp4: %v, ", vm.ComputedIP)) + } + if vm.PublicIP6 { + addresses.WriteString(fmt.Sprintf("publicIp6: %v, ", vm.ComputedIP6)) + } + if len(vm.MyceliumIPSeed) != 0 { + addresses.WriteString(fmt.Sprintf("mycelium: %v, ", vm.MyceliumIP)) + } + + return strings.TrimSuffix(addresses.String(), ", ") +} diff --git a/grid-compose/internal/app/up.go b/grid-compose/internal/app/up.go new file mode 100644 index 000000000..5274e68ba --- /dev/null +++ b/grid-compose/internal/app/up.go @@ -0,0 +1,273 @@ +package app + +import ( + "context" + "crypto/rand" + "fmt" + "net" + "strconv" + "strings" + + "github.com/rs/zerolog/log" + "github.com/threefoldtech/tfgrid-sdk-go/grid-client/deployer" + "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/convert" + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/generator" + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/types" + proxy_types "github.com/threefoldtech/tfgrid-sdk-go/grid-proxy/pkg/types" + "github.com/threefoldtech/zos/pkg/gridtypes" + "github.com/threefoldtech/zos/pkg/gridtypes/zos" +) + +// Up deploys the services described in the config file +func (a *App) Up(ctx context.Context) error { + deploymentData, err := convert.ConvertToDeploymentData(a.Config) + if err != nil { + return err + } + + err = getMissingNodes(ctx, deploymentData.NetworkNodeMap, a.Client) + if err != nil { + return err + } + + networks := generateNetworks(deploymentData.NetworkNodeMap, a) + + resolvedServices, err := deploymentData.ServicesGraph.ResolveDependencies(deploymentData.ServicesGraph.Root, []*types.DRNode{}, []*types.DRNode{}) + + if err != nil { + return err + } + + dls := make([]*workloads.Deployment, 0) + for _, resService := range resolvedServices { + if resService.Name == "root" { + continue + } + + serviceName := resService.Name + service := deploymentData.ServicesGraph.Nodes[serviceName].Service + var network *workloads.ZNet + + if service.Network == "" { + network = networks[generator.GenerateDefaultNetworkName(a.Config.Services)] + service.Network = network.Name + } else { + network = networks[service.Network] + } + + vm := workloads.VM{ + Name: serviceName, + Flist: service.Flist, + Entrypoint: service.Entrypoint, + CPU: int(service.Resources.CPU), + Memory: int(service.Resources.Memory), + RootfsSize: int(service.Resources.Rootfs), + NetworkName: network.Name, + } + + assignEnvs(&vm, service.Environment) + + disks, err := assignMounts(&vm, service.Volumes, a.Config.Volumes) + if err != nil { + return fmt.Errorf("failed to assign mounts %w", err) + } + + if err := assignNetworksTypes(&vm, service.IPTypes); err != nil { + return fmt.Errorf("failed to assign networks %w", err) + } + + dl := &workloads.Deployment{ + Name: a.getDeploymentName(serviceName), + NodeID: deploymentData.NetworkNodeMap[service.Network].NodeID, + SolutionType: a.getProjectName(service.Network), + NetworkName: network.Name, + } + + dl.Vms = append(dl.Vms, vm) + dl.Disks = append(dl.Disks, disks...) + + dls = append(dls, dl) + } + + log.Info().Str("status", "started").Msg("deploying networks...") + + for _, network := range networks { + log.Info().Str("network name", network.Name).Uint32("node_id", network.Nodes[0]).Msg("deploying...") + + if err := a.Client.NetworkDeployer.Deploy(ctx, network); err != nil { + return err + } + } + log.Info().Str("status", "done").Msg("networks deployed successfully") + + deployed := make([]*workloads.Deployment, 0) + for _, dl := range dls { + log.Info().Str("deployment", dl.Name).Msg("deploying...") + if err := a.Client.DeploymentDeployer.Deploy(ctx, dl); err != nil { + log.Info().Msg("an error occurred while deploying the deployment, canceling all deployments") + + for _, network := range networks { + if err := a.Client.NetworkDeployer.Cancel(ctx, network); err != nil { + return err + } + } + + for _, deployment := range deployed { + if err := a.Client.DeploymentDeployer.Cancel(ctx, deployment); err != nil { + return err + } + } + log.Info().Msg("all deployments canceled successfully") + return err + } + log.Info().Str("deployment", dl.Name).Msg("deployed successfully") + + deployed = append(deployed, dl) + } + + log.Info().Msg("all services deployed successfully") + return nil +} + +func generateNetworks(networkNodeMap map[string]*types.NetworkData, app *App) map[string]*workloads.ZNet { + zNets := make(map[string]*workloads.ZNet, 0) + defaultNetName := generator.GenerateDefaultNetworkName(app.Config.Services) + + if _, ok := networkNodeMap[defaultNetName]; ok { + zNets[defaultNetName] = &workloads.ZNet{ + Name: defaultNetName, + IPRange: gridtypes.NewIPNet(net.IPNet{ + IP: net.IPv4(10, 20, 0, 0), + Mask: net.CIDRMask(16, 32), + }), + AddWGAccess: false, + Nodes: []uint32{networkNodeMap[defaultNetName].NodeID}, + SolutionType: app.getProjectName(defaultNetName), + } + } + + for networkName := range app.Config.Networks { + network := app.Config.Networks[networkName] + zNets[networkName] = &workloads.ZNet{ + Name: network.Name, + Description: network.Description, + IPRange: gridtypes.NewIPNet(generator.GenerateIPNet(network.IPRange.IP, network.IPRange.Mask)), + AddWGAccess: network.AddWGAccess, + MyceliumKeys: network.MyceliumKeys, + Nodes: []uint32{networkNodeMap[networkName].NodeID}, + SolutionType: app.getProjectName(networkName), + } + } + + return zNets +} + +// TODO: Calculate total MRU and SRU while populating the deployment data +func getMissingNodes(ctx context.Context, networkNodeMap map[string]*types.NetworkData, client *deployer.TFPluginClient) error { + for _, deploymentData := range networkNodeMap { + if deploymentData.NodeID != 0 { + continue + } + + // freeCRU is not in NodeFilter? + var freeMRU, freeSRU uint64 + + for _, service := range deploymentData.Services { + freeMRU += service.Resources.Memory + freeSRU += service.Resources.Rootfs + } + + filter := proxy_types.NodeFilter{ + Status: []string{"up"}, + FreeSRU: &freeSRU, + FreeMRU: &freeMRU, + } + + nodes, _, err := client.GridProxyClient.Nodes(ctx, filter, proxy_types.Limit{}) + if err != nil { + return err + } + + if len(nodes) == 0 || (len(nodes) == 1 && nodes[0].NodeID == 1) { + return fmt.Errorf("no available nodes") + } + + // TODO: still need to agree on logic to select the node + for _, node := range nodes { + if node.NodeID != 1 { + deploymentData.NodeID = uint32(node.NodeID) + break + } + } + } + + return nil +} + +func assignEnvs(vm *workloads.VM, envs []string) { + env := make(map[string]string, 0) + for _, envVar := range envs { + keyValuePair := strings.Split(envVar, "=") + env[keyValuePair[0]] = keyValuePair[1] + } + + vm.EnvVars = env +} + +// TODO: Create a parser to parse the size given to each field in service +func assignMounts(vm *workloads.VM, serviceVolumes []string, volumes map[string]types.Volume) ([]workloads.Disk, error) { + var disks []workloads.Disk + mounts := make([]workloads.Mount, 0) + for _, volumeName := range serviceVolumes { + volume := volumes[volumeName] + + size, err := strconv.Atoi(strings.TrimSuffix(volume.Size, "GB")) + + if err != nil { + return nil, err + } + + disk := workloads.Disk{ + Name: volumeName, + SizeGB: size, + } + + disks = append(disks, disk) + + mounts = append(mounts, workloads.Mount{ + DiskName: disk.Name, + MountPoint: volume.MountPoint, + }) + } + vm.Mounts = mounts + + return disks, nil +} + +func assignNetworksTypes(vm *workloads.VM, ipTypes []string) error { + for _, ipType := range ipTypes { + switch ipType { + case "ipv4": + vm.PublicIP = true + case "ipv6": + vm.PublicIP6 = true + case "ygg": + vm.Planetary = true + case "myc": + seed, err := getRandomMyceliumIPSeed() + if err != nil { + return fmt.Errorf("failed to get mycelium seed %w", err) + } + vm.MyceliumIPSeed = seed + } + } + + return nil +} + +func getRandomMyceliumIPSeed() ([]byte, error) { + key := make([]byte, zos.MyceliumIPSeedLen) + _, err := rand.Read(key) + return key, err +} diff --git a/grid-compose/internal/app/version.go b/grid-compose/internal/app/version.go new file mode 100644 index 000000000..4879f7a48 --- /dev/null +++ b/grid-compose/internal/app/version.go @@ -0,0 +1 @@ +package app diff --git a/grid-compose/internal/config/config.go b/grid-compose/internal/config/config.go index 174893cfd..81df7a47c 100644 --- a/grid-compose/internal/config/config.go +++ b/grid-compose/internal/config/config.go @@ -6,9 +6,7 @@ import ( "io" "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/types" - "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/utils" - - "gopkg.in/yaml.v3" + "gopkg.in/yaml.v2" ) var ( @@ -28,11 +26,12 @@ type Config struct { Services map[string]types.Service `yaml:"services"` Volumes map[string]types.Volume `yaml:"volumes"` - // Constructed map from config file content to be used to generate deployments - DeploymentData map[string]*struct { - Services []*types.Service - NodeID uint32 - } + // ServicesGraph *dependency.DRGraph + // // Constructed map from config file content to be used to generate deployments + // DeploymentData map[string]*struct { + // NodeID uint32 + // Services map[string]*Service + // } } // NewConfig creates a new instance of the configuration @@ -44,41 +43,16 @@ func NewConfig() *Config { } } -// ValidateConfig validates the configuration file content -// TODO: Create more validation rules -func (c *Config) ValidateConfig() (err error) { - if c.Version == "" { - return ErrVersionNotSet - } - - for name, service := range c.Services { - if service.Flist == "" { - return fmt.Errorf("%w for service %s", ErrServiceFlistNotSet, name) - } - - if service.Resources.CPU == 0 { - return fmt.Errorf("%w for service %s", ErrServiceCPUResourceNotSet, name) - } - - if service.Resources.Memory == 0 { - return fmt.Errorf("%w for service %s", ErrServiceMemoryResourceNotSet, name) - } - } - - return nil -} - // LoadConfigFromReader loads the configuration file content from a reader func (c *Config) LoadConfigFromReader(configFile io.Reader) error { content, err := io.ReadAll(configFile) if err != nil { return fmt.Errorf("failed to read file %w", err) } - - if err := c.UnmarshalYAML(content); err != nil { - return err + err = c.UnmarshalYAML(content) + if err != nil { + return fmt.Errorf("failed to unmarshal yaml %w", err) } - return nil } @@ -88,42 +62,28 @@ func (c *Config) UnmarshalYAML(content []byte) error { return err } - defaultNetName := utils.GenerateDefaultNetworkName(c.Services) - c.DeploymentData = make(map[string]*struct { - Services []*types.Service - NodeID uint32 - }) + return nil +} - for serviceName, service := range c.Services { - svc := service - var netName string - if svc.Network == "" { - netName = defaultNetName - } else { - netName = svc.Network - } +// ValidateConfig validates the configuration file content +// TODO: Create more validation rules +func (c *Config) ValidateConfig() (err error) { + if c.Version == "" { + return ErrVersionNotSet + } - if _, ok := c.DeploymentData[netName]; !ok { - c.DeploymentData[netName] = &struct { - Services []*types.Service - NodeID uint32 - }{ - Services: make([]*types.Service, 0), - NodeID: svc.NodeID, - } + for name, service := range c.Services { + if service.Flist == "" { + return fmt.Errorf("%w for service %s", ErrServiceFlistNotSet, name) } - if c.DeploymentData[netName].NodeID == 0 && svc.NodeID != 0 { - c.DeploymentData[netName].NodeID = svc.NodeID + if service.Resources.CPU == 0 { + return fmt.Errorf("%w for service %s", ErrServiceCPUResourceNotSet, name) } - if svc.NodeID != 0 && svc.NodeID != c.DeploymentData[netName].NodeID { - return fmt.Errorf("service name %s node_id %d should be the same for all or some or left blank for services in the same network", serviceName, svc.NodeID) + if service.Resources.Memory == 0 { + return fmt.Errorf("%w for service %s", ErrServiceMemoryResourceNotSet, name) } - - svc.Name = serviceName - - c.DeploymentData[netName].Services = append(c.DeploymentData[netName].Services, &svc) } return nil diff --git a/grid-compose/internal/convert/convert.go b/grid-compose/internal/convert/convert.go new file mode 100644 index 000000000..8b07a8306 --- /dev/null +++ b/grid-compose/internal/convert/convert.go @@ -0,0 +1,100 @@ +package convert + +import ( + "fmt" + + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/config" + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/generator" + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/types" +) + +func ConvertToDeploymentData(config *config.Config) (*types.DeploymentData, error) { + deploymentData := &types.DeploymentData{ + NetworkNodeMap: make(map[string]*types.NetworkData, 0), + ServicesGraph: types.NewDRGraph(types.NewDRNode("root", nil)), + } + + defaultNetName := generator.GenerateDefaultNetworkName(config.Services) + + for serviceName, service := range config.Services { + svc := service + + var netName string + if svc.Network == "" { + netName = defaultNetName + } else { + netName = svc.Network + } + + if _, ok := deploymentData.NetworkNodeMap[netName]; !ok { + deploymentData.NetworkNodeMap[netName] = &types.NetworkData{ + NodeID: svc.NodeID, + Services: make(map[string]*types.Service, 0), + } + } + + if deploymentData.NetworkNodeMap[netName].NodeID == 0 && svc.NodeID != 0 { + deploymentData.NetworkNodeMap[netName].NodeID = svc.NodeID + } + + if svc.NodeID != 0 && svc.NodeID != deploymentData.NetworkNodeMap[netName].NodeID { + return nil, fmt.Errorf("service name %s node_id %d should be the same for all or some or left blank for services in the same network", serviceName, svc.NodeID) + } + + deploymentData.NetworkNodeMap[netName].Services[serviceName] = &svc + + svcNode, ok := deploymentData.ServicesGraph.Nodes[serviceName] + if !ok { + svcNode = types.NewDRNode( + serviceName, + &svc, + ) + + deploymentData.ServicesGraph.AddNode(serviceName, svcNode) + } + + svcRootNode := deploymentData.ServicesGraph.Root + + for _, dep := range svc.DependsOn { + if _, ok := config.Services[dep]; !ok { + return nil, fmt.Errorf("service %s depends on %s which does not exist", serviceName, dep) + } + + depNode, ok := deploymentData.ServicesGraph.Nodes[dep] + if !ok { + depService := config.Services[dep] + depNode = types.NewDRNode(dep, &depService) + } + + svcNode.AddDependency(depNode) + depNode.Parent = svcNode + deploymentData.ServicesGraph.AddNode(dep, depNode) + } + + if svcNode.Parent == nil { + svcNode.Parent = svcRootNode + svcRootNode.AddDependency(svcNode) + } + } + + // for netName, data := range deploymentData.NetworkNodeMap { + // log.Println(netName) + // log.Println(data.NodeID) + + // for svcName := range data.Services { + // log.Println(svcName) + // } + + // } + + // resolvedServices, err := deploymentData.ServicesGraph.ResolveDependencies(deploymentData.ServicesGraph.Root, []*types.DRNode{}, []*types.DRNode{}) + + // if err != nil { + // return nil, err + // } + + // for _, svc := range resolvedServices { + // log.Println(svc.Name) + // } + return deploymentData, nil +} diff --git a/grid-compose/internal/utils/network.go b/grid-compose/internal/generator/network.go similarity index 98% rename from grid-compose/internal/utils/network.go rename to grid-compose/internal/generator/network.go index 19d9abd0d..912a1e6ae 100644 --- a/grid-compose/internal/utils/network.go +++ b/grid-compose/internal/generator/network.go @@ -1,4 +1,4 @@ -package utils +package generator import ( "fmt" diff --git a/grid-compose/internal/types/common.go b/grid-compose/internal/types/common.go new file mode 100644 index 000000000..7cf4f429c --- /dev/null +++ b/grid-compose/internal/types/common.go @@ -0,0 +1,11 @@ +package types + +type DeploymentData struct { + ServicesGraph *DRGraph + NetworkNodeMap map[string]*NetworkData +} + +type NetworkData struct { + NodeID uint32 + Services map[string]*Service +} diff --git a/grid-compose/internal/types/dependency.go b/grid-compose/internal/types/dependency.go new file mode 100644 index 000000000..20f7b0824 --- /dev/null +++ b/grid-compose/internal/types/dependency.go @@ -0,0 +1,76 @@ +package types + +import ( + "fmt" + "log" + "slices" +) + +type DRGraph struct { + Root *DRNode + Nodes map[string]*DRNode +} + +func NewDRGraph(root *DRNode) *DRGraph { + return &DRGraph{ + Nodes: make(map[string]*DRNode), + Root: root, + } +} + +type DRNode struct { + Name string + Dependencies []*DRNode + Parent *DRNode + Service *Service +} + +func NewDRNode(name string, service *Service) *DRNode { + return &DRNode{ + Name: name, + Dependencies: []*DRNode{}, + Service: service, + } +} + +func (n *DRNode) AddDependency(dependency *DRNode) { + log.Printf("adding dependency %s -> %s", n.Name, dependency.Name) + n.Dependencies = append(n.Dependencies, dependency) +} + +func (g *DRGraph) AddNode(name string, node *DRNode) *DRNode { + g.Nodes[name] = node + + return node +} + +func (g *DRGraph) ResolveDependencies(node *DRNode, resolved []*DRNode, unresolved []*DRNode) ([]*DRNode, error) { + unresolved = append(unresolved, node) + + for _, dep := range node.Dependencies { + if slices.Contains(resolved, dep) { + continue + } + + if slices.Contains(unresolved, dep) { + return nil, fmt.Errorf("circular dependency detected %s -> %s", node.Name, dep.Name) + } + + var err error + resolved, err = g.ResolveDependencies(dep, resolved, unresolved) + if err != nil { + return nil, err + } + } + + resolved = append(resolved, node) + + for i, n := range unresolved { + if n == node { + unresolved = append(unresolved[:i], unresolved[i+1:]...) + break + } + } + + return resolved, nil +} diff --git a/grid-compose/internal/types/network.go b/grid-compose/internal/types/network.go new file mode 100644 index 000000000..f05a6072b --- /dev/null +++ b/grid-compose/internal/types/network.go @@ -0,0 +1,28 @@ +package types + +// Network represents the network configuration +type Network struct { + Name string `yaml:"name"` + Description string `yaml:"description"` + IPRange IPNet `yaml:"range"` + AddWGAccess bool `yaml:"wg"` + MyceliumKeys map[uint32][]byte `yaml:"mycelium_keys"` +} + +// IPNet represents the IP and mask of a network +type IPNet struct { + IP IP `yaml:"ip"` + Mask IPMask `yaml:"mask"` +} + +// IP represents the IP of a network +type IP struct { + Type string `yaml:"type"` + IP string `yaml:"ip"` +} + +// IPMask represents the mask of a network +type IPMask struct { + Type string `yaml:"type"` + Mask string `yaml:"mask"` +} diff --git a/grid-compose/internal/types/types.go b/grid-compose/internal/types/service.go similarity index 62% rename from grid-compose/internal/types/types.go rename to grid-compose/internal/types/service.go index b42fb1f02..41801795c 100644 --- a/grid-compose/internal/types/types.go +++ b/grid-compose/internal/types/service.go @@ -12,8 +12,6 @@ type Service struct { Network string `yaml:"network"` HealthCheck *HealthCheck `yaml:"healthcheck,omitempty"` DependsOn []string `yaml:"depends_on,omitempty"` - - Name string } // Resources represents the resources required by the service @@ -36,30 +34,3 @@ type Volume struct { MountPoint string `yaml:"mountpoint"` Size string `yaml:"size"` } - -// Network represents the network configuration -type Network struct { - Name string `yaml:"name"` - Description string `yaml:"description"` - IPRange IPNet `yaml:"range"` - AddWGAccess bool `yaml:"wg"` - MyceliumKeys map[uint32][]byte `yaml:"mycelium_keys"` -} - -// IPNet represents the IP and mask of a network -type IPNet struct { - IP IP `yaml:"ip"` - Mask IPMask `yaml:"mask"` -} - -// IP represents the IP of a network -type IP struct { - Type string `yaml:"type"` - IP string `yaml:"ip"` -} - -// IPMask represents the mask of a network -type IPMask struct { - Type string `yaml:"type"` - Mask string `yaml:"mask"` -} diff --git a/grid-compose/internal/utils/mycelium_seed.go b/grid-compose/internal/utils/mycelium_seed.go deleted file mode 100644 index 2e5a04dbd..000000000 --- a/grid-compose/internal/utils/mycelium_seed.go +++ /dev/null @@ -1,13 +0,0 @@ -package utils - -import ( - "crypto/rand" - - "github.com/threefoldtech/zos/pkg/gridtypes/zos" -) - -func GetRandomMyceliumIPSeed() ([]byte, error) { - key := make([]byte, zos.MyceliumIPSeedLen) - _, err := rand.Read(key) - return key, err -} diff --git a/grid-compose/internal/utils/validation.go b/grid-compose/internal/utils/validation.go deleted file mode 100644 index 908f6baf5..000000000 --- a/grid-compose/internal/utils/validation.go +++ /dev/null @@ -1,20 +0,0 @@ -package utils - -import "github.com/cosmos/go-bip39" - -func ValidateCredentials(mnemonics, network string) bool { - return validateMnemonics(mnemonics) && validateNetwork(network) -} - -func validateMnemonics(mnemonics string) bool { - return bip39.IsMnemonicValid(mnemonics) -} - -func validateNetwork(network string) bool { - switch network { - case "test", "dev", "main", "qa": - return true - default: - return false - } -} diff --git a/grid-compose/internal/utils/vm.go b/grid-compose/internal/utils/vm.go deleted file mode 100644 index 748aa6fac..000000000 --- a/grid-compose/internal/utils/vm.go +++ /dev/null @@ -1,93 +0,0 @@ -package utils - -import ( - "fmt" - "strconv" - "strings" - - "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" - "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/types" -) - -func AssignEnvs(vm *workloads.VM, envs []string) { - env := make(map[string]string, 0) - for _, envVar := range envs { - keyValuePair := strings.Split(envVar, "=") - env[keyValuePair[0]] = keyValuePair[1] - } - - vm.EnvVars = env -} - -// TODO: Create a parser to parse the size given to each field in service -func AssignMounts(vm *workloads.VM, serviceVolumes []string, volumes map[string]types.Volume) ([]workloads.Disk, error) { - var disks []workloads.Disk - mounts := make([]workloads.Mount, 0) - for _, volumeName := range serviceVolumes { - volume := volumes[volumeName] - - size, err := strconv.Atoi(strings.TrimSuffix(volume.Size, "GB")) - - if err != nil { - return nil, err - } - - disk := workloads.Disk{ - Name: volumeName, - SizeGB: size, - } - - disks = append(disks, disk) - - mounts = append(mounts, workloads.Mount{ - DiskName: disk.Name, - MountPoint: volume.MountPoint, - }) - } - vm.Mounts = mounts - - return disks, nil -} - -func AssignNetworksTypes(vm *workloads.VM, ipTypes []string) error { - for _, ipType := range ipTypes { - switch ipType { - case "ipv4": - vm.PublicIP = true - case "ipv6": - vm.PublicIP6 = true - case "ygg": - vm.Planetary = true - case "myc": - seed, err := GetRandomMyceliumIPSeed() - if err != nil { - return fmt.Errorf("failed to get mycelium seed %w", err) - } - vm.MyceliumIPSeed = seed - } - } - - return nil -} - -func GetVmAddresses(vm workloads.VM) string { - var addresses strings.Builder - - if vm.IP != "" { - addresses.WriteString(fmt.Sprintf("wireguard: %v, ", vm.IP)) - } - if vm.Planetary { - addresses.WriteString(fmt.Sprintf("yggdrasil: %v, ", vm.PlanetaryIP)) - } - if vm.PublicIP { - addresses.WriteString(fmt.Sprintf("publicIp4: %v, ", vm.ComputedIP)) - } - if vm.PublicIP6 { - addresses.WriteString(fmt.Sprintf("publicIp6: %v, ", vm.ComputedIP6)) - } - if len(vm.MyceliumIPSeed) != 0 { - addresses.WriteString(fmt.Sprintf("mycelium: %v, ", vm.MyceliumIP)) - } - - return strings.TrimSuffix(addresses.String(), ", ") -} From 352ef37f71b4b672c53b07ac48093917df12de08 Mon Sep 17 00:00:00 2001 From: eyad-hussein Date: Sun, 18 Aug 2024 05:44:19 +0300 Subject: [PATCH 14/20] feat: add support for health check option refactor: refcator project structure --- grid-compose/go.mod | 5 + grid-compose/go.sum | 46 ++++ grid-compose/internal/app/app.go | 4 +- grid-compose/internal/app/down.go | 8 +- grid-compose/internal/app/ps.go | 13 +- grid-compose/internal/app/up.go | 225 ++------------------ grid-compose/internal/config/config.go | 7 - grid-compose/internal/convert/convert.go | 55 ++++- grid-compose/internal/deploy/deploy_vm.go | 166 +++++++++++++++ grid-compose/internal/deploy/healthcheck.go | 124 +++++++++++ grid-compose/internal/deploy/network.go | 86 ++++++++ grid-compose/internal/generator/network.go | 53 ----- grid-compose/main.go | 4 +- 13 files changed, 517 insertions(+), 279 deletions(-) create mode 100644 grid-compose/internal/deploy/deploy_vm.go create mode 100644 grid-compose/internal/deploy/healthcheck.go create mode 100644 grid-compose/internal/deploy/network.go delete mode 100644 grid-compose/internal/generator/network.go diff --git a/grid-compose/go.mod b/grid-compose/go.mod index 7a49569cd..dcfa82eeb 100644 --- a/grid-compose/go.mod +++ b/grid-compose/go.mod @@ -9,8 +9,13 @@ require ( require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/kr/fs v0.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect + github.com/melbahja/goph v1.4.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pkg/sftp v1.13.5 // indirect github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/crypto v0.6.0 // indirect golang.org/x/sys v0.12.0 // indirect ) diff --git a/grid-compose/go.sum b/grid-compose/go.sum index affd0fa4e..0fa958173 100644 --- a/grid-compose/go.sum +++ b/grid-compose/go.sum @@ -1,14 +1,23 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/melbahja/goph v1.4.0 h1:z0PgDbBFe66lRYl3v5dGb9aFgPy0kotuQ37QOwSQFqs= +github.com/melbahja/goph v1.4.0/go.mod h1:uG+VfK2Dlhk+O32zFrRlc3kYKTlV6+BtvPWd/kK7U68= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.5 h1:a3RLUqkyjYRtBTZJZ1VRrKbN3zhuPLlUc3sphVz81go= +github.com/pkg/sftp v1.13.5/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= @@ -17,9 +26,46 @@ github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/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-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/grid-compose/internal/app/app.go b/grid-compose/internal/app/app.go index e468f86c0..79552ead3 100644 --- a/grid-compose/internal/app/app.go +++ b/grid-compose/internal/app/app.go @@ -50,11 +50,11 @@ func NewApp(net, mnemonic, configPath string) (*App, error) { }, nil } -func (a *App) getProjectName(key string) string { +func (a *App) GetProjectName(key string) string { return fmt.Sprintf("compose/%v/%v", a.Client.TwinID, key) } -func (a *App) getDeploymentName(key string) string { +func (a *App) GetDeploymentName(key string) string { return fmt.Sprintf("dl_%v", key) } diff --git a/grid-compose/internal/app/down.go b/grid-compose/internal/app/down.go index 1cebbbcb4..06e1a6b71 100644 --- a/grid-compose/internal/app/down.go +++ b/grid-compose/internal/app/down.go @@ -2,17 +2,17 @@ package app import ( "github.com/rs/zerolog/log" - "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/generator" + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/deploy" "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/types" ) // Down cancels all the deployments func (a *App) Down() error { - if a.Config.Networks == nil { - a.Config.Networks[generator.GenerateDefaultNetworkName(a.Config.Services)] = types.Network{} + if len(a.Config.Networks) == 0 { + a.Config.Networks[deploy.GenerateDefaultNetworkName(a.Config.Services)] = types.Network{} } for networkName := range a.Config.Networks { - projectName := a.getProjectName(networkName) + projectName := a.GetProjectName(networkName) log.Info().Str("projectName", projectName).Msg("canceling deployments") if err := a.Client.CancelByProjectName(projectName); err != nil { return err diff --git a/grid-compose/internal/app/ps.go b/grid-compose/internal/app/ps.go index fda437960..a53da0eb0 100644 --- a/grid-compose/internal/app/ps.go +++ b/grid-compose/internal/app/ps.go @@ -8,12 +8,13 @@ import ( "strings" "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" - "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/generator" + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/deploy" "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/types" "github.com/threefoldtech/zos/pkg/gridtypes" ) // Ps lists the deployed services +// TODO: error handling and better output(mounts index out of range) func (a *App) Ps(ctx context.Context, verbose bool) error { var output strings.Builder @@ -21,14 +22,14 @@ func (a *App) Ps(ctx context.Context, verbose bool) error { if !verbose { output.WriteString(fmt.Sprintf("%-15s | %-15s | %-15s | %-15s | %-15s | %-10s | %s\n", "Deployment Name", "Node ID", "Network", "Services", "Storage", "State", "IP Address")) - output.WriteString(strings.Repeat("-", 100) + "\n") + output.WriteString(strings.Repeat("-", 150) + "\n") } - if a.Config.Networks == nil { - a.Config.Networks[generator.GenerateDefaultNetworkName(a.Config.Services)] = types.Network{} + if len(a.Config.Networks) == 0 { + a.Config.Networks[deploy.GenerateDefaultNetworkName(a.Config.Services)] = types.Network{} } for networkName := range a.Config.Networks { - projectName := a.getProjectName(networkName) + projectName := a.GetProjectName(networkName) if err := a.loadCurrentNodeDeployments(projectName); err != nil { return err @@ -62,7 +63,7 @@ func (a *App) Ps(ctx context.Context, verbose bool) error { if err != nil { return err } - output.WriteString(fmt.Sprintf("%-15s | %-15d | %-15s | %-15s | %-15s | %-10s | %s\n", a.getDeploymentName(wl.Name.String()), contract.NodeID, vm.NetworkName, vm.Name, vm.Mounts[0].DiskName, wl.Result.State, getVmAddresses(vm))) + output.WriteString(fmt.Sprintf("%-15s | %-15d | %-15s | %-15s | %-15s | %-10s | %s\n", a.GetDeploymentName(wl.Name.String()), contract.NodeID, vm.NetworkName, vm.Name, vm.Mounts[0].DiskName, wl.Result.State, getVmAddresses(vm))) } dlData, err := workloads.ParseDeploymentData(dl.Metadata) diff --git a/grid-compose/internal/app/up.go b/grid-compose/internal/app/up.go index 5274e68ba..c43e75664 100644 --- a/grid-compose/internal/app/up.go +++ b/grid-compose/internal/app/up.go @@ -2,44 +2,30 @@ package app import ( "context" - "crypto/rand" - "fmt" - "net" - "strconv" - "strings" "github.com/rs/zerolog/log" - "github.com/threefoldtech/tfgrid-sdk-go/grid-client/deployer" "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/convert" - "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/generator" + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/deploy" "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/types" - proxy_types "github.com/threefoldtech/tfgrid-sdk-go/grid-proxy/pkg/types" - "github.com/threefoldtech/zos/pkg/gridtypes" - "github.com/threefoldtech/zos/pkg/gridtypes/zos" ) // Up deploys the services described in the config file func (a *App) Up(ctx context.Context) error { - deploymentData, err := convert.ConvertToDeploymentData(a.Config) + deploymentData, err := convert.ConvertConfigToDeploymentData(ctx, a.Client, a.Config) if err != nil { return err } - err = getMissingNodes(ctx, deploymentData.NetworkNodeMap, a.Client) - if err != nil { - return err - } - - networks := generateNetworks(deploymentData.NetworkNodeMap, a) + networks := deploy.BuildNetworks(deploymentData.NetworkNodeMap, a.Config.Networks, deploy.GenerateDefaultNetworkName(a.Config.Services), a.GetProjectName) resolvedServices, err := deploymentData.ServicesGraph.ResolveDependencies(deploymentData.ServicesGraph.Root, []*types.DRNode{}, []*types.DRNode{}) - if err != nil { return err } - dls := make([]*workloads.Deployment, 0) + deployedDls := make([]*workloads.Deployment, 0) + deployedNets := make([]*workloads.ZNet, 0) for _, resService := range resolvedServices { if resService.Name == "root" { continue @@ -50,13 +36,13 @@ func (a *App) Up(ctx context.Context) error { var network *workloads.ZNet if service.Network == "" { - network = networks[generator.GenerateDefaultNetworkName(a.Config.Services)] + network = networks[deploy.GenerateDefaultNetworkName(a.Config.Services)] service.Network = network.Name } else { network = networks[service.Network] } - vm := workloads.VM{ + vm := &workloads.VM{ Name: serviceName, Flist: service.Flist, Entrypoint: service.Entrypoint, @@ -66,208 +52,41 @@ func (a *App) Up(ctx context.Context) error { NetworkName: network.Name, } - assignEnvs(&vm, service.Environment) - - disks, err := assignMounts(&vm, service.Volumes, a.Config.Volumes) - if err != nil { - return fmt.Errorf("failed to assign mounts %w", err) - } - - if err := assignNetworksTypes(&vm, service.IPTypes); err != nil { - return fmt.Errorf("failed to assign networks %w", err) - } - - dl := &workloads.Deployment{ - Name: a.getDeploymentName(serviceName), - NodeID: deploymentData.NetworkNodeMap[service.Network].NodeID, - SolutionType: a.getProjectName(service.Network), - NetworkName: network.Name, + if err := deploy.BuildVM(vm, service); err != nil { + return err } - dl.Vms = append(dl.Vms, vm) - dl.Disks = append(dl.Disks, disks...) - - dls = append(dls, dl) - } - - log.Info().Str("status", "started").Msg("deploying networks...") - - for _, network := range networks { - log.Info().Str("network name", network.Name).Uint32("node_id", network.Nodes[0]).Msg("deploying...") - - if err := a.Client.NetworkDeployer.Deploy(ctx, network); err != nil { + disks, err := deploy.BuildDisks(vm, service.Volumes, a.Config.Volumes) + if err != nil { return err } - } - log.Info().Str("status", "done").Msg("networks deployed successfully") - deployed := make([]*workloads.Deployment, 0) - for _, dl := range dls { - log.Info().Str("deployment", dl.Name).Msg("deploying...") - if err := a.Client.DeploymentDeployer.Deploy(ctx, dl); err != nil { - log.Info().Msg("an error occurred while deploying the deployment, canceling all deployments") + deployedDl, err := deploy.DeployVM(ctx, a.Client, *vm, disks, network, a.GetDeploymentName(serviceName), *service.HealthCheck) - for _, network := range networks { + deployedDls = append(deployedDls, &deployedDl) + deployedNets = append(deployedNets, network) + if err != nil { + log.Info().Msg("an error occurred while deploying the deployment, canceling all deployments") + log.Info().Msg("canceling networks...") + for _, network := range deployedNets { if err := a.Client.NetworkDeployer.Cancel(ctx, network); err != nil { return err } } - for _, deployment := range deployed { + log.Info().Msg("canceling deployments...") + for _, deployment := range deployedDls { if err := a.Client.DeploymentDeployer.Cancel(ctx, deployment); err != nil { return err } } - log.Info().Msg("all deployments canceled successfully") - return err - } - log.Info().Str("deployment", dl.Name).Msg("deployed successfully") - - deployed = append(deployed, dl) - } - - log.Info().Msg("all services deployed successfully") - return nil -} - -func generateNetworks(networkNodeMap map[string]*types.NetworkData, app *App) map[string]*workloads.ZNet { - zNets := make(map[string]*workloads.ZNet, 0) - defaultNetName := generator.GenerateDefaultNetworkName(app.Config.Services) - - if _, ok := networkNodeMap[defaultNetName]; ok { - zNets[defaultNetName] = &workloads.ZNet{ - Name: defaultNetName, - IPRange: gridtypes.NewIPNet(net.IPNet{ - IP: net.IPv4(10, 20, 0, 0), - Mask: net.CIDRMask(16, 32), - }), - AddWGAccess: false, - Nodes: []uint32{networkNodeMap[defaultNetName].NodeID}, - SolutionType: app.getProjectName(defaultNetName), - } - } - - for networkName := range app.Config.Networks { - network := app.Config.Networks[networkName] - zNets[networkName] = &workloads.ZNet{ - Name: network.Name, - Description: network.Description, - IPRange: gridtypes.NewIPNet(generator.GenerateIPNet(network.IPRange.IP, network.IPRange.Mask)), - AddWGAccess: network.AddWGAccess, - MyceliumKeys: network.MyceliumKeys, - Nodes: []uint32{networkNodeMap[networkName].NodeID}, - SolutionType: app.getProjectName(networkName), - } - } - - return zNets -} -// TODO: Calculate total MRU and SRU while populating the deployment data -func getMissingNodes(ctx context.Context, networkNodeMap map[string]*types.NetworkData, client *deployer.TFPluginClient) error { - for _, deploymentData := range networkNodeMap { - if deploymentData.NodeID != 0 { - continue - } - - // freeCRU is not in NodeFilter? - var freeMRU, freeSRU uint64 - - for _, service := range deploymentData.Services { - freeMRU += service.Resources.Memory - freeSRU += service.Resources.Rootfs - } - - filter := proxy_types.NodeFilter{ - Status: []string{"up"}, - FreeSRU: &freeSRU, - FreeMRU: &freeMRU, - } - - nodes, _, err := client.GridProxyClient.Nodes(ctx, filter, proxy_types.Limit{}) - if err != nil { + log.Info().Msg("all deployments canceled successfully") return err } - - if len(nodes) == 0 || (len(nodes) == 1 && nodes[0].NodeID == 1) { - return fmt.Errorf("no available nodes") - } - - // TODO: still need to agree on logic to select the node - for _, node := range nodes { - if node.NodeID != 1 { - deploymentData.NodeID = uint32(node.NodeID) - break - } - } } - return nil -} - -func assignEnvs(vm *workloads.VM, envs []string) { - env := make(map[string]string, 0) - for _, envVar := range envs { - keyValuePair := strings.Split(envVar, "=") - env[keyValuePair[0]] = keyValuePair[1] - } - - vm.EnvVars = env -} - -// TODO: Create a parser to parse the size given to each field in service -func assignMounts(vm *workloads.VM, serviceVolumes []string, volumes map[string]types.Volume) ([]workloads.Disk, error) { - var disks []workloads.Disk - mounts := make([]workloads.Mount, 0) - for _, volumeName := range serviceVolumes { - volume := volumes[volumeName] - - size, err := strconv.Atoi(strings.TrimSuffix(volume.Size, "GB")) - - if err != nil { - return nil, err - } - - disk := workloads.Disk{ - Name: volumeName, - SizeGB: size, - } - - disks = append(disks, disk) - - mounts = append(mounts, workloads.Mount{ - DiskName: disk.Name, - MountPoint: volume.MountPoint, - }) - } - vm.Mounts = mounts - - return disks, nil -} - -func assignNetworksTypes(vm *workloads.VM, ipTypes []string) error { - for _, ipType := range ipTypes { - switch ipType { - case "ipv4": - vm.PublicIP = true - case "ipv6": - vm.PublicIP6 = true - case "ygg": - vm.Planetary = true - case "myc": - seed, err := getRandomMyceliumIPSeed() - if err != nil { - return fmt.Errorf("failed to get mycelium seed %w", err) - } - vm.MyceliumIPSeed = seed - } - } + log.Info().Msg("all deployments deployed successfully") return nil } - -func getRandomMyceliumIPSeed() ([]byte, error) { - key := make([]byte, zos.MyceliumIPSeedLen) - _, err := rand.Read(key) - return key, err -} diff --git a/grid-compose/internal/config/config.go b/grid-compose/internal/config/config.go index 81df7a47c..94a5718a9 100644 --- a/grid-compose/internal/config/config.go +++ b/grid-compose/internal/config/config.go @@ -25,13 +25,6 @@ type Config struct { Networks map[string]types.Network `yaml:"networks"` Services map[string]types.Service `yaml:"services"` Volumes map[string]types.Volume `yaml:"volumes"` - - // ServicesGraph *dependency.DRGraph - // // Constructed map from config file content to be used to generate deployments - // DeploymentData map[string]*struct { - // NodeID uint32 - // Services map[string]*Service - // } } // NewConfig creates a new instance of the configuration diff --git a/grid-compose/internal/convert/convert.go b/grid-compose/internal/convert/convert.go index 8b07a8306..3c25bc69e 100644 --- a/grid-compose/internal/convert/convert.go +++ b/grid-compose/internal/convert/convert.go @@ -1,20 +1,23 @@ package convert import ( + "context" "fmt" + "github.com/threefoldtech/tfgrid-sdk-go/grid-client/deployer" "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/config" - "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/generator" + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/deploy" "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/types" + proxy_types "github.com/threefoldtech/tfgrid-sdk-go/grid-proxy/pkg/types" ) -func ConvertToDeploymentData(config *config.Config) (*types.DeploymentData, error) { +func ConvertConfigToDeploymentData(ctx context.Context, t *deployer.TFPluginClient, config *config.Config) (*types.DeploymentData, error) { deploymentData := &types.DeploymentData{ NetworkNodeMap: make(map[string]*types.NetworkData, 0), ServicesGraph: types.NewDRGraph(types.NewDRNode("root", nil)), } - defaultNetName := generator.GenerateDefaultNetworkName(config.Services) + defaultNetName := deploy.GenerateDefaultNetworkName(config.Services) for serviceName, service := range config.Services { svc := service @@ -77,6 +80,10 @@ func ConvertToDeploymentData(config *config.Config) (*types.DeploymentData, erro } } + if err := getMissingNodes(ctx, deploymentData.NetworkNodeMap, t); err != nil { + return nil, err + } + // for netName, data := range deploymentData.NetworkNodeMap { // log.Println(netName) // log.Println(data.NodeID) @@ -98,3 +105,45 @@ func ConvertToDeploymentData(config *config.Config) (*types.DeploymentData, erro // } return deploymentData, nil } + +// TODO: Calculate total MRU and SRU while populating the deployment data +func getMissingNodes(ctx context.Context, networkNodeMap map[string]*types.NetworkData, t *deployer.TFPluginClient) error { + for _, deploymentData := range networkNodeMap { + if deploymentData.NodeID != 0 { + continue + } + + // freeCRU is not in NodeFilter? + var freeMRU, freeSRU uint64 + + for _, service := range deploymentData.Services { + freeMRU += service.Resources.Memory + freeSRU += service.Resources.Rootfs + } + + filter := proxy_types.NodeFilter{ + Status: []string{"up"}, + FreeSRU: &freeSRU, + FreeMRU: &freeMRU, + } + + nodes, _, err := t.GridProxyClient.Nodes(ctx, filter, proxy_types.Limit{}) + if err != nil { + return err + } + + if len(nodes) == 0 || (len(nodes) == 1 && nodes[0].NodeID == 1) { + return fmt.Errorf("no available nodes") + } + + // TODO: still need to agree on logic to select the node + for _, node := range nodes { + if node.NodeID != 1 { + deploymentData.NodeID = uint32(node.NodeID) + break + } + } + } + + return nil +} diff --git a/grid-compose/internal/deploy/deploy_vm.go b/grid-compose/internal/deploy/deploy_vm.go new file mode 100644 index 000000000..a06033854 --- /dev/null +++ b/grid-compose/internal/deploy/deploy_vm.go @@ -0,0 +1,166 @@ +package deploy + +import ( + "context" + "crypto/rand" + "fmt" + "strconv" + "strings" + + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + "github.com/threefoldtech/tfgrid-sdk-go/grid-client/deployer" + "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/types" + proxy_types "github.com/threefoldtech/tfgrid-sdk-go/grid-proxy/pkg/types" + "github.com/threefoldtech/zos/pkg/gridtypes/zos" +) + +func DeployVM(ctx context.Context, t *deployer.TFPluginClient, vm workloads.VM, disks []workloads.Disk, network *workloads.ZNet, dlName string, healthChecks ...types.HealthCheck) (workloads.Deployment, error) { + log.Info().Str("name", network.Name).Uint32("node_id", network.Nodes[0]).Msg("deploying network...") + if err := t.NetworkDeployer.Deploy(ctx, network); err != nil { + return workloads.Deployment{}, err + } + log.Info().Msg("deployed successfully") + + dl := workloads.NewDeployment(dlName, network.Nodes[0], network.SolutionType, nil, network.Name, disks, nil, []workloads.VM{vm}, nil) + log.Info().Str("name", vm.Name).Uint32("node_id", dl.NodeID).Msg("deploying vm...") + if err := t.DeploymentDeployer.Deploy(ctx, &dl); err != nil { + return workloads.Deployment{}, err + } + log.Info().Msg("deployed successfully") + + resDl, err := t.State.LoadDeploymentFromGrid(ctx, dl.NodeID, dl.Name) + if err != nil { + return workloads.Deployment{}, errors.Wrapf(err, "failed to load vm from node %d", dl.NodeID) + } + + if len(healthChecks) > 0 { + log.Info().Msg("running health checks...") + for _, hc := range healthChecks { + log.Info().Str("addr", strings.Split(resDl.Vms[0].ComputedIP, "/")[0]).Msg("") + if err := runHealthCheck(hc, "/home/eyad/Downloads/temp/id_rsa", "root", strings.Split(resDl.Vms[0].ComputedIP, "/")[0]); err != nil { + return resDl, err + } + } + } + + return resDl, nil +} + +func BuildVM(vm *workloads.VM, service *types.Service) error { + assignEnvs(vm, service.Environment) + if err := assignNetworksTypes(vm, service.IPTypes); err != nil { + return err + } + return nil +} + +// TODO: Create a parser to parse the size given to each field in service +func BuildDisks(vm *workloads.VM, serviceVolumes []string, volumes map[string]types.Volume) ([]workloads.Disk, error) { + var disks []workloads.Disk + mounts := make([]workloads.Mount, 0) + for _, volumeName := range serviceVolumes { + volume := volumes[volumeName] + + size, err := strconv.Atoi(strings.TrimSuffix(volume.Size, "GB")) + + if err != nil { + return nil, err + } + + disk := workloads.Disk{ + Name: volumeName, + SizeGB: size, + } + + disks = append(disks, disk) + + mounts = append(mounts, workloads.Mount{ + DiskName: disk.Name, + MountPoint: volume.MountPoint, + }) + } + vm.Mounts = mounts + + return disks, nil +} + +// TODO: Calculate total MRU and SRU while populating the deployment data +func getMissingNodes(ctx context.Context, networkNodeMap map[string]*types.NetworkData, client *deployer.TFPluginClient) error { + for _, deploymentData := range networkNodeMap { + if deploymentData.NodeID != 0 { + continue + } + + // freeCRU is not in NodeFilter? + var freeMRU, freeSRU uint64 + + for _, service := range deploymentData.Services { + freeMRU += service.Resources.Memory + freeSRU += service.Resources.Rootfs + } + + filter := proxy_types.NodeFilter{ + Status: []string{"up"}, + FreeSRU: &freeSRU, + FreeMRU: &freeMRU, + } + + nodes, _, err := client.GridProxyClient.Nodes(ctx, filter, proxy_types.Limit{}) + if err != nil { + return err + } + + if len(nodes) == 0 || (len(nodes) == 1 && nodes[0].NodeID == 1) { + return fmt.Errorf("no available nodes") + } + + // TODO: still need to agree on logic to select the node + for _, node := range nodes { + if node.NodeID != 1 { + deploymentData.NodeID = uint32(node.NodeID) + break + } + } + } + + return nil +} + +func assignEnvs(vm *workloads.VM, envs []string) { + env := make(map[string]string, 0) + for _, envVar := range envs { + key, value, _ := strings.Cut(envVar, "=") + env[key] = value + } + + vm.EnvVars = env +} + +func assignNetworksTypes(vm *workloads.VM, ipTypes []string) error { + for _, ipType := range ipTypes { + switch ipType { + case "ipv4": + vm.PublicIP = true + case "ipv6": + vm.PublicIP6 = true + case "ygg": + vm.Planetary = true + case "myc": + seed, err := getRandomMyceliumIPSeed() + if err != nil { + return fmt.Errorf("failed to get mycelium seed %w", err) + } + vm.MyceliumIPSeed = seed + } + } + + return nil +} + +func getRandomMyceliumIPSeed() ([]byte, error) { + key := make([]byte, zos.MyceliumIPSeedLen) + _, err := rand.Read(key) + return key, err +} diff --git a/grid-compose/internal/deploy/healthcheck.go b/grid-compose/internal/deploy/healthcheck.go new file mode 100644 index 000000000..7a742c68c --- /dev/null +++ b/grid-compose/internal/deploy/healthcheck.go @@ -0,0 +1,124 @@ +package deploy + +import ( + "context" + "fmt" + + "net" + "strings" + "time" + + "github.com/melbahja/goph" + "github.com/rs/zerolog/log" + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/types" + "golang.org/x/crypto/ssh" +) + +func verifyHost(host string, remote net.Addr, key ssh.PublicKey) error { + + // + // If you want to connect to new hosts. + // here your should check new connections public keys + // if the key not trusted you shuld return an error + // + + // hostFound: is host in known hosts file. + // err: error if key not in known hosts file OR host in known hosts file but key changed! + hostFound, err := goph.CheckKnownHost(host, remote, key, "") + + // Host in known hosts but key mismatch! + // Maybe because of MAN IN THE MIDDLE ATTACK! + if hostFound && err != nil { + + return err + } + + // handshake because public key already exists. + if hostFound && err == nil { + return nil + } + + // Add the new host to known hosts file. + return goph.AddKnownHost(host, remote, key, "") +} + +// needs refactoring +func runHealthCheck(healthCheck types.HealthCheck, privKeyPath, user, ipAddr string) error { + auth, err := goph.Key(privKeyPath, "") + if err != nil { + return err + } + + time.Sleep(5 * time.Second) + + var client *goph.Client + for i := 0; i < 5; i++ { + client, err = goph.NewConn(&goph.Config{ + User: user, + Port: 22, + Addr: ipAddr, + Auth: auth, + Callback: verifyHost, + }) + + if err == nil { + break + } + + log.Info().Str("attempt", fmt.Sprintf("%d", i+1)).Err(err).Msg("ssh connection attempt failed, retrying...") + time.Sleep(5 * time.Second) + } + + if err != nil { + return fmt.Errorf("failed to establish ssh connection after retries %w", err) + } + + defer client.Close() + + command := strings.Join(healthCheck.Test, " ") + + intervalDuration, err := time.ParseDuration(healthCheck.Interval) + if err != nil { + return fmt.Errorf("invalid interval format %w", err) + } + + timeoutDuration, err := time.ParseDuration(healthCheck.Timeout) + if err != nil { + return fmt.Errorf("invalid timeout format %w", err) + } + + var out []byte + for i := 0; i < int(healthCheck.Retries); i++ { + ctx, cancel := context.WithTimeout(context.Background(), timeoutDuration) + defer cancel() + + out, err = runCommandWithContext(ctx, client, command) + if err == nil { + log.Info().Msgf("health check succeeded %s", string(out)) + return nil + } + + log.Info().Str("attempt", fmt.Sprintf("%d/%d", i+1, healthCheck.Retries)).Err(err).Msg("health check failed, retrying...") + time.Sleep(intervalDuration) + } + + return fmt.Errorf("health check failed after %d retries %w", healthCheck.Retries, err) +} + +func runCommandWithContext(ctx context.Context, client *goph.Client, command string) ([]byte, error) { + done := make(chan struct{}) + var out []byte + var err error + + go func() { + out, err = client.Run(command) + close(done) + }() + + select { + case <-ctx.Done(): + return nil, fmt.Errorf("command timed out") + case <-done: + return out, err + } +} diff --git a/grid-compose/internal/deploy/network.go b/grid-compose/internal/deploy/network.go new file mode 100644 index 000000000..83b725517 --- /dev/null +++ b/grid-compose/internal/deploy/network.go @@ -0,0 +1,86 @@ +package deploy + +import ( + "fmt" + "net" + "strconv" + "strings" + + "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/types" + "github.com/threefoldtech/zos/pkg/gridtypes" +) + +// TODO: needs to be refactored +func BuildNetworks(networkNodeMap map[string]*types.NetworkData, networks map[string]types.Network, defaultNetName string, getProjectName func(string) string) map[string]*workloads.ZNet { + zNets := make(map[string]*workloads.ZNet, 0) + if _, ok := networkNodeMap[defaultNetName]; ok { + zNets[defaultNetName] = &workloads.ZNet{ + Name: defaultNetName, + IPRange: gridtypes.NewIPNet(net.IPNet{ + IP: net.IPv4(10, 20, 0, 0), + Mask: net.CIDRMask(16, 32), + }), + AddWGAccess: false, + Nodes: []uint32{networkNodeMap[defaultNetName].NodeID}, + SolutionType: getProjectName(defaultNetName), + } + } + + for networkName, network := range networks { + zNets[networkName] = &workloads.ZNet{ + Name: network.Name, + Description: network.Description, + IPRange: gridtypes.NewIPNet(GenerateIPNet(network.IPRange.IP, network.IPRange.Mask)), + AddWGAccess: network.AddWGAccess, + MyceliumKeys: network.MyceliumKeys, + Nodes: []uint32{networkNodeMap[networkName].NodeID}, + SolutionType: getProjectName(networkName), + } + } + + return zNets +} + +func GenerateDefaultNetworkName(services map[string]types.Service) string { + var defaultNetName string + + for serviceName := range services { + defaultNetName += serviceName[:2] + } + + return fmt.Sprintf("net_%s", defaultNetName) +} + +func GenerateIPNet(ip types.IP, mask types.IPMask) net.IPNet { + var ipNet net.IPNet + + switch ip.Type { + case "ipv4": + ipSplit := strings.Split(ip.IP, ".") + byte1, _ := strconv.ParseUint(ipSplit[0], 10, 8) + byte2, _ := strconv.ParseUint(ipSplit[1], 10, 8) + byte3, _ := strconv.ParseUint(ipSplit[2], 10, 8) + byte4, _ := strconv.ParseUint(ipSplit[3], 10, 8) + + ipNet.IP = net.IPv4(byte(byte1), byte(byte2), byte(byte3), byte(byte4)) + default: + return ipNet + } + + var maskIP net.IPMask + + switch mask.Type { + case "cidr": + maskSplit := strings.Split(mask.Mask, "/") + maskOnes, _ := strconv.ParseInt(maskSplit[0], 10, 8) + maskBits, _ := strconv.ParseInt(maskSplit[1], 10, 8) + + maskIP = net.CIDRMask(int(maskOnes), int(maskBits)) + ipNet.Mask = maskIP + default: + return ipNet + } + + return ipNet +} diff --git a/grid-compose/internal/generator/network.go b/grid-compose/internal/generator/network.go deleted file mode 100644 index 912a1e6ae..000000000 --- a/grid-compose/internal/generator/network.go +++ /dev/null @@ -1,53 +0,0 @@ -package generator - -import ( - "fmt" - "net" - "strconv" - "strings" - - "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/types" -) - -func GenerateDefaultNetworkName(services map[string]types.Service) string { - var defaultNetName string - - for serviceName := range services { - defaultNetName += serviceName[:2] - } - - return fmt.Sprintf("net_%s", defaultNetName) -} - -func GenerateIPNet(ip types.IP, mask types.IPMask) net.IPNet { - var ipNet net.IPNet - - switch ip.Type { - case "ipv4": - ipSplit := strings.Split(ip.IP, ".") - byte1, _ := strconv.ParseUint(ipSplit[0], 10, 8) - byte2, _ := strconv.ParseUint(ipSplit[1], 10, 8) - byte3, _ := strconv.ParseUint(ipSplit[2], 10, 8) - byte4, _ := strconv.ParseUint(ipSplit[3], 10, 8) - - ipNet.IP = net.IPv4(byte(byte1), byte(byte2), byte(byte3), byte(byte4)) - default: - return ipNet - } - - var maskIP net.IPMask - - switch mask.Type { - case "cidr": - maskSplit := strings.Split(mask.Mask, "/") - maskOnes, _ := strconv.ParseInt(maskSplit[0], 10, 8) - maskBits, _ := strconv.ParseInt(maskSplit[1], 10, 8) - - maskIP = net.CIDRMask(int(maskOnes), int(maskBits)) - ipNet.Mask = maskIP - default: - return ipNet - } - - return ipNet -} diff --git a/grid-compose/main.go b/grid-compose/main.go index 3a493a75e..626715cfd 100644 --- a/grid-compose/main.go +++ b/grid-compose/main.go @@ -1,6 +1,8 @@ package main -import "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/cmd" +import ( + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/cmd" +) func main() { cmd.Execute() From de9f63a3b7492825bbb9db61216eedce0c42bbd8 Mon Sep 17 00:00:00 2001 From: eyad-hussein Date: Sun, 18 Aug 2024 12:46:51 +0300 Subject: [PATCH 15/20] docs: add future work file --- grid-compose/docs/future_work.md | 1 + grid-compose/internal/app/down.go | 1 + 2 files changed, 2 insertions(+) create mode 100644 grid-compose/docs/future_work.md diff --git a/grid-compose/docs/future_work.md b/grid-compose/docs/future_work.md new file mode 100644 index 000000000..604b13638 --- /dev/null +++ b/grid-compose/docs/future_work.md @@ -0,0 +1 @@ +1- Add path for ssh keys instead of hardcoding it in the config file. diff --git a/grid-compose/internal/app/down.go b/grid-compose/internal/app/down.go index 06e1a6b71..77047e3ec 100644 --- a/grid-compose/internal/app/down.go +++ b/grid-compose/internal/app/down.go @@ -7,6 +7,7 @@ import ( ) // Down cancels all the deployments +// TODO: remove known hosts func (a *App) Down() error { if len(a.Config.Networks) == 0 { a.Config.Networks[deploy.GenerateDefaultNetworkName(a.Config.Services)] = types.Network{} From 07c5da909a9de36b3da24293d96e13b67a4c6834 Mon Sep 17 00:00:00 2001 From: eyad-hussein Date: Wed, 21 Aug 2024 05:20:24 +0300 Subject: [PATCH 16/20] refactor: refactor project structure and code tests: test each example manually docs: populate readme file, add future_work, add config.md, and extend cases --- grid-compose/.gitignore | 1 + grid-compose/README.md | 86 +++++++++++- grid-compose/cmd/root.go | 2 +- grid-compose/docs/cases.md | 33 ++++- grid-compose/docs/config.md | 116 ++++++++++++++++ grid-compose/docs/future_work.md | 8 +- .../examples/dependency/diff_networks.yml | 12 +- .../dependency/multiple_dependencies.yml | 22 ++- .../examples/dependency/same_network.yml | 19 +-- .../multiple_services_diff_network_1.yml | 11 +- .../multiple_services_diff_network_2.yml | 12 +- .../multiple_services_diff_network_3.yml | 12 +- .../two_services_same_network_1.yml | 15 +-- .../two_services_same_network_2.yml | 7 +- .../two_services_same_network_3.yml | 4 +- .../{ => single-service}/single_service_1.yml | 4 +- .../{ => single-service}/single_service_2.yml | 2 +- .../{ => single-service}/single_service_3.yml | 6 +- grid-compose/grid-compose.yaml | 50 ------- grid-compose/internal/app/app.go | 4 +- .../{types => app/dependency}/dependency.go | 14 +- grid-compose/internal/app/down.go | 2 +- grid-compose/internal/app/ps.go | 45 ++----- grid-compose/internal/app/up.go | 84 +++++++----- grid-compose/internal/app/version.go | 1 - grid-compose/internal/app/vm_addresses.go | 30 +++++ grid-compose/internal/config/config.go | 1 + grid-compose/internal/convert/convert.go | 112 ++++++++++++---- grid-compose/internal/deploy/deploy_vm.go | 126 +++--------------- grid-compose/internal/deploy/healthcheck.go | 83 ++++++++---- grid-compose/internal/deploy/network.go | 31 ++++- grid-compose/internal/types/common.go | 11 -- grid-compose/internal/types/deployment.go | 16 +++ grid-compose/pkg/log/vm_details.go | 42 ++++++ 34 files changed, 627 insertions(+), 397 deletions(-) create mode 100644 grid-compose/docs/config.md rename grid-compose/examples/{ => multiple-services}/multiple_services_diff_network_1.yml (77%) rename grid-compose/examples/{ => multiple-services}/multiple_services_diff_network_2.yml (76%) rename grid-compose/examples/{ => multiple-services}/multiple_services_diff_network_3.yml (76%) rename grid-compose/examples/{ => multiple-services}/two_services_same_network_1.yml (70%) rename grid-compose/examples/{ => multiple-services}/two_services_same_network_2.yml (72%) rename grid-compose/examples/{ => multiple-services}/two_services_same_network_3.yml (73%) rename grid-compose/examples/{ => single-service}/single_service_1.yml (54%) rename grid-compose/examples/{ => single-service}/single_service_2.yml (65%) rename grid-compose/examples/{ => single-service}/single_service_3.yml (73%) delete mode 100644 grid-compose/grid-compose.yaml rename grid-compose/internal/{types => app/dependency}/dependency.go (73%) delete mode 100644 grid-compose/internal/app/version.go create mode 100644 grid-compose/internal/app/vm_addresses.go delete mode 100644 grid-compose/internal/types/common.go create mode 100644 grid-compose/internal/types/deployment.go create mode 100644 grid-compose/pkg/log/vm_details.go diff --git a/grid-compose/.gitignore b/grid-compose/.gitignore index e30629ae3..4bb6ca521 100644 --- a/grid-compose/.gitignore +++ b/grid-compose/.gitignore @@ -3,3 +3,4 @@ bin/* out full_example.yml invalid +grid-compose.yml diff --git a/grid-compose/README.md b/grid-compose/README.md index 6c285c284..1667732ec 100644 --- a/grid-compose/README.md +++ b/grid-compose/README.md @@ -1,6 +1,8 @@ # Grid-Compose -is a tool for running multi-vm applications on TFGrid defined using a Yaml formatted file. +is a tool similar to docker-compose created for running multi-vm applications on TFGrid defined using a Yaml formatted file. + +The yaml file's structure is defined in [docs/config](docs/config.md). ## Usage @@ -13,7 +15,7 @@ is a tool for running multi-vm applications on TFGrid defined using a Yaml forma grid-compose [OPTIONS] [COMMAND] OPTIONS: - -f, --file: path to yaml file, default is ./grid-compose.yaml + -f, --file: path to yaml file, default is ./grid-compose.yml COMMANDS: - version: shows the project version @@ -22,7 +24,6 @@ COMMANDS: - ps: list deployments on the grid OPTIONS: - -v, --verbose: show full details of each deployment - - -o, --output: redirects the output to a file given its path ``` Export env vars using: @@ -66,26 +67,97 @@ Refer to examples in the [examples](examples) directory to have a look at differ OPTIONS: -- `-f, --file`: path to the yaml file, default is `./grid-compose.yaml` +- `-f, --file`: path to the yaml file, default is `./grid-compose.yml` + +### Example + +```bash +./bin/grid-compose up +``` + +output: + +```bash +3:40AM INF starting peer session=tf-848216 twin=8658 +3:40AM INF deploying network... name=miaminet node_id=14 +3:41AM INF deployed successfully +3:41AM INF deploying vm... name=database node_id=14 +3:41AM INF deployed successfully +3:41AM INF deploying network... name=miaminet node_id=14 +3:41AM INF deployed successfully +3:41AM INF deploying vm... name=server node_id=14 +3:41AM INF deployed successfully +3:41AM INF all deployments deployed successfully +``` ### down +The down command cancels all deployments on the grid. + ```bash ./bin/grid-compose down [OPTIONS] ``` OPTIONS: -- `-f, --file`: path to the yaml file, default is `./grid-compose.yaml` +- `-f, --file`: path to the yaml file, default is `./grid-compose.yml` + +### Example + +```bash +./bin/grid-compose down +``` + +output: + +```bash +3:45AM INF starting peer session=tf-854215 twin=8658 +3:45AM INF canceling deployments projectName=compose/8658/net1 +3:45AM INF canceling contracts project name=compose/8658/net1 +3:45AM INF project is canceled project name=compose/8658/net1 +``` ### ps +The ps command lists all deployments on the grid. + ```bash ./bin/grid-compose ps [FLAGS] [OPTIONS] ``` OPTIONS: -- `-f, --file`: path to the yaml file, default is `./grid-compose.yaml` +- `-f, --file`: path to the yaml file, default is `./grid-compose.yml` + +### Example + +```bash +./bin/grid-compose ps +``` + +output: + +```bash +3:43AM INF starting peer session=tf-851312 twin=8658 + +Deployment Name | Node ID | Network | Services | Storage | State | IP Address +------------------------------------------------------------------------------------------------------------------------------------------------------ +dl_database | 14 | miaminet | database | dbdata | ok | wireguard: 10.20.2.2 +dl_server | 14 | miaminet | server | webdata | ok | wireguard: 10.20.2.3 +``` + +FLAGS: + - `-v, --verbose`: show full details of each deployment -- `-o, --output`: redirects the output to a file given its path(in json format) + +### version + +The version command shows the project's current version. + +```bash +./bin/grid-compose version +``` + +## Future Work + +Refer to [docs/future_work.md](docs/future_work.md) for more information on the future work that is to be done on the grid-compose project. diff --git a/grid-compose/cmd/root.go b/grid-compose/cmd/root.go index 8b1d7ffbe..cc5897cf9 100644 --- a/grid-compose/cmd/root.go +++ b/grid-compose/cmd/root.go @@ -37,7 +37,7 @@ var rootCmd = &cobra.Command{ } func init() { - rootCmd.PersistentFlags().StringP("file", "f", "./grid-compose.yaml", "the grid-compose configuration file") + rootCmd.PersistentFlags().StringP("file", "f", "./grid-compose.yml", "the grid-compose configuration file") rootCmd.AddCommand(downCmd) rootCmd.AddCommand(upCmd) diff --git a/grid-compose/docs/cases.md b/grid-compose/docs/cases.md index 12e178342..79b31afac 100644 --- a/grid-compose/docs/cases.md +++ b/grid-compose/docs/cases.md @@ -1,4 +1,4 @@ -These are most if not all the cases supported by the grid compose cli when deploying one or more service to the grid. +These are most if not all the cases supported by the grid compose tool when deploying one or more services on the grid. ## Single Service @@ -9,7 +9,7 @@ This is probably the simplest case there is. - Filter the nodes based on the resources given to the service and choose a random one. - Generate a default network and assign it to the deployment. -Refer to example [single_service_1.yml](../examples/single_service_1.yml) +Refer to example [single_service_1.yml](/examples/single-service/single_service_1.yml) ### Case 2 - Node ID Given + No Assigned Network @@ -19,14 +19,14 @@ Refer to example [single_service_1.yml](../examples/single_service_1.yml) - If there is a node available, prompt the user if they would like to use it instead. - Generate a default network and assign it to the deployment. -Refer to example [single_service_2.yml](../examples/single_service_2.yml) +Refer to example [single_service_2.yml](/examples/single-service/single_service_2.yml) ### Case 3 - Assigned Network - Either use the assigned node id or filter the nodes for an available node if no node id given. - Use the network assigned to the service when deploying. -Refer to example [single_service_3.yml](../examples/single_service_3.yml) +Refer to example [single_service_3.yml](/examples/single-service/single_service_3.yml) ## Multiple Services @@ -55,10 +55,29 @@ If no networks are defined, then all the services will use the **default generat Refer to examples -- [two_services_same_network_1.yml](../examples/two_services_same_network_1.yml) -- [two_services_same_network_2.yml](../examples/two_services_same_network_2.yml) -- [two_services_same_network_3.yml](../examples/two_services_same_network_3.yml) +- [two_services_same_network_1.yml](/examples/multiple-services/two_services_same_network_1.yml) +- [two_services_same_network_2.yml](/examples/multiple-services/two_services_same_network_2.yml) +- [two_services_same_network_3.yml](/examples/multiple-services/two_services_same_network_3.yml) ### Different Networks Simple divide the services into groups having the same network(given or generated) and deal with each group using the approached described in the previous [section](#same-networkno-network). + +Refer to examples + +- [multiple_services_diff_network_1.yml](/examples/multiple-services/multiple_services_diff_network_1.yml) +- [multiple_services_diff_network_2.yml](/examples/multiple-services/multiple_services_diff_network_2.yml) +- [multiple_services_diff_network_3.yml](/examples/multiple-services/multiple_services_diff_network_3.yml) + +## Dependencies + +The tool supports deploying services that depend on each other. You can define dependencies in the yaml file by using the `depends_on` key, just like in docker-compose. + +Refer to examples: + +- deploying services that depend on each other on different networks: + - [diff_networks.yml](/examples/dependency/diff_networks.yml) +- deploying services that depend on each other on the same network: + - [same_network.yml](/examples/dependency/same_network.yml) +- a service that would depend on multiple services: + - [multiple_dependencies.yml](/examples/dependency/multiple_dependencies.yml) diff --git a/grid-compose/docs/config.md b/grid-compose/docs/config.md new file mode 100644 index 000000000..c67a97b25 --- /dev/null +++ b/grid-compose/docs/config.md @@ -0,0 +1,116 @@ +## Configuration File + +This document describes the configuration file for the grid-compose project. + +```yaml +version: '1.0.0' + +networks: + net1: + name: 'miaminet' + range: + ip: + type: ipv4 + ip: 10.20.0.0 + mask: + type: cidr + mask: 16/32 + wg: true + +services: + server: + flist: 'https://hub.grid.tf/tf-official-apps/threefoldtech-ubuntu-22.04.flist' + resources: + cpu: 2 + memory: 2048 + rootfs: 2048 + entrypoint: '/sbin/zinit init' + ip_types: + - ipv4 + environment: + - SSH_KEY= + node_id: 11 + healthcheck: + test: + interval: '10s' + timeout: '1m30s' + retries: 3 + volumes: + - webdata + - dbdata + network: net1 + depends_on: + - database + database: + flist: 'https://hub.grid.tf/tf-official-apps/threefoldtech-ubuntu-22.04.flist' + resources: + cpu: 2 + memory: 2048 + rootfs: 2048 + entrypoint: '/sbin/zinit init' + ip_types: + - ipv4 + environment: + - SSH_KEY= + network: net1 + +volumes: + webdata: + mountpoint: '/data' + size: 10GB + dbdata: + mountpoint: '/var/lib/postgresql/data' + size: 10GB +``` + +The configuration file is a YAML file that contains the following sections: + +- `version`: The version of the configuration file. +- `networks`: A list of networks that the services can use `optional`. + - By default, the tool will create a network that will use to deploy each service. +- `services`: A list of services to deploy. +- `volumes`: A list of volumes that the services can use `optional`. + +### Networks + +The `networks` section defines the networks that the services can use. Each network has the following properties: + +- `name`: The name of the network. +- `range`: The IP range of the network. + - `ip`: The IP address of the network. + - `type`: The type of the IP address. + - `ip`: The IP address. + - `mask`: The subnet mask of the network. + - `type`: The type of the subnet mask. + - `mask`: The subnet mask. +- `wg`: A boolean value that indicates whether to add WireGuard access to the network. + +### Services + +The `services` section defines the services to deploy. Each service has the following properties: + +- `flist`: The URL of the flist to deploy. +- `resources`: The resources required by the service (CPU, memory, and rootfs) `optional`. + - By default, the tool will use the minimum resources required to deploy the service. +- `entrypoint`: The entrypoint command to run when the service starts. +- `ip_types`: The types of IP addresses to assign to the service `optional`. + - ip type can be ipv4, ipv6, mycelium, yggdrasil. +- `environment`: The environment variables to set in the virtual machine. +- `node_id`: The ID of the node to deploy the service on `optional`. + - By default, the tool will filter the nodes based on the resources required by the service. +- `healthcheck`: The healthcheck configuration for the service `optional`. + - `test`: The command/script to run to test if the service is deployed as expected. + - `interval`: The interval between health checks. + - `timeout`: The timeout for the health check(includes the time the vm takes until it is up and ready to be connected to). + - `retries`: The number of retries for the health check. +- `volumes`: The volumes to mount in the service `optional`. +- `network`: The network to deploy the service on `optional`. + - By default, the tool will use the general network created automatically. +- `depends_on`: The services that this service depends on `optional`. + +### Volumes + +The `volumes` section defines the volumes that the services can use. Each volume has the following properties: + +- `mountpoint`: The mountpoint of the volume. +- `size`: The size of the volume. diff --git a/grid-compose/docs/future_work.md b/grid-compose/docs/future_work.md index 604b13638..ac2856a4b 100644 --- a/grid-compose/docs/future_work.md +++ b/grid-compose/docs/future_work.md @@ -1 +1,7 @@ -1- Add path for ssh keys instead of hardcoding it in the config file. +This document outlines the future work that is to be done on the grid-compose project. + +## Future Work + +1. Add path for ssh keys instead of hardcoding it in the config file. +2. Ensure backward compatibility with versions of grid-compose. +3. Create a parser for the different disk sizes(MB, GB). diff --git a/grid-compose/examples/dependency/diff_networks.yml b/grid-compose/examples/dependency/diff_networks.yml index a4d1bb639..74fda684e 100644 --- a/grid-compose/examples/dependency/diff_networks.yml +++ b/grid-compose/examples/dependency/diff_networks.yml @@ -23,7 +23,7 @@ networks: services: server: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + flist: 'https://hub.grid.tf/tf-official-apps/threefoldtech-ubuntu-22.04.flist' ip_types: - ipv4 - ygg @@ -38,7 +38,7 @@ services: depends_on: - frontend frontend: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + flist: 'https://hub.grid.tf/tf-official-apps/threefoldtech-ubuntu-22.04.flist' entrypoint: '/sbin/zinit init' volumes: - dbdata @@ -47,13 +47,11 @@ services: memory: 2048 rootfs: 2048 network: net1 - node_id: 144 + node_id: 14 depends_on: - database database: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 + flist: 'https://hub.grid.tf/tf-official-apps/threefoldtech-ubuntu-22.04.flist' entrypoint: '/sbin/zinit init' volumes: - dbdata @@ -62,7 +60,7 @@ services: memory: 2048 rootfs: 2048 network: net2 - node_id: 144 + node_id: 14 volumes: webdata: diff --git a/grid-compose/examples/dependency/multiple_dependencies.yml b/grid-compose/examples/dependency/multiple_dependencies.yml index 779e81b05..20487c26f 100644 --- a/grid-compose/examples/dependency/multiple_dependencies.yml +++ b/grid-compose/examples/dependency/multiple_dependencies.yml @@ -13,45 +13,41 @@ networks: services: server: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 - - ygg + flist: 'https://hub.grid.tf/tf-official-apps/threefoldtech-ubuntu-22.04.flist' entrypoint: '/sbin/zinit init' volumes: - webdata resources: cpu: 1 memory: 2048 - rootfs: 25600 + rootfs: 2048 network: net2 + depends_on: + - database frontend: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 + flist: 'https://hub.grid.tf/tf-official-apps/threefoldtech-ubuntu-22.04.flist' entrypoint: '/sbin/zinit init' volumes: - dbdata resources: cpu: 1 memory: 2048 - rootfs: 25600 + rootfs: 2048 network: net2 node_id: 144 depends_on: - server - database database: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 + flist: 'https://hub.grid.tf/tf-official-apps/threefoldtech-ubuntu-22.04.flist' entrypoint: '/sbin/zinit init' volumes: - dbdata + - webdata resources: cpu: 1 memory: 2048 - rootfs: 25600 + rootfs: 2048 network: net2 volumes: diff --git a/grid-compose/examples/dependency/same_network.yml b/grid-compose/examples/dependency/same_network.yml index c529cb270..5c3622992 100644 --- a/grid-compose/examples/dependency/same_network.yml +++ b/grid-compose/examples/dependency/same_network.yml @@ -2,7 +2,7 @@ version: '1.0.0' services: server: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + flist: 'https://hub.grid.tf/tf-official-apps/threefoldtech-ubuntu-22.04.flist' ip_types: - ipv4 - ygg @@ -15,10 +15,15 @@ services: rootfs: 2048 depends_on: - database + environment: + - SSH_KEY=ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCmxUAdlMvIIDRplCLAufygUBPN3VKPIkl+ogxKzRLl00Y1j/mIm86M//axY1cH3NNCxap+8hZ2aMR5fnoKGMERWVUxC1tYuV0CBiSU7dMHWV66f58W2Cf+La6CE3aXd97Uoodku4k6k7ENuRd/+oBjRHM3qfhZ95Rhsb74vVe8PPobQuU/3JkoXQ8yf5y73yh8hirqipBBonAQ4CQiz4qGyj8e1Wj5nzezvYj/ioPLdXwOOql9gISb9iNwqj2qVfSnv8ksEBLCNeNSzUd3TRyVcOuIuCdUEYx5EnyQntZNDb8IyGZUNvqWerz8S1bMQePzCNj7xUrQHbR8+7bbh7TEVa0UHOBK3dBeFYh0UReRWRwKnXC/2IPX1MeHjYGTSbgXP5Wxn7H+AxLGNPnezMFZRpqrs8ULc788RUfvkGX259xC/YboPrIVvY/R/cQeTEdmtq+xmC2XRR1icon/dCeDpAymW0IRMb8+VCKyNLumDhMwiHIVTLbJrg+TtqGzg+43BuxGhW7eS5FP5lK2R+7WS4uu/Eg5LGHVgv7WiyfE/SrMzmf+LPejQuQJVPIRrfps+b4hHtn7+w6jF6QxZMv7VVib9cJvpd9+QWvYqWeRf+ojXWvlBgYZHa18LO/pauylsWbVoQgEmGnDUHMP8WtlMJ6wv7YuSutgApqN+xgh+w== pluto + healthcheck: + test: ['mkdir', 'test'] + interval: '10s' + timeout: '1m30s' + retries: 3 frontend: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 + flist: 'https://hub.grid.tf/tf-official-apps/threefoldtech-ubuntu-22.04.flist' entrypoint: '/sbin/zinit init' volumes: - dbdata @@ -26,13 +31,11 @@ services: cpu: 1 memory: 2048 rootfs: 2048 - node_id: 144 + node_id: 14 depends_on: - server database: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 + flist: 'https://hub.grid.tf/tf-official-apps/threefoldtech-ubuntu-22.04.flist' entrypoint: '/sbin/zinit init' volumes: - dbdata diff --git a/grid-compose/examples/multiple_services_diff_network_1.yml b/grid-compose/examples/multiple-services/multiple_services_diff_network_1.yml similarity index 77% rename from grid-compose/examples/multiple_services_diff_network_1.yml rename to grid-compose/examples/multiple-services/multiple_services_diff_network_1.yml index 6a8848161..4f19d6a97 100644 --- a/grid-compose/examples/multiple_services_diff_network_1.yml +++ b/grid-compose/examples/multiple-services/multiple_services_diff_network_1.yml @@ -23,9 +23,8 @@ networks: services: server: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + flist: 'https://hub.grid.tf/tf-official-apps/threefoldtech-ubuntu-22.04.flist' ip_types: - - ipv4 - ygg entrypoint: '/sbin/zinit init' volumes: @@ -36,9 +35,7 @@ services: rootfs: 2048 network: net2 frontend: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 + flist: 'https://hub.grid.tf/tf-official-apps/threefoldtech-ubuntu-22.04.flist' entrypoint: '/sbin/zinit init' volumes: - dbdata @@ -49,9 +46,7 @@ services: network: net1 node_id: 144 database: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 + flist: 'https://hub.grid.tf/tf-official-apps/threefoldtech-ubuntu-22.04.flist' entrypoint: '/sbin/zinit init' volumes: - dbdata diff --git a/grid-compose/examples/multiple_services_diff_network_2.yml b/grid-compose/examples/multiple-services/multiple_services_diff_network_2.yml similarity index 76% rename from grid-compose/examples/multiple_services_diff_network_2.yml rename to grid-compose/examples/multiple-services/multiple_services_diff_network_2.yml index cda999749..f87bc0571 100644 --- a/grid-compose/examples/multiple_services_diff_network_2.yml +++ b/grid-compose/examples/multiple-services/multiple_services_diff_network_2.yml @@ -23,7 +23,7 @@ networks: services: server: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + flist: 'https://hub.grid.tf/tf-official-apps/threefoldtech-ubuntu-22.04.flist' entrypoint: '/sbin/zinit init' volumes: - webdata @@ -35,7 +35,7 @@ services: network: net2 server2: node_id: 11 - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + flist: 'https://hub.grid.tf/tf-official-apps/threefoldtech-ubuntu-22.04.flist' entrypoint: '/sbin/zinit init' volumes: - webdata @@ -45,9 +45,7 @@ services: rootfs: 2048 network: net1 frontend: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 + flist: 'https://hub.grid.tf/tf-official-apps/threefoldtech-ubuntu-22.04.flist' entrypoint: '/sbin/zinit init' volumes: - dbdata @@ -57,9 +55,7 @@ services: rootfs: 2048 network: net1 database: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 + flist: 'https://hub.grid.tf/tf-official-apps/threefoldtech-ubuntu-22.04.flist' entrypoint: '/sbin/zinit init' volumes: - dbdata diff --git a/grid-compose/examples/multiple_services_diff_network_3.yml b/grid-compose/examples/multiple-services/multiple_services_diff_network_3.yml similarity index 76% rename from grid-compose/examples/multiple_services_diff_network_3.yml rename to grid-compose/examples/multiple-services/multiple_services_diff_network_3.yml index 5d503ef99..e8a1c8d8d 100644 --- a/grid-compose/examples/multiple_services_diff_network_3.yml +++ b/grid-compose/examples/multiple-services/multiple_services_diff_network_3.yml @@ -23,7 +23,7 @@ networks: services: server: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + flist: 'https://hub.grid.tf/tf-official-apps/threefoldtech-ubuntu-22.04.flist' entrypoint: '/sbin/zinit init' volumes: - webdata @@ -33,7 +33,7 @@ services: rootfs: 2048 network: net2 server2: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + flist: 'https://hub.grid.tf/tf-official-apps/threefoldtech-ubuntu-22.04.flist' entrypoint: '/sbin/zinit init' volumes: - webdata @@ -43,9 +43,9 @@ services: rootfs: 2048 network: net1 frontend: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + flist: 'https://hub.grid.tf/tf-official-apps/threefoldtech-ubuntu-22.04.flist' ip_types: - - ipv4 + - ygg entrypoint: '/sbin/zinit init' volumes: - dbdata @@ -55,9 +55,7 @@ services: rootfs: 2048 network: net1 database: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 + flist: 'https://hub.grid.tf/tf-official-apps/threefoldtech-ubuntu-22.04.flist' entrypoint: '/sbin/zinit init' volumes: - dbdata diff --git a/grid-compose/examples/two_services_same_network_1.yml b/grid-compose/examples/multiple-services/two_services_same_network_1.yml similarity index 70% rename from grid-compose/examples/two_services_same_network_1.yml rename to grid-compose/examples/multiple-services/two_services_same_network_1.yml index df5aea784..be885efa9 100644 --- a/grid-compose/examples/two_services_same_network_1.yml +++ b/grid-compose/examples/multiple-services/two_services_same_network_1.yml @@ -10,34 +10,31 @@ networks: mask: type: cidr mask: 16/32 - wg: true + wg: true services: server: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + flist: 'https://hub.grid.tf/tf-official-apps/threefoldtech-ubuntu-22.04.flist' ip_types: - - ipv4 - ygg entrypoint: '/sbin/zinit init' volumes: - webdata - node_id: 144 + node_id: 14 resources: cpu: 1 memory: 2048 - rootfs: 25600 + rootfs: 2048 network: net1 database: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 + flist: 'https://hub.grid.tf/tf-official-apps/threefoldtech-ubuntu-22.04.flist' entrypoint: '/sbin/zinit init' volumes: - dbdata resources: cpu: 1 memory: 2048 - rootfs: 25600 + rootfs: 2048 network: net1 volumes: diff --git a/grid-compose/examples/two_services_same_network_2.yml b/grid-compose/examples/multiple-services/two_services_same_network_2.yml similarity index 72% rename from grid-compose/examples/two_services_same_network_2.yml rename to grid-compose/examples/multiple-services/two_services_same_network_2.yml index f3549b68f..606516db9 100644 --- a/grid-compose/examples/two_services_same_network_2.yml +++ b/grid-compose/examples/multiple-services/two_services_same_network_2.yml @@ -2,9 +2,8 @@ version: '1.0.0' services: server: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + flist: 'https://hub.grid.tf/tf-official-apps/threefoldtech-ubuntu-22.04.flist' ip_types: - - ipv4 - ygg entrypoint: '/sbin/zinit init' volumes: @@ -16,9 +15,7 @@ services: rootfs: 2048 database: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - ip_types: - - ipv4 + flist: 'https://hub.grid.tf/tf-official-apps/threefoldtech-ubuntu-22.04.flist' entrypoint: '/sbin/zinit init' volumes: - dbdata diff --git a/grid-compose/examples/two_services_same_network_3.yml b/grid-compose/examples/multiple-services/two_services_same_network_3.yml similarity index 73% rename from grid-compose/examples/two_services_same_network_3.yml rename to grid-compose/examples/multiple-services/two_services_same_network_3.yml index 1a5630a85..093538697 100644 --- a/grid-compose/examples/two_services_same_network_3.yml +++ b/grid-compose/examples/multiple-services/two_services_same_network_3.yml @@ -2,7 +2,7 @@ version: '1.0.0' services: server: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + flist: 'https://hub.grid.tf/tf-official-apps/threefoldtech-ubuntu-22.04.flist' entrypoint: '/sbin/zinit init' volumes: - webdata @@ -12,7 +12,7 @@ services: rootfs: 2048 database: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + flist: 'https://hub.grid.tf/tf-official-apps/threefoldtech-ubuntu-22.04.flist' ip_types: - ipv4 entrypoint: '/sbin/zinit init' diff --git a/grid-compose/examples/single_service_1.yml b/grid-compose/examples/single-service/single_service_1.yml similarity index 54% rename from grid-compose/examples/single_service_1.yml rename to grid-compose/examples/single-service/single_service_1.yml index fdc031cce..361b7c7c3 100644 --- a/grid-compose/examples/single_service_1.yml +++ b/grid-compose/examples/single-service/single_service_1.yml @@ -2,9 +2,9 @@ version: '1.0.0' services: server: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + flist: 'https://hub.grid.tf/tf-official-apps/threefoldtech-ubuntu-22.04.flist' entrypoint: '/sbin/zinit init' resources: cpu: 1 memory: 2048 - rootfs: 25600 + rootfs: 2048 diff --git a/grid-compose/examples/single_service_2.yml b/grid-compose/examples/single-service/single_service_2.yml similarity index 65% rename from grid-compose/examples/single_service_2.yml rename to grid-compose/examples/single-service/single_service_2.yml index d984bf0fa..f44ffa5ef 100644 --- a/grid-compose/examples/single_service_2.yml +++ b/grid-compose/examples/single-service/single_service_2.yml @@ -2,7 +2,7 @@ version: '1.0.0' services: server: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + flist: 'https://hub.grid.tf/tf-official-apps/threefoldtech-ubuntu-22.04.flist' entrypoint: '/sbin/zinit init' resources: cpu: 1 diff --git a/grid-compose/examples/single_service_3.yml b/grid-compose/examples/single-service/single_service_3.yml similarity index 73% rename from grid-compose/examples/single_service_3.yml rename to grid-compose/examples/single-service/single_service_3.yml index 8775065a5..4ffe76969 100644 --- a/grid-compose/examples/single_service_3.yml +++ b/grid-compose/examples/single-service/single_service_3.yml @@ -10,15 +10,15 @@ networks: mask: type: cidr mask: 16/32 - wg: true services: server: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' + flist: 'https://hub.grid.tf/tf-official-apps/threefoldtech-ubuntu-22.04.flist' entrypoint: '/sbin/zinit init' resources: cpu: 1 memory: 2048 rootfs: 2048 - node_id: 144 network: net1 + ip_types: + - myc diff --git a/grid-compose/grid-compose.yaml b/grid-compose/grid-compose.yaml deleted file mode 100644 index 7594400ba..000000000 --- a/grid-compose/grid-compose.yaml +++ /dev/null @@ -1,50 +0,0 @@ -version: '1.0.0' - -networks: - net1: - type: 'wg' - net2: - type: 'myc' - net3: - type: 'ygg' - net4: - type: 'ip4' - net5: - type: 'ip6' - -services: - web5: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - networks: - - net3 - - net4 - - net5 - node_id: 144 - entrypoint: '/sbin/zinit init' - volumes: - - webdata:/data - resources: - cpu: 1 - memory: 2048 - disk: 25600 - database: - flist: 'https://hub.grid.tf/tf-official-vms/ubuntu-24.04-full.flist' - networks: - - net3 - - net1 - volumes: - - dbdata:/var/lib/postgresql/data - node_id: 14 - entrypoint: '/sbin/zinit init' - resources: - cpu: 1 - memory: 2048 - disk: 25600 - -storage: - webdata: - type: 'zmount' - size: 10GB - dbdata: - type: 'zmount' - size: 10GB diff --git a/grid-compose/internal/app/app.go b/grid-compose/internal/app/app.go index 79552ead3..7e4a33dac 100644 --- a/grid-compose/internal/app/app.go +++ b/grid-compose/internal/app/app.go @@ -50,10 +50,12 @@ func NewApp(net, mnemonic, configPath string) (*App, error) { }, nil } +// GetProjectName returns the project name for the given key func (a *App) GetProjectName(key string) string { return fmt.Sprintf("compose/%v/%v", a.Client.TwinID, key) } +// GetDeploymentName returns the deployment name for the given key func (a *App) GetDeploymentName(key string) string { return fmt.Sprintf("dl_%v", key) } @@ -89,7 +91,7 @@ func (a *App) checkIfExistAndAppend(node uint32, contractID uint64) { a.Client.State.CurrentNodeDeployments[node] = append(a.Client.State.CurrentNodeDeployments[node], contractID) } -// validateCredentials validates the mnemonic and network +// validateCredentials validates the mnemonics and network values of the user func validateCredentials(mnemonics, network string) bool { return validateMnemonics(mnemonics) && validateNetwork(network) } diff --git a/grid-compose/internal/types/dependency.go b/grid-compose/internal/app/dependency/dependency.go similarity index 73% rename from grid-compose/internal/types/dependency.go rename to grid-compose/internal/app/dependency/dependency.go index 20f7b0824..a42b73c88 100644 --- a/grid-compose/internal/types/dependency.go +++ b/grid-compose/internal/app/dependency/dependency.go @@ -1,4 +1,5 @@ -package types +// Dependency package provides a directed graph of dependencies between services and resolves them +package dependency import ( "fmt" @@ -6,11 +7,13 @@ import ( "slices" ) +// DRGraph represents a directed graph of dependencies type DRGraph struct { Root *DRNode Nodes map[string]*DRNode } +// NewDRGraph creates a new directed graph func NewDRGraph(root *DRNode) *DRGraph { return &DRGraph{ Nodes: make(map[string]*DRNode), @@ -18,32 +21,35 @@ func NewDRGraph(root *DRNode) *DRGraph { } } +// DRNode represents a node in the directed graph type DRNode struct { Name string Dependencies []*DRNode Parent *DRNode - Service *Service } -func NewDRNode(name string, service *Service) *DRNode { +// NewDRNode creates a new node in the directed graph +func NewDRNode(name string) *DRNode { return &DRNode{ Name: name, Dependencies: []*DRNode{}, - Service: service, } } +// AddDependency adds a dependency to the node func (n *DRNode) AddDependency(dependency *DRNode) { log.Printf("adding dependency %s -> %s", n.Name, dependency.Name) n.Dependencies = append(n.Dependencies, dependency) } +// AddNode adds a node to the graph func (g *DRGraph) AddNode(name string, node *DRNode) *DRNode { g.Nodes[name] = node return node } +// ResolveDependencies resolves the dependencies of the node func (g *DRGraph) ResolveDependencies(node *DRNode, resolved []*DRNode, unresolved []*DRNode) ([]*DRNode, error) { unresolved = append(unresolved, node) diff --git a/grid-compose/internal/app/down.go b/grid-compose/internal/app/down.go index 77047e3ec..ac5ffadef 100644 --- a/grid-compose/internal/app/down.go +++ b/grid-compose/internal/app/down.go @@ -6,7 +6,7 @@ import ( "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/types" ) -// Down cancels all the deployments +// Down cancels all the deployments on the grid // TODO: remove known hosts func (a *App) Down() error { if len(a.Config.Networks) == 0 { diff --git a/grid-compose/internal/app/ps.go b/grid-compose/internal/app/ps.go index a53da0eb0..09bc36118 100644 --- a/grid-compose/internal/app/ps.go +++ b/grid-compose/internal/app/ps.go @@ -10,24 +10,25 @@ import ( "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/deploy" "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/types" + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/pkg/log" "github.com/threefoldtech/zos/pkg/gridtypes" ) -// Ps lists the deployed services -// TODO: error handling and better output(mounts index out of range) +// Ps lists deployments on the grid with the option to show more details func (a *App) Ps(ctx context.Context, verbose bool) error { - var output strings.Builder outputMap := make(map[string]gridtypes.Deployment) if !verbose { - output.WriteString(fmt.Sprintf("%-15s | %-15s | %-15s | %-15s | %-15s | %-10s | %s\n", "Deployment Name", "Node ID", "Network", "Services", "Storage", "State", "IP Address")) + output.WriteString(fmt.Sprintf("%-15s | %-15s | %-15s | %-15s | %-15s | %-10s | %s\n", + "Deployment Name", "Node ID", "Network", "Services", "Storage", "State", "IP Address")) output.WriteString(strings.Repeat("-", 150) + "\n") } if len(a.Config.Networks) == 0 { a.Config.Networks[deploy.GenerateDefaultNetworkName(a.Config.Services)] = types.Network{} } + for networkName := range a.Config.Networks { projectName := a.GetProjectName(networkName) @@ -42,7 +43,6 @@ func (a *App) Ps(ctx context.Context, verbose bool) error { for _, contract := range contracts.NodeContracts { nodeClient, err := a.Client.State.NcPool.GetNodeClient(a.Client.SubstrateConn, contract.NodeID) - if err != nil { return err } @@ -53,17 +53,19 @@ func (a *App) Ps(ctx context.Context, verbose bool) error { return err } + dlAdded := false for _, wl := range dl.Workloads { if wl.Type.String() != "zmachine" { continue } vm, err := workloads.NewVMFromWorkload(&wl, &dl) - if err != nil { return err } - output.WriteString(fmt.Sprintf("%-15s | %-15d | %-15s | %-15s | %-15s | %-10s | %s\n", a.GetDeploymentName(wl.Name.String()), contract.NodeID, vm.NetworkName, vm.Name, vm.Mounts[0].DiskName, wl.Result.State, getVmAddresses(vm))) + + log.WriteVmDetails(&output, vm, wl, a.GetDeploymentName(wl.Name.String()), contract.NodeID, dlAdded, getVmAddresses(vm)) + dlAdded = true } dlData, err := workloads.ParseDeploymentData(dl.Metadata) @@ -74,13 +76,6 @@ func (a *App) Ps(ctx context.Context, verbose bool) error { if verbose { outputMap[dlData.Name] = dl } - - // if !verbose { - // output.WriteString(fmt.Sprintf("%-15s | %-15s | %-15s | %-15s | %-15s | %-10s | %s\n", contractDlData.Name, contract.NodeID, vm.NetworkName, vm.Name, vm.Mounts[0].DiskName, wl.Result.State, getVmAddresses(vm))) - // // output.WriteString(fmt.Sprintf("%-15s | %-15s | %-15s | %-15s | %-10s | %s\n", strings.Repeat("-", 15), strings.Repeat("-", 15), vm.Name, vm.Mounts[0].DiskName, wl.Result.State, getVmAddresses(vm))) - // } - // } - } } @@ -98,25 +93,3 @@ func (a *App) Ps(ctx context.Context, verbose bool) error { fmt.Printf("\n%s\n", output.String()) return nil } - -func getVmAddresses(vm workloads.VM) string { - var addresses strings.Builder - - if vm.IP != "" { - addresses.WriteString(fmt.Sprintf("wireguard: %v, ", vm.IP)) - } - if vm.Planetary { - addresses.WriteString(fmt.Sprintf("yggdrasil: %v, ", vm.PlanetaryIP)) - } - if vm.PublicIP { - addresses.WriteString(fmt.Sprintf("publicIp4: %v, ", vm.ComputedIP)) - } - if vm.PublicIP6 { - addresses.WriteString(fmt.Sprintf("publicIp6: %v, ", vm.ComputedIP6)) - } - if len(vm.MyceliumIPSeed) != 0 { - addresses.WriteString(fmt.Sprintf("mycelium: %v, ", vm.MyceliumIP)) - } - - return strings.TrimSuffix(addresses.String(), ", ") -} diff --git a/grid-compose/internal/app/up.go b/grid-compose/internal/app/up.go index c43e75664..bb0e64c95 100644 --- a/grid-compose/internal/app/up.go +++ b/grid-compose/internal/app/up.go @@ -4,10 +4,11 @@ import ( "context" "github.com/rs/zerolog/log" + "github.com/threefoldtech/tfgrid-sdk-go/grid-client/deployer" "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/app/dependency" "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/convert" "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/deploy" - "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/types" ) // Up deploys the services described in the config file @@ -17,76 +18,85 @@ func (a *App) Up(ctx context.Context) error { return err } - networks := deploy.BuildNetworks(deploymentData.NetworkNodeMap, a.Config.Networks, deploy.GenerateDefaultNetworkName(a.Config.Services), a.GetProjectName) + defaultNetName := deploy.GenerateDefaultNetworkName(a.Config.Services) + networks := deploy.BuildNetworks(deploymentData.NetworkNodeMap, a.Config.Networks, defaultNetName, a.GetProjectName) - resolvedServices, err := deploymentData.ServicesGraph.ResolveDependencies(deploymentData.ServicesGraph.Root, []*types.DRNode{}, []*types.DRNode{}) + resolvedServices, err := deploymentData.ServicesGraph.ResolveDependencies(deploymentData.ServicesGraph.Root, []*dependency.DRNode{}, []*dependency.DRNode{}) if err != nil { return err } deployedDls := make([]*workloads.Deployment, 0) - deployedNets := make([]*workloads.ZNet, 0) + deployedNets := make(map[string]*workloads.ZNet, 0) for _, resService := range resolvedServices { - if resService.Name == "root" { + serviceName := resService.Name + if serviceName == "root" { continue } - serviceName := resService.Name - service := deploymentData.ServicesGraph.Nodes[serviceName].Service - var network *workloads.ZNet + service := a.Config.Services[serviceName] + var network *workloads.ZNet if service.Network == "" { - network = networks[deploy.GenerateDefaultNetworkName(a.Config.Services)] + network = networks[defaultNetName] service.Network = network.Name } else { network = networks[service.Network] } - vm := &workloads.VM{ - Name: serviceName, - Flist: service.Flist, - Entrypoint: service.Entrypoint, - CPU: int(service.Resources.CPU), - Memory: int(service.Resources.Memory), - RootfsSize: int(service.Resources.Rootfs), - NetworkName: network.Name, + vm, err := convert.ConvertServiceToVM(&service, serviceName, network.Name) + if err != nil { + return err } - if err := deploy.BuildVM(vm, service); err != nil { + err = deploy.AssignMyCeliumKeys(network, vm.MyceliumIPSeed) + if err != nil { return err } - disks, err := deploy.BuildDisks(vm, service.Volumes, a.Config.Volumes) + disks, mounts, err := deploy.BuildStorage(service.Volumes, a.Config.Volumes) if err != nil { return err } + vm.Mounts = mounts - deployedDl, err := deploy.DeployVM(ctx, a.Client, *vm, disks, network, a.GetDeploymentName(serviceName), *service.HealthCheck) + if _, ok := deployedNets[network.Name]; !ok { + deployedNets[network.Name] = network + log.Info().Str("name", network.Name).Uint32("node_id", network.Nodes[0]).Msg("deploying network...") + if err := a.Client.NetworkDeployer.Deploy(ctx, network); err != nil { + return rollback(ctx, a.Client, deployedDls, deployedNets, err) + } + log.Info().Msg("deployed successfully") + } + deployedDl, err := deploy.DeployVM(ctx, a.Client, vm, disks, network, a.GetDeploymentName(serviceName), service.HealthCheck) deployedDls = append(deployedDls, &deployedDl) - deployedNets = append(deployedNets, network) if err != nil { - log.Info().Msg("an error occurred while deploying the deployment, canceling all deployments") - log.Info().Msg("canceling networks...") - for _, network := range deployedNets { - if err := a.Client.NetworkDeployer.Cancel(ctx, network); err != nil { - return err - } - } + return rollback(ctx, a.Client, deployedDls, deployedNets, err) + } + } - log.Info().Msg("canceling deployments...") - for _, deployment := range deployedDls { - if err := a.Client.DeploymentDeployer.Cancel(ctx, deployment); err != nil { - return err - } - } + log.Info().Msg("all deployments deployed successfully") + + return nil +} - log.Info().Msg("all deployments canceled successfully") +func rollback(ctx context.Context, client *deployer.TFPluginClient, deployedDls []*workloads.Deployment, deployedNets map[string]*workloads.ZNet, err error) error { + log.Info().Msg("an error occurred while deploying, canceling all deployments") + log.Info().Msg("canceling networks...") + for _, network := range deployedNets { + if err := client.NetworkDeployer.Cancel(ctx, network); err != nil { return err } } - log.Info().Msg("all deployments deployed successfully") + log.Info().Msg("canceling deployments...") + for _, deployment := range deployedDls { + if err := client.DeploymentDeployer.Cancel(ctx, deployment); err != nil { + return err + } + } - return nil + log.Info().Msg("all deployments canceled successfully") + return err } diff --git a/grid-compose/internal/app/version.go b/grid-compose/internal/app/version.go deleted file mode 100644 index 4879f7a48..000000000 --- a/grid-compose/internal/app/version.go +++ /dev/null @@ -1 +0,0 @@ -package app diff --git a/grid-compose/internal/app/vm_addresses.go b/grid-compose/internal/app/vm_addresses.go new file mode 100644 index 000000000..843b4407d --- /dev/null +++ b/grid-compose/internal/app/vm_addresses.go @@ -0,0 +1,30 @@ +package app + +import ( + "fmt" + "strings" + + "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" +) + +func getVmAddresses(vm workloads.VM) string { + var addresses strings.Builder + + if vm.IP != "" { + addresses.WriteString(fmt.Sprintf("wireguard: %v, ", vm.IP)) + } + if vm.Planetary { + addresses.WriteString(fmt.Sprintf("yggdrasil: %v, ", vm.PlanetaryIP)) + } + if vm.PublicIP { + addresses.WriteString(fmt.Sprintf("publicIp4: %v, ", vm.ComputedIP)) + } + if vm.PublicIP6 { + addresses.WriteString(fmt.Sprintf("publicIp6: %v, ", vm.ComputedIP6)) + } + if len(vm.MyceliumIPSeed) != 0 { + addresses.WriteString(fmt.Sprintf("mycelium: %v, ", vm.MyceliumIP)) + } + + return strings.TrimSuffix(addresses.String(), ", ") +} diff --git a/grid-compose/internal/config/config.go b/grid-compose/internal/config/config.go index 94a5718a9..b244fa70f 100644 --- a/grid-compose/internal/config/config.go +++ b/grid-compose/internal/config/config.go @@ -50,6 +50,7 @@ func (c *Config) LoadConfigFromReader(configFile io.Reader) error { } // UnmarshalYAML unmarshals the configuration file content and populates the DeploymentData map +// TODO: Implement unmarshaler func (c *Config) UnmarshalYAML(content []byte) error { if err := yaml.Unmarshal(content, c); err != nil { return err diff --git a/grid-compose/internal/convert/convert.go b/grid-compose/internal/convert/convert.go index 3c25bc69e..60718cb1e 100644 --- a/grid-compose/internal/convert/convert.go +++ b/grid-compose/internal/convert/convert.go @@ -2,26 +2,37 @@ package convert import ( "context" + "crypto/rand" "fmt" + "strings" "github.com/threefoldtech/tfgrid-sdk-go/grid-client/deployer" + "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/app/dependency" "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/config" "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/deploy" "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/types" proxy_types "github.com/threefoldtech/tfgrid-sdk-go/grid-proxy/pkg/types" + "github.com/threefoldtech/zos/pkg/gridtypes/zos" ) -func ConvertConfigToDeploymentData(ctx context.Context, t *deployer.TFPluginClient, config *config.Config) (*types.DeploymentData, error) { +const ( + minCPU = 1 + minMemory = 2048 + minRootfs = 2048 +) + +// ConvertConfigToDeploymentData converts the config to deployment data that will be used to deploy the services +func ConvertConfigToDeploymentData(ctx context.Context, client *deployer.TFPluginClient, config *config.Config) (*types.DeploymentData, error) { deploymentData := &types.DeploymentData{ NetworkNodeMap: make(map[string]*types.NetworkData, 0), - ServicesGraph: types.NewDRGraph(types.NewDRNode("root", nil)), + ServicesGraph: dependency.NewDRGraph(dependency.NewDRNode("root")), } defaultNetName := deploy.GenerateDefaultNetworkName(config.Services) for serviceName, service := range config.Services { svc := service - var netName string if svc.Network == "" { netName = defaultNetName @@ -48,9 +59,8 @@ func ConvertConfigToDeploymentData(ctx context.Context, t *deployer.TFPluginClie svcNode, ok := deploymentData.ServicesGraph.Nodes[serviceName] if !ok { - svcNode = types.NewDRNode( + svcNode = dependency.NewDRNode( serviceName, - &svc, ) deploymentData.ServicesGraph.AddNode(serviceName, svcNode) @@ -65,8 +75,7 @@ func ConvertConfigToDeploymentData(ctx context.Context, t *deployer.TFPluginClie depNode, ok := deploymentData.ServicesGraph.Nodes[dep] if !ok { - depService := config.Services[dep] - depNode = types.NewDRNode(dep, &depService) + depNode = dependency.NewDRNode(dep) } svcNode.AddDependency(depNode) @@ -80,34 +89,47 @@ func ConvertConfigToDeploymentData(ctx context.Context, t *deployer.TFPluginClie } } - if err := getMissingNodes(ctx, deploymentData.NetworkNodeMap, t); err != nil { + if err := getMissingNodes(ctx, deploymentData.NetworkNodeMap, client); err != nil { return nil, err } - // for netName, data := range deploymentData.NetworkNodeMap { - // log.Println(netName) - // log.Println(data.NodeID) - - // for svcName := range data.Services { - // log.Println(svcName) - // } + return deploymentData, nil +} - // } +// ConvertServiceToVM converts the service to a the VM workload that will be used to deploy a virtual machine on the grid +func ConvertServiceToVM(service *types.Service, serviceName, networkName string) (workloads.VM, error) { + vm := workloads.VM{ + Name: serviceName, + Flist: service.Flist, + Entrypoint: service.Entrypoint, + CPU: int(service.Resources.CPU), + Memory: int(service.Resources.Memory), + RootfsSize: int(service.Resources.Rootfs), + NetworkName: networkName, + } - // resolvedServices, err := deploymentData.ServicesGraph.ResolveDependencies(deploymentData.ServicesGraph.Root, []*types.DRNode{}, []*types.DRNode{}) + if vm.RootfsSize < minRootfs { + vm.RootfsSize = minRootfs + } + if vm.CPU < minCPU { + vm.CPU = minCPU + } + if vm.Memory < minMemory { + vm.Memory = minMemory + } - // if err != nil { - // return nil, err - // } + assignEnvs(&vm, service.Environment) - // for _, svc := range resolvedServices { - // log.Println(svc.Name) - // } - return deploymentData, nil + if err := assignNetworksTypes(&vm, service.IPTypes); err != nil { + return workloads.VM{}, err + } + return vm, nil } +// getMissingNodes gets the missing nodes for the deployment data. +// It filters the nodes based on the resources required by the services in one network. // TODO: Calculate total MRU and SRU while populating the deployment data -func getMissingNodes(ctx context.Context, networkNodeMap map[string]*types.NetworkData, t *deployer.TFPluginClient) error { +func getMissingNodes(ctx context.Context, networkNodeMap map[string]*types.NetworkData, client *deployer.TFPluginClient) error { for _, deploymentData := range networkNodeMap { if deploymentData.NodeID != 0 { continue @@ -127,7 +149,7 @@ func getMissingNodes(ctx context.Context, networkNodeMap map[string]*types.Netwo FreeMRU: &freeMRU, } - nodes, _, err := t.GridProxyClient.Nodes(ctx, filter, proxy_types.Limit{}) + nodes, _, err := client.GridProxyClient.Nodes(ctx, filter, proxy_types.Limit{}) if err != nil { return err } @@ -147,3 +169,41 @@ func getMissingNodes(ctx context.Context, networkNodeMap map[string]*types.Netwo return nil } + +// all assign functions will be removed when the unmarshler is implemented +func assignEnvs(vm *workloads.VM, envs []string) { + env := make(map[string]string, 0) + for _, envVar := range envs { + key, value, _ := strings.Cut(envVar, "=") + env[key] = value + } + + vm.EnvVars = env +} + +func assignNetworksTypes(vm *workloads.VM, ipTypes []string) error { + for _, ipType := range ipTypes { + switch ipType { + case "ipv4": + vm.PublicIP = true + case "ipv6": + vm.PublicIP6 = true + case "ygg": + vm.Planetary = true + case "myc": + seed, err := getRandomMyceliumIPSeed() + if err != nil { + return fmt.Errorf("failed to get mycelium seed %w", err) + } + vm.MyceliumIPSeed = seed + } + } + + return nil +} + +func getRandomMyceliumIPSeed() ([]byte, error) { + key := make([]byte, zos.MyceliumIPSeedLen) + _, err := rand.Read(key) + return key, err +} diff --git a/grid-compose/internal/deploy/deploy_vm.go b/grid-compose/internal/deploy/deploy_vm.go index a06033854..6ef97bb74 100644 --- a/grid-compose/internal/deploy/deploy_vm.go +++ b/grid-compose/internal/deploy/deploy_vm.go @@ -2,8 +2,6 @@ package deploy import ( "context" - "crypto/rand" - "fmt" "strconv" "strings" @@ -12,52 +10,38 @@ import ( "github.com/threefoldtech/tfgrid-sdk-go/grid-client/deployer" "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/types" - proxy_types "github.com/threefoldtech/tfgrid-sdk-go/grid-proxy/pkg/types" - "github.com/threefoldtech/zos/pkg/gridtypes/zos" ) -func DeployVM(ctx context.Context, t *deployer.TFPluginClient, vm workloads.VM, disks []workloads.Disk, network *workloads.ZNet, dlName string, healthChecks ...types.HealthCheck) (workloads.Deployment, error) { - log.Info().Str("name", network.Name).Uint32("node_id", network.Nodes[0]).Msg("deploying network...") - if err := t.NetworkDeployer.Deploy(ctx, network); err != nil { - return workloads.Deployment{}, err - } - log.Info().Msg("deployed successfully") - +// DeployVM deploys a vm on the grid +func DeployVM(ctx context.Context, client *deployer.TFPluginClient, vm workloads.VM, disks []workloads.Disk, network *workloads.ZNet, dlName string, healthCheck *types.HealthCheck) (workloads.Deployment, error) { dl := workloads.NewDeployment(dlName, network.Nodes[0], network.SolutionType, nil, network.Name, disks, nil, []workloads.VM{vm}, nil) log.Info().Str("name", vm.Name).Uint32("node_id", dl.NodeID).Msg("deploying vm...") - if err := t.DeploymentDeployer.Deploy(ctx, &dl); err != nil { + if err := client.DeploymentDeployer.Deploy(ctx, &dl); err != nil { return workloads.Deployment{}, err } log.Info().Msg("deployed successfully") - resDl, err := t.State.LoadDeploymentFromGrid(ctx, dl.NodeID, dl.Name) + resDl, err := client.State.LoadDeploymentFromGrid(ctx, dl.NodeID, dl.Name) if err != nil { return workloads.Deployment{}, errors.Wrapf(err, "failed to load vm from node %d", dl.NodeID) } - if len(healthChecks) > 0 { - log.Info().Msg("running health checks...") - for _, hc := range healthChecks { - log.Info().Str("addr", strings.Split(resDl.Vms[0].ComputedIP, "/")[0]).Msg("") - if err := runHealthCheck(hc, "/home/eyad/Downloads/temp/id_rsa", "root", strings.Split(resDl.Vms[0].ComputedIP, "/")[0]); err != nil { - return resDl, err - } + if healthCheck != nil { + log.Info().Msg("running health check...") + + log.Info().Str("addr", strings.Split(resDl.Vms[0].ComputedIP, "/")[0]).Msg("") + if err := runHealthCheck(*healthCheck, "root", strings.Split(resDl.Vms[0].ComputedIP, "/")[0]); err != nil { + return resDl, err } + } return resDl, nil } -func BuildVM(vm *workloads.VM, service *types.Service) error { - assignEnvs(vm, service.Environment) - if err := assignNetworksTypes(vm, service.IPTypes); err != nil { - return err - } - return nil -} - +// BuildStorage converts the config volumes to disks and mounts and returns them. // TODO: Create a parser to parse the size given to each field in service -func BuildDisks(vm *workloads.VM, serviceVolumes []string, volumes map[string]types.Volume) ([]workloads.Disk, error) { +func BuildStorage(serviceVolumes []string, volumes map[string]types.Volume) ([]workloads.Disk, []workloads.Mount, error) { var disks []workloads.Disk mounts := make([]workloads.Mount, 0) for _, volumeName := range serviceVolumes { @@ -66,7 +50,7 @@ func BuildDisks(vm *workloads.VM, serviceVolumes []string, volumes map[string]ty size, err := strconv.Atoi(strings.TrimSuffix(volume.Size, "GB")) if err != nil { - return nil, err + return nil, nil, err } disk := workloads.Disk{ @@ -81,86 +65,6 @@ func BuildDisks(vm *workloads.VM, serviceVolumes []string, volumes map[string]ty MountPoint: volume.MountPoint, }) } - vm.Mounts = mounts - - return disks, nil -} - -// TODO: Calculate total MRU and SRU while populating the deployment data -func getMissingNodes(ctx context.Context, networkNodeMap map[string]*types.NetworkData, client *deployer.TFPluginClient) error { - for _, deploymentData := range networkNodeMap { - if deploymentData.NodeID != 0 { - continue - } - - // freeCRU is not in NodeFilter? - var freeMRU, freeSRU uint64 - - for _, service := range deploymentData.Services { - freeMRU += service.Resources.Memory - freeSRU += service.Resources.Rootfs - } - - filter := proxy_types.NodeFilter{ - Status: []string{"up"}, - FreeSRU: &freeSRU, - FreeMRU: &freeMRU, - } - - nodes, _, err := client.GridProxyClient.Nodes(ctx, filter, proxy_types.Limit{}) - if err != nil { - return err - } - - if len(nodes) == 0 || (len(nodes) == 1 && nodes[0].NodeID == 1) { - return fmt.Errorf("no available nodes") - } - - // TODO: still need to agree on logic to select the node - for _, node := range nodes { - if node.NodeID != 1 { - deploymentData.NodeID = uint32(node.NodeID) - break - } - } - } - - return nil -} - -func assignEnvs(vm *workloads.VM, envs []string) { - env := make(map[string]string, 0) - for _, envVar := range envs { - key, value, _ := strings.Cut(envVar, "=") - env[key] = value - } - - vm.EnvVars = env -} - -func assignNetworksTypes(vm *workloads.VM, ipTypes []string) error { - for _, ipType := range ipTypes { - switch ipType { - case "ipv4": - vm.PublicIP = true - case "ipv6": - vm.PublicIP6 = true - case "ygg": - vm.Planetary = true - case "myc": - seed, err := getRandomMyceliumIPSeed() - if err != nil { - return fmt.Errorf("failed to get mycelium seed %w", err) - } - vm.MyceliumIPSeed = seed - } - } - - return nil -} -func getRandomMyceliumIPSeed() ([]byte, error) { - key := make([]byte, zos.MyceliumIPSeedLen) - _, err := rand.Read(key) - return key, err + return disks, mounts, nil } diff --git a/grid-compose/internal/deploy/healthcheck.go b/grid-compose/internal/deploy/healthcheck.go index 7a742c68c..06651a975 100644 --- a/grid-compose/internal/deploy/healthcheck.go +++ b/grid-compose/internal/deploy/healthcheck.go @@ -3,6 +3,8 @@ package deploy import ( "context" "fmt" + "os" + "path/filepath" "net" "strings" @@ -14,16 +16,9 @@ import ( "golang.org/x/crypto/ssh" ) +// verifyHost verifies the host key of the server. func verifyHost(host string, remote net.Addr, key ssh.PublicKey) error { - - // - // If you want to connect to new hosts. - // here your should check new connections public keys - // if the key not trusted you shuld return an error - // - - // hostFound: is host in known hosts file. - // err: error if key not in known hosts file OR host in known hosts file but key changed! + // Check if the host is in known hosts file. hostFound, err := goph.CheckKnownHost(host, remote, key, "") // Host in known hosts but key mismatch! @@ -42,17 +37,45 @@ func verifyHost(host string, remote net.Addr, key ssh.PublicKey) error { return goph.AddKnownHost(host, remote, key, "") } -// needs refactoring -func runHealthCheck(healthCheck types.HealthCheck, privKeyPath, user, ipAddr string) error { - auth, err := goph.Key(privKeyPath, "") +// runHealthCheck runs the health check on the VM +func runHealthCheck(healthCheck types.HealthCheck, user, ipAddr string) error { + var auth goph.Auth + var err error + + if goph.HasAgent() { + log.Info().Msg("using ssh agent for authentication") + auth, err = goph.UseAgent() + } else { + log.Info().Msg("using private key for authentication") + var sshDir, privKeyPath string + sshDir, err = getUserSSHDir() + if err != nil { + return err + } + privKeyPath = filepath.Join(sshDir, "id_rsa") + auth, err = goph.Key(privKeyPath, "") + } + if err != nil { return err } - time.Sleep(5 * time.Second) + timeoutDuration, err := time.ParseDuration(healthCheck.Timeout) + if err != nil { + return fmt.Errorf("invalid timeout format %w", err) + } + startTime := time.Now() var client *goph.Client - for i := 0; i < 5; i++ { + + for { + elapsedTime := time.Since(startTime) + if elapsedTime >= timeoutDuration { + return fmt.Errorf("timeout reached while waiting for SSH connection") + } + + remainingTime := timeoutDuration - elapsedTime + client, err = goph.NewConn(&goph.Config{ User: user, Port: 22, @@ -62,19 +85,18 @@ func runHealthCheck(healthCheck types.HealthCheck, privKeyPath, user, ipAddr str }) if err == nil { + defer client.Close() break } - log.Info().Str("attempt", fmt.Sprintf("%d", i+1)).Err(err).Msg("ssh connection attempt failed, retrying...") - time.Sleep(5 * time.Second) - } + log.Info().Err(err).Msg("ssh connection attempt failed, retrying...") + time.Sleep(time.Second) - if err != nil { - return fmt.Errorf("failed to establish ssh connection after retries %w", err) + if remainingTime < time.Second { + time.Sleep(remainingTime) + } } - defer client.Close() - command := strings.Join(healthCheck.Test, " ") intervalDuration, err := time.ParseDuration(healthCheck.Interval) @@ -82,17 +104,11 @@ func runHealthCheck(healthCheck types.HealthCheck, privKeyPath, user, ipAddr str return fmt.Errorf("invalid interval format %w", err) } - timeoutDuration, err := time.ParseDuration(healthCheck.Timeout) - if err != nil { - return fmt.Errorf("invalid timeout format %w", err) - } - - var out []byte for i := 0; i < int(healthCheck.Retries); i++ { ctx, cancel := context.WithTimeout(context.Background(), timeoutDuration) defer cancel() - out, err = runCommandWithContext(ctx, client, command) + out, err := runCommandWithContext(ctx, client, command) if err == nil { log.Info().Msgf("health check succeeded %s", string(out)) return nil @@ -105,6 +121,7 @@ func runHealthCheck(healthCheck types.HealthCheck, privKeyPath, user, ipAddr str return fmt.Errorf("health check failed after %d retries %w", healthCheck.Retries, err) } +// runCommandWithContext runs the command on the client with context and returns the output and error func runCommandWithContext(ctx context.Context, client *goph.Client, command string) ([]byte, error) { done := make(chan struct{}) var out []byte @@ -122,3 +139,13 @@ func runCommandWithContext(ctx context.Context, client *goph.Client, command str return out, err } } + +// getUserSSHDir returns the path to the user's SSH directory(e.g. ~/.ssh) +func getUserSSHDir() (string, error) { + home, err := os.UserHomeDir() + if err != nil { + return "", err + } + + return filepath.Join(home, ".ssh"), nil +} diff --git a/grid-compose/internal/deploy/network.go b/grid-compose/internal/deploy/network.go index 83b725517..272b669db 100644 --- a/grid-compose/internal/deploy/network.go +++ b/grid-compose/internal/deploy/network.go @@ -3,6 +3,7 @@ package deploy import ( "fmt" "net" + "sort" "strconv" "strings" @@ -11,6 +12,7 @@ import ( "github.com/threefoldtech/zos/pkg/gridtypes" ) +// BuildNetworks converts the networks in the config to ZNet workloads. // TODO: needs to be refactored func BuildNetworks(networkNodeMap map[string]*types.NetworkData, networks map[string]types.Network, defaultNetName string, getProjectName func(string) string) map[string]*workloads.ZNet { zNets := make(map[string]*workloads.ZNet, 0) @@ -37,21 +39,29 @@ func BuildNetworks(networkNodeMap map[string]*types.NetworkData, networks map[st Nodes: []uint32{networkNodeMap[networkName].NodeID}, SolutionType: getProjectName(networkName), } + } return zNets } +// GenerateDefaultNetworkName generates a default network name based on the sorted service names. func GenerateDefaultNetworkName(services map[string]types.Service) string { - var defaultNetName string - + var serviceNames []string for serviceName := range services { + serviceNames = append(serviceNames, serviceName) + } + sort.Strings(serviceNames) + + var defaultNetName string + for _, serviceName := range serviceNames { defaultNetName += serviceName[:2] } return fmt.Sprintf("net_%s", defaultNetName) } +// GenerateIPNet generates a net.IPNet from the given IP and mask. func GenerateIPNet(ip types.IP, mask types.IPMask) net.IPNet { var ipNet net.IPNet @@ -84,3 +94,20 @@ func GenerateIPNet(ip types.IP, mask types.IPMask) net.IPNet { return ipNet } + +// AssignMyCeliumKeys assigns mycelium keys to the network nodes. +func AssignMyCeliumKeys(network *workloads.ZNet, myceliumIPSeed []byte) error { + keys := make(map[uint32][]byte) + if len(myceliumIPSeed) != 0 { + for _, node := range network.Nodes { + key, err := workloads.RandomMyceliumKey() + if err != nil { + return err + } + keys[node] = key + } + } + + network.MyceliumKeys = keys + return nil +} diff --git a/grid-compose/internal/types/common.go b/grid-compose/internal/types/common.go deleted file mode 100644 index 7cf4f429c..000000000 --- a/grid-compose/internal/types/common.go +++ /dev/null @@ -1,11 +0,0 @@ -package types - -type DeploymentData struct { - ServicesGraph *DRGraph - NetworkNodeMap map[string]*NetworkData -} - -type NetworkData struct { - NodeID uint32 - Services map[string]*Service -} diff --git a/grid-compose/internal/types/deployment.go b/grid-compose/internal/types/deployment.go new file mode 100644 index 000000000..2730bf18f --- /dev/null +++ b/grid-compose/internal/types/deployment.go @@ -0,0 +1,16 @@ +package types + +import "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/app/dependency" + +// DeploymentData is a helper struct to hold the deployment data to ease the deployment process. +type DeploymentData struct { + ServicesGraph *dependency.DRGraph + NetworkNodeMap map[string]*NetworkData +} + +// NetworkData is a helper struct to hold the network data to ease the deployment process. +// It holds the node id and the services that are part of a network. +type NetworkData struct { + NodeID uint32 + Services map[string]*Service +} diff --git a/grid-compose/pkg/log/vm_details.go b/grid-compose/pkg/log/vm_details.go new file mode 100644 index 000000000..ae7626a22 --- /dev/null +++ b/grid-compose/pkg/log/vm_details.go @@ -0,0 +1,42 @@ +package log + +import ( + "fmt" + "strings" + + "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" + "github.com/threefoldtech/zos/pkg/gridtypes" +) + +// WriteVmDetails writes the details of a VM to the output string builder +func WriteVmDetails(output *strings.Builder, vm workloads.VM, wl gridtypes.Workload, deploymentName string, nodeID uint32, dlAdded bool, vmAddresses string) { + if !dlAdded { + if len(vm.Mounts) < 1 { + output.WriteString(fmt.Sprintf("%-15s | %-15d | %-15s | %-15s | %-15s | %-10s | %s\n", + deploymentName, nodeID, vm.NetworkName, vm.Name, "None", wl.Result.State, vmAddresses)) + return + } + + output.WriteString(fmt.Sprintf("%-15s | %-15d | %-15s | %-15s | %-15s | %-10s | %s\n", + deploymentName, nodeID, vm.NetworkName, vm.Name, vm.Mounts[0].DiskName, wl.Result.State, vmAddresses)) + + for _, mount := range vm.Mounts[1:] { + output.WriteString(fmt.Sprintf("%-15s | %-15s | %-15s | %-15s | %-15s | %-10s | %s\n", + strings.Repeat("-", 15), strings.Repeat("-", 15), strings.Repeat("-", 15), strings.Repeat("-", 15), mount.DiskName, wl.Result.State, strings.Repeat("-", 47))) + } + } else { + if len(vm.Mounts) < 1 { + output.WriteString(fmt.Sprintf("%-15s | %-15s | %-15s | %-15s | %-15s | %-10s | %s\n", + strings.Repeat("-", 15), strings.Repeat("-", 15), strings.Repeat("-", 15), vm.Name, "None", wl.Result.State, vmAddresses)) + return + } + + output.WriteString(fmt.Sprintf("%-15s | %-15s | %-15s | %-15s | %-15s | %-10s | %s\n", + strings.Repeat("-", 15), strings.Repeat("-", 15), strings.Repeat("-", 15), vm.Name, vm.Mounts[0].DiskName, wl.Result.State, vmAddresses)) + + for _, mount := range vm.Mounts[1:] { + output.WriteString(fmt.Sprintf("%-15s | %-15s | %-15s | %-15s | %-15s | %-10s | %s\n", + strings.Repeat("-", 15), strings.Repeat("-", 15), strings.Repeat("-", 15), strings.Repeat("-", 15), mount.DiskName, wl.Result.State, strings.Repeat("-", 15))) + } + } +} From 124fe563097c44b9ec82dbdc1fd5b2c7b6b56ab5 Mon Sep 17 00:00:00 2001 From: eyad-hussein Date: Thu, 22 Aug 2024 05:55:17 +0300 Subject: [PATCH 17/20] fix: add check for app type assertion, refactor: remove deploy package and move its components to app --- grid-compose/cmd/down.go | 6 +- grid-compose/cmd/ps.go | 6 +- grid-compose/cmd/up.go | 6 +- grid-compose/internal/app/app.go | 17 ++ grid-compose/internal/app/down.go | 3 +- .../internal/{deploy => app}/healthcheck.go | 5 +- grid-compose/internal/app/ps.go | 3 +- grid-compose/internal/app/up.go | 35 +--- grid-compose/internal/app/up_utils.go | 176 ++++++++++++++++++ grid-compose/internal/convert/convert.go | 5 +- grid-compose/internal/deploy/deploy_vm.go | 70 ------- grid-compose/internal/deploy/network.go | 113 ----------- 12 files changed, 220 insertions(+), 225 deletions(-) rename grid-compose/internal/{deploy => app}/healthcheck.go (99%) create mode 100644 grid-compose/internal/app/up_utils.go delete mode 100644 grid-compose/internal/deploy/deploy_vm.go delete mode 100644 grid-compose/internal/deploy/network.go diff --git a/grid-compose/cmd/down.go b/grid-compose/cmd/down.go index a04cec5a8..2c7897c43 100644 --- a/grid-compose/cmd/down.go +++ b/grid-compose/cmd/down.go @@ -10,7 +10,11 @@ var downCmd = &cobra.Command{ Use: "down", Short: "cancel your project on the grid", Run: func(cmd *cobra.Command, args []string) { - app := cmd.Context().Value("app").(*app.App) + app, ok := cmd.Context().Value("app").(*app.App) + if !ok { + log.Fatal().Msg("app not found in context") + } + if err := app.Down(); err != nil { log.Fatal().Err(err).Send() } diff --git a/grid-compose/cmd/ps.go b/grid-compose/cmd/ps.go index f677f6ccc..c61a7d46d 100644 --- a/grid-compose/cmd/ps.go +++ b/grid-compose/cmd/ps.go @@ -15,7 +15,11 @@ var psCmd = &cobra.Command{ log.Fatal().Err(err).Send() } - app := cmd.Context().Value("app").(*app.App) + app, ok := cmd.Context().Value("app").(*app.App) + if !ok { + log.Fatal().Msg("app not found in context") + } + if err := app.Ps(cmd.Context(), verbose); err != nil { log.Fatal().Err(err).Send() } diff --git a/grid-compose/cmd/up.go b/grid-compose/cmd/up.go index 8030f43bb..bc8c305d4 100644 --- a/grid-compose/cmd/up.go +++ b/grid-compose/cmd/up.go @@ -10,7 +10,11 @@ var upCmd = &cobra.Command{ Use: "up", Short: "deploy application on the grid", Run: func(cmd *cobra.Command, args []string) { - app := cmd.Context().Value("app").(*app.App) + app, ok := cmd.Context().Value("app").(*app.App) + if !ok { + log.Fatal().Msg("app not found in context") + } + if err := app.Up(cmd.Context()); err != nil { log.Fatal().Err(err).Send() } diff --git a/grid-compose/internal/app/app.go b/grid-compose/internal/app/app.go index 7e4a33dac..bf673502e 100644 --- a/grid-compose/internal/app/app.go +++ b/grid-compose/internal/app/app.go @@ -3,6 +3,7 @@ package app import ( "fmt" "os" + "sort" "strconv" "github.com/cosmos/go-bip39" @@ -60,6 +61,22 @@ func (a *App) GetDeploymentName(key string) string { return fmt.Sprintf("dl_%v", key) } +// GenerateDefaultNetworkName generates a default network name based on the sorted service names. +func (a *App) GenerateDefaultNetworkName() string { + var serviceNames []string + for serviceName := range a.Config.Services { + serviceNames = append(serviceNames, serviceName) + } + sort.Strings(serviceNames) + + var defaultNetName string + for _, serviceName := range serviceNames { + defaultNetName += serviceName[:2] + } + + return fmt.Sprintf("net_%s", defaultNetName) +} + func (a *App) loadCurrentNodeDeployments(projectName string) error { contracts, err := a.Client.ContractsGetter.ListContractsOfProjectName(projectName, true) if err != nil { diff --git a/grid-compose/internal/app/down.go b/grid-compose/internal/app/down.go index ac5ffadef..ac014c6f0 100644 --- a/grid-compose/internal/app/down.go +++ b/grid-compose/internal/app/down.go @@ -2,7 +2,6 @@ package app import ( "github.com/rs/zerolog/log" - "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/deploy" "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/types" ) @@ -10,7 +9,7 @@ import ( // TODO: remove known hosts func (a *App) Down() error { if len(a.Config.Networks) == 0 { - a.Config.Networks[deploy.GenerateDefaultNetworkName(a.Config.Services)] = types.Network{} + a.Config.Networks[a.GenerateDefaultNetworkName()] = types.Network{} } for networkName := range a.Config.Networks { projectName := a.GetProjectName(networkName) diff --git a/grid-compose/internal/deploy/healthcheck.go b/grid-compose/internal/app/healthcheck.go similarity index 99% rename from grid-compose/internal/deploy/healthcheck.go rename to grid-compose/internal/app/healthcheck.go index 06651a975..c75c524ae 100644 --- a/grid-compose/internal/deploy/healthcheck.go +++ b/grid-compose/internal/app/healthcheck.go @@ -1,12 +1,11 @@ -package deploy +package app import ( "context" "fmt" + "net" "os" "path/filepath" - - "net" "strings" "time" diff --git a/grid-compose/internal/app/ps.go b/grid-compose/internal/app/ps.go index 09bc36118..28437da68 100644 --- a/grid-compose/internal/app/ps.go +++ b/grid-compose/internal/app/ps.go @@ -8,7 +8,6 @@ import ( "strings" "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" - "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/deploy" "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/types" "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/pkg/log" "github.com/threefoldtech/zos/pkg/gridtypes" @@ -26,7 +25,7 @@ func (a *App) Ps(ctx context.Context, verbose bool) error { } if len(a.Config.Networks) == 0 { - a.Config.Networks[deploy.GenerateDefaultNetworkName(a.Config.Services)] = types.Network{} + a.Config.Networks[a.GenerateDefaultNetworkName()] = types.Network{} } for networkName := range a.Config.Networks { diff --git a/grid-compose/internal/app/up.go b/grid-compose/internal/app/up.go index bb0e64c95..a4a7c5e9a 100644 --- a/grid-compose/internal/app/up.go +++ b/grid-compose/internal/app/up.go @@ -4,28 +4,27 @@ import ( "context" "github.com/rs/zerolog/log" - "github.com/threefoldtech/tfgrid-sdk-go/grid-client/deployer" "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/app/dependency" "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/convert" - "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/deploy" ) // Up deploys the services described in the config file func (a *App) Up(ctx context.Context) error { - deploymentData, err := convert.ConvertConfigToDeploymentData(ctx, a.Client, a.Config) + defaultNetName := a.GenerateDefaultNetworkName() + deploymentData, err := convert.ConvertConfigToDeploymentData(ctx, a.Client, a.Config, defaultNetName) if err != nil { return err } - defaultNetName := deploy.GenerateDefaultNetworkName(a.Config.Services) - networks := deploy.BuildNetworks(deploymentData.NetworkNodeMap, a.Config.Networks, defaultNetName, a.GetProjectName) + networks := buildNetworks(deploymentData.NetworkNodeMap, a.Config.Networks, defaultNetName, a.GetProjectName) resolvedServices, err := deploymentData.ServicesGraph.ResolveDependencies(deploymentData.ServicesGraph.Root, []*dependency.DRNode{}, []*dependency.DRNode{}) if err != nil { return err } + // maybe add a deployed field for both services and networks instead of using maps and slices deployedDls := make([]*workloads.Deployment, 0) deployedNets := make(map[string]*workloads.ZNet, 0) for _, resService := range resolvedServices { @@ -49,12 +48,12 @@ func (a *App) Up(ctx context.Context) error { return err } - err = deploy.AssignMyCeliumKeys(network, vm.MyceliumIPSeed) + err = assignMyCeliumKeys(network, vm.MyceliumIPSeed) if err != nil { return err } - disks, mounts, err := deploy.BuildStorage(service.Volumes, a.Config.Volumes) + disks, mounts, err := buildStorage(service.Volumes, a.Config.Volumes) if err != nil { return err } @@ -69,7 +68,7 @@ func (a *App) Up(ctx context.Context) error { log.Info().Msg("deployed successfully") } - deployedDl, err := deploy.DeployVM(ctx, a.Client, vm, disks, network, a.GetDeploymentName(serviceName), service.HealthCheck) + deployedDl, err := deployVM(ctx, a.Client, vm, disks, network, a.GetDeploymentName(serviceName), service.HealthCheck) deployedDls = append(deployedDls, &deployedDl) if err != nil { return rollback(ctx, a.Client, deployedDls, deployedNets, err) @@ -80,23 +79,3 @@ func (a *App) Up(ctx context.Context) error { return nil } - -func rollback(ctx context.Context, client *deployer.TFPluginClient, deployedDls []*workloads.Deployment, deployedNets map[string]*workloads.ZNet, err error) error { - log.Info().Msg("an error occurred while deploying, canceling all deployments") - log.Info().Msg("canceling networks...") - for _, network := range deployedNets { - if err := client.NetworkDeployer.Cancel(ctx, network); err != nil { - return err - } - } - - log.Info().Msg("canceling deployments...") - for _, deployment := range deployedDls { - if err := client.DeploymentDeployer.Cancel(ctx, deployment); err != nil { - return err - } - } - - log.Info().Msg("all deployments canceled successfully") - return err -} diff --git a/grid-compose/internal/app/up_utils.go b/grid-compose/internal/app/up_utils.go new file mode 100644 index 000000000..4a2a858c9 --- /dev/null +++ b/grid-compose/internal/app/up_utils.go @@ -0,0 +1,176 @@ +package app + +import ( + "context" + "net" + "strconv" + "strings" + + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + "github.com/threefoldtech/tfgrid-sdk-go/grid-client/deployer" + "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/types" + "github.com/threefoldtech/zos/pkg/gridtypes" +) + +// deployVM deploys a vm on the grid +func deployVM(ctx context.Context, client *deployer.TFPluginClient, vm workloads.VM, disks []workloads.Disk, network *workloads.ZNet, dlName string, healthCheck *types.HealthCheck) (workloads.Deployment, error) { + dl := workloads.NewDeployment(dlName, network.Nodes[0], network.SolutionType, nil, network.Name, disks, nil, []workloads.VM{vm}, nil) + log.Info().Str("name", vm.Name).Uint32("node_id", dl.NodeID).Msg("deploying vm...") + if err := client.DeploymentDeployer.Deploy(ctx, &dl); err != nil { + return workloads.Deployment{}, err + } + log.Info().Msg("deployed successfully") + + resDl, err := client.State.LoadDeploymentFromGrid(ctx, dl.NodeID, dl.Name) + if err != nil { + return workloads.Deployment{}, errors.Wrapf(err, "failed to load vm from node %d", dl.NodeID) + } + + if healthCheck != nil { + log.Info().Msg("running health check...") + + log.Info().Str("addr", strings.Split(resDl.Vms[0].ComputedIP, "/")[0]).Msg("") + if err := runHealthCheck(*healthCheck, "root", strings.Split(resDl.Vms[0].ComputedIP, "/")[0]); err != nil { + return resDl, err + } + + } + + return resDl, nil +} + +// buildStorage converts the config volumes to disks and mounts and returns them. +// TODO: Create a parser to parse the size given to each field in service +func buildStorage(serviceVolumes []string, volumes map[string]types.Volume) ([]workloads.Disk, []workloads.Mount, error) { + var disks []workloads.Disk + mounts := make([]workloads.Mount, 0) + for _, volumeName := range serviceVolumes { + volume := volumes[volumeName] + + size, err := strconv.Atoi(strings.TrimSuffix(volume.Size, "GB")) + + if err != nil { + return nil, nil, err + } + + disk := workloads.Disk{ + Name: volumeName, + SizeGB: size, + } + + disks = append(disks, disk) + + mounts = append(mounts, workloads.Mount{ + DiskName: disk.Name, + MountPoint: volume.MountPoint, + }) + } + + return disks, mounts, nil +} + +// buildNetworks converts the networks in the config to ZNet workloads. +// TODO: needs to be refactored +func buildNetworks(networkNodeMap map[string]*types.NetworkData, networks map[string]types.Network, defaultNetName string, getProjectName func(string) string) map[string]*workloads.ZNet { + zNets := make(map[string]*workloads.ZNet, 0) + if _, ok := networkNodeMap[defaultNetName]; ok { + zNets[defaultNetName] = &workloads.ZNet{ + Name: defaultNetName, + IPRange: gridtypes.NewIPNet(net.IPNet{ + IP: net.IPv4(10, 20, 0, 0), + Mask: net.CIDRMask(16, 32), + }), + AddWGAccess: false, + Nodes: []uint32{networkNodeMap[defaultNetName].NodeID}, + SolutionType: getProjectName(defaultNetName), + } + } + + for networkName, network := range networks { + zNets[networkName] = &workloads.ZNet{ + Name: network.Name, + Description: network.Description, + IPRange: gridtypes.NewIPNet(generateIPNet(network.IPRange.IP, network.IPRange.Mask)), + AddWGAccess: network.AddWGAccess, + MyceliumKeys: network.MyceliumKeys, + Nodes: []uint32{networkNodeMap[networkName].NodeID}, + SolutionType: getProjectName(networkName), + } + + } + + return zNets +} + +// generateIPNet generates a net.IPNet from the given IP and mask. +func generateIPNet(ip types.IP, mask types.IPMask) net.IPNet { + var ipNet net.IPNet + + switch ip.Type { + case "ipv4": + ipSplit := strings.Split(ip.IP, ".") + byte1, _ := strconv.ParseUint(ipSplit[0], 10, 8) + byte2, _ := strconv.ParseUint(ipSplit[1], 10, 8) + byte3, _ := strconv.ParseUint(ipSplit[2], 10, 8) + byte4, _ := strconv.ParseUint(ipSplit[3], 10, 8) + + ipNet.IP = net.IPv4(byte(byte1), byte(byte2), byte(byte3), byte(byte4)) + default: + return ipNet + } + + var maskIP net.IPMask + + switch mask.Type { + case "cidr": + maskSplit := strings.Split(mask.Mask, "/") + maskOnes, _ := strconv.ParseInt(maskSplit[0], 10, 8) + maskBits, _ := strconv.ParseInt(maskSplit[1], 10, 8) + + maskIP = net.CIDRMask(int(maskOnes), int(maskBits)) + ipNet.Mask = maskIP + default: + return ipNet + } + + return ipNet +} + +// assignMyCeliumKeys assigns mycelium keys to the network nodes. +func assignMyCeliumKeys(network *workloads.ZNet, myceliumIPSeed []byte) error { + keys := make(map[uint32][]byte) + if len(myceliumIPSeed) != 0 { + for _, node := range network.Nodes { + key, err := workloads.RandomMyceliumKey() + if err != nil { + return err + } + keys[node] = key + } + } + + network.MyceliumKeys = keys + return nil +} + +func rollback(ctx context.Context, client *deployer.TFPluginClient, deployedDls []*workloads.Deployment, deployedNets map[string]*workloads.ZNet, err error) error { + log.Info().Msg("an error occurred while deploying, canceling all deployments") + log.Info().Msg("canceling networks...") + for _, network := range deployedNets { + if err := client.NetworkDeployer.Cancel(ctx, network); err != nil { + return err + } + } + + log.Info().Msg("canceling deployments...") + for _, deployment := range deployedDls { + if err := client.DeploymentDeployer.Cancel(ctx, deployment); err != nil { + return err + } + } + + log.Info().Msg("all deployments canceled successfully") + return err +} diff --git a/grid-compose/internal/convert/convert.go b/grid-compose/internal/convert/convert.go index 60718cb1e..02c54f963 100644 --- a/grid-compose/internal/convert/convert.go +++ b/grid-compose/internal/convert/convert.go @@ -10,7 +10,6 @@ import ( "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/app/dependency" "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/config" - "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/deploy" "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/types" proxy_types "github.com/threefoldtech/tfgrid-sdk-go/grid-proxy/pkg/types" "github.com/threefoldtech/zos/pkg/gridtypes/zos" @@ -23,14 +22,12 @@ const ( ) // ConvertConfigToDeploymentData converts the config to deployment data that will be used to deploy the services -func ConvertConfigToDeploymentData(ctx context.Context, client *deployer.TFPluginClient, config *config.Config) (*types.DeploymentData, error) { +func ConvertConfigToDeploymentData(ctx context.Context, client *deployer.TFPluginClient, config *config.Config, defaultNetName string) (*types.DeploymentData, error) { deploymentData := &types.DeploymentData{ NetworkNodeMap: make(map[string]*types.NetworkData, 0), ServicesGraph: dependency.NewDRGraph(dependency.NewDRNode("root")), } - defaultNetName := deploy.GenerateDefaultNetworkName(config.Services) - for serviceName, service := range config.Services { svc := service var netName string diff --git a/grid-compose/internal/deploy/deploy_vm.go b/grid-compose/internal/deploy/deploy_vm.go deleted file mode 100644 index 6ef97bb74..000000000 --- a/grid-compose/internal/deploy/deploy_vm.go +++ /dev/null @@ -1,70 +0,0 @@ -package deploy - -import ( - "context" - "strconv" - "strings" - - "github.com/pkg/errors" - "github.com/rs/zerolog/log" - "github.com/threefoldtech/tfgrid-sdk-go/grid-client/deployer" - "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" - "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/types" -) - -// DeployVM deploys a vm on the grid -func DeployVM(ctx context.Context, client *deployer.TFPluginClient, vm workloads.VM, disks []workloads.Disk, network *workloads.ZNet, dlName string, healthCheck *types.HealthCheck) (workloads.Deployment, error) { - dl := workloads.NewDeployment(dlName, network.Nodes[0], network.SolutionType, nil, network.Name, disks, nil, []workloads.VM{vm}, nil) - log.Info().Str("name", vm.Name).Uint32("node_id", dl.NodeID).Msg("deploying vm...") - if err := client.DeploymentDeployer.Deploy(ctx, &dl); err != nil { - return workloads.Deployment{}, err - } - log.Info().Msg("deployed successfully") - - resDl, err := client.State.LoadDeploymentFromGrid(ctx, dl.NodeID, dl.Name) - if err != nil { - return workloads.Deployment{}, errors.Wrapf(err, "failed to load vm from node %d", dl.NodeID) - } - - if healthCheck != nil { - log.Info().Msg("running health check...") - - log.Info().Str("addr", strings.Split(resDl.Vms[0].ComputedIP, "/")[0]).Msg("") - if err := runHealthCheck(*healthCheck, "root", strings.Split(resDl.Vms[0].ComputedIP, "/")[0]); err != nil { - return resDl, err - } - - } - - return resDl, nil -} - -// BuildStorage converts the config volumes to disks and mounts and returns them. -// TODO: Create a parser to parse the size given to each field in service -func BuildStorage(serviceVolumes []string, volumes map[string]types.Volume) ([]workloads.Disk, []workloads.Mount, error) { - var disks []workloads.Disk - mounts := make([]workloads.Mount, 0) - for _, volumeName := range serviceVolumes { - volume := volumes[volumeName] - - size, err := strconv.Atoi(strings.TrimSuffix(volume.Size, "GB")) - - if err != nil { - return nil, nil, err - } - - disk := workloads.Disk{ - Name: volumeName, - SizeGB: size, - } - - disks = append(disks, disk) - - mounts = append(mounts, workloads.Mount{ - DiskName: disk.Name, - MountPoint: volume.MountPoint, - }) - } - - return disks, mounts, nil -} diff --git a/grid-compose/internal/deploy/network.go b/grid-compose/internal/deploy/network.go deleted file mode 100644 index 272b669db..000000000 --- a/grid-compose/internal/deploy/network.go +++ /dev/null @@ -1,113 +0,0 @@ -package deploy - -import ( - "fmt" - "net" - "sort" - "strconv" - "strings" - - "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" - "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/types" - "github.com/threefoldtech/zos/pkg/gridtypes" -) - -// BuildNetworks converts the networks in the config to ZNet workloads. -// TODO: needs to be refactored -func BuildNetworks(networkNodeMap map[string]*types.NetworkData, networks map[string]types.Network, defaultNetName string, getProjectName func(string) string) map[string]*workloads.ZNet { - zNets := make(map[string]*workloads.ZNet, 0) - if _, ok := networkNodeMap[defaultNetName]; ok { - zNets[defaultNetName] = &workloads.ZNet{ - Name: defaultNetName, - IPRange: gridtypes.NewIPNet(net.IPNet{ - IP: net.IPv4(10, 20, 0, 0), - Mask: net.CIDRMask(16, 32), - }), - AddWGAccess: false, - Nodes: []uint32{networkNodeMap[defaultNetName].NodeID}, - SolutionType: getProjectName(defaultNetName), - } - } - - for networkName, network := range networks { - zNets[networkName] = &workloads.ZNet{ - Name: network.Name, - Description: network.Description, - IPRange: gridtypes.NewIPNet(GenerateIPNet(network.IPRange.IP, network.IPRange.Mask)), - AddWGAccess: network.AddWGAccess, - MyceliumKeys: network.MyceliumKeys, - Nodes: []uint32{networkNodeMap[networkName].NodeID}, - SolutionType: getProjectName(networkName), - } - - } - - return zNets -} - -// GenerateDefaultNetworkName generates a default network name based on the sorted service names. -func GenerateDefaultNetworkName(services map[string]types.Service) string { - var serviceNames []string - for serviceName := range services { - serviceNames = append(serviceNames, serviceName) - } - sort.Strings(serviceNames) - - var defaultNetName string - for _, serviceName := range serviceNames { - defaultNetName += serviceName[:2] - } - - return fmt.Sprintf("net_%s", defaultNetName) -} - -// GenerateIPNet generates a net.IPNet from the given IP and mask. -func GenerateIPNet(ip types.IP, mask types.IPMask) net.IPNet { - var ipNet net.IPNet - - switch ip.Type { - case "ipv4": - ipSplit := strings.Split(ip.IP, ".") - byte1, _ := strconv.ParseUint(ipSplit[0], 10, 8) - byte2, _ := strconv.ParseUint(ipSplit[1], 10, 8) - byte3, _ := strconv.ParseUint(ipSplit[2], 10, 8) - byte4, _ := strconv.ParseUint(ipSplit[3], 10, 8) - - ipNet.IP = net.IPv4(byte(byte1), byte(byte2), byte(byte3), byte(byte4)) - default: - return ipNet - } - - var maskIP net.IPMask - - switch mask.Type { - case "cidr": - maskSplit := strings.Split(mask.Mask, "/") - maskOnes, _ := strconv.ParseInt(maskSplit[0], 10, 8) - maskBits, _ := strconv.ParseInt(maskSplit[1], 10, 8) - - maskIP = net.CIDRMask(int(maskOnes), int(maskBits)) - ipNet.Mask = maskIP - default: - return ipNet - } - - return ipNet -} - -// AssignMyCeliumKeys assigns mycelium keys to the network nodes. -func AssignMyCeliumKeys(network *workloads.ZNet, myceliumIPSeed []byte) error { - keys := make(map[uint32][]byte) - if len(myceliumIPSeed) != 0 { - for _, node := range network.Nodes { - key, err := workloads.RandomMyceliumKey() - if err != nil { - return err - } - keys[node] = key - } - } - - network.MyceliumKeys = keys - return nil -} From deb627583cccc7cc97edd1ba92201a1766bf857d Mon Sep 17 00:00:00 2001 From: eyad-hussein Date: Sat, 24 Aug 2024 21:23:59 +0300 Subject: [PATCH 18/20] feat: add support for different storage units for service resources refactor: move config to pkg and create storage parser validation: add extensive validation for services and config --- grid-compose/docs/cases.md | 4 +- grid-compose/docs/config.md | 3 + grid-compose/docs/future_work.md | 1 - grid-compose/internal/app/app.go | 4 +- grid-compose/internal/app/up.go | 8 +++ grid-compose/internal/app/up_utils.go | 8 ++- grid-compose/internal/convert/convert.go | 22 ++---- grid-compose/internal/types/service.go | 67 ++++++++++++++++++- .../{internal => pkg/parser}/config/config.go | 47 ++++++------- grid-compose/pkg/parser/storage.go | 44 ++++++++++++ 10 files changed, 155 insertions(+), 53 deletions(-) rename grid-compose/{internal => pkg/parser}/config/config.go (53%) create mode 100644 grid-compose/pkg/parser/storage.go diff --git a/grid-compose/docs/cases.md b/grid-compose/docs/cases.md index 79b31afac..7e939c55f 100644 --- a/grid-compose/docs/cases.md +++ b/grid-compose/docs/cases.md @@ -14,9 +14,7 @@ Refer to example [single_service_1.yml](/examples/single-service/single_service_ ### Case 2 - Node ID Given + No Assigned Network - Simply use the the node id given to deploy the service. - - (**CURRENT BEHAVIOR**) Return an error if node is not available - - (**TODO BEHAVIOR**) If the available resources in the given node are less than what the service needs, filter the nodes and check if there is one that can be used. - - If there is a node available, prompt the user if they would like to use it instead. + - Return an error if node is not available - Generate a default network and assign it to the deployment. Refer to example [single_service_2.yml](/examples/single-service/single_service_2.yml) diff --git a/grid-compose/docs/config.md b/grid-compose/docs/config.md index c67a97b25..f53533e00 100644 --- a/grid-compose/docs/config.md +++ b/grid-compose/docs/config.md @@ -92,6 +92,9 @@ The `services` section defines the services to deploy. Each service has the foll - `flist`: The URL of the flist to deploy. - `resources`: The resources required by the service (CPU, memory, and rootfs) `optional`. - By default, the tool will use the minimum resources required to deploy the service. + - `cpu`: 1 + - `memory`: 256MB + - `rootfs`: 2GB - `entrypoint`: The entrypoint command to run when the service starts. - `ip_types`: The types of IP addresses to assign to the service `optional`. - ip type can be ipv4, ipv6, mycelium, yggdrasil. diff --git a/grid-compose/docs/future_work.md b/grid-compose/docs/future_work.md index ac2856a4b..0fa11e885 100644 --- a/grid-compose/docs/future_work.md +++ b/grid-compose/docs/future_work.md @@ -4,4 +4,3 @@ This document outlines the future work that is to be done on the grid-compose pr 1. Add path for ssh keys instead of hardcoding it in the config file. 2. Ensure backward compatibility with versions of grid-compose. -3. Create a parser for the different disk sizes(MB, GB). diff --git a/grid-compose/internal/app/app.go b/grid-compose/internal/app/app.go index bf673502e..7008ddb9e 100644 --- a/grid-compose/internal/app/app.go +++ b/grid-compose/internal/app/app.go @@ -8,7 +8,7 @@ import ( "github.com/cosmos/go-bip39" "github.com/threefoldtech/tfgrid-sdk-go/grid-client/deployer" - "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/config" + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/pkg/parser/config" ) // App is the main application struct that holds the client and the config data @@ -36,7 +36,7 @@ func NewApp(net, mnemonic, configPath string) (*App, error) { return nil, fmt.Errorf("failed to load config from file %w", err) } - if err := config.ValidateConfig(); err != nil { + if err := config.Validate(); err != nil { return nil, fmt.Errorf("failed to validate config %w", err) } diff --git a/grid-compose/internal/app/up.go b/grid-compose/internal/app/up.go index a4a7c5e9a..3aa6a6445 100644 --- a/grid-compose/internal/app/up.go +++ b/grid-compose/internal/app/up.go @@ -59,9 +59,17 @@ func (a *App) Up(ctx context.Context) error { } vm.Mounts = mounts + if err := vm.Validate(); err != nil { + return rollback(ctx, a.Client, deployedDls, deployedNets, err) + } + if _, ok := deployedNets[network.Name]; !ok { deployedNets[network.Name] = network log.Info().Str("name", network.Name).Uint32("node_id", network.Nodes[0]).Msg("deploying network...") + if err := network.Validate(); err != nil { + return rollback(ctx, a.Client, deployedDls, deployedNets, err) + } + if err := a.Client.NetworkDeployer.Deploy(ctx, network); err != nil { return rollback(ctx, a.Client, deployedDls, deployedNets, err) } diff --git a/grid-compose/internal/app/up_utils.go b/grid-compose/internal/app/up_utils.go index 4a2a858c9..5deb8b7fb 100644 --- a/grid-compose/internal/app/up_utils.go +++ b/grid-compose/internal/app/up_utils.go @@ -16,7 +16,12 @@ import ( // deployVM deploys a vm on the grid func deployVM(ctx context.Context, client *deployer.TFPluginClient, vm workloads.VM, disks []workloads.Disk, network *workloads.ZNet, dlName string, healthCheck *types.HealthCheck) (workloads.Deployment, error) { - dl := workloads.NewDeployment(dlName, network.Nodes[0], network.SolutionType, nil, network.Name, disks, nil, []workloads.VM{vm}, nil) + // volumes is nil until it is clear what it is used for + dl := workloads.NewDeployment(dlName, network.Nodes[0], network.SolutionType, nil, network.Name, disks, nil, []workloads.VM{vm}, nil, nil) + if err := dl.Validate(); err != nil { + return workloads.Deployment{}, err + } + log.Info().Str("name", vm.Name).Uint32("node_id", dl.NodeID).Msg("deploying vm...") if err := client.DeploymentDeployer.Deploy(ctx, &dl); err != nil { return workloads.Deployment{}, err @@ -42,7 +47,6 @@ func deployVM(ctx context.Context, client *deployer.TFPluginClient, vm workloads } // buildStorage converts the config volumes to disks and mounts and returns them. -// TODO: Create a parser to parse the size given to each field in service func buildStorage(serviceVolumes []string, volumes map[string]types.Volume) ([]workloads.Disk, []workloads.Mount, error) { var disks []workloads.Disk mounts := make([]workloads.Mount, 0) diff --git a/grid-compose/internal/convert/convert.go b/grid-compose/internal/convert/convert.go index 02c54f963..cbb41e953 100644 --- a/grid-compose/internal/convert/convert.go +++ b/grid-compose/internal/convert/convert.go @@ -4,13 +4,12 @@ import ( "context" "crypto/rand" "fmt" - "strings" "github.com/threefoldtech/tfgrid-sdk-go/grid-client/deployer" "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/app/dependency" - "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/config" "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/types" + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/pkg/parser/config" proxy_types "github.com/threefoldtech/tfgrid-sdk-go/grid-proxy/pkg/types" "github.com/threefoldtech/zos/pkg/gridtypes/zos" ) @@ -105,18 +104,16 @@ func ConvertServiceToVM(service *types.Service, serviceName, networkName string) NetworkName: networkName, } - if vm.RootfsSize < minRootfs { + if vm.RootfsSize == 0 { vm.RootfsSize = minRootfs } - if vm.CPU < minCPU { + if vm.CPU == 0 { vm.CPU = minCPU } - if vm.Memory < minMemory { + if vm.Memory == 0 { vm.Memory = minMemory } - assignEnvs(&vm, service.Environment) - if err := assignNetworksTypes(&vm, service.IPTypes); err != nil { return workloads.VM{}, err } @@ -167,17 +164,6 @@ func getMissingNodes(ctx context.Context, networkNodeMap map[string]*types.Netwo return nil } -// all assign functions will be removed when the unmarshler is implemented -func assignEnvs(vm *workloads.VM, envs []string) { - env := make(map[string]string, 0) - for _, envVar := range envs { - key, value, _ := strings.Cut(envVar, "=") - env[key] = value - } - - vm.EnvVars = env -} - func assignNetworksTypes(vm *workloads.VM, ipTypes []string) error { for _, ipType := range ipTypes { switch ipType { diff --git a/grid-compose/internal/types/service.go b/grid-compose/internal/types/service.go index 41801795c..c9e2a1aa9 100644 --- a/grid-compose/internal/types/service.go +++ b/grid-compose/internal/types/service.go @@ -1,10 +1,19 @@ package types +import ( + "fmt" + "strconv" + "strings" + + "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/pkg/parser" + "gopkg.in/yaml.v3" +) + // Service represents a service in the deployment type Service struct { Flist string `yaml:"flist"` Entrypoint string `yaml:"entrypoint,omitempty"` - Environment []string `yaml:"environment"` + Environment KVMap `yaml:"environment"` Resources Resources `yaml:"resources"` Volumes []string `yaml:"volumes"` NodeID uint32 `yaml:"node_id"` @@ -14,13 +23,67 @@ type Service struct { DependsOn []string `yaml:"depends_on,omitempty"` } +// KVMap represents a key-value map and implements the Unmarshaler interface +type KVMap map[string]string + +// UnmarshalYAML unmarshals a YAML node into a KVMap +func (m *KVMap) UnmarshalYAML(value *yaml.Node) error { + var raw []string + if err := value.Decode(&raw); err != nil { + return err + } + + *m = make(map[string]string) + for _, ele := range raw { + kv := strings.SplitN(ele, "=", 2) + if len(kv) != 2 { + return fmt.Errorf("invalid kvmap format %s", ele) + } + (*m)[kv[0]] = kv[1] + } + + return nil +} + // Resources represents the resources required by the service type Resources struct { - CPU uint64 `yaml:"cpu"` + CPU uint16 `yaml:"cpu"` Memory uint64 `yaml:"memory"` Rootfs uint64 `yaml:"rootfs"` } +// UnmarshalYAML implements the yaml.Unmarshaler interface for custom unmarshalling. +func (r *Resources) UnmarshalYAML(value *yaml.Node) error { + for i := 0; i < len(value.Content); i += 2 { + key := value.Content[i].Value + val := value.Content[i+1].Value + + switch key { + case "cpu": + cpuVal, err := strconv.ParseUint(val, 10, 16) + if err != nil { + return fmt.Errorf("invalid cpu value %w", err) + } + r.CPU = uint16(cpuVal) + case "memory": + memVal, err := parser.ParseStorage(val) + if err != nil { + return fmt.Errorf("invalid memory value %w", err) + } + r.Memory = memVal + + case "rootfs": + rootfsVal, err := parser.ParseStorage(val) + if err != nil { + return fmt.Errorf("invalid rootfs value %w", err) + } + r.Rootfs = rootfsVal + } + } + + return nil +} + // HealthCheck represents the health check configuration for the service type HealthCheck struct { Test []string `yaml:"test"` diff --git a/grid-compose/internal/config/config.go b/grid-compose/pkg/parser/config/config.go similarity index 53% rename from grid-compose/internal/config/config.go rename to grid-compose/pkg/parser/config/config.go index b244fa70f..f8e453f09 100644 --- a/grid-compose/internal/config/config.go +++ b/grid-compose/pkg/parser/config/config.go @@ -6,17 +6,15 @@ import ( "io" "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/types" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" ) var ( - ErrVersionNotSet = errors.New("version not set") - ErrNetworkTypeNotSet = errors.New("network type not set") - ErrServiceFlistNotSet = errors.New("service flist not set") - ErrServiceCPUResourceNotSet = errors.New("service cpu resource not set") - ErrServiceMemoryResourceNotSet = errors.New("service memory resource not set") - ErrStorageTypeNotSet = errors.New("storage type not set") - ErrStorageSizeNotSet = errors.New("storage size not set") + ErrVersionNotSet = errors.New("version not set") + ErrNetworkTypeNotSet = errors.New("network type not set") + ErrServiceFlistNotSet = errors.New("service flist not set") + ErrStorageTypeNotSet = errors.New("storage type not set") + ErrStorageSizeNotSet = errors.New("storage size not set") ) // Config represents the configuration file content @@ -42,42 +40,41 @@ func (c *Config) LoadConfigFromReader(configFile io.Reader) error { if err != nil { return fmt.Errorf("failed to read file %w", err) } - err = c.UnmarshalYAML(content) + err = yaml.Unmarshal(content, &c) if err != nil { return fmt.Errorf("failed to unmarshal yaml %w", err) } return nil } -// UnmarshalYAML unmarshals the configuration file content and populates the DeploymentData map -// TODO: Implement unmarshaler -func (c *Config) UnmarshalYAML(content []byte) error { - if err := yaml.Unmarshal(content, c); err != nil { - return err - } - - return nil -} - -// ValidateConfig validates the configuration file content +// Validate validates the configuration file content // TODO: Create more validation rules -func (c *Config) ValidateConfig() (err error) { +func (c *Config) Validate() (err error) { if c.Version == "" { return ErrVersionNotSet } for name, service := range c.Services { + if len(name) > 50 { + return fmt.Errorf("service name %s is too long", name) + } + if service.Flist == "" { return fmt.Errorf("%w for service %s", ErrServiceFlistNotSet, name) } - if service.Resources.CPU == 0 { - return fmt.Errorf("%w for service %s", ErrServiceCPUResourceNotSet, name) + if service.Entrypoint == "" { + return fmt.Errorf("entrypoint not set for service %s", name) } - if service.Resources.Memory == 0 { - return fmt.Errorf("%w for service %s", ErrServiceMemoryResourceNotSet, name) + if service.Resources.Memory != 0 && service.Resources.Memory < 256 { + return fmt.Errorf("minimum memory resource is 256 megabytes for service %s", name) } + + if service.Resources.Rootfs != 0 && service.Resources.Rootfs < 2048 { + return fmt.Errorf("minimum rootfs resource is 2 gigabytes for service %s", name) + } + } return nil diff --git a/grid-compose/pkg/parser/storage.go b/grid-compose/pkg/parser/storage.go new file mode 100644 index 000000000..9c25d9463 --- /dev/null +++ b/grid-compose/pkg/parser/storage.go @@ -0,0 +1,44 @@ +package parser + +import ( + "fmt" + "strconv" + "strings" +) + +func ParseStorage(storage string) (uint64, error) { + var multiplier uint64 = 1 + var numString, unit string + + storage = strings.ToLower(strings.TrimSpace(storage)) + + if !strings.HasSuffix(storage, "b") && !strings.HasSuffix(storage, "kb") && !strings.HasSuffix(storage, "mb") && !strings.HasSuffix(storage, "gb") && !strings.HasSuffix(storage, "tb") { + unit = "b" + numString = storage + } else { + unit = storage[len(storage)-2:] + numString = storage[:len(storage)-2] + } + + number, err := strconv.ParseUint(numString, 10, 64) + if err != nil { + return 0, fmt.Errorf("invalid number format %s", numString) + } + + switch unit { + case "b": + multiplier = 1 + case "kb": + multiplier = 1024 + case "mb": + multiplier = 1024 * 1024 + case "gb": + multiplier = 1024 * 1024 * 1024 + case "tb": + multiplier = 1024 * 1024 * 1024 * 1024 + default: + return 0, fmt.Errorf("unsupported storage unit %s", unit) + } + + return (number * multiplier), nil +} From f21cf97db88037b63e4bb198337f2f5ed20e2aec Mon Sep 17 00:00:00 2001 From: eyad-hussein Date: Tue, 27 Aug 2024 02:31:58 +0300 Subject: [PATCH 19/20] fix: fix storage parser --- grid-compose/pkg/parser/storage.go | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/grid-compose/pkg/parser/storage.go b/grid-compose/pkg/parser/storage.go index 9c25d9463..8b937fec1 100644 --- a/grid-compose/pkg/parser/storage.go +++ b/grid-compose/pkg/parser/storage.go @@ -12,8 +12,8 @@ func ParseStorage(storage string) (uint64, error) { storage = strings.ToLower(strings.TrimSpace(storage)) - if !strings.HasSuffix(storage, "b") && !strings.HasSuffix(storage, "kb") && !strings.HasSuffix(storage, "mb") && !strings.HasSuffix(storage, "gb") && !strings.HasSuffix(storage, "tb") { - unit = "b" + if !strings.HasSuffix(storage, "mb") && !strings.HasSuffix(storage, "gb") && !strings.HasSuffix(storage, "tb") { + unit = "mb" numString = storage } else { unit = storage[len(storage)-2:] @@ -26,16 +26,12 @@ func ParseStorage(storage string) (uint64, error) { } switch unit { - case "b": - multiplier = 1 - case "kb": - multiplier = 1024 case "mb": - multiplier = 1024 * 1024 + multiplier = 1 case "gb": - multiplier = 1024 * 1024 * 1024 + multiplier = 1024 case "tb": - multiplier = 1024 * 1024 * 1024 * 1024 + multiplier = 1024 * 1024 default: return 0, fmt.Errorf("unsupported storage unit %s", unit) } From bf550b0d1c8adda272058a68c19c40f2d4d7ef23 Mon Sep 17 00:00:00 2001 From: eyad-hussein Date: Sat, 31 Aug 2024 11:41:12 +0300 Subject: [PATCH 20/20] fix: change project name so type becomes Micro Virtual Machine in dashboard --- grid-compose/README.md | 6 +++--- grid-compose/internal/app/app.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/grid-compose/README.md b/grid-compose/README.md index 1667732ec..9299a023b 100644 --- a/grid-compose/README.md +++ b/grid-compose/README.md @@ -112,9 +112,9 @@ output: ```bash 3:45AM INF starting peer session=tf-854215 twin=8658 -3:45AM INF canceling deployments projectName=compose/8658/net1 -3:45AM INF canceling contracts project name=compose/8658/net1 -3:45AM INF project is canceled project name=compose/8658/net1 +3:45AM INF canceling deployments projectName=vm/compose/8658/net1 +3:45AM INF canceling contracts project name=vm/compose/8658/net1 +3:45AM INF project is canceled project name=vm/compose/8658/net1 ``` ### ps diff --git a/grid-compose/internal/app/app.go b/grid-compose/internal/app/app.go index 7008ddb9e..b71afbc48 100644 --- a/grid-compose/internal/app/app.go +++ b/grid-compose/internal/app/app.go @@ -53,7 +53,7 @@ func NewApp(net, mnemonic, configPath string) (*App, error) { // GetProjectName returns the project name for the given key func (a *App) GetProjectName(key string) string { - return fmt.Sprintf("compose/%v/%v", a.Client.TwinID, key) + return fmt.Sprintf("vm/compose/%v/%v", a.Client.TwinID, key) } // GetDeploymentName returns the deployment name for the given key