Community discussions

MikroTik App
 
belthesar
just joined
Topic Author
Posts: 2
Joined: Tue Mar 07, 2023 7:24 pm

Easy container update script

Sun Jan 14, 2024 7:29 am

Hey folks. I'm just getting started with containers on Mikrotik. I wanted a way to easily update a container I have running on the routers I support. I found some examples from folks, but they all seemed dependent on magic delays to detect when the routine should be complete. This works out a little nicer, because it poll for container status at container stop and container creation, so it should be reusable across any number of containers you have on your device.

:local containerToUpdate
:set $containerToUpdate "name-of-container"

:local containerVeth
:set $containerVeth "container-veth-interface"

:local containerImage
:set $containerImage "org/image:tag"

:local containerEnvlist
:set $containerEnvlist "name-of-envlist"

:local containerMounts
:set $containerMounts "name-of-mount"

# Stop Container 
/container/stop [find where comment~$containerToUpdate]

# Wait for container(s) to stop 
:while ( [/container print count-only where comment~$containerToUpdate status=stopping] > 0) do={ :delay 1 }

# Remove old Container(s)
/container/remove [find where comment~$containerToUpdate]

# short delay 
:delay 3

# create new container
/container/add interface=$containerVeth remote-image=$containerImage mounts=$containerMounts envlist=$containerEnvlist start-on-boot=yes logging=yes comment=$containerToUpdate
:delay 1

# Wait for new container to be ready
:while ( [/container print count-only where comment~$containerToUpdate status=extracting] > 0 ) do { :delay 1 }

# Start the new container
/container/start [find where comment~$containerToUpdate]

Last edited by belthesar on Sat Feb 03, 2024 6:38 pm, edited 1 time in total.
 
foegra
just joined
Posts: 2
Joined: Wed Jan 31, 2024 6:53 pm

Re: Easy container update script

Wed Jan 31, 2024 6:55 pm

Noice!

How can You use container name if it changes every time You re-create container?

Update: You are using comment, awesome. I get it now.

There is a mistake on this row:
:set %containerMounts "name-of-mount"

instead of % needs to be $
Last edited by foegra on Wed Jan 31, 2024 9:44 pm, edited 2 times in total.
 
optio
Long time Member
Long time Member
Posts: 694
Joined: Mon Dec 26, 2022 2:57 pm

Re: Easy container update script

Wed Jan 31, 2024 9:07 pm

Some can have other config properties set to container like CMD, host, DNS, etc... Why just not find current container by comment variable, read all its config properties and set it for new container?
 
User avatar
Amm0
Forum Guru
Forum Guru
Posts: 3604
Joined: Sun May 01, 2016 7:12 pm
Location: California

Re: Easy container update script

Wed Jan 31, 2024 9:22 pm

I like the simplicity of the OP's posting. e.g. you create the container "by hand" once, then just use a script to update what you setup. Great work!

Some can have other config properties set to container like CMD, host, DNS, etc... Why just not find current container by comment variable, read all its config properties and set it for new container?
Once you start trying to make the script generic, you do end up with a lot of code and variables. I went down this road myself... ;)

For example, below is function for a netinstall, based on a container "template script" (e.g. change vars and function name from a base script). But as you see it become pretty complicated quickly if you want to handle all the cases for envs/mounts/veth/vlan/bridge/container-repo/etc/etc.
###
###  Installer for "NETINSTALL" 
###
:put "** Loading \$NETINSTALL"

:global NETINSTALL do={
	:global NETINSTALL
	:local arg1 $1
	:local arg2 $2
	:local action 0
	:local lpath
	:local lbranch

	:if ([:typeof $arg1]="str") do={
		:set action $arg1
	}
	:if ([:typeof $path]="str") do={
		:set lpath $path
		:put "setting path=$path"
	}
	:if ([:typeof $branch]="str") do={
		:set lbranch $branch
	}

	## MAIN SETTINGS FOR CONTAINER

	# name of container, used in comment to find - could be multiple so add a "containername1[2,...]" to things 
	:local ocipkg "netinstall"
	:local ocipushuser "tikoci"
	:local containerregistry "ghcr.io"
	:local scripthelpername "NETINSTALL"
	:local containernum "6" 
	:local containeripbase "192.168.88."
	:local containerprefix "24"
	:local rosver "7.12.1"
	:local containerver "v$rosver"
	:local containerbootatstart "yes"
	:local containeraddresslist "LAN"
	:local containerhostip "1"
	:local containerbridge "netinstall"
	:local containerpvid ""
	:local containerlogging "yes"
	:local maxwaitforstart "3m"
	:local containerenvs [:toarray ""]
	:set ($containerenvs->"NETINSTALL_NPK") "routeros"
	:set ($containerenvs->"NETINSTALL_ARCH") "arm"
	
 
	:local containermounts [:toarray ""]
	#:set ($containermounts->"data") "/data"
 
	# TODO figure out where files go...

	# container specific variables
	:local netserverport ($containerenvs->"PORT")

	:if ([:typeof $hostid]~"str|num") do={
		:set containernum $hostid
	}

	# calculate name and tag (do not change these)
	:local containername "$ocipkg" 
	:local containertag "$containername$containernum"
	:local ociprojsrcroot "https://raw.githubusercontent.com/$(ocipushuser)/$(containername)/$containerver"

	# RouterOS IP config
	:local containerethname "veth-$containertag"
	:local containerip "$containeripbase$containernum"
	:local containergw "$($containeripbase)254"

	# path= option
	:local rootdisk ""
	:if ([:typeof $lpath]="str") do={
		:set rootdisk $lpath
	} else={
		:set rootdisk "nfs1"
	}
	:local rootpath "$rootdisk/$containertag"
	#:put "using disk path prefix of $rootpath, use path= option to change"

	# branch= option
	:if ([:typeof $branch]="str") do={
		:set containerver $branch
	} 

	:if ($dump = "yes") do={
		:put "NETINSTALL dump (as calculated, any existing container settings rule)"
		:put "     branch (ARG)           =   $branch                   "
		:put "     force (ARG)            =   $force                    "
		:put "     path (ARG)             =   $path                     "
		:put "     url (ARG)              =   $url                      "
		:put "     rootpath               =   $rootpath                  "
		:put "     ocipkg                 =   $ocipkg                    "
		:put "     ocipushuser            =   $ocipushuser               "
		:put "     ociprojsrcroot         =   $ociprojsrcroot            "        
		:put "     containerregistry      =   $containerregistry         "      
		:put "     scripthelpername       =   $scripthelpername          "     
		:put "     containernum           =   $containernum              " 
		:put "     containeripbase        =   $containeripbase           "    
		:put "     containerprefix        =   $containerprefix           "    
		:put "     containerver           =   $containerver              " 
		:put "     containerbootatstart   =   $containerbootatstart      "         
		:put "     containeraddresslist   =   $containeraddresslist      "         
		:put "     containerhostip        =   $containerhostip           "       
		:put "     containerbridge        =   $containerbridge           "    
		:put "     containerpvid          =   $containerpvid             "    
		:put "     containerenvs          =   $containerenvs             "  
		:put "     containermounts        =   $containermounts           "    
		:put "     containerethname       =   $containerethname          "     
		:put "     containerip            =   $containerip               "
		:put "     containergw            =   $containergw               "
		:put "     rootdisk               =   $rootdisk                  "
		:put "     rootpath               =   $rootpath                  "
	}

	:local REGISTRY do={
		/container/config {
			:local curregurl [get registry-url]
			:if ([:typeof $url]="str") do={
				:put "registry set to provided url: $url"
				set registry-url=$url 
				/;
				:return $curregurl 
			}
			:if ([:typeof $arg2]="str") do={ 
				:if ($arg2~"github|ghcr") do={
					set registry-url="https://ghcr.io"
					:put "registry updated from $curregurl to GitHub Container Store (ghcs.io)"
					/;
					:return $curregurl
				}
				:if ($arg2~"docker") do={
					set registry-url="https://registry-1.docker.io"
					:put "registry updated from $curregurl to Docker Hub"
					/;
					:return $curregurl
				} else={
					:error "setting invalid or unknown registry - failing"
				}
			} else={
				:put "current container registry is: $curregurl"
				/;
				:return $curregurl
			}
		}
		:error "unhandled path in \$NETINSTALL registry - should return something"
	}

	# "$NETINSTALL make" - removes any existing and install new container
	:if ($action = "make") do={

		## WARN BEFORE CONTINUE
		:put "continuing will install $containertag and modify configuration"
		:put "...starting in 5 seconds - hit ctrl-c now to STOP"
		:delay 5s

		# add veth
		/interface/veth {
			:put "check veth"
			:local veth [add name="$containerethname" address="$containerip/$containerprefix" gateway=$containergw comment="#$containertag"]
			:put "added veth - $containerethname address=$(containerip)/$(containerprefix) gateway=$containergw "
    }

    # add port as bridge
    :if ($containerbridge != "") do={
        :local bridgeid [/interface/bridge/find name=$containerbridge]
        :if ([:len $bridgeid] != 1) do={
	  :if ([:tostr $containerpvid] = "") do={
	  	/interface/bridge/add name=$containerbridge
	  } else={
          	:error "bridge named $containerbridge not found"
	  }
        }
        :put "adding port to bridge"
        :if ([/interface/bridge/get $bridgeid vlan-filtering]) do={
          /interface/bridge/port add interface=$containerethname bridge=$containerbridge pvid=[:tonum $containerpvid] frame-types="admit-only-untagged-and-priority-tagged" comment="#$containertag"
        } else={
          /interface/bridge/port add interface=$containerethname bridge=$containerbridge comment="#$containertag"
        }
    }
	

		# envs= option
		/container/envs {
			:put "check envs"
			add name="_$containername" key="lasttag" value=$containertag
			:foreach k,v in=$containerenvs do={
				:put "setting $containertag env $k to $v"
				add name="$containertag" key="$k" value=$v comment="#$containertag"
			}
		}
		# mounts= option
		/container/mounts {
			:put "check mounts"
			:foreach k,v in=$containermounts do={
				:put "setting $containertag env $k to $v"
				add name="$containertag-$k" src="$rootpath-$[:tostr $k]" dst="$[:tostr $v]" comment="#$containertag"
			}
		}

		# add ip address to routeros
		:if ([:tostr $containerbridge] = "") do={
			/ip/address {
				:put "check hostip"
				:if ([:typeof [:tonum $containerhostip]] = "num") do={
					:local ipaddr [add interface="$containerethname" address="$(containergw)/$(containerprefix)" comment="#$containertag"]
					:put "added IP address=$(containergw)/$(containerprefix) interface=$containerethname"
				}
			}
		}

  
		/interface/list/member {
			:if ([:len $containeraddresslist] > 0) do={
				:local iflistmem [add interface="$containerethname" list="$containeraddresslist" comment="#$containertag"]
				:put "added $containerethname to $containeraddresslist interface list"
			}
		}

		# TODO figure out if any are needed?
		/ip/firewall/nat {}
		/ip/firewall/filter {}
		/ip/route {}

		/container {
			:local containerid
			# tarfile= option
			:if ([:typeof $tarfile]="str") do={
				:put "adding new $containertag container on $containerethname using $(rootdisk)/$(containername).tar"
				:set containerid [add file="$tarfile" interface="$containerethname" logging=$containerlogging root-dir="$(rootpath)-root"]
			} else={
				:local lastreg [$REGISTRY github]
				# TODO handle no building paths here if options messing
				:local containerpulltag "$(containerregistry)/$(ocipushuser)/$(containername):$(containerver)"
				:put "pulling new $containertag container on $containerethname using $containerpulltag"
				:set containerid [add remote-image="$containerpulltag" interface="$containerethname" logging=$containerlogging root-dir="$(rootpath)-root"]
				[$REGISTRY url=$lastreg]
			}
			:put "setting comment #$containertag"
			set $containerid comment="#$containertag"
			:put "setting start-on-boot $containerbootatstart"

			:local lmountstr [mount find comment~"#$containertag"]
			:local lmountstrlen [:len $lmountstr]
			:put "setting $lmountstrlen mounts $[:tostr $lmountstr]"
			:if ($lmountstrlen > 0) do={
				set $containerid mounts=$lmountstr
			}

			:put "setting any envs"
			:if [/container/envs find name="$containertag"] do={
				:put "setting env tag tp $containertag"
				set $containerid env="$containertag" 
			}

			:put "make done, sending start"
			[$NETINSTALL start hostid=$containernum]
		}
		/ {
			:put "** done"
		}
		:return ""
	}

	:if ($action = "start") do={
		/container {
			:local waitstart [:timestamp]
			:local startrequested 0
			:local containerid [find comment~"#$containertag"]
			:while ([get $containerid status]!="running") do={
				:put "$containertag is $[get $containerid status]";
				:if ([get $containerid status] = "error") do={
					:error "opps! some error importing container"
				}
				:delay 3s
				:if ([get $containerid status] = "stopped" && $startrequested = 0) do={
					:put "$containertag sending start";
					:do { 
						start $containerid;
						:set startrequested 1
					} on-error={}
					
				}
				:delay 7s
				:if ( [:timestamp] > ($waitstart+[:totime $maxwaitforstart]) ) do={
					/log print proplist=
					:put "opps. took too long..."
					:put "dumping logs..."
					/log print proplist=message where topics~"container"
					:error "opps. timeout while waiting for start.  check logs above for clues and retry build."
				}
			}
			:if ([get $containerid status] = "running") do={
				:put "$containertag started"
			} else={
				:error "$containertag failed to start"
			}
		}
		/ {
			:return ""
		}
	}

	

	:if ($action = "stop") do={
		/container {
			:local activeid [find comment~"#$containertag"]
			#:local activecontainer [get $activeid ]
			:foreach containerinstance in=$activeid do={
				:put "$containertag found existing container to stop..."            
				:while (!([get $containerinstance status]~"stopped|error")) do={
					:do { stop $containerinstance } on-error={}
					:delay 5s
					:put "$containerinstance awaiting waiting stop..."
				}
			}
		}
		:return ""
	}

	:if ($action = "clean") do={
		/container {
			:put "$containertag removing any existing container..."            
			:local containerexisting [find comment~"#$containertag"]
			:if ([:len $containerexisting] > 0) do={
				$NETINSTALL stop
				:delay 1s
				remove $containerexisting
				:put "old container $containerinstance stopped and removed"
			} else={
				:put "no existing container found to remove"
			}
		} 
		/interface/veth {
			:local rveth [remove [find comment~"#$containertag"]]
			:put "$containertag removing veth $rveth"
		}
		/ip/address {
			:local ripaddr [remove [find comment~"#$containertag"]]
			:put "$containertag removing ip address from router $ripaddr"
		}
		/container/envs {
      # remove lasttag
      remove [find name="_$containername" key="lasttag"]
			remove [find comment~"#$containertag"]
			:put "$containertag removing envs"
		}
		/container/mounts {
			:local rmounts [remove [find comment~"#$containertag"]]
			:put "$containertag removing mounts $rmounts"
		}
		/interface/bridge/port {
			remove [find comment~"#$containertag"]
			:put "$containertag removing any bridge ports"
		}
		/interface/list/member {
			remove [find comment~"$containertag"]
			:put "$containertag removing from interface list"
		}
		/ip/firewall/nat {
			:local rdstnats [remove [find comment~"#$containertag"]]
			:put "$containertag removing dst-nats $rdstnats"
		}
		/ip/firewall/filter {}
		/ip/route {}

		/ {
			:put "$containertag $containerinstance clean done"
			:return ""
		}
	}

	:if ($action = "shell") do={
		/container {
			:local activeid [find comment~"#$containertag"]
			:if ([:len $activeid] = 0) do={
				:local activename [/container/envs get [/container/envs find name="_$containername" key="lasttag"] value]
				:set activeid [find comment~"#$activename"]
			}
			:if ([:len $activeid] < 1) do={
				:error "$containertag could not find the container, shell is not possible"
			}
			:if ([get $activeid "status"] != "running") do={
				:put "container is not running. force=$force"
				$NETINSTALL stop
				:local oldcmd [get $activeid cmd]
				:if ([:tostr $force] = "yes") do={
					:put "saving old cmd $oldcmd"
					set $activeid cmd="tail -f /dev/null"
					:put "attempting start with force=$force with 'tail'"
					$NETINSTALL start
					:put "started, connected without builtin cmd"
					:do { shell $activeid } on-error={}
					:put "back from shell, resetting $oldcmd"
					set $activeid cmd=$oldcmd
					:return ""
				} else={
					:put "container is not running, starting for shell"
					$NETINSTALL start
				}
			} 
			:put "starting shell"
			:do { shell $activeid } on-error={}
			:put "back from shell"
		}
		:return ""
	}

	:if ($action = "log") do={
		/log print where topics~"container" and time>([/system clock get time] - 2m)
		:return ""
	}
	
	:if ($action = "tail") do={
		/log print follow where topics~"container" and time>([/system clock get time] - 2m)
		:return ""
	}
	:put  "Usage: \$$(scripthelpername) make|clean|shell|dump|start|stop|registry "
	:error "Bad Command: see docs at https://github.com/$(ocipushuser)/$(containername)"
}


# Some examples:

# To build:
# $NETINSTALL clean [hostid=<num=1>] [dump=yes]
# $NETINSTALL make [path=<disk_prefix>] [branch=<tagver>] [hostid=<num=1>] [dump=yes]

# To control/manage
# $NETINSTALL stop [hostid=<num=1>]
# $NETINSTALL start [hostid=<num=1>] 
# $NETINSTALL shell [force=<"no"|"yes">] [hostid=<num=1>] 

#$NETINSTALL dump=yes

 
belthesar
just joined
Topic Author
Posts: 2
Joined: Tue Mar 07, 2023 7:24 pm

Re: Easy container update script

Sat Feb 03, 2024 6:43 pm

There is a mistake on this row:
Thanks for catching my typo there! Appreciated.
Some can have other config properties set to container like CMD, host, DNS, etc... Why just not find current container by comment variable, read all its config properties and set it for new container?
This is a neat idea. I patterned my script from my experience with Docker Compose, where it's expected that you declaratively define the settings for a container service. Following that model, extending this script to match your container's needs would be my recommended path instead. By doing that, you can also use the script to redefine the configuration of the container on update instead of changing settings via the CLI.
 
optio
Long time Member
Long time Member
Posts: 694
Joined: Mon Dec 26, 2022 2:57 pm

Re: Easy container update script

Sun Feb 04, 2024 1:41 pm

Created some more generic approach for updating containers from remote repository since I have various with different config set:
:local contComment "<CONTAINER_COMMENT>"
:local logPrefix "Container update: "


:local abortWithMessage do={
  :log error "$($logPrefix)Error - $1"
  :error $1
}

/container

:local contId [find where comment=$contComment]
:if ($contId = "") do={ $abortWithMessage ("container $contComment not found, aborting") logPrefix=$logPrefix }

:local contStatus ([get $contId]->"status")
:if ($contStatus = "extracting") do={ $abortWithMessage ("container $contComment is extracting, aborting") logPrefix=$logPrefix }

:log info "$($logPrefix)Container $contComment update started"
:local contStarted false

:if ($contStatus != "stopped") do={
  :if ($contStatus = "running") do={
    :log warn "$($logPrefix)Container $contComment is running, stopping container"
    :set contStarted true
    stop $contId
  }

  :local stopTimeoutSec 120
  :local sec 0

  :while (([get $contId]->"status") != "stopped" && $sec <= $stopTimeoutSec) do={
    :delay 1
    :set sec ($sec + 1)
  }

  :if ($sec > $stopTimeoutSec) do={ $abortWithMessage ("container $contComment stop timed out, aborting") logPrefix=$logPrefix }
  :log info "$($logPrefix)Container $contComment stopped"
}

:local cont [get $contId]
remove $contId
:delay 5


# after ROS upgrade check below if something has changed
# ------------------------------------------------------
# validAddArgs - array of container add command argument names which are named exact as config property names
:local validAddArgs ( "cmd", "comment", "dns", "domain-name", "entrypoint", "envlist", "hostname", "interface", "logging", "mounts", "root-dir", "start-on-boot", "workdir" )
# replaceArgs - key-value array for mapping container add command argument name from different config property name, where key is config property name and value is argument name
:local replaceArgs { "tag"="remote-image" }
# addCmd - ROS CLI container add command
:local addCmd "/container add"
# -----------------------------------------------------


:local escapeStr do={
  :local strLen [:len $1]
  :local escStr ""

  :for i from=0 to=($strLen - 1) do={
    :local chr [:pick $1 $i ($i + 1)]

    :if ($chr = "\$") do={ :set escStr "$escStr\\\$" } else={
      :if ($chr = "\\") do={ :set escStr "$escStr\\\\" } else={
        :if ($chr = "\"") do={ :set escStr "$escStr\\\"" } else={
          :set escStr "$escStr$chr"
        }
      }
    }
  }

  :return "\"$escStr\""
}

:local arrayToStr do={
  :local arrLen [:len $1]
  :local strArr "("

  :for i from=0 to=($arrLen - 1) do={
    :local item [:pick $1 $i]

    :if ([:len $strArr] > 1) do={ :set $strArr "$strArr," }
    :if ([:typeof $item] = "str") do={
      :set $strArr "$strArr$([$escapeStr $item])"
    } else={
      :set $strArr "$strArr$item"
    }
  }

  :return "$strArr)"
}

:local escapeValue do={
  :if ([:typeof $1] = "str") do={ :return [$escapeStr $1] }
  :if ([:typeof $1] = "array") do={ :return [$arrayToStr $1 escapeStr=$escapeStr] }
  :if ([:typeof $1] = "bool") do={:if ($1) do={ :return "yes" } else={ :return "no" }}
  :return $1
}

:foreach k,v in=$cont do={
  :if ([:len $v] != 0) do={
    :if ([:find $validAddArgs $k] >= 0) do={
      :set addCmd "$addCmd $k=$([$escapeValue $v escapeStr=$escapeStr arrayToStr=$arrayToStr])"
    } else={
      :local rk ($replaceArgs->"$k")
      :if ([:typeof $rk] != "nothing") do={ :set addCmd "$addCmd $rk=$([$escapeValue $v escapeStr=$escapeStr arrayToStr=$arrayToStr])" }
    }
  }
}

:execute "$addCmd" as-string
:set contId [find where comment=$contComment]
:if ($contId = "") do={ $abortWithMessage ("unable to add container $contComment, check add command: $addCmd") logPrefix=$logPrefix }

# rise if not enough (30min) on slow network or device
:local extrTimeoutSec 1800
:local sec 0

:while (([get $contId]->"status") = "extracting" && $sec <= $extrTimeoutSec) do={
  :delay 1
  :set sec ($sec + 1)
}

:if ($sec > $extrTimeoutSec) do={ $abortWithMessage ("container $contComment extract wait timed out, something is failed or slower than expected") logPrefix=$logPrefix }
:if ($contStarted) do={ start $contId }
:log info "$($logPrefix)Container $contComment update finished"

Script is building :execute command string based on current container config where container add command aruments are built with config property names. I find easier to maintain validAddArgs and replaceArgs variables than introducing new variables for each argument on ROS changes.
This can be used as standalone script or global function (then contComment must be set to function argument or environment variable) for use from other script or CLI.

Who is online

Users browsing this forum: Amazon [Bot] and 7 guests