Development guide

Release

Before release testing

Some tests are required before release:

  • task test-unit (done by CI)
  • task test-integ-full-lifecycle-all (done manually since it requires real Cloud accounts)
  • task test-integ-scaleway-lifecycle-with-server-deletion (done manually since it requires real Cloud accounts)

Do release

Set export GITHUB_TOKEN=xxx variable and run:

# Will prompt for details and describe required actions
task release-create

Tests

Unit tests

Can be run without prior setup:

task test-unit

Integration tests

Integration tests are manual for now:

  • They require real Cloud accounts
  • They create real Cloud resources which are billed and may persist in case of failure, requiring manual cleanup and knowing about underlying tools

Do NOT run these integration tests if you're not sure about what you're doing to avoid being billed for Cloud resources.

Integration tests using CLI

test/integ/cli-full-lifecycle/run.sh [aws|gcp|azure|paperspace]

Using Mocha

npx mocha test/integ/pulumi/preview.spec.ts
npx mocha test/integ/pulumi/up.spec.ts
npx mocha test/integ/paperspace/client.spec.ts

Development

Development can be done under Nix development shell:

nix develop

Various dev tasks

Run app directly with npx:

npx tsx src/index.ts configure test-create-destroy-gcp

To debug Ansible playbook easily:

  • Run cloudypad with verbose log to show Ansible inventory temporary file, eg:
npx tsx src/index.ts configure test-create-destroy-gcp -v 4
# ...
# 2024-11-07 10:06:53.036 DEBUG   /dist/src/tools/ansible.js:14   AnsibleClient   Ansible command: ansible-playbook ["-i","/tmp/cloudypad-KlRsDX/inventory.yml","/cloudypad/dist/ansible/playbook.yml"]
# ...

Then run Ansible directly:

ansible-playbook -i /tmp/nix-shell.fD63LM/cloudypad-BX2kYb/inventory.yml ansible/playbook.yml -t wolf --start-at-task="Copy docker-compose file"

Will eventually add an easier way to pass custom Ansible options such as --ansible-additional-flag option or environment variable.

Development Virtual Machine

A local Virtual Machine can be created with Vagrant:

  • Machine IP is hardcoded in Vagrantfile to 192.168.56.43
  • See Vagrantfile for details

Initial setup

# Create machine
vagrant up

# Setup Cloudy Pad
task dev-ansible-config

# Might fail as :local tag doesn't exists
# Push local container image to VM
task dev-docker-sunshine-to-vm

To pair connect to Sunshine web UI and use Moonlight manually:

https://192.168.56.43:47990/

Development usage

Build Sunshine container locally and import inn VM:

task dev-docker-sunshine-to-vm

For faster development feedback loop, it's possible to build container image directly in VM:

vagrant ssh
$ cd /vagrant 
# this folder is mapped to host Git project's directory
# Run the command defined by dev-docker-sunshine-to-vm directly in this folder

During Sunshine container development, it's possible to use a Vagrant-specific Docker Compose which will mount folders selectively to test local Git changes directly in container:

vagrant ssh
$ docker compose -f /vagrant/test/resources/docker-compose.vagrant.yml -p sunshine up -d --force-recreate

For example, while working on scripts in /cloudy/bin, use a bind mount such as:

  volumes:
  # [...]
  #
  # Mount local project's folder directly in container to test changes live
  - "/vagrant/containers/sunshine/overlay/cloudy/bin:/cloudy/bin"

Which will make the local Git project files directly from host into container through /vagrant mount point, allowing faster testing. Note this only work well with read-only files since permission might now match for read-write.

Local Pulumi stack manipulation

Nix development shell automatically set PULUMI_BACKEND_URL and PULUMI_CONFIG_PASSPHRASE environment variables, allowing to manipulate Pulumi stacks locally.

# List stacks
pulumi stack ls -a

# Show stack resources
pulumi stack -s <organization/CloudyPad-XXX/STACK> --show-ids

# Destroy stack
pulumi destroy -s <organization/CloudyPad-XXX/STACK>

Maintenance

Regular updates

  • Node deps
    npm update
    
  • NVIDIA Display driver version
    • Take latest Display Production Linux x86_64 version at NVIDIA Unix Driver archive page
    • Update in ansible/roles/nvidia-driver/defaults/main.yml nvidia_driver_display_dotrun_install_version
  • NVIDIA Datacenter driver version
    • Take latest Datacenter Linux x86_64 version at NVIDIA Unix Driver archive page
    • Update in ansible/roles/nvidia-driver/defaults/main.yml nvidia_driver_datacenter_dotrun_install_version
  • Wolf version and config
    • Run hack/update-wolf-images.sh to update default images in Ansible role
    • Update Wolf config template in ansible/roles/wolf/templates/wolf-config.toml using default config
      • Generate default config by running a Wolf container (will generate default config on start) and copy it:
      docker pull ghcr.io/games-on-whales/wolf:stable
      
      # Run Wolf in the background to auto-generate config file on startup
      docker run -d --name wolf-config ghcr.io/games-on-whales/wolf:stable
      
      # Copy config file
      docker cp wolf-config:/etc/wolf/cfg/config.toml .
      
      • Ensure proper Ansible templates variables are used in TOML config:
        hostname = "{{ wolf_instance_name }}"
        uuid = "{{ wolf_instance_name | ansible.builtin.to_uuid }}"
        # ...
        
        # For each app, use the related Ansible image, eg for Firefox:
        [apps.runner]
        type = "docker"
        name = "WolfFirefox"
        image = '{{ wolf_app_firefox_image }}'
        
  • Sunshine Dockerfile containers/sunshine/Dockerfile
  • CLI Dockerfile (Dockerfile at root)

Adding a new provider

This section outlines how to add a new provider. It's still scarce but provides a basic entrypoint to help implemented a new provider.

Implementing a new provider requires to:

  • Implement various components to manage instance lifecycle (Runner, Provisioner, Initializer...)
  • Integrating these components into main code

Provider components

Each component must implement a stricly defined interface, allowing seamless integration in Cloudy Pad core:

  • Initializer - Prompt users for required options during creation.
  • Provisioner - Use Pulumi to deploy the instance.
    • Will require a Pulumi stack definition under src/tools/pulumi
  • Runner - Start/stop/restart instance.
  • State - Describe configuration and current state (outputs) for the instance.
  • Factory - Create Provisioner and Runner instances for global Instance Manager

Along with clients, Pulumi stack, etc. as required.

See existing providers for example in src/providers.

Integrate provider in Core

Integrate Provider in core:

  • Add provider name and classes in src/core/const.ts
  • Add provider registration in src/core/manager-builder.ts
  • Add a create sub-command for provider in src/cli/program.ts with options matching provider state interface.

Tests

  • Add unit test in test/unit/<provider>
  • Add CLI full lifecycle tests in test/integ/cli-full-lifecycle
  • Add Pulumi tests in test/integ/pulumi
  • Add provider tests in test/integ/<provider>

Adding new CLI args

To add a new global or commong CLI argument used by all or multiple providers:

  • Add new CLI_ option variable in src/cli/command.ts
  • Ensure CLI option variable is used by all relevant providers in src/providers
  • Ensure CLI option is handled by cliArgsIntoPartialInput
  • Update state Inputs if needed
  • Update tests default values if needed