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/.gitignore b/grid-compose/.gitignore
new file mode 100644
index 000000000..4bb6ca521
--- /dev/null
+++ b/grid-compose/.gitignore
@@ -0,0 +1,6 @@
+bin/*
+.pre-commit-config.yaml
+out
+full_example.yml
+invalid
+grid-compose.yml
diff --git a/grid-compose/Makefile b/grid-compose/Makefile
new file mode 100644
index 000000000..5704400b7
--- /dev/null
+++ b/grid-compose/Makefile
@@ -0,0 +1,21 @@
+test:
+ @echo "Running Tests"
+ go test -v ./...
+
+clean:
+ rm ./bin -rf
+
+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
+
+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
new file mode 100644
index 000000000..9299a023b
--- /dev/null
+++ b/grid-compose/README.md
@@ -0,0 +1,163 @@
+# Grid-Compose
+
+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
+
+`REQUIRED` EnvVars:
+
+- `MNEMONIC`: your secret words
+- `NETWORK`: one of (dev, qa, test, main)
+
+```bash
+grid-compose [OPTIONS] [COMMAND]
+
+OPTIONS:
+ -f, --file: path to yaml file, default is ./grid-compose.yml
+
+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
+```
+
+Export env vars using:
+
+```bash
+export MNEMONIC=your_mnemonics
+export NETWORK=working_network
+```
+
+Run:
+
+```bash
+make build
+```
+
+To use any of the commands, run:
+
+```bash
+./bin/grid-compose [COMMAND]
+```
+
+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.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.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=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
+
+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.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
+
+### 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/down.go b/grid-compose/cmd/down.go
new file mode 100644
index 000000000..2c7897c43
--- /dev/null
+++ b/grid-compose/cmd/down.go
@@ -0,0 +1,22 @@
+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, 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
new file mode 100644
index 000000000..c61a7d46d
--- /dev/null
+++ b/grid-compose/cmd/ps.go
@@ -0,0 +1,27 @@
+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) {
+ verbose, err := cmd.Flags().GetBool("verbose")
+ if err != nil {
+ log.Fatal().Err(err).Send()
+ }
+
+ 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/root.go b/grid-compose/cmd/root.go
new file mode 100644
index 000000000..cc5897cf9
--- /dev/null
+++ b/grid-compose/cmd/root.go
@@ -0,0 +1,50 @@
+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/app"
+)
+
+func Execute() {
+ if err := rootCmd.Execute(); err != nil {
+ log.Fatal().Err(err).Send()
+ }
+}
+
+// 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) {
+ 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() {
+ rootCmd.PersistentFlags().StringP("file", "f", "./grid-compose.yml", "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
new file mode 100644
index 000000000..bc8c305d4
--- /dev/null
+++ b/grid-compose/cmd/up.go
@@ -0,0 +1,22 @@
+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, 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/cmd/version.go b/grid-compose/cmd/version.go
new file mode 100644
index 000000000..ebe132cf0
--- /dev/null
+++ b/grid-compose/cmd/version.go
@@ -0,0 +1,22 @@
+// Package cmd for parsing command line arguments
+package cmd
+
+import (
+ "log"
+
+ "github.com/spf13/cobra"
+)
+
+// set at build time
+var commit string
+var version string
+
+// versionCmd represents the version command
+var versionCmd = &cobra.Command{
+ Use: "version",
+ Short: "Get latest build tag",
+ Run: func(cmd *cobra.Command, args []string) {
+ log.Println(version)
+ log.Println(commit)
+ },
+}
diff --git a/grid-compose/docs/cases.md b/grid-compose/docs/cases.md
new file mode 100644
index 000000000..7e939c55f
--- /dev/null
+++ b/grid-compose/docs/cases.md
@@ -0,0 +1,81 @@
+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
+
+### 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/single_service_1.yml)
+
+### Case 2 - Node ID Given + No Assigned Network
+
+- Simply use the the node id given to deploy the service.
+ - 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)
+
+### 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/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/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..f53533e00
--- /dev/null
+++ b/grid-compose/docs/config.md
@@ -0,0 +1,119 @@
+## 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.
+ - `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.
+- `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
new file mode 100644
index 000000000..0fa11e885
--- /dev/null
+++ b/grid-compose/docs/future_work.md
@@ -0,0 +1,6 @@
+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.
diff --git a/grid-compose/examples/dependency/diff_networks.yml b/grid-compose/examples/dependency/diff_networks.yml
new file mode 100644
index 000000000..74fda684e
--- /dev/null
+++ b/grid-compose/examples/dependency/diff_networks.yml
@@ -0,0 +1,71 @@
+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-apps/threefoldtech-ubuntu-22.04.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-apps/threefoldtech-ubuntu-22.04.flist'
+ entrypoint: '/sbin/zinit init'
+ volumes:
+ - dbdata
+ resources:
+ cpu: 1
+ memory: 2048
+ rootfs: 2048
+ network: net1
+ node_id: 14
+ depends_on:
+ - database
+ database:
+ 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: 2048
+ network: net2
+ node_id: 14
+
+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..20487c26f
--- /dev/null
+++ b/grid-compose/examples/dependency/multiple_dependencies.yml
@@ -0,0 +1,59 @@
+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-apps/threefoldtech-ubuntu-22.04.flist'
+ entrypoint: '/sbin/zinit init'
+ volumes:
+ - webdata
+ resources:
+ cpu: 1
+ memory: 2048
+ rootfs: 2048
+ network: net2
+ depends_on:
+ - database
+ frontend:
+ 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: 2048
+ network: net2
+ node_id: 144
+ depends_on:
+ - server
+ - database
+ database:
+ 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: 2048
+ 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..5c3622992
--- /dev/null
+++ b/grid-compose/examples/dependency/same_network.yml
@@ -0,0 +1,53 @@
+version: '1.0.0'
+
+services:
+ server:
+ flist: 'https://hub.grid.tf/tf-official-apps/threefoldtech-ubuntu-22.04.flist'
+ ip_types:
+ - ipv4
+ - ygg
+ entrypoint: '/sbin/zinit init'
+ volumes:
+ - webdata
+ resources:
+ cpu: 1
+ memory: 2048
+ 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-apps/threefoldtech-ubuntu-22.04.flist'
+ entrypoint: '/sbin/zinit init'
+ volumes:
+ - dbdata
+ resources:
+ cpu: 1
+ memory: 2048
+ rootfs: 2048
+ node_id: 14
+ depends_on:
+ - server
+ database:
+ 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: 2048
+
+volumes:
+ webdata:
+ mountpoint: '/data'
+ size: 10GB
+ dbdata:
+ mountpoint: '/var/lib/postgresql/data'
+ size: 10GB
diff --git a/grid-compose/examples/multiple-services/multiple_services_diff_network_1.yml b/grid-compose/examples/multiple-services/multiple_services_diff_network_1.yml
new file mode 100644
index 000000000..4f19d6a97
--- /dev/null
+++ b/grid-compose/examples/multiple-services/multiple_services_diff_network_1.yml
@@ -0,0 +1,65 @@
+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-apps/threefoldtech-ubuntu-22.04.flist'
+ ip_types:
+ - ygg
+ entrypoint: '/sbin/zinit init'
+ volumes:
+ - webdata
+ resources:
+ cpu: 1
+ memory: 2048
+ rootfs: 2048
+ network: net2
+ frontend:
+ 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: 2048
+ network: net1
+ node_id: 144
+ database:
+ 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: 2048
+ network: net2
+
+volumes:
+ webdata:
+ mountpoint: '/data'
+ size: 10GB
+ dbdata:
+ mountpoint: '/var/lib/postgresql/data'
+ size: 10GB
diff --git a/grid-compose/examples/multiple-services/multiple_services_diff_network_2.yml b/grid-compose/examples/multiple-services/multiple_services_diff_network_2.yml
new file mode 100644
index 000000000..f87bc0571
--- /dev/null
+++ b/grid-compose/examples/multiple-services/multiple_services_diff_network_2.yml
@@ -0,0 +1,74 @@
+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-apps/threefoldtech-ubuntu-22.04.flist'
+ entrypoint: '/sbin/zinit init'
+ volumes:
+ - webdata
+ node_id: 144
+ resources:
+ cpu: 1
+ memory: 2048
+ rootfs: 2048
+ network: net2
+ server2:
+ node_id: 11
+ 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: 2048
+ network: net1
+ frontend:
+ 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: 2048
+ network: net1
+ database:
+ 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: 2048
+ network: net2
+
+volumes:
+ webdata:
+ mountpoint: '/data'
+ size: 10GB
+ dbdata:
+ mountpoint: '/var/lib/postgresql/data'
+ size: 10GB
diff --git a/grid-compose/examples/multiple-services/multiple_services_diff_network_3.yml b/grid-compose/examples/multiple-services/multiple_services_diff_network_3.yml
new file mode 100644
index 000000000..e8a1c8d8d
--- /dev/null
+++ b/grid-compose/examples/multiple-services/multiple_services_diff_network_3.yml
@@ -0,0 +1,74 @@
+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-apps/threefoldtech-ubuntu-22.04.flist'
+ entrypoint: '/sbin/zinit init'
+ volumes:
+ - webdata
+ resources:
+ cpu: 1
+ memory: 2048
+ rootfs: 2048
+ network: net2
+ server2:
+ 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: 2048
+ network: net1
+ frontend:
+ flist: 'https://hub.grid.tf/tf-official-apps/threefoldtech-ubuntu-22.04.flist'
+ ip_types:
+ - ygg
+ entrypoint: '/sbin/zinit init'
+ volumes:
+ - dbdata
+ resources:
+ cpu: 1
+ memory: 2048
+ rootfs: 2048
+ network: net1
+ database:
+ 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: 2048
+ network: net2
+
+volumes:
+ webdata:
+ mountpoint: '/data'
+ size: 10GB
+ dbdata:
+ mountpoint: '/var/lib/postgresql/data'
+ size: 10GB
diff --git a/grid-compose/examples/multiple-services/two_services_same_network_1.yml b/grid-compose/examples/multiple-services/two_services_same_network_1.yml
new file mode 100644
index 000000000..be885efa9
--- /dev/null
+++ b/grid-compose/examples/multiple-services/two_services_same_network_1.yml
@@ -0,0 +1,46 @@
+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'
+ ip_types:
+ - ygg
+ entrypoint: '/sbin/zinit init'
+ volumes:
+ - webdata
+ node_id: 14
+ resources:
+ cpu: 1
+ memory: 2048
+ rootfs: 2048
+ network: net1
+ database:
+ 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: 2048
+ network: net1
+
+volumes:
+ webdata:
+ mountpoint: '/data'
+ size: 10GB
+ dbdata:
+ mountpoint: '/var/lib/postgresql/data'
+ size: 10GB
diff --git a/grid-compose/examples/multiple-services/two_services_same_network_2.yml b/grid-compose/examples/multiple-services/two_services_same_network_2.yml
new file mode 100644
index 000000000..606516db9
--- /dev/null
+++ b/grid-compose/examples/multiple-services/two_services_same_network_2.yml
@@ -0,0 +1,33 @@
+version: '1.0.0'
+
+services:
+ server:
+ flist: 'https://hub.grid.tf/tf-official-apps/threefoldtech-ubuntu-22.04.flist'
+ ip_types:
+ - ygg
+ entrypoint: '/sbin/zinit init'
+ volumes:
+ - webdata
+ node_id: 144
+ resources:
+ cpu: 1
+ memory: 2048
+ rootfs: 2048
+
+ database:
+ 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: 2048
+
+volumes:
+ webdata:
+ mountpoint: '/data'
+ size: 10GB
+ dbdata:
+ mountpoint: '/var/lib/postgresql/data'
+ size: 10GB
diff --git a/grid-compose/examples/multiple-services/two_services_same_network_3.yml b/grid-compose/examples/multiple-services/two_services_same_network_3.yml
new file mode 100644
index 000000000..093538697
--- /dev/null
+++ b/grid-compose/examples/multiple-services/two_services_same_network_3.yml
@@ -0,0 +1,32 @@
+version: '1.0.0'
+
+services:
+ server:
+ 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: 2048
+
+ database:
+ flist: 'https://hub.grid.tf/tf-official-apps/threefoldtech-ubuntu-22.04.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/single-service/single_service_1.yml b/grid-compose/examples/single-service/single_service_1.yml
new file mode 100644
index 000000000..361b7c7c3
--- /dev/null
+++ b/grid-compose/examples/single-service/single_service_1.yml
@@ -0,0 +1,10 @@
+version: '1.0.0'
+
+services:
+ server:
+ flist: 'https://hub.grid.tf/tf-official-apps/threefoldtech-ubuntu-22.04.flist'
+ entrypoint: '/sbin/zinit init'
+ resources:
+ cpu: 1
+ memory: 2048
+ rootfs: 2048
diff --git a/grid-compose/examples/single-service/single_service_2.yml b/grid-compose/examples/single-service/single_service_2.yml
new file mode 100644
index 000000000..f44ffa5ef
--- /dev/null
+++ b/grid-compose/examples/single-service/single_service_2.yml
@@ -0,0 +1,11 @@
+version: '1.0.0'
+
+services:
+ server:
+ 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
diff --git a/grid-compose/examples/single-service/single_service_3.yml b/grid-compose/examples/single-service/single_service_3.yml
new file mode 100644
index 000000000..4ffe76969
--- /dev/null
+++ b/grid-compose/examples/single-service/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
+
+services:
+ server:
+ flist: 'https://hub.grid.tf/tf-official-apps/threefoldtech-ubuntu-22.04.flist'
+ entrypoint: '/sbin/zinit init'
+ resources:
+ cpu: 1
+ memory: 2048
+ rootfs: 2048
+ network: net1
+ ip_types:
+ - myc
diff --git a/grid-compose/go.mod b/grid-compose/go.mod
new file mode 100644
index 000000000..dcfa82eeb
--- /dev/null
+++ b/grid-compose/go.mod
@@ -0,0 +1,21 @@
+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/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
new file mode 100644
index 000000000..0fa958173
--- /dev/null
+++ b/grid-compose/go.sum
@@ -0,0 +1,71 @@
+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=
+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=
+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
new file mode 100644
index 000000000..b71afbc48
--- /dev/null
+++ b/grid-compose/internal/app/app.go
@@ -0,0 +1,127 @@
+package app
+
+import (
+ "fmt"
+ "os"
+ "sort"
+ "strconv"
+
+ "github.com/cosmos/go-bip39"
+ "github.com/threefoldtech/tfgrid-sdk-go/grid-client/deployer"
+ "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
+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.Validate(); 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
+}
+
+// GetProjectName returns the project name for the given key
+func (a *App) GetProjectName(key string) string {
+ return fmt.Sprintf("vm/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)
+}
+
+// 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 {
+ 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 mnemonics and network values of the user
+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/dependency/dependency.go b/grid-compose/internal/app/dependency/dependency.go
new file mode 100644
index 000000000..a42b73c88
--- /dev/null
+++ b/grid-compose/internal/app/dependency/dependency.go
@@ -0,0 +1,82 @@
+// Dependency package provides a directed graph of dependencies between services and resolves them
+package dependency
+
+import (
+ "fmt"
+ "log"
+ "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),
+ Root: root,
+ }
+}
+
+// DRNode represents a node in the directed graph
+type DRNode struct {
+ Name string
+ Dependencies []*DRNode
+ Parent *DRNode
+}
+
+// NewDRNode creates a new node in the directed graph
+func NewDRNode(name string) *DRNode {
+ return &DRNode{
+ Name: name,
+ Dependencies: []*DRNode{},
+ }
+}
+
+// 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)
+
+ 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/app/down.go b/grid-compose/internal/app/down.go
new file mode 100644
index 000000000..ac014c6f0
--- /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/types"
+)
+
+// Down cancels all the deployments on the grid
+// TODO: remove known hosts
+func (a *App) Down() error {
+ if len(a.Config.Networks) == 0 {
+ a.Config.Networks[a.GenerateDefaultNetworkName()] = 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/healthcheck.go b/grid-compose/internal/app/healthcheck.go
new file mode 100644
index 000000000..c75c524ae
--- /dev/null
+++ b/grid-compose/internal/app/healthcheck.go
@@ -0,0 +1,150 @@
+package app
+
+import (
+ "context"
+ "fmt"
+ "net"
+ "os"
+ "path/filepath"
+ "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"
+)
+
+// verifyHost verifies the host key of the server.
+func verifyHost(host string, remote net.Addr, key ssh.PublicKey) error {
+ // Check if the host is in known hosts file.
+ 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, "")
+}
+
+// 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
+ }
+
+ 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 {
+ 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,
+ Addr: ipAddr,
+ Auth: auth,
+ Callback: verifyHost,
+ })
+
+ if err == nil {
+ defer client.Close()
+ break
+ }
+
+ log.Info().Err(err).Msg("ssh connection attempt failed, retrying...")
+ time.Sleep(time.Second)
+
+ if remainingTime < time.Second {
+ time.Sleep(remainingTime)
+ }
+ }
+
+ command := strings.Join(healthCheck.Test, " ")
+
+ intervalDuration, err := time.ParseDuration(healthCheck.Interval)
+ if err != nil {
+ return fmt.Errorf("invalid interval format %w", err)
+ }
+
+ 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)
+}
+
+// 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
+ 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
+ }
+}
+
+// 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/app/ps.go b/grid-compose/internal/app/ps.go
new file mode 100644
index 000000000..28437da68
--- /dev/null
+++ b/grid-compose/internal/app/ps.go
@@ -0,0 +1,94 @@
+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/types"
+ "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/pkg/log"
+ "github.com/threefoldtech/zos/pkg/gridtypes"
+)
+
+// 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(strings.Repeat("-", 150) + "\n")
+ }
+
+ if len(a.Config.Networks) == 0 {
+ a.Config.Networks[a.GenerateDefaultNetworkName()] = 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
+ }
+
+ dlAdded := false
+ for _, wl := range dl.Workloads {
+ if wl.Type.String() != "zmachine" {
+ continue
+ }
+
+ vm, err := workloads.NewVMFromWorkload(&wl, &dl)
+ if err != nil {
+ return err
+ }
+
+ log.WriteVmDetails(&output, vm, wl, a.GetDeploymentName(wl.Name.String()), contract.NodeID, dlAdded, getVmAddresses(vm))
+ dlAdded = true
+ }
+
+ dlData, err := workloads.ParseDeploymentData(dl.Metadata)
+ if err != nil {
+ return err
+ }
+
+ if verbose {
+ outputMap[dlData.Name] = dl
+ }
+ }
+ }
+
+ 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
+}
diff --git a/grid-compose/internal/app/up.go b/grid-compose/internal/app/up.go
new file mode 100644
index 000000000..3aa6a6445
--- /dev/null
+++ b/grid-compose/internal/app/up.go
@@ -0,0 +1,89 @@
+package app
+
+import (
+ "context"
+
+ "github.com/rs/zerolog/log"
+ "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"
+)
+
+// Up deploys the services described in the config file
+func (a *App) Up(ctx context.Context) error {
+ defaultNetName := a.GenerateDefaultNetworkName()
+ deploymentData, err := convert.ConvertConfigToDeploymentData(ctx, a.Client, a.Config, defaultNetName)
+ if err != nil {
+ return err
+ }
+
+ 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 {
+ serviceName := resService.Name
+ if serviceName == "root" {
+ continue
+ }
+
+ service := a.Config.Services[serviceName]
+
+ var network *workloads.ZNet
+ if service.Network == "" {
+ network = networks[defaultNetName]
+ service.Network = network.Name
+ } else {
+ network = networks[service.Network]
+ }
+
+ vm, err := convert.ConvertServiceToVM(&service, serviceName, network.Name)
+ if err != nil {
+ return err
+ }
+
+ err = assignMyCeliumKeys(network, vm.MyceliumIPSeed)
+ if err != nil {
+ return err
+ }
+
+ disks, mounts, err := buildStorage(service.Volumes, a.Config.Volumes)
+ if err != nil {
+ return err
+ }
+ 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)
+ }
+ log.Info().Msg("deployed successfully")
+ }
+
+ 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)
+ }
+ }
+
+ log.Info().Msg("all deployments deployed successfully")
+
+ return nil
+}
diff --git a/grid-compose/internal/app/up_utils.go b/grid-compose/internal/app/up_utils.go
new file mode 100644
index 000000000..5deb8b7fb
--- /dev/null
+++ b/grid-compose/internal/app/up_utils.go
@@ -0,0 +1,180 @@
+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) {
+ // 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
+ }
+ 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.
+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/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/convert/convert.go b/grid-compose/internal/convert/convert.go
new file mode 100644
index 000000000..cbb41e953
--- /dev/null
+++ b/grid-compose/internal/convert/convert.go
@@ -0,0 +1,192 @@
+package convert
+
+import (
+ "context"
+ "crypto/rand"
+ "fmt"
+
+ "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/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"
+)
+
+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, defaultNetName string) (*types.DeploymentData, error) {
+ deploymentData := &types.DeploymentData{
+ NetworkNodeMap: make(map[string]*types.NetworkData, 0),
+ ServicesGraph: dependency.NewDRGraph(dependency.NewDRNode("root")),
+ }
+
+ 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 = dependency.NewDRNode(
+ serviceName,
+ )
+
+ 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 {
+ depNode = dependency.NewDRNode(dep)
+ }
+
+ svcNode.AddDependency(depNode)
+ depNode.Parent = svcNode
+ deploymentData.ServicesGraph.AddNode(dep, depNode)
+ }
+
+ if svcNode.Parent == nil {
+ svcNode.Parent = svcRootNode
+ svcRootNode.AddDependency(svcNode)
+ }
+ }
+
+ if err := getMissingNodes(ctx, deploymentData.NetworkNodeMap, client); err != nil {
+ return nil, err
+ }
+
+ 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,
+ }
+
+ if vm.RootfsSize == 0 {
+ vm.RootfsSize = minRootfs
+ }
+ if vm.CPU == 0 {
+ vm.CPU = minCPU
+ }
+ if vm.Memory == 0 {
+ vm.Memory = minMemory
+ }
+
+ 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, 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 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/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/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/service.go b/grid-compose/internal/types/service.go
new file mode 100644
index 000000000..c9e2a1aa9
--- /dev/null
+++ b/grid-compose/internal/types/service.go
@@ -0,0 +1,99 @@
+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 KVMap `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"`
+}
+
+// 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 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"`
+ 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"`
+}
diff --git a/grid-compose/main.go b/grid-compose/main.go
new file mode 100644
index 000000000..626715cfd
--- /dev/null
+++ b/grid-compose/main.go
@@ -0,0 +1,9 @@
+package main
+
+import (
+ "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/cmd"
+)
+
+func main() {
+ cmd.Execute()
+}
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)))
+ }
+ }
+}
diff --git a/grid-compose/pkg/parser/config/config.go b/grid-compose/pkg/parser/config/config.go
new file mode 100644
index 000000000..f8e453f09
--- /dev/null
+++ b/grid-compose/pkg/parser/config/config.go
@@ -0,0 +1,81 @@
+package config
+
+import (
+ "errors"
+ "fmt"
+ "io"
+
+ "github.com/threefoldtech/tfgrid-sdk-go/grid-compose/internal/types"
+ "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")
+ 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"`
+ Volumes map[string]types.Volume `yaml:"volumes"`
+}
+
+// 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),
+ Volumes: make(map[string]types.Volume),
+ }
+}
+
+// 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)
+ }
+ err = yaml.Unmarshal(content, &c)
+ if err != nil {
+ return fmt.Errorf("failed to unmarshal yaml %w", err)
+ }
+ return nil
+}
+
+// Validate validates the configuration file content
+// TODO: Create more validation rules
+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.Entrypoint == "" {
+ return fmt.Errorf("entrypoint not set for service %s", 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..8b937fec1
--- /dev/null
+++ b/grid-compose/pkg/parser/storage.go
@@ -0,0 +1,40 @@
+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, "mb") && !strings.HasSuffix(storage, "gb") && !strings.HasSuffix(storage, "tb") {
+ unit = "mb"
+ 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 "mb":
+ multiplier = 1
+ case "gb":
+ multiplier = 1024
+ case "tb":
+ multiplier = 1024 * 1024
+ default:
+ return 0, fmt.Errorf("unsupported storage unit %s", unit)
+ }
+
+ return (number * multiplier), nil
+}