diff --git a/Makefile-man.am b/Makefile-man.am
index e2f88a16e6..4d99cde1b2 100644
--- a/Makefile-man.am
+++ b/Makefile-man.am
@@ -26,6 +26,7 @@ ostree-admin-config-diff.1 ostree-admin-deploy.1 \
ostree-admin-init-fs.1 ostree-admin-instutil.1 ostree-admin-os-init.1 \
ostree-admin-status.1 ostree-admin-set-origin.1 ostree-admin-switch.1 \
ostree-admin-undeploy.1 ostree-admin-upgrade.1 ostree-admin-unlock.1 \
+ostree-admin-pin.1 \
ostree-admin.1 ostree-cat.1 ostree-checkout.1 ostree-checksum.1 \
ostree-commit.1 ostree-export.1 ostree-gpg-sign.1 ostree-config.1 \
ostree-diff.1 ostree-fsck.1 ostree-init.1 ostree-log.1 ostree-ls.1 \
diff --git a/Makefile-ostree.am b/Makefile-ostree.am
index c366c84f94..cccbe30068 100644
--- a/Makefile-ostree.am
+++ b/Makefile-ostree.am
@@ -74,6 +74,7 @@ ostree_SOURCES += \
src/ostree/ot-admin-builtin-set-origin.c \
src/ostree/ot-admin-builtin-status.c \
src/ostree/ot-admin-builtin-switch.c \
+ src/ostree/ot-admin-builtin-pin.c \
src/ostree/ot-admin-builtin-upgrade.c \
src/ostree/ot-admin-builtin-unlock.c \
src/ostree/ot-admin-builtins.h \
diff --git a/apidoc/ostree-sections.txt b/apidoc/ostree-sections.txt
index 892aa0c08f..55f2e7a956 100644
--- a/apidoc/ostree-sections.txt
+++ b/apidoc/ostree-sections.txt
@@ -169,6 +169,7 @@ ostree_deployment_get_bootconfig
ostree_deployment_get_origin
ostree_deployment_get_origin_relpath
ostree_deployment_get_unlocked
+ostree_deployment_is_pinned
ostree_deployment_set_index
ostree_deployment_set_bootserial
ostree_deployment_set_bootconfig
@@ -509,6 +510,7 @@ ostree_sysroot_init_osname
ostree_sysroot_deployment_set_kargs
ostree_sysroot_deployment_set_mutable
ostree_sysroot_deployment_unlock
+ostree_sysroot_deployment_set_pinned
ostree_sysroot_write_deployments
ostree_sysroot_write_deployments_with_options
ostree_sysroot_write_origin_file
diff --git a/bash/ostree b/bash/ostree
index fe8e3d2c0a..edef6cff87 100644
--- a/bash/ostree
+++ b/bash/ostree
@@ -399,6 +399,37 @@ _ostree_admin_os_init() {
return 0
}
+_ostree_admin_pin() {
+ local boolean_options="
+ $main_boolean_options
+ "
+
+ local options_with_args="
+ --sysroot
+ "
+
+ local options_with_args_glob=$( __ostree_to_extglob "$options_with_args" )
+
+ case "$prev" in
+ --sysroot)
+ __ostree_compreply_dirs_only
+ return 0
+ ;;
+ $options_with_args_glob )
+ return 0
+ ;;
+ esac
+
+ case "$cur" in
+ -*)
+ local all_options="$boolean_options $options_with_args"
+ __ostree_compreply_all_options
+ ;;
+ esac
+
+ return 0
+}
+
_ostree_admin_set_origin() {
local boolean_options="
$main_boolean_options
diff --git a/man/ostree-admin-pin.xml b/man/ostree-admin-pin.xml
new file mode 100644
index 0000000000..db0787ae2d
--- /dev/null
+++ b/man/ostree-admin-pin.xml
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+ ostree admin pin
+ OSTree
+
+
+
+ Developer
+ Colin
+ Walters
+ walters@verbum.org
+
+
+
+
+
+ ostree admin pin
+ 1
+
+
+
+ ostree-admin-pin
+ Explicitly retain deployment at a given index
+
+
+
+
+ ostree admin pin INDEX
+
+
+
+
+ Description
+
+
+ Ensures the deployment at INDEX, will not be garbage
+ collected by default. This is termed "pinning". If the
+ -u option is provided, undoes a pinning operation.
+
+
+
+
+ Options
+
+
+
+ ,
+
+
+ Undoes a pinning operation.
+
+
+
+
+
+
diff --git a/src/libostree/libostree-devel.sym b/src/libostree/libostree-devel.sym
index 123627a4f9..285ba5f5f3 100644
--- a/src/libostree/libostree-devel.sym
+++ b/src/libostree/libostree-devel.sym
@@ -20,6 +20,8 @@
/* Add new symbols here. Release commits should copy this section into -released.sym. */
LIBOSTREE_2018.3 {
ostree_deployment_origin_remove_transient_state;
+ ostree_sysroot_deployment_set_pinned;
+ ostree_deployment_is_pinned;
} LIBOSTREE_2018.2;
/* Stub section for the stable release *after* this development one; don't
diff --git a/src/libostree/ostree-deployment.c b/src/libostree/ostree-deployment.c
index 2c479fdb74..75a5bd1dae 100644
--- a/src/libostree/ostree-deployment.c
+++ b/src/libostree/ostree-deployment.c
@@ -322,3 +322,20 @@ ostree_deployment_get_unlocked (OstreeDeployment *self)
{
return self->unlocked;
}
+
+/**
+ * ostree_deployment_is_pinned:
+ * @self: Deployment
+ *
+ * See ostree_sysroot_deployment_set_pinned().
+ *
+ * Returns: `TRUE` if deployment will not be subject to GC
+ * Since: 2018.3
+ */
+gboolean
+ostree_deployment_is_pinned (OstreeDeployment *self)
+{
+ if (!self->origin)
+ return FALSE;
+ return g_key_file_get_boolean (self->origin, OSTREE_ORIGIN_TRANSIENT_GROUP, "pinned", NULL);
+}
diff --git a/src/libostree/ostree-deployment.h b/src/libostree/ostree-deployment.h
index 985c813312..612222a2ab 100644
--- a/src/libostree/ostree-deployment.h
+++ b/src/libostree/ostree-deployment.h
@@ -74,6 +74,9 @@ _OSTREE_PUBLIC
GKeyFile *ostree_deployment_get_origin (OstreeDeployment *self);
+_OSTREE_PUBLIC
+gboolean ostree_deployment_is_pinned (OstreeDeployment *self);
+
_OSTREE_PUBLIC
void ostree_deployment_set_index (OstreeDeployment *self, int index);
_OSTREE_PUBLIC
diff --git a/src/libostree/ostree-sysroot.c b/src/libostree/ostree-sysroot.c
index 3479944472..2c12b78b33 100644
--- a/src/libostree/ostree-sysroot.c
+++ b/src/libostree/ostree-sysroot.c
@@ -1572,12 +1572,14 @@ ostree_sysroot_simple_write_deployment (OstreeSysroot *sysroot,
/* Retain deployment if:
* - we're explicitly asked to, or
+ * - it's pinned
* - the deployment is for another osname, or
* - we're keeping pending deployments and this is a pending deployment, or
* - this is the merge or boot deployment, or
* - we're keeping rollback deployments and this is a rollback deployment
*/
if (retain
+ || ostree_deployment_is_pinned (deployment)
|| !osname_matches
|| (retain_pending && !passed_crossover)
|| (is_booted || is_merge)
@@ -1832,3 +1834,45 @@ ostree_sysroot_deployment_unlock (OstreeSysroot *self,
return TRUE;
}
+
+/**
+ * ostree_sysroot_deployment_set_pinned:
+ * @self: Sysroot
+ * @deployment: A deployment
+ * @is_pinned: Whether or not deployment will be automatically GC'd
+ * @error: Error
+ *
+ * By default, deployments may be subject to garbage collection. Typical uses of
+ * libostree only retain at most 2 deployments. If @is_pinned is `TRUE`, a
+ * metadata bit will be set causing libostree to avoid automatic GC of the
+ * deployment. However, this is really an "advisory" note; it's still possible
+ * for e.g. older versions of libostree unaware of pinning to GC the deployment.
+ *
+ * This function does nothing and returns successfully if the deployment
+ * is already in the desired pinning state.
+ *
+ * Since: 2018.3
+ */
+gboolean
+ostree_sysroot_deployment_set_pinned (OstreeSysroot *self,
+ OstreeDeployment *deployment,
+ gboolean is_pinned,
+ GError **error)
+{
+ const gboolean current_pin = ostree_deployment_is_pinned (deployment);
+ if (is_pinned == current_pin)
+ return TRUE;
+
+ g_autoptr(OstreeDeployment) deployment_clone = ostree_deployment_clone (deployment);
+ GKeyFile *origin_clone = ostree_deployment_get_origin (deployment_clone);
+
+ if (is_pinned)
+ g_key_file_set_boolean (origin_clone, OSTREE_ORIGIN_TRANSIENT_GROUP, "pinned", TRUE);
+ else
+ g_key_file_remove_key (origin_clone, OSTREE_ORIGIN_TRANSIENT_GROUP, "pinned", NULL);
+
+ if (!ostree_sysroot_write_origin_file (self, deployment, origin_clone, NULL, error))
+ return FALSE;
+
+ return TRUE;
+}
diff --git a/src/libostree/ostree-sysroot.h b/src/libostree/ostree-sysroot.h
index 830ed272d3..e4763d3755 100644
--- a/src/libostree/ostree-sysroot.h
+++ b/src/libostree/ostree-sysroot.h
@@ -181,6 +181,12 @@ gboolean ostree_sysroot_deployment_set_mutable (OstreeSysroot *self,
GCancellable *cancellable,
GError **error);
+_OSTREE_PUBLIC
+gboolean ostree_sysroot_deployment_set_pinned (OstreeSysroot *self,
+ OstreeDeployment *deployment,
+ gboolean is_pinned,
+ GError **error);
+
_OSTREE_PUBLIC
gboolean ostree_sysroot_deployment_unlock (OstreeSysroot *self,
OstreeDeployment *deployment,
diff --git a/src/ostree/ot-admin-builtin-pin.c b/src/ostree/ot-admin-builtin-pin.c
new file mode 100644
index 0000000000..e26ea92637
--- /dev/null
+++ b/src/ostree/ot-admin-builtin-pin.c
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2018 Colin Walters
+ *
+ * SPDX-License-Identifier: LGPL-2.0+
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ * Boston, MA 02111-1307, USA.
+ */
+
+#include "config.h"
+
+#include
+
+#include "ot-main.h"
+#include "ot-admin-builtins.h"
+#include "ot-admin-functions.h"
+#include "ostree.h"
+#include "otutil.h"
+
+/* ATTENTION:
+ * Please remember to update the bash-completion script (bash/ostree) and
+ * man page (man/ostree-admin-pin.xml) when changing the option list.
+ */
+
+static gboolean opt_unpin;
+
+static GOptionEntry options[] = {
+ { "unpin", 'u', 0, G_OPTION_ARG_NONE, &opt_unpin, "Unset pin", NULL },
+ { NULL }
+};
+
+gboolean
+ot_admin_builtin_pin (int argc, char **argv, OstreeCommandInvocation *invocation, GCancellable *cancellable, GError **error)
+{
+ g_autoptr(GOptionContext) context = g_option_context_new ("INDEX");
+ g_autoptr(OstreeSysroot) sysroot = NULL;
+ if (!ostree_admin_option_context_parse (context, options, &argc, &argv,
+ OSTREE_ADMIN_BUILTIN_FLAG_SUPERUSER,
+ invocation, &sysroot, cancellable, error))
+ return FALSE;
+
+ if (argc < 2)
+ {
+ ot_util_usage_error (context, "INDEX must be specified", error);
+ return FALSE;
+ }
+
+ const char *deploy_index_str = argv[1];
+ const int deploy_index = atoi (deploy_index_str);
+
+ g_autoptr(OstreeDeployment) target_deployment = ot_admin_get_indexed_deployment (sysroot, deploy_index, error);
+ if (!target_deployment)
+ return FALSE;
+
+
+ gboolean current_pin = ostree_deployment_is_pinned (target_deployment);
+ const gboolean desired_pin = !opt_unpin;
+ if (current_pin == desired_pin)
+ g_print ("Deployment is already %s\n", current_pin ? "pinned" : "unpinned");
+ else
+ {
+ if (!ostree_sysroot_deployment_set_pinned (sysroot, target_deployment, desired_pin, error))
+ return FALSE;
+ g_print ("Deployment is now %s\n", desired_pin ? "pinned" : "unpinned");
+ }
+
+ return TRUE;
+}
diff --git a/src/ostree/ot-admin-builtin-status.c b/src/ostree/ot-admin-builtin-status.c
index 0279c5aff2..0299130944 100644
--- a/src/ostree/ot-admin-builtin-status.c
+++ b/src/ostree/ot-admin-builtin-status.c
@@ -149,6 +149,8 @@ ot_admin_builtin_status (int argc, char **argv, OstreeCommandInvocation *invocat
ostree_deployment_unlocked_state_to_string (unlocked),
red_bold_suffix);
}
+ if (ostree_deployment_is_pinned (deployment))
+ g_print (" Pinned: yes\n");
if (!origin)
g_print (" origin: none\n");
else
diff --git a/src/ostree/ot-admin-builtins.h b/src/ostree/ot-admin-builtins.h
index 3999624393..a81f4d62d4 100644
--- a/src/ostree/ot-admin-builtins.h
+++ b/src/ostree/ot-admin-builtins.h
@@ -39,6 +39,7 @@ BUILTINPROTO(init_fs);
BUILTINPROTO(undeploy);
BUILTINPROTO(deploy);
BUILTINPROTO(cleanup);
+BUILTINPROTO(pin);
BUILTINPROTO(unlock);
BUILTINPROTO(status);
BUILTINPROTO(set_origin);
diff --git a/src/ostree/ot-builtin-admin.c b/src/ostree/ot-builtin-admin.c
index f4e687e982..fd6d9a8f92 100644
--- a/src/ostree/ot-builtin-admin.c
+++ b/src/ostree/ot-builtin-admin.c
@@ -54,6 +54,9 @@ static OstreeCommand admin_subcommands[] = {
{ "set-origin", OSTREE_BUILTIN_FLAG_NO_REPO,
ot_admin_builtin_set_origin,
"Set Origin and create a new origin file" },
+ { "pin", OSTREE_BUILTIN_FLAG_NO_REPO,
+ ot_admin_builtin_pin,
+ "Change the \"pinning\" state of a deployment" },
{ "status", OSTREE_BUILTIN_FLAG_NO_REPO,
ot_admin_builtin_status,
"List deployments" },
diff --git a/tests/test-admin-deploy-2.sh b/tests/test-admin-deploy-2.sh
index 4ecaf67ad9..eab0a3d3db 100755
--- a/tests/test-admin-deploy-2.sh
+++ b/tests/test-admin-deploy-2.sh
@@ -26,7 +26,7 @@ set -euo pipefail
# Exports OSTREE_SYSROOT so --sysroot not needed.
setup_os_repository "archive" "syslinux"
-echo "1..3"
+echo "1..6"
${CMD_PREFIX} ostree --repo=sysroot/ostree/repo pull-local --remote=testos testos-repo testos/buildmaster/x86_64-runtime
rev=$(${CMD_PREFIX} ostree --repo=sysroot/ostree/repo rev-parse testos/buildmaster/x86_64-runtime)
@@ -63,3 +63,50 @@ assert_has_dir sysroot/boot/ostree/testos-${bootcsum}
assert_file_has_content sysroot/ostree/deploy/testos/deploy/${newrev}.0/etc/os-release 'NAME=TestOS'
echo "ok manual cleanup"
+
+assert_n_pinned() {
+ local n=$1
+ ${CMD_PREFIX} ostree admin status > status.txt
+ local n_pinned="$(grep -F -c -e 'Pinned: yes' < status.txt)"
+ if test "${n_pinned}" '!=' "${n}"; then
+ cat status.txt
+ fatal "${n_pinned} != ${n}"
+ fi
+}
+assert_n_deployments() {
+ local n=$1
+ ${CMD_PREFIX} ostree admin status > status.txt
+ local n_deployments="$(grep -F -c -e 'Version: ' < status.txt)"
+ if test "${n_deployments}" '!=' "${n}"; then
+ cat status.txt
+ fatal "${n_deployments} != ${n}"
+ fi
+}
+assert_n_pinned 0
+${CMD_PREFIX} ostree admin pin 0
+assert_n_pinned 1
+${CMD_PREFIX} ostree admin pin -u 0
+assert_n_pinned 0
+echo "ok pin unpin"
+
+${CMD_PREFIX} ostree admin pin 0
+${CMD_PREFIX} ostree admin pin 1
+assert_n_pinned 2
+assert_n_deployments 2
+os_repository_new_commit
+${CMD_PREFIX} ostree admin upgrade --os=testos
+assert_n_pinned 2
+assert_n_deployments 3
+echo "ok pin across upgrades"
+
+${CMD_PREFIX} ostree admin pin -u 1
+os_repository_new_commit
+${CMD_PREFIX} ostree admin upgrade --os=testos
+assert_n_pinned 1
+assert_n_deployments 3
+os_repository_new_commit
+${CMD_PREFIX} ostree admin upgrade --os=testos
+assert_n_pinned 1
+assert_n_deployments 3
+
+echo "ok pinning"