How can I get all device env variables with service name through the SDK?

I seem to struggle at something basic… Using the node SDK I’m trying to grab all environment variables from a device. But I need the service name for each as well.

balena.models.device.serviceVar.getAllByDevice(“uuid”)

Doesn’t give you a service name.

And I don’t know what this “service_install” property is supposed to be, I thought I could match that with the actual service. But I can’t find the same IDs in the service list if I query:

balena.models.device.getWithServiceDetails(“uuid”)

Any help is appreciated!

Regards,
ada

Why I need this: to make a script that copies all variables from either a device or a fleet to either a device or a fleet. Maybe this can be done in another way?

I took another look at this but still didn’t find a way. Basically you get this when you query service environment vars on a device:

    await balena.models.device.serviceVar.getAllByDevice(device).then(function(vars) {
        /*
          {
            created_at: '2024-07-22T20:21:05.366Z',
            service_install: { __id: 43427514 },
            value: '[ "some value 1", "some other value" ]',
            id: 45947234,
            name: 'SOME_ENV_VAR'
          },
        */

        for (let i = 0; i < vars.length; i++) {
            // do something with vars
        }
    });

On the fleet level you can’t directly query for the service env vars but you can get them by service. So for the fleet you can get it in 2 steps:

    let services = undefined;

    await balena.models.service.getAllByApplication(app).then(function(res) {
        services = res;
    });

    for (let i = 0; i < services.length; i++) {

        async function addVars(service) {

            await balena.models.service.var.getAllByService({ application: app, service_name: service.service_name }).then(function(vars) {
                /*
                  {
                    created_at: '2023-08-16T20:19:18.166Z',
                    service: { __id: 1994636 },
                    name: 'SOME_VAR',
                    value: 'string value',
                    id: 306106
                  }
                */
                for (let i = 0; i < vars.length; i++) {
                    // do something with the vars
                }
            });    
        }

        await addVars(services[i]);
    };

What almost gets me there is the following sequence:

  1. query device info with service details
  2. query the device’s fleet for all service env vars
  3. query the device service env vars
  4. match the fleet env vas with device env vars to add service information

This doesn’t work for env vars that are only set on the device level though.

Hello @ada thanks for your detailed message.

I asked internally to the balena SDK team, however what i discovered is that this is possible using the balenaCloud API. Did you try to use this?

Let me know if that works!

I also have tested the SDK and couldn’t get the same results as the API (yet) but maybe check if you can get your desired output through the API.

Thanks!

@ada after some help from the SDK team, basically, there are two types of env vars, device env vars (which map to those on the UI that you see service name beeing “all services”). These you can get with:

const deviceEnvVars = await sdk.models.device.envVar.getAllByDevice(deviceUUID);

And these do not have service name associated (as they will apply to all services). There is also (as you pointed) device service env var, which apply specifically for a single device on a single service. You can further expand into the service install resource to get the service name, as follows:

const deviceServiceVars = await sdk.models.device.serviceVar.getAllByDevice(deviceUUID, {
    $expand: {
        service_install: {
            $expand: {
                installs__service: {
                    $select: ['id', 'service_name']
                }
            }
        }
    }
})

You will have an object similar to:

[
    {
        "service_install": [
            {
                "installs__service": [
                    {
                        "id": 2998312,
                        "service_name": "balena-hello-world"
                    }
                ],
                "created_at": "2024-10-15T15:52:49.737Z",
                "device": {
                    "__id": 12864513
                },
                "id": 44442160
            }
        ],
        "created_at": "2024-10-22T10:03:38.802Z",
        "value": "123",
        "id": 51510637,
        "name": "test"
    }
]

So you can get the serviceName with serviceVar[index]["service_install"][0]["installs__service"][0]["service_name"].

I would also recommend to add some nullability checks in case service_install is null (as these might be created asynchronously) with a retry.

Here is a script that might serve you:

const fromDeviceUuid = ''; 
const toDeviceUuid = '';

const envVars = await sdk.models.device.envVar.getAllByDevice(fromDeviceUuid);
const serviceVars = await sdk.models.device.serviceVar.getAllByDevice(deviceUUID, {
    $expand: {
        service_install: {
            $expand: {
                installs__service: {
                    $select: ['id', 'service_name']
                }
            }
        }
    }
});

for (const ev of envVars) {
    await sdk.models.device.envVar.set(toDeviceUuid, ev.name, ev.value);
}

for (const sv of serviceVars) {
    const serviceName = sv?.service_install?.[0].installs__service[0].service_name;
    if (serviceName == null) {
         // TODO. wait a bit and retry from the start (do another `sdk.models.device.serviceVar.getAllByDevice`)
    }
    await sdk.models.device.serviceVar.set(toDeviceUuid, serviceName, sv.name, sv.value);
}

More over, several of those calls can be paralelised using Promise.all (Promise.all() - JavaScript | MDN) in case they want to implement it.

Let us know if this works for you!