diff --git a/apps/gui/root/usr/bin/proot-apps-gui b/apps/gui/root/usr/bin/proot-apps-gui index b6934213..fd562bf7 100755 --- a/apps/gui/root/usr/bin/proot-apps-gui +++ b/apps/gui/root/usr/bin/proot-apps-gui @@ -56,17 +56,28 @@ class MainWindow(Gtk.ApplicationWindow): # Ingest external metadata def getAppData(self): - try: - res = requests.get(self.metaUrl + 'metadata.yml', allow_redirects=True) - txt = res.content.decode("utf-8") - self.appData = yaml.safe_load(txt) - self.renderHome(None) - except Exception as e: - main = Gtk.Box() - text = Gtk.Label() - text.set_text(str(e)) - main.append(text) - self.set_child(main) + if "PA_REPO_FOLDER" in os.environ: + try: + f = open(os.environ['PA_REPO_FOLDER'] + '/metadata/metadata.yml'); self.appData = yaml.safe_load(f); f.close() + self.renderHome(None) + except Exception as e: + main = Gtk.Box() + text = Gtk.Label() + text.set_text(str(e)) + main.append(text) + self.set_child(main) + else: + try: + res = requests.get(self.metaUrl + 'metadata.yml', allow_redirects=True) + txt = res.content.decode("utf-8") + self.appData = yaml.safe_load(txt) + self.renderHome(None) + except Exception as e: + main = Gtk.Box() + text = Gtk.Label() + text.set_text(str(e)) + main.append(text) + self.set_child(main) # Render the landing page def renderHome(self, button): @@ -93,9 +104,14 @@ class MainWindow(Gtk.ApplicationWindow): if app['name'] in self.logos: content = self.logos[app['name']] else: - res = requests.get(self.metaUrl + 'img/' + app['icon'], allow_redirects=True) - content = res.content - self.logos[app['name']] = content + if "PA_REPO_FOLDER" in os.environ: + content = open(os.environ['PA_REPO_FOLDER'] + '/metadata/img/' + app['icon'], "rb").read() + self.logos[app['name']] = content + time.sleep(.04) + else: + res = requests.get(self.metaUrl + 'img/' + app['icon'], allow_redirects=True) + content = res.content + self.logos[app['name']] = content # Setup buttons and append to grid loader = GdkPixbuf.PixbufLoader() loader.write_bytes(GLib.Bytes.new(content)) diff --git a/ci-scripts/README.template b/ci-scripts/README.template index 254725c4..d39e096f 100644 --- a/ci-scripts/README.template +++ b/ci-scripts/README.template @@ -144,3 +144,76 @@ Three are three things needed for an app to work with all build and ingestion lo * A logo for the application placed in the `/metadata/img` folder of the repository, svg is preffered here, but can also use 192x192 pngs When adding new applications we highly encourage copying an existing application folder as a start to understand what files are needed. Most apps can simply be installed from a distribution's repository and then it is just housekeeping to ensure the desktop file and icon conform to PRoot apps standards. + +# For Administrators + +PRoot Apps can use a local folder for app management and updates. This essentially replaces the remote repository with a folder of tar files. This is setup by using the environment variable `PA_REPO_FOLDER`, when set the user will pull their apps from a local folder of tar files at that path instead of a remote repo including updates. + +## Setup Local Repo + +In this example we will use the path `/mnt/apps` to act as our local repository. First create the directory and set your env: + +``` +mkdir /mnt/apps +export PA_REPO_FOLDER=/mnt/apps +``` + +We can use the `localrepo` action to perform get, update, or remove. Update and remove support passing `all` as a string to perform the action on all locally stored apps. + +Get some apps: + +``` +proot-apps localrepo get firefox chrome +``` + +Inside this folder will be: + +``` +└── /mnt/apps/ + ├── ghcr.io_YOURNAMESPACE_proot-apps_chrome/ + │ ├── app.tar.gz + │ └── SHALAYER + └── ghcr.io_YOURNAMESPACE_proot-apps_firefox/ + ├── app.tar.gz + └── SHALAYER +``` + +`localrepo` can be used to update this repo as well: + +``` +proot-apps localrepo update all +``` + +This will sync down any updates from remote. + +To have users leverage the gui app in your namespace to install and remove applications you will also need to place the metadata from your repository (metadata folder) in this directory IE: + +``` +└── /mnt/apps/ + └── metadata/ + ├── metadata.yml + └── img/ + ├── logo1.svg + └── logo2.svg +``` + +The metadata can be customized to what you want to present to the user in the gui application. + +## Userspace ingesting the repo + +The most likely scenario would be mounting your repo into the users session read only at a specific mount point like `/mnt/apps`. + +To achieve this if it is a Docker container just mount in with a bind and set the env: + +`` +-e PA_REPO_FOLDER=/mnt/apps +-v /mnt/apps:/mnt/apps:ro +``` + +When the user uses proot-apps in this session it will all be connected into this folders contents instead of a remote repository. + +On the administration side the apps can be updated in the folder and the users with this mount will be able to ingest them with the normal command: + +``` +proot-apps update all +``` diff --git a/proot-apps b/proot-apps index da6d422a..91500223 100755 --- a/proot-apps +++ b/proot-apps @@ -4,6 +4,12 @@ TYPE=$1 DEFAULT_GH_USER=REPLACE_USER DEFAULT_GH_REPO=REPLACE_REPO +if [[ ! -z ${LOCALREPO+x} ]] && [[ ! -z ${PA_REPO_FOLDER+x} ]]; then + ROOT_DIR=$PA_REPO_FOLDER +else + ROOT_DIR=$HOME/proot-apps +fi + # Check for deps if [[ ! -f /usr/bin/curl ]]; then echo "Curl was not found on this system please install them to continue" @@ -84,29 +90,32 @@ registry_setup() { # Download layer function dl_layer() { - mkdir -p $HOME/proot-apps + mkdir -p $ROOT_DIR # CLI vars IMAGE=$1 IMAGE_FOLDER=$(echo "${IMAGE}"| sed 's|/|_|g'| sed 's|:|_|g') - DLPATH=$HOME/proot-apps ARCH=$(uname -m| sed 's/x86_64/amd64/g'| sed 's/aarch64/arm64/') UA="Mozilla/5.0 (Linux $(uname -m)) kasmweb.com" # Destination directory - mkdir -p "${DLPATH}/${IMAGE_FOLDER}" - touch "${DLPATH}/${IMAGE_FOLDER}/DOWNLOADING" + mkdir -p "${ROOT_DIR}/${IMAGE_FOLDER}" + touch "${ROOT_DIR}/${IMAGE_FOLDER}/DOWNLOADING" ## Functions ## # Cleanup and exit 1 if something went wrong cleanup() { - rm -Rf "${DLPATH}/${IMAGE_FOLDER}" + rm -Rf "${ROOT_DIR}/${IMAGE_FOLDER}" exit 1 } if [[ -z ${SHALAYER+x} ]]; then - registry_setup ${IMAGE} - SHALAYER=$(get_blob_sha "${TOKEN}" "${MANIFEST_URL}" "${TAG}" "${ARCH}") + if [[ ! -z ${PA_REPO_FOLDER+x} ]] && [[ -z ${LOCALREPO+x} ]]; then + SHALAYER=$(cat $PA_REPO_FOLDER/${IMAGE_FOLDER}/SHALAYER) + else + registry_setup ${IMAGE} + SHALAYER=$(get_blob_sha "${TOKEN}" "${MANIFEST_URL}" "${TAG}" "${ARCH}") + fi fi if [[ $? -eq 1 ]]; then echo "No manifest available for ${IMAGE}, cannot fetch" @@ -116,22 +125,38 @@ function dl_layer() { cleanup fi - # Download and extract layer - curl -f --retry 3 --retry-max-time 20 \ - --location \ - --header "Authorization: Bearer ${TOKEN}" \ - --user-agent "${UA}" \ - "${BLOB_URL}${SHALAYER}" \ - | tar -xzf - -C "${DLPATH}/${IMAGE_FOLDER}/" - if [[ $? -ne 0 ]]; then - echo "Error downloading ${IMAGE}" - cleanup + # Download layer + if [[ ! -z ${PA_REPO_FOLDER+x} ]] && [[ -z ${LOCALREPO+x} ]]; then + echo "Extracting from local repo" + tar -xf \ + $PA_REPO_FOLDER/${IMAGE_FOLDER}/app.tar.gz -C \ + "${ROOT_DIR}/${IMAGE_FOLDER}/" + else + if [[ ! -z ${LOCALREPO+x} ]] && [[ ! -z ${PA_REPO_FOLDER+x} ]]; then + curl -f --retry 3 --retry-max-time 20 \ + -o ${ROOT_DIR}/${IMAGE_FOLDER}/app.tar.gz \ + --location \ + --header "Authorization: Bearer ${TOKEN}" \ + --user-agent "${UA}" \ + "${BLOB_URL}${SHALAYER}" + else + curl -f --retry 3 --retry-max-time 20 \ + --location \ + --header "Authorization: Bearer ${TOKEN}" \ + --user-agent "${UA}" \ + "${BLOB_URL}${SHALAYER}" \ + | tar -xzf - -C "${ROOT_DIR}/${IMAGE_FOLDER}/" + fi + if [[ $? -ne 0 ]]; then + echo "Error downloading ${IMAGE}" + cleanup + fi fi # Tag image - echo "${SHALAYER}" > "${DLPATH}/${IMAGE_FOLDER}/SHALAYER" + echo "${SHALAYER}" > "${ROOT_DIR}/${IMAGE_FOLDER}/SHALAYER" # Cleanup - rm -f "${DLPATH}/${IMAGE_FOLDER}/DOWNLOADING" + rm -f "${ROOT_DIR}/${IMAGE_FOLDER}/DOWNLOADING" } # Update Icon cache @@ -164,39 +189,54 @@ start_system_socket() { function update() { IMAGE_FOLDER=$1 IMAGE=$(echo "${IMAGE_FOLDER}"| sed 's/\(.*\)_/\1:/' | sed 's|_|/|g') - if [ ! -d "$HOME/proot-apps/${IMAGE_FOLDER}/" ]; then + if [ ! -d "$ROOT_DIR/${IMAGE_FOLDER}/" ]; then echo "${IMAGE} not present on system run install or get first" exit 1 fi - LOCAL_SHA=$(cat "$HOME/proot-apps/${IMAGE_FOLDER}/SHALAYER") - # Check remote SHA - ARCH=$(uname -m| sed 's/x86_64/amd64/g'| sed 's/aarch64/arm64/') - UA="Mozilla/5.0 (Linux $(uname -m)) kasmweb.com" - registry_setup ${IMAGE} - SHALAYER=$(get_blob_sha "${TOKEN}" "${MANIFEST_URL}" "${TAG}" "${ARCH}") + LOCAL_SHA=$(cat "$ROOT_DIR/${IMAGE_FOLDER}/SHALAYER") + if [[ ! -z ${PA_REPO_FOLDER+x} ]] && [[ -z ${LOCALREPO+x} ]]; then + if [ ! -f $PA_REPO_FOLDER/${IMAGE_FOLDER}/SHALAYER ]; then + echo "${IMAGE} is not available in local repo" + exit 1 + fi + # Use local SHA + SHALAYER=$(cat $PA_REPO_FOLDER/${IMAGE_FOLDER}/SHALAYER) + else + # Check remote SHA + ARCH=$(uname -m| sed 's/x86_64/amd64/g'| sed 's/aarch64/arm64/') + UA="Mozilla/5.0 (Linux $(uname -m)) kasmweb.com" + registry_setup ${IMAGE} + SHALAYER=$(get_blob_sha "${TOKEN}" "${MANIFEST_URL}" "${TAG}" "${ARCH}") + fi if [[ "${SHALAYER}" == "${LOCAL_SHA}" ]]; then echo "${IMAGE} is up to date: ${LOCAL_SHA}" else echo "Updating ${IMAGE_FOLDER}" # Run remove logic - if [ -f "$HOME/proot-apps/${IMAGE_FOLDER}/remove" ]; then + if [[ ! -z ${LOCALREPO+x} ]] && [[ ! -z ${PA_REPO_FOLDER+x} ]]; then + echo "Removing app root local" + rm -Rf $ROOT_DIR/${IMAGE_FOLDER}/ + dl_layer ${IMAGE} + else + if [ -f "$ROOT_DIR/${IMAGE_FOLDER}/remove" ]; then + $HOME/.local/bin/proot \ + -R $ROOT_DIR/${IMAGE_FOLDER}/ \ + /remove + fi + # Remove app layer + echo "Removing app root" + rm -Rf $ROOT_DIR/${IMAGE_FOLDER}/ + dl_layer ${IMAGE} $HOME/.local/bin/proot \ - -R $HOME/proot-apps/${IMAGE_FOLDER}/ \ - /remove + -R $ROOT_DIR/${IMAGE_FOLDER}/ \ + /install >/dev/null 2>&1 + # Refresh icon cache + update_icon_cache + # Install + $HOME/.local/bin/proot \ + -R $ROOT_DIR/${IMAGE_FOLDER}/ \ + /install fi - # Remove app layer - echo "Removing app root" - rm -Rf $HOME/proot-apps/${IMAGE_FOLDER}/ - dl_layer ${IMAGE} - $HOME/.local/bin/proot \ - -R $HOME/proot-apps/${IMAGE_FOLDER}/ \ - /install >/dev/null 2>&1 - # Refresh icon cache - update_icon_cache - # Install - $HOME/.local/bin/proot \ - -R $HOME/proot-apps/${IMAGE_FOLDER}/ \ - /install fi } @@ -205,23 +245,27 @@ function remove() { IMAGE_FOLDER=$1 IMAGE=$(echo "${IMAGE_FOLDER}"| sed 's/\(.*\)_/\1:/' | sed 's|_|/|g') echo "Removing ${IMAGE}" - if [ ! -d "$HOME/proot-apps/${IMAGE_FOLDER}/" ]; then + if [ ! -d "$ROOT_DIR/${IMAGE_FOLDER}/" ]; then echo "${IMAGE} not present on system run install or get first" exit 1 fi - # Run remove logic - if [ -f "$HOME/proot-apps/${IMAGE_FOLDER}/remove" ]; then - $HOME/.local/bin/proot \ - -R $HOME/proot-apps/${IMAGE_FOLDER}/ \ - /remove - fi - # Remove bin wrapper if defined - if [ -f "$HOME/proot-apps/${IMAGE_FOLDER}/bin-name" ]; then - rm -f $HOME/.local/bin/$(cat $HOME/proot-apps/${IMAGE_FOLDER}/bin-name) + if [[ ! -z ${LOCALREPO+x} ]] && [[ ! -z ${PA_REPO_FOLDER+x} ]]; then + echo "localremove ${IMAGE}" + else + # Run remove logic + if [ -f "$ROOT_DIR/${IMAGE_FOLDER}/remove" ]; then + $HOME/.local/bin/proot \ + -R $ROOT_DIR/${IMAGE_FOLDER}/ \ + /remove + fi + # Remove bin wrapper if defined + if [ -f "$ROOT_DIR/${IMAGE_FOLDER}/bin-name" ]; then + rm -f $HOME/.local/bin/$(cat $ROOT_DIR/${IMAGE_FOLDER}/bin-name) + fi fi # Remove app layer echo "Removing app root" - rm -Rf $HOME/proot-apps/${IMAGE_FOLDER}/ + rm -Rf $ROOT_DIR/${IMAGE_FOLDER}/ } # Run uninstall @@ -229,19 +273,19 @@ function uninstall() { IMAGE_FOLDER=$1 IMAGE=$(echo "${IMAGE_FOLDER}"| sed 's/\(.*\)_/\1:/' | sed 's|_|/|g') echo "Uninstalling ${IMAGE}" - if [ ! -d "$HOME/proot-apps/${IMAGE_FOLDER}/" ]; then + if [ ! -d "$ROOT_DIR/${IMAGE_FOLDER}/" ]; then echo "${IMAGE} not present on system run install or get first" exit 1 fi # Run remove logic - if [ -f "$HOME/proot-apps/${IMAGE_FOLDER}/remove" ]; then + if [ -f "$ROOT_DIR/${IMAGE_FOLDER}/remove" ]; then $HOME/.local/bin/proot \ - -R $HOME/proot-apps/${IMAGE_FOLDER}/ \ + -R $ROOT_DIR/${IMAGE_FOLDER}/ \ /remove fi # Remove bin wrapper if defined - if [ -f "$HOME/proot-apps/${IMAGE_FOLDER}/bin-name" ]; then - rm -f $HOME/.local/bin/$(cat $HOME/proot-apps/${IMAGE_FOLDER}/bin-name) + if [ -f "$ROOT_DIR/${IMAGE_FOLDER}/bin-name" ]; then + rm -f $HOME/.local/bin/$(cat $ROOT_DIR/${IMAGE_FOLDER}/bin-name) fi } @@ -251,23 +295,23 @@ function install_app() { IMAGE_FOLDER=$(echo "${IMAGE}"| sed 's|/|_|g'| sed 's|:|_|g') echo "Installing ${IMAGE}" # Download if not present - if [ ! -d "$HOME/proot-apps/${IMAGE_FOLDER}" ]; then + if [ ! -d "$ROOT_DIR/${IMAGE_FOLDER}" ]; then dl_layer ${IMAGE} fi # Add bin wrapper if defined - if [ -f "$HOME/proot-apps/${IMAGE_FOLDER}/bin-name" ]; then - echo "\$HOME/.local/bin/proot-apps run ${IMAGE} \"\$@\"" > $HOME/.local/bin/$(cat $HOME/proot-apps/${IMAGE_FOLDER}/bin-name) - chmod +x $HOME/.local/bin/$(cat $HOME/proot-apps/${IMAGE_FOLDER}/bin-name) - echo "$(cat $HOME/proot-apps/${IMAGE_FOLDER}/bin-name) is now available from the command line" + if [ -f "$ROOT_DIR/${IMAGE_FOLDER}/bin-name" ]; then + echo "\$HOME/.local/bin/proot-apps run ${IMAGE} \"\$@\"" > $HOME/.local/bin/$(cat $ROOT_DIR/${IMAGE_FOLDER}/bin-name) + chmod +x $HOME/.local/bin/$(cat $ROOT_DIR/${IMAGE_FOLDER}/bin-name) + echo "$(cat $ROOT_DIR/${IMAGE_FOLDER}/bin-name) is now available from the command line" fi $HOME/.local/bin/proot \ - -R $HOME/proot-apps/${IMAGE_FOLDER}/ \ + -R $ROOT_DIR/${IMAGE_FOLDER}/ \ /install >/dev/null 2>&1 # Refresh icon cache update_icon_cache # Install $HOME/.local/bin/proot \ - -R $HOME/proot-apps/${IMAGE_FOLDER}/ \ + -R $ROOT_DIR/${IMAGE_FOLDER}/ \ /install } @@ -277,10 +321,10 @@ function get_app() { IMAGE_FOLDER=$(echo "${IMAGE}"| sed 's|/|_|g'| sed 's|:|_|g') echo "Getting ${IMAGE}" # Download if not present - if [ ! -d "$HOME/proot-apps/${IMAGE_FOLDER}" ]; then + if [ ! -d "$ROOT_DIR/${IMAGE_FOLDER}" ]; then dl_layer ${IMAGE} else - echo "${IMAGE} present on this system use update to update" + echo "${IMAGE} present on this system use update to update" fi } @@ -303,7 +347,7 @@ if [ "${TYPE}" == "run" ]; then exit 1 fi IMAGE_FOLDER=$(echo "${IMAGE}"| sed 's|/|_|g'| sed 's|:|_|g') - if [ ! -d "$HOME/proot-apps/${IMAGE_FOLDER}/" ]; then + if [ ! -d "$ROOT_DIR/${IMAGE_FOLDER}/" ]; then echo "${IMAGE} not present on system run install or get first" exit 1 fi @@ -319,9 +363,9 @@ if [ "${TYPE}" == "run" ]; then fi $HOME/.local/bin/proot \ ${PULSE_BIND} ${FONTS_BIND} -n \ - -R $HOME/proot-apps/${IMAGE_FOLDER}/ \ + -R $ROOT_DIR/${IMAGE_FOLDER}/ \ /entrypoint "${@:3}" & - start_system_socket "$HOME/proot-apps/${IMAGE_FOLDER}/" $! + start_system_socket "$ROOT_DIR/${IMAGE_FOLDER}/" $! fi # Run installation @@ -346,8 +390,8 @@ if [ "${TYPE}" == "install" ]; then if [[ "${IMAGE}" != "all" ]]; then install_app "${IMAGE}" else - if [ -n "$(ls -A $HOME/proot-apps 2>/dev/null)" ]; then - for IMAGE_FOLDER in $(ls $HOME/proot-apps); do + if [ -n "$(ls -A $ROOT_DIR 2>/dev/null)" ]; then + for IMAGE_FOLDER in $(ls $ROOT_DIR); do IMAGE=$(echo "${IMAGE_FOLDER}"| sed 's/\(.*\)_/\1:/' | sed 's|_|/|g') install_app "${IMAGE}" done @@ -383,8 +427,8 @@ if [ "${TYPE}" == "remove" ]; then IMAGE_FOLDER=$(echo "${IMAGE}"| sed 's|/|_|g'| sed 's|:|_|g') remove "${IMAGE_FOLDER}" else - if [ -n "$(ls -A $HOME/proot-apps 2>/dev/null)" ]; then - for IMAGE_FOLDER in $(ls $HOME/proot-apps); do + if [ -n "$(ls -A $ROOT_DIR 2>/dev/null)" ]; then + for IMAGE_FOLDER in $(ls $ROOT_DIR); do remove "${IMAGE_FOLDER}" done else @@ -419,8 +463,8 @@ if [ "${TYPE}" == "uninstall" ]; then IMAGE_FOLDER=$(echo "${IMAGE}"| sed 's|/|_|g'| sed 's|:|_|g') uninstall "${IMAGE_FOLDER}" else - if [ -n "$(ls -A $HOME/proot-apps 2>/dev/null)" ]; then - for IMAGE_FOLDER in $(ls $HOME/proot-apps); do + if [ -n "$(ls -A $ROOT_DIR 2>/dev/null)" ]; then + for IMAGE_FOLDER in $(ls $ROOT_DIR); do uninstall "${IMAGE_FOLDER}" done else @@ -455,8 +499,8 @@ if [ "${TYPE}" == "update" ]; then IMAGE_FOLDER=$(echo "${IMAGE}"| sed 's|/|_|g'| sed 's|:|_|g') update "${IMAGE_FOLDER}" else - if [ -n "$(ls -A $HOME/proot-apps 2>/dev/null)" ]; then - for IMAGE_FOLDER in $(ls $HOME/proot-apps); do + if [ -n "$(ls -A $ROOT_DIR 2>/dev/null)" ]; then + for IMAGE_FOLDER in $(ls $ROOT_DIR); do update "${IMAGE_FOLDER}" done else @@ -490,6 +534,22 @@ if [ "${TYPE}" == "get" ]; then fi fi +# Run localrepo actions +if [ "${TYPE}" == "localrepo" ]; then + if [ -z ${PA_REPO_FOLDER+x} ]; then + echo "error: PA_REPO_FOLDER is not set in env" + exit 1 + fi + if [[ -z ${2+x} ]] || [[ -z ${3+x} ]]; then + echo "Usage: proot-apps localrepo [get,remove,update] myorg/myimage:tag" + exit 1 + fi + export LOCALREPO=true + if [[ "${2}" =~ ^(get|remove|update)$ ]]; then + "$(realpath "$0")" "${@:2}" + fi +fi + # Usage if [[ -z ${1+x} ]]; then echo "+-----------------------------------------------------------+" diff --git a/release-notes/0.2.0 b/release-notes/0.2.0 new file mode 100644 index 00000000..5f02b444 --- /dev/null +++ b/release-notes/0.2.0 @@ -0,0 +1,3 @@ +What's new in this Version: + +* If fonts are available on host mount them into guest diff --git a/release-notes/current b/release-notes/current index 5f02b444..903c1f30 100644 --- a/release-notes/current +++ b/release-notes/current @@ -1,3 +1,6 @@ What's new in this Version: -* If fonts are available on host mount them into guest +* GTK based application available as "gui" to manage installing apps from the supported list +* Lots of build logic updates to easily facilitate forking +* Updates to proot-apps to allow managing and using a local folder of files instead of a remote repository for administrators +