How do I upgrade child canisters that were dynamically created from a parent canister of which I am the controlller (In Motoko)?

This question comes as a result of conducting further research to resolve the issue that I’m having in this thread. I think the cause of the issue that I’m having is that I’m upgrading a canister class that is dynamically created from within my main.mo file- making my backend canister a controller of each of the child canisters that it created, but I’m not a controller.

I’m not the controller of those child canisters, so I’m unable to upgrade them. so how do I upgrade them? is there an upgrade function that I can call from within the parent canister that can initiate the canister upgrade for the child canister class? I’ve seen threads with similar questions but they’re all from months ago with no clear resolutions.

1 Like

There is currently no language or library support for doing this though I agree there should be.

Some (hopefully not dead-end) suggestions for now:

The parent actor could be upgraded to add additional controllers to its children, which you could then manually upgrade using dfx.

Alternatively, add a carefully locked down helper method to the parent that, given a new binary as blob, uses the IC ManagementCanister to install the new binaries for each child.

Any other suggestions are most welcome.

6 Likes

Is this easier done in Rust than in Motoko, or do they both face the same obstacles?

In rust it’s just a matter of passing an arg when calling canister_install and you read it in the init() function of the child canister.

I’ve done exactly this in the scaling quickstart bounty. Sending here and receiving the args here.

2 Likes

You can upgrade the child canister through your main canister.

You → send bytes to main → main send bytes to child.

At least it what I do in my project, seems to work so far :wink:

In motoko it starts there (your “main” is my “manager”): ic/manager.mo at 09b7731800210f7273de11526c7d65ad4e88f84b · papyrs/ic · GitHub

In nodejs: ic/ic.installcode.mjs at main · papyrs/ic · GitHub

(I still got in plan to release some blog posts about interacting from NodeJS with Motoko canisters, hopefully will have the time soon to write about it)

4 Likes

@peterparker , I think this was exactly what i need. I’m attempting to implement it but am getting an error with the loadWasm() method that you have here ic/ic.installcode.mjs at 09b7731800210f7273de11526c7d65ad4e88f84b · papyrs/ic · GitHub(type,%7D%3B

The error that I’m getting is:

Uncaught (in promise) File not found

here is my code:

import {readFile} from 'fs-web';

const loadWasm = async (type) => {
        console.log("Working Directory: ",`${process.cwd()}.dfx/ic/canisters/${type}/${type}.wasm`);
        
        const buffer = await readFile(`${process.cwd()}.dfx/local/canisters/dtc/dtc.wasm`);
        return [...new Uint8Array(buffer)];
    };

I’m assuming the issue is with the file path. when I call process.cwd() its just returning /. Is that what it should be returning? Could you share with me what your process.cwd() returns so that i can compare it to your file structure to get the correct file path?

process.cwd() is a NodeJS built-in command. You can replace it with an absolute directory path to your file if it blocks you.

The process.cwd() method returns the current working directory of the Node.js process.

https://nodejs.org/api/process.html#process_process_cwd

Do you have an example of what the absolute path looks like when the app is deployed to the IC as opposed to when it’s deployed locally? I’ve tried every file path i Can think of. Still seeing this error.

Oh I see. My script works locally or on the server side - i.e. on a NodeJS environment not in the browser. It needs to be able to access the file system to read the wasm that has been compiled. Does that answer your question?

Not quite. My question is a two-parter. 1.) can you confirm that it is indeed possible to access the wasm files from the front end? And 2.) if it is the case that this is possible, how should the file path be defined?

Yes and no :wink:

No: you cannot program a script that runs in the browser which would for example read a file from a local or server path like /my/machine/local/something.wasm automatically and directly

Yes: you can use the Filesystem web api or an <input type="file"> that load a file but this would need a user interaction

1 Like
<input type=“file”> 

Looks like the best option right now. I didn’t know it was possible to set up a NodeJs server that runs on the IC. I’ll have to look into setting one up.

No no that’s not possible. I was just mentioning to answer your question.

Let me know if it works out with the input.

1 Like

What server is it that you have set up that allows you to call the fs.readFileSync function within your project?

I run the script locally :sweat_smile:

Any server or CI that has NodeJS and a filesystem context can execute fs.readFileSync.

1 Like

Ahhh. That makes sense. Thanks for explaining :pray:t5:.

1 Like

My pleasure, keep me posted

ok. I think I screwed something up. Now I’m getting this error in the browser console:

index.js:2 Uncaught (in promise) Error: Call was rejected:
  Request ID: b7118a33259f089463a9c200c6d6e3223bb1f03043d8600f665f6549fc614b09
  Reject code: 5
  Reject text: Canister cxi6d-5iaaa-aaaap-qaaka-cai trapped explicitly: IDL error: too few arguments v(Nr(date:t,draft:b,emailOne:t,emailThree:t,emailTwo:t,entryTitle:t,file1MetaData:6=r(fileName:t,fileType:t,lastModified:I),file2MetaData:!6,location:t,lockTime:I,read:b,sent:b,text:t,unlockTime:I))r(dedications:t,dob:t,name:t,pob:t,preface:t)

This comes after I updated the child canisters using the strategy that you suggested @peterparker. Any idea why I would be getting a too few arguments error? and how I’d resolve this? The canisters did upgrade without throwing any errors. But now, when I try to call the backend to retrieve the data within the canister, this error pops up.

My code:
as I previously mentioned, I decided to create an interface on my frontend that allows me to upload the wasm file and press a button that takes that wasm file and performs the necessary procedures.

front end:

import { IDL } from "@dfinity/candid";


const handleUpgrade = async () => {

        let promises =[];

        const wasmModule = await loadWasm();

        const principalsList = await actor.getPrincipalsList();

        principalsList.forEach((principal) => promises.push(upgradeJournalData(principal, wasmModule)));

        await Promise.all(promises);

        console.log("wasm module: ",wasmModule);
    };
const upgradeJournalData = async (principal, wasmModule) => {

        console.log(`Upgrading: ${principal.toText()}`);
        const arg = IDL.encode([IDL.Principal], [principal]);
        await actor.installCode(principal, [...arg], wasmModule);
        console.log(`Done: ${principal.toText()}`);

    };

function that takes the wasm file buffer and returns a Uint8Array from it.

const loadWasm = async () => {

        const buffer = await getArrayBuffer(inputRef.current.files[0]);
        return [...new Uint8Array(buffer)];
    };

function that takes the wasm file and returns the buffer from it:

const getArrayBuffer = (inputFile) => {
        const reader = new FileReader();

        return new Promise((resolve, reject) => {
            reader.onload = () => {
                resolve(reader.result);
            }
            reader.readAsArrayBuffer(inputFile)
        
        });
    }; 

Back end

public shared(msg) func installCode( userPrincipal: Principal, args: Blob, wasmModule: Blob): async() {
        let callerId = msg.caller;

        let profile = Trie.find(
            profiles,
            key(callerId),
            Principal.equal
        );

        switch(profile){
            case null{
                throw Error.reject("Unauthorized access. Caller is not an admin.");
            };
            case ( ? existingProfile){

                if (Option.get(existingProfile.userName, "null") == "admin") {

                    let theUserProfile = Trie.find(
                        profiles,
                        key(userPrincipal),
                        Principal.equal
                    );

                    switch(theUserProfile){
                        case null{
                            throw Error.reject("No profile for this principal.");
                        };
                        case ( ? existingProfile){
                            let userJournal = existingProfile.journal;
                            let journalCanisterId = Principal.fromActor(userJournal);

                            await ic.install_code({
                                arg = args;
                                wasm_module = wasmModule;
                                mode = #upgrade;
                                canister_id = journalCanisterId;
                            });

                        };
                    };

                } else {
                    throw Error.reject("Unauthorized access. Caller is not an admin.");

                }

            };

        };
    };

The canister Id that I passed into the ic.install_code function is the principal of the user’s canister actor. when the users’ canisters are created from the actor class, their principal is passed in as an argument.

1 Like

Does your child canister as a principal as parameter or more parameters?

In above piece of code you path one principal as argument for the install code operation. This matches for example the definition in Motoko of a child actor such as

actor class DataBucket(owner: Principal) = this {
};

So if you have more or less arguments, that’s maybe the root cause of the issue?

my child canister class has the following signature:

shared(msg) actor class Journal (principal : Principal) = this {}

Maybe the shared keyword needs to be accounted for in the IDL.ecode() function? If so, how would i do that?

Also, the wasm module that i passed into the install_code() method is the wasm module i retrieved by taking my code, and deploying it to another canister on the IC. The code works just fine on the IC if it’s a newly deployed canister. So i took the backend canister’s wasm module module of the newly deployed canister and used that to pass into the install_code function to upgrade the child canisters of my already-deployed canisters running on the IC