From 13e1fe47c6d7994be1aa6aa74a0ae2aed16633a7 Mon Sep 17 00:00:00 2001 From: Alex K <8418476+fearful-symmetry@users.noreply.github.com> Date: Wed, 25 Oct 2023 09:04:15 -0700 Subject: [PATCH] Use third-party library for reporting install/uninstall progress (#3623) * tinker with mutexies in writer * add changelog, flag variable * use third-party spinner library * update changelog * linter * update notice * try notice again * add test to make linter happy * add newline to install message * add initial message * add done message * update notice --- NOTICE.txt | 122 +++++++--- .../1697565877-progress-race-fixes.yaml | 32 +++ go.mod | 12 +- go.sum | 14 +- internal/pkg/agent/cmd/install.go | 54 +++-- internal/pkg/agent/cmd/uninstall.go | 15 +- internal/pkg/agent/install/install.go | 41 ++-- internal/pkg/agent/install/progress.go | 212 +++--------------- internal/pkg/agent/install/progress_test.go | 129 +---------- internal/pkg/agent/install/uninstall.go | 86 ++++--- 10 files changed, 266 insertions(+), 451 deletions(-) create mode 100644 changelog/fragments/1697565877-progress-race-fixes.yaml diff --git a/NOTICE.txt b/NOTICE.txt index 2eb7dcd2da0..b155bf4fd6c 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -4733,6 +4733,37 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +-------------------------------------------------------------------------------- +Dependency : github.com/schollz/progressbar/v3 +Version: v3.13.1 +Licence type (autodetected): MIT +-------------------------------------------------------------------------------- + +Contents of probable licence file $GOMODCACHE/github.com/schollz/progressbar/v3@v3.13.1/LICENSE: + +MIT License + +Copyright (c) 2017 Zack + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + -------------------------------------------------------------------------------- Dependency : github.com/shirou/gopsutil/v3 Version: v3.23.2 @@ -5020,11 +5051,11 @@ Contents of probable licence file $GOMODCACHE/github.com/spf13/cobra@v1.7.0/LICE -------------------------------------------------------------------------------- Dependency : github.com/stretchr/testify -Version: v1.8.2 +Version: v1.8.4 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/stretchr/testify@v1.8.2/LICENSE: +Contents of probable licence file $GOMODCACHE/github.com/stretchr/testify@v1.8.4/LICENSE: MIT License @@ -6209,14 +6240,14 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- -Dependency : golang.org/x/exp -Version: v0.0.0-20220722155223-a9213eeb770e +Dependency : golang.org/x/lint +Version: v0.0.0-20210508222113-6edffad5e616 Licence type (autodetected): BSD-3-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/golang.org/x/exp@v0.0.0-20220722155223-a9213eeb770e/LICENSE: +Contents of probable licence file $GOMODCACHE/golang.org/x/lint@v0.0.0-20210508222113-6edffad5e616/LICENSE: -Copyright (c) 2009 The Go Authors. All rights reserved. +Copyright (c) 2013 The Go Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are @@ -6246,14 +6277,14 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- -Dependency : golang.org/x/lint -Version: v0.0.0-20210508222113-6edffad5e616 +Dependency : golang.org/x/sync +Version: v0.3.0 Licence type (autodetected): BSD-3-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/golang.org/x/lint@v0.0.0-20210508222113-6edffad5e616/LICENSE: +Contents of probable licence file $GOMODCACHE/golang.org/x/sync@v0.3.0/LICENSE: -Copyright (c) 2013 The Go Authors. All rights reserved. +Copyright (c) 2009 The Go Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are @@ -6283,12 +6314,12 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- -Dependency : golang.org/x/sync -Version: v0.3.0 +Dependency : golang.org/x/sys +Version: v0.10.0 Licence type (autodetected): BSD-3-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/golang.org/x/sync@v0.3.0/LICENSE: +Contents of probable licence file $GOMODCACHE/golang.org/x/sys@v0.10.0/LICENSE: Copyright (c) 2009 The Go Authors. All rights reserved. @@ -6320,12 +6351,12 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- -Dependency : golang.org/x/sys +Dependency : golang.org/x/term Version: v0.10.0 Licence type (autodetected): BSD-3-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/golang.org/x/sys@v0.10.0/LICENSE: +Contents of probable licence file $GOMODCACHE/golang.org/x/term@v0.10.0/LICENSE: Copyright (c) 2009 The Go Authors. All rights reserved. @@ -14201,11 +14232,11 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI -------------------------------------------------------------------------------- Dependency : github.com/mattn/go-runewidth -Version: v0.0.14 +Version: v0.0.15 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/mattn/go-runewidth@v0.0.14/LICENSE: +Contents of probable licence file $GOMODCACHE/github.com/mattn/go-runewidth@v0.0.15/LICENSE: The MIT License (MIT) @@ -14230,6 +14261,37 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +-------------------------------------------------------------------------------- +Dependency : github.com/mitchellh/colorstring +Version: v0.0.0-20190213212951-d06e56a500db +Licence type (autodetected): MIT +-------------------------------------------------------------------------------- + +Contents of probable licence file $GOMODCACHE/github.com/mitchellh/colorstring@v0.0.0-20190213212951-d06e56a500db/LICENSE: + +The MIT License (MIT) + +Copyright (c) 2014 Mitchell Hashimoto + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + -------------------------------------------------------------------------------- Dependency : github.com/mitchellh/go-homedir Version: v1.1.0 @@ -15962,11 +16024,11 @@ Contents of probable licence file $GOMODCACHE/github.com/prometheus/procfs@v0.9. -------------------------------------------------------------------------------- Dependency : github.com/rivo/uniseg -Version: v0.4.3 +Version: v0.4.4 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/rivo/uniseg@v0.4.3/LICENSE.txt: +Contents of probable licence file $GOMODCACHE/github.com/rivo/uniseg@v0.4.4/LICENSE.txt: MIT License @@ -17456,12 +17518,12 @@ THE SOFTWARE. -------------------------------------------------------------------------------- -Dependency : golang.org/x/mod -Version: v0.9.0 +Dependency : golang.org/x/exp +Version: v0.0.0-20220722155223-a9213eeb770e Licence type (autodetected): BSD-3-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/golang.org/x/mod@v0.9.0/LICENSE: +Contents of probable licence file $GOMODCACHE/golang.org/x/exp@v0.0.0-20220722155223-a9213eeb770e/LICENSE: Copyright (c) 2009 The Go Authors. All rights reserved. @@ -17493,12 +17555,12 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- -Dependency : golang.org/x/net -Version: v0.12.0 +Dependency : golang.org/x/mod +Version: v0.9.0 Licence type (autodetected): BSD-3-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/golang.org/x/net@v0.12.0/LICENSE: +Contents of probable licence file $GOMODCACHE/golang.org/x/mod@v0.9.0/LICENSE: Copyright (c) 2009 The Go Authors. All rights reserved. @@ -17530,12 +17592,12 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- -Dependency : golang.org/x/oauth2 -Version: v0.10.0 +Dependency : golang.org/x/net +Version: v0.12.0 Licence type (autodetected): BSD-3-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/golang.org/x/oauth2@v0.10.0/LICENSE: +Contents of probable licence file $GOMODCACHE/golang.org/x/net@v0.12.0/LICENSE: Copyright (c) 2009 The Go Authors. All rights reserved. @@ -17567,12 +17629,12 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- -Dependency : golang.org/x/term +Dependency : golang.org/x/oauth2 Version: v0.10.0 Licence type (autodetected): BSD-3-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/golang.org/x/term@v0.10.0/LICENSE: +Contents of probable licence file $GOMODCACHE/golang.org/x/oauth2@v0.10.0/LICENSE: Copyright (c) 2009 The Go Authors. All rights reserved. diff --git a/changelog/fragments/1697565877-progress-race-fixes.yaml b/changelog/fragments/1697565877-progress-race-fixes.yaml new file mode 100644 index 00000000000..b87799857fa --- /dev/null +++ b/changelog/fragments/1697565877-progress-race-fixes.yaml @@ -0,0 +1,32 @@ +# Kind can be one of: +# - breaking-change: a change to previously-documented behavior +# - deprecation: functionality that is being removed in a later release +# - bug-fix: fixes a problem in a previous version +# - enhancement: extends functionality but does not break or fix existing behavior +# - feature: new functionality +# - known-issue: problems that we are aware of in a given version +# - security: impacts on the security of a product or a user’s deployment. +# - upgrade: important information for someone upgrading from a prior version +# - other: does not fit into any of the other categories +kind: bug-fix + +# Change summary; a 80ish characters long description of the change. +summary: Use third-party library for tracking progress in install/uninstall + +# Long description; in case the summary is not enough to describe the change +# this field accommodate a description without length limits. +# NOTE: This field will be rendered only for breaking-change and known-issue kinds at the moment. +#description: + +# Affected component; usually one of "elastic-agent", "fleet-server", "filebeat", "metricbeat", "auditbeat", "all", etc. +component: progress tracker + +# PR URL; optional; the PR number that added the changeset. +# If not present is automatically filled by the tooling finding the PR where this changelog fragment has been added. +# NOTE: the tooling supports backports, so it's able to fill the original PR number instead of the backport PR number. +# Please provide it if you are adding a fragment for a different PR. +pr: https://github.com/elastic/elastic-agent/pull/3623 + +# Issue URL; optional; the GitHub issue related to this changeset (either closes or is part of). +# If not present is automatically filled by the tooling with the issue linked to the PR number. +issue: https://github.com/elastic/elastic-agent/issues/3607 diff --git a/go.mod b/go.mod index 4cab4536513..4a3fdee5308 100644 --- a/go.mod +++ b/go.mod @@ -44,20 +44,21 @@ require ( github.com/pierrre/gotestcover v0.0.0-20160517101806-924dca7d15f0 github.com/pkg/errors v0.9.1 github.com/rs/zerolog v1.27.0 + github.com/schollz/progressbar/v3 v3.13.1 github.com/shirou/gopsutil/v3 v3.23.2 github.com/sirupsen/logrus v1.9.0 github.com/spf13/cobra v1.7.0 - github.com/stretchr/testify v1.8.2 + github.com/stretchr/testify v1.8.4 github.com/tsg/go-daemon v0.0.0-20200207173439-e704b93fd89b go.elastic.co/apm/module/apmgorilla v1.15.0 go.elastic.co/ecszap v1.0.1 go.elastic.co/go-licence-detector v0.5.0 go.uber.org/zap v1.25.0 golang.org/x/crypto v0.11.0 - golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 golang.org/x/sync v0.3.0 golang.org/x/sys v0.10.0 + golang.org/x/term v0.10.0 golang.org/x/text v0.11.0 golang.org/x/time v0.3.0 golang.org/x/tools v0.7.0 @@ -117,7 +118,8 @@ require ( github.com/markbates/pkger v0.17.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.17 // indirect - github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/iochan v1.0.0 // indirect github.com/moby/spdystream v0.2.0 // indirect @@ -130,7 +132,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/prometheus/procfs v0.9.0 // indirect - github.com/rivo/uniseg v0.4.3 // indirect + github.com/rivo/uniseg v0.4.4 // indirect github.com/santhosh-tekuri/jsonschema v1.2.4 // indirect github.com/sergi/go-diff v1.2.0 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect @@ -145,10 +147,10 @@ require ( go.elastic.co/apm/v2 v2.0.0 // indirect go.elastic.co/fastjson v1.1.0 // indirect go.uber.org/multierr v1.11.0 // indirect + golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect golang.org/x/mod v0.9.0 // indirect golang.org/x/net v0.12.0 // indirect golang.org/x/oauth2 v0.10.0 // indirect - golang.org/x/term v0.10.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect google.golang.org/grpc/examples v0.0.0-20220304170021-431ea809a767 // indirect diff --git a/go.sum b/go.sum index c44d94e86b4..e5a24568e6d 100644 --- a/go.sum +++ b/go.sum @@ -1222,6 +1222,7 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= github.com/kardianos/service v1.2.1-0.20210728001519-a323c3813bc7 h1:oohm9Rk9JAxxmp2NLZa7Kebgz9h4+AJDcc64txg3dQ0= github.com/kardianos/service v1.2.1-0.20210728001519-a323c3813bc7/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= github.com/karrick/godirwalk v1.15.6/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= @@ -1316,8 +1317,9 @@ github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzp github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= @@ -1340,6 +1342,8 @@ github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= github.com/mitchellh/cli v1.1.5/go.mod h1:v8+iFts2sPIKUV1ltktPXMCC8fumSKFItNcD2cLtRR4= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -1566,8 +1570,9 @@ github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJf github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -1590,6 +1595,8 @@ github.com/sagikazarmark/crypt v0.9.0/go.mod h1:RnH7sEhxfdnPm1z+XMgSLjWTEIjyK4z2 github.com/santhosh-tekuri/jsonschema v1.2.4 h1:hNhW8e7t+H1vgY+1QeEQpveR6D4+OwKPXCfD2aieJis= github.com/santhosh-tekuri/jsonschema v1.2.4/go.mod h1:TEAUOeZSmIxTTuHatJzrvARHiuO9LYd+cIxzgEHCQI4= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/schollz/progressbar/v3 v3.13.1 h1:o8rySDYiQ59Mwzy2FELeHY5ZARXZTVJC7iHD6PEFUiE= +github.com/schollz/progressbar/v3 v3.13.1/go.mod h1:xvrbki8kfT1fzWzBT/UZd9L6GA+jdL7HAgq2RFnO6fQ= github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= @@ -1678,8 +1685,9 @@ github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= diff --git a/internal/pkg/agent/cmd/install.go b/internal/pkg/agent/cmd/install.go index 4fbd37f40da..3d6100b3ff2 100644 --- a/internal/pkg/agent/cmd/install.go +++ b/internal/pkg/agent/cmd/install.go @@ -51,7 +51,7 @@ would like the Agent to operate. func installCmd(streams *cli.IOStreams, cmd *cobra.Command) error { err := validateEnrollFlags(cmd) if err != nil { - return err + return fmt.Errorf("could not validate flags: %w", err) } basePath, _ := cmd.Flags().GetString(flagInstallBasePath) @@ -90,7 +90,7 @@ func installCmd(streams *cli.IOStreams, cmd *cobra.Command) error { if errors.Is(err, filelock.ErrAppAlreadyRunning) { return fmt.Errorf("cannot perform installation as Elastic Agent is already running from this directory") } - return err + return fmt.Errorf("error obtaining lock: %w", err) } _ = locker.Unlock() @@ -173,53 +173,45 @@ func installCmd(streams *cli.IOStreams, cmd *cobra.Command) error { } } - pt := install.NewProgressTracker(streams.Out) - s := pt.Start() - defer func() { - if err != nil { - s.Failed() - } else { - s.Succeeded() - } - }() + progBar := install.CreateAndStartNewSpinner(streams.Out, "Installing Elastic Agent...") cfgFile := paths.ConfigFile() if status != install.PackageInstall { - err = install.Install(cfgFile, topPath, s) + err = install.Install(cfgFile, topPath, progBar) if err != nil { - return err + return fmt.Errorf("error installing package: %w", err) } defer func() { if err != nil { - uninstallStep := s.StepStart("Uninstalling") - innerErr := install.Uninstall(cfgFile, topPath, "", uninstallStep) + progBar.Describe("Uninstalling") + innerErr := install.Uninstall(cfgFile, topPath, "", progBar) if innerErr != nil { - uninstallStep.Failed() + progBar.Describe("Failed to Uninstall") } else { - uninstallStep.Succeeded() + progBar.Describe("Uninstalled") } } }() if !delayEnroll { - startServiceStep := s.StepStart("Starting service") + progBar.Describe("Starting Service") err = install.StartService(topPath) if err != nil { - startServiceStep.Failed() + progBar.Describe("Start Service failed, exiting...") fmt.Fprintf(streams.Out, "Installation failed to start Elastic Agent service.\n") - return err + return fmt.Errorf("error starting service: %w", err) } - startServiceStep.Succeeded() + progBar.Describe("Service Started") defer func() { if err != nil { - stoppingServiceStep := s.StepStart("Stopping service") + progBar.Describe("Stopping Service") innerErr := install.StopService(topPath) if innerErr != nil { - stoppingServiceStep.Failed() + progBar.Describe("Failed to Stop Service") } else { - stoppingServiceStep.Succeeded() + progBar.Describe("Successfully Stopped Service") } } }() @@ -234,26 +226,30 @@ func installCmd(streams *cli.IOStreams, cmd *cobra.Command) error { enrollCmd.Stdout = os.Stdout enrollCmd.Stderr = os.Stderr - enrollStep := s.StepStart("Enrolling Elastic Agent with Fleet") + progBar.Describe("Enrolling Elastic Agent with Fleet") err = enrollCmd.Start() if err != nil { - enrollStep.Failed() + progBar.Describe("Failed to Enroll") return fmt.Errorf("failed to execute enroll command: %w", err) } + progBar.Describe("Waiting For Enroll...") err = enrollCmd.Wait() if err != nil { - enrollStep.Failed() + progBar.Describe("Failed to Enroll") // uninstall doesn't need to be performed here the defer above will // catch the error and perform the uninstall return fmt.Errorf("enroll command failed for unknown reason: %w", err) } - enrollStep.Succeeded() + progBar.Describe("Enroll Completed") } if err := info.CreateInstallMarker(topPath); err != nil { return fmt.Errorf("failed to create install marker: %w", err) } - fmt.Fprint(streams.Out, "Elastic Agent has been successfully installed.\n") + progBar.Describe("Done") + _ = progBar.Finish() + _ = progBar.Exit() + fmt.Fprint(streams.Out, "\nElastic Agent has been successfully installed.\n") return nil } diff --git a/internal/pkg/agent/cmd/uninstall.go b/internal/pkg/agent/cmd/uninstall.go index e9446757c04..96dc1f4d4ee 100644 --- a/internal/pkg/agent/cmd/uninstall.go +++ b/internal/pkg/agent/cmd/uninstall.go @@ -80,17 +80,18 @@ func uninstallCmd(streams *cli.IOStreams, cmd *cobra.Command) error { } } - pt := install.NewProgressTracker(streams.Out) - s := pt.Start() + progBar := install.CreateAndStartNewSpinner(streams.Out, "Uninstalling Elastic Agent...") - err = install.Uninstall(paths.ConfigFile(), paths.Top(), uninstallToken, s) + err = install.Uninstall(paths.ConfigFile(), paths.Top(), uninstallToken, progBar) if err != nil { - s.Failed() - return err + progBar.Describe("Failed to uninstall agent") + return fmt.Errorf("error uninstalling agent: %w", err) } else { - s.Succeeded() + progBar.Describe("Done") } - fmt.Fprintf(streams.Out, "Elastic Agent has been uninstalled.\n") + _ = progBar.Finish() + _ = progBar.Exit() + fmt.Fprintf(streams.Out, "\nElastic Agent has been uninstalled.\n") _ = install.RemovePath(paths.Top()) return nil diff --git a/internal/pkg/agent/install/install.go b/internal/pkg/agent/install/install.go index 08617db2fcc..55b22ad8ba2 100644 --- a/internal/pkg/agent/install/install.go +++ b/internal/pkg/agent/install/install.go @@ -15,6 +15,7 @@ import ( "github.com/jaypipes/ghw" "github.com/otiai10/copy" + "github.com/schollz/progressbar/v3" "github.com/elastic/elastic-agent/internal/pkg/agent/application/paths" "github.com/elastic/elastic-agent/internal/pkg/agent/errors" @@ -25,7 +26,7 @@ const ( ) // Install installs Elastic Agent persistently on the system including creating and starting its service. -func Install(cfgFile, topPath string, pt ProgressTrackerStep) error { +func Install(cfgFile, topPath string, pt *progressbar.ProgressBar) error { dir, err := findDirectory() if err != nil { return errors.New(err, "failed to discover the source directory for installation", errors.TypeFilesystem) @@ -39,16 +40,16 @@ func Install(cfgFile, topPath string, pt ProgressTrackerStep) error { // There is no uninstall token for "install" command. // Uninstall will fail on protected agent. // The protected Agent will need to be uninstalled first before it can be installed. - s := pt.StepStart("Uninstalling current Elastic Agent") - err = Uninstall(cfgFile, topPath, "", s) + pt.Describe("Uninstalling current Elastic Agent") + err = Uninstall(cfgFile, topPath, "", pt) if err != nil { - s.Failed() + pt.Describe("Failed to uninstall current Elastic Agent") return errors.New( err, fmt.Sprintf("failed to uninstall Agent at (%s)", filepath.Dir(topPath)), errors.M("directory", filepath.Dir(topPath))) } - s.Succeeded() + pt.Describe("Successfully uninstalled current Elastic Agent") } // ensure parent directory exists @@ -70,7 +71,7 @@ func Install(cfgFile, topPath string, pt ProgressTrackerStep) error { if HasAllSSDs(*block) { copyConcurrency = runtime.NumCPU() * 4 } - s := pt.StepStart("Copying files") + pt.Describe("Copying install files") err = copy.Copy(dir, topPath, copy.Options{ OnSymlink: func(_ string) copy.SymlinkAction { return copy.Shallow @@ -79,13 +80,13 @@ func Install(cfgFile, topPath string, pt ProgressTrackerStep) error { NumOfWorkers: int64(copyConcurrency), }) if err != nil { - s.Failed() + pt.Describe("Error copying files") return errors.New( err, fmt.Sprintf("failed to copy source directory (%s) to destination (%s)", dir, topPath), errors.M("source", dir), errors.M("destination", topPath)) } - s.Succeeded() + pt.Describe("Successfully copied files") // place shell wrapper, if present on platform if paths.ShellWrapperPath != "" { @@ -136,7 +137,7 @@ func Install(cfgFile, topPath string, pt ProgressTrackerStep) error { // post install (per platform) err = postInstall(topPath) if err != nil { - return err + return fmt.Errorf("error running post-install steps: %w", err) } // fix permissions @@ -149,21 +150,21 @@ func Install(cfgFile, topPath string, pt ProgressTrackerStep) error { } // install service - s = pt.StepStart("Installing service") + pt.Describe("Installing service") svc, err := newService(topPath) if err != nil { - s.Failed() - return err + pt.Describe("Failed to install service") + return fmt.Errorf("error installing new service: %w", err) } err = svc.Install() if err != nil { - s.Failed() + pt.Describe("Failed to install service") return errors.New( err, fmt.Sprintf("failed to install service (%s)", paths.ServiceName), errors.M("service", paths.ServiceName)) } - s.Succeeded() + pt.Describe("Installed service") return nil } @@ -174,7 +175,7 @@ func Install(cfgFile, topPath string, pt ProgressTrackerStep) error { func StartService(topPath string) error { svc, err := newService(topPath) if err != nil { - return err + return fmt.Errorf("error creating new service handler: %w", err) } err = svc.Start() if err != nil { @@ -190,7 +191,7 @@ func StartService(topPath string) error { func StopService(topPath string) error { svc, err := newService(topPath) if err != nil { - return err + return fmt.Errorf("error creating new service handler: %w", err) } err = svc.Stop() if err != nil { @@ -206,7 +207,7 @@ func StopService(topPath string) error { func RestartService(topPath string) error { svc, err := newService(topPath) if err != nil { - return err + return fmt.Errorf("error creating new service handler: %w", err) } err = svc.Restart() if err != nil { @@ -238,16 +239,16 @@ func FixPermissions(topPath string) error { func findDirectory() (string, error) { execPath, err := os.Executable() if err != nil { - return "", err + return "", fmt.Errorf("error fetching executable of current process: %w", err) } execPath, err = filepath.Abs(execPath) if err != nil { - return "", err + return "", fmt.Errorf("error fetching absolute file path: %w", err) } sourceDir := paths.ExecDir(filepath.Dir(execPath)) err = verifyDirectory(sourceDir) if err != nil { - return "", err + return "", fmt.Errorf("error verifying directory: %w", err) } return sourceDir, nil } diff --git a/internal/pkg/agent/install/progress.go b/internal/pkg/agent/install/progress.go index bc797a2c58d..e75132ea81a 100644 --- a/internal/pkg/agent/install/progress.go +++ b/internal/pkg/agent/install/progress.go @@ -5,200 +5,38 @@ package install import ( - "fmt" "io" - "math" - "strings" - "sync" + "os" "time" - "golang.org/x/exp/rand" + "github.com/schollz/progressbar/v3" + "golang.org/x/term" ) -// ProgressTrackerStep is a currently running step. -// -// A step can produce a sub-step that is a step that is part of another step. -type ProgressTrackerStep interface { - // Succeeded step is done and successful. - Succeeded() - // Failed step has failed. - Failed() - // StepStart creates a new step. - StepStart(msg string) ProgressTrackerStep -} - -type progressTrackerStep struct { - tracker *ProgressTracker - prefix string - - finalizeFunc func() - - rootstep bool - substeps bool - mu sync.Mutex - step *progressTrackerStep -} - -func newProgressTrackerStep(tracker *ProgressTracker, prefix string, finalizeFunc func()) *progressTrackerStep { - return &progressTrackerStep{ - tracker: tracker, - prefix: prefix, - finalizeFunc: finalizeFunc, - } -} - -// Succeeded step is done and successful. -func (pts *progressTrackerStep) Succeeded() { - prefix := " " - if pts.substeps { - prefix = pts.prefix + " " - } - if !pts.rootstep { - pts.tracker.printf("%sDONE\n", prefix) - } - pts.finalizeFunc() -} - -// Failed step has failed. -func (pts *progressTrackerStep) Failed() { - prefix := " " - if pts.substeps { - prefix = pts.prefix + " " - } - if !pts.rootstep { - pts.tracker.printf("%sFAILED\n", prefix) - } - pts.finalizeFunc() -} - -// StepStart creates a new step. -func (pts *progressTrackerStep) StepStart(msg string) ProgressTrackerStep { - prefix := pts.prefix - if !pts.rootstep { - prefix += " " - if !pts.substeps { - prefix = "\n" + prefix - pts.substeps = true - } - } - pts.tracker.printf("%s%s...", prefix, strings.TrimSpace(msg)) - s := newProgressTrackerStep(pts.tracker, prefix, func() { - pts.setStep(nil) - }) - pts.setStep(s) - return s -} - -func (pts *progressTrackerStep) getStep() *progressTrackerStep { - pts.mu.Lock() - defer pts.mu.Unlock() - return pts.step -} - -func (pts *progressTrackerStep) setStep(step *progressTrackerStep) { - pts.mu.Lock() - defer pts.mu.Unlock() - pts.step = step -} - -func (pts *progressTrackerStep) tick() { - step := pts.getStep() - if step != nil { - step.tick() - return - } - if !pts.rootstep { - pts.tracker.printf(".") - } -} - -type ProgressTracker struct { - writer io.Writer - - tickInterval time.Duration - randomizeTickInterval bool - - step *progressTrackerStep - mu sync.Mutex - stop chan struct{} -} - -func NewProgressTracker(writer io.Writer) *ProgressTracker { - return &ProgressTracker{ - writer: writer, - tickInterval: 200 * time.Millisecond, - randomizeTickInterval: true, - stop: make(chan struct{}), - } -} - -func (pt *ProgressTracker) SetTickInterval(d time.Duration) { - pt.tickInterval = d -} - -func (pt *ProgressTracker) DisableRandomizedTickIntervals() { - pt.randomizeTickInterval = false -} - -func (pt *ProgressTracker) Start() ProgressTrackerStep { - timer := time.NewTimer(pt.calculateTickInterval()) - go func() { - defer timer.Stop() - for { - select { - case <-pt.stop: - return - case <-timer.C: - step := pt.getStep() - if step != nil { - step.tick() +// CreateAndStartNewSpinner starts a new spinner that will update every 40ms. +// when finished, it should be closed with Finish() +func CreateAndStartNewSpinner(stream io.Writer, initialMessage string) *progressbar.ProgressBar { + progBar := progressbar.NewOptions(-1, + progressbar.OptionSetWriter(stream), + progressbar.OptionEnableColorCodes(true), + progressbar.OptionSpinnerType(51), + progressbar.OptionSetDescription(initialMessage), + ) + + // don't bother with the spinner refresh if we're not connected to stdout + if term.IsTerminal(int(os.Stdout.Fd())) { + // This keeps the progress spinner running while we're idling + // Otherwise, the spinner would freeze until it got an update + go func() { + for { + if progBar.IsFinished() { + return } - timer = time.NewTimer(pt.calculateTickInterval()) + _ = progBar.RenderBlank() + time.Sleep(time.Millisecond * 40) } - } - }() - - s := newProgressTrackerStep(pt, "", func() { - pt.setStep(nil) - pt.stop <- struct{}{} - }) - s.rootstep = true // is the root step - pt.setStep(s) - return s -} - -func (pt *ProgressTracker) printf(format string, a ...any) { - pt.mu.Lock() - defer pt.mu.Unlock() - _, _ = fmt.Fprintf(pt.writer, format, a...) -} - -func (pt *ProgressTracker) getStep() *progressTrackerStep { - pt.mu.Lock() - defer pt.mu.Unlock() - return pt.step -} - -func (pt *ProgressTracker) setStep(step *progressTrackerStep) { - pt.mu.Lock() - defer pt.mu.Unlock() - pt.step = step -} - -func (pt *ProgressTracker) calculateTickInterval() time.Duration { - if !pt.randomizeTickInterval { - return pt.tickInterval - } - - // Randomize interval between 65% and 250% of configured interval - // to make it look like the progress is non-linear. :) - floor := int64(math.Floor(float64(pt.tickInterval.Milliseconds()) * 0.65)) - ceiling := int64(math.Floor(float64(pt.tickInterval.Milliseconds()) * 2.5)) - - randomDuration := rand.Int63() % ceiling - if randomDuration < floor { - randomDuration = floor + }() } - return time.Duration(randomDuration) * time.Millisecond + return progBar } diff --git a/internal/pkg/agent/install/progress_test.go b/internal/pkg/agent/install/progress_test.go index 2bb9bb6b1d3..d6b605aff03 100644 --- a/internal/pkg/agent/install/progress_test.go +++ b/internal/pkg/agent/install/progress_test.go @@ -5,134 +5,19 @@ package install import ( - "regexp" + "bytes" "testing" - "time" "github.com/stretchr/testify/require" ) -type testWriter struct { - buf []byte -} - -func newTestWriter() *testWriter { - return &testWriter{ - buf: []byte{}, - } -} - -func (tw *testWriter) Write(p []byte) (int, error) { - tw.buf = append(tw.buf, p...) - return len(p), nil -} - -func TestProgress(t *testing.T) { - t.Run("single_step_immediate_failure", func(t *testing.T) { - w := newTestWriter() - pt := NewProgressTracker(w) - - rs := pt.Start() - - s := rs.StepStart("step 1 starting") - s.Failed() - - rs.Failed() - - require.Equal(t, "step 1 starting... FAILED\n", string(w.buf)) - }) - - t.Run("single_step_delayed_failure", func(t *testing.T) { - w := newTestWriter() - pt := NewProgressTracker(w) - pt.SetTickInterval(10 * time.Millisecond) // to speed up testing - pt.DisableRandomizedTickIntervals() - - rs := pt.Start() - - s := rs.StepStart("step 1 starting") - time.Sleep(100 * time.Millisecond) // to simulate work being done - s.Failed() - - rs.Failed() - - require.Regexp(t, regexp.MustCompile(`step 1 starting\.{3,}\.+ FAILED\n`), string(w.buf)) - }) - - t.Run("multi_step_immediate_success", func(t *testing.T) { - w := newTestWriter() - pt := NewProgressTracker(w) - pt.DisableRandomizedTickIntervals() - - rs := pt.Start() - - s := rs.StepStart("step 1 starting") - s.Succeeded() - s = rs.StepStart("step 2 starting") - s.Succeeded() - - rs.Succeeded() - - require.Equal(t, "step 1 starting... DONE\nstep 2 starting... DONE\n", string(w.buf)) - }) - - t.Run("multi_step_delayed_success", func(t *testing.T) { - w := newTestWriter() - pt := NewProgressTracker(w) - pt.SetTickInterval(10 * time.Millisecond) // to speed up testing - pt.DisableRandomizedTickIntervals() - - rs := pt.Start() - - s := rs.StepStart("step 1 starting") - time.Sleep(55 * time.Millisecond) // to simulate work being done - s.Succeeded() - s = rs.StepStart("step 2 starting") - time.Sleep(25 * time.Millisecond) // to simulate work being done - s.Succeeded() - - rs.Succeeded() - - require.Regexp(t, regexp.MustCompile(`step 1 starting\.{3,}\.+ DONE\nstep 2 starting\.{3,}\.+ DONE`), string(w.buf)) - }) - - t.Run("single_step_delay_after_failed", func(t *testing.T) { - w := newTestWriter() - pt := NewProgressTracker(w) - pt.SetTickInterval(10 * time.Millisecond) // to speed up testing - pt.DisableRandomizedTickIntervals() - - rs := pt.Start() - - s := rs.StepStart("step 1 starting") - s.Failed() - time.Sleep(15 * time.Millisecond) - - rs.Failed() - - require.Regexp(t, regexp.MustCompile(`step 1 starting.{3,} FAILED\n`), string(w.buf)) - - }) - - t.Run("nested_step_delayed_success", func(t *testing.T) { - w := newTestWriter() - pt := NewProgressTracker(w) - pt.SetTickInterval(10 * time.Millisecond) // to speed up testing - pt.DisableRandomizedTickIntervals() - - rs := pt.Start() +func TestProgressSpinner(t *testing.T) { + stringWriter := &bytes.Buffer{} - s := rs.StepStart("step starting") - ss := s.StepStart("substep 1 starting") - time.Sleep(55 * time.Millisecond) // to simulate work being done - ss.Succeeded() - ss = s.StepStart("substep 2 starting") - time.Sleep(25 * time.Millisecond) // to simulate work being done - ss.Succeeded() - s.Succeeded() + spinner := CreateAndStartNewSpinner(stringWriter, "example") - rs.Succeeded() + spinner.Describe("test input") - require.Regexp(t, regexp.MustCompile(`step starting\.{3,}\n substep 1 starting\.{3,}\.+ DONE\n substep 2 starting\.{3,}\.+ DONE\n DONE\n`), string(w.buf)) - }) + res := stringWriter.String() + require.Contains(t, res, "test input") } diff --git a/internal/pkg/agent/install/uninstall.go b/internal/pkg/agent/install/uninstall.go index 0d34f990bf5..4a453d3953c 100644 --- a/internal/pkg/agent/install/uninstall.go +++ b/internal/pkg/agent/install/uninstall.go @@ -15,6 +15,7 @@ import ( "time" "github.com/kardianos/service" + "github.com/schollz/progressbar/v3" "github.com/elastic/elastic-agent-libs/logp" "github.com/elastic/elastic-agent/internal/pkg/agent/application/paths" @@ -32,26 +33,26 @@ import ( ) // Uninstall uninstalls persistently Elastic Agent on the system. -func Uninstall(cfgFile, topPath, uninstallToken string, pt ProgressTrackerStep) error { +func Uninstall(cfgFile, topPath, uninstallToken string, pt *progressbar.ProgressBar) error { // uninstall the current service svc, err := newService(topPath) if err != nil { - return err + return fmt.Errorf("error creating new service handler: %w", err) } status, _ := svc.Status() - s := pt.StepStart("Stopping service") + pt.Describe("Stopping service") if status == service.StatusRunning { err := svc.Stop() if err != nil { - s.Failed() + pt.Describe("Failed to stop service") return aerrors.New( err, fmt.Sprintf("failed to stop service (%s)", paths.ServiceName), aerrors.M("service", paths.ServiceName)) } } - s.Succeeded() + pt.Describe("Successfully stopped service") // kill any running watcher if err := killWatcher(pt); err != nil { @@ -70,16 +71,17 @@ func Uninstall(cfgFile, topPath, uninstallToken string, pt ProgressTrackerStep) aerrors.M("service", paths.ServiceName)) } } - return err + return fmt.Errorf("error uninstalling components: %w", err) } // Uninstall service only after components were uninstalled successfully - s = pt.StepStart("Removing service") + pt.Describe("Removing service") err = svc.Uninstall() + // Is there a reason why we don't want to hard-fail on this? if err != nil { - s.Failed() + pt.Describe(fmt.Sprintf("Failed to Uninstall existing service: %s", err)) } else { - s.Succeeded() + pt.Describe("Successfully uninstalled service") } // remove, if present on platform @@ -94,16 +96,16 @@ func Uninstall(cfgFile, topPath, uninstallToken string, pt ProgressTrackerStep) } // remove existing directory - s = pt.StepStart("Removing install directory") + pt.Describe("Removing install directory") err = RemovePath(topPath) if err != nil { - s.Failed() + pt.Describe("Failed to remove install directory") return aerrors.New( err, fmt.Sprintf("failed to remove installation directory (%s)", paths.Top()), aerrors.M("directory", paths.Top())) } - s.Succeeded() + pt.Describe("Removed install directory") return nil } @@ -144,7 +146,7 @@ func RemoveBut(path string, bestEffort bool, exceptions ...string) error { files, err := os.ReadDir(path) if err != nil { - return err + return fmt.Errorf("error reading directory %s: %w", path, err) } for _, f := range files { @@ -154,7 +156,7 @@ func RemoveBut(path string, bestEffort bool, exceptions ...string) error { err = RemovePath(filepath.Join(path, f.Name())) if !bestEffort && err != nil { - return err + return fmt.Errorf("error removing path %s: %w", f.Name(), err) } } @@ -177,10 +179,10 @@ func containsString(str string, a []string, caseSensitive bool) bool { return false } -func uninstallComponents(ctx context.Context, cfgFile string, uninstallToken string, pt ProgressTrackerStep) error { +func uninstallComponents(ctx context.Context, cfgFile string, uninstallToken string, pt *progressbar.ProgressBar) error { log, err := logger.NewWithLogpLevel("", logp.ErrorLevel, false) if err != nil { - return err + return fmt.Errorf("error creating logger: %w", err) } platform, err := component.LoadPlatformDetail() @@ -195,17 +197,17 @@ func uninstallComponents(ctx context.Context, cfgFile string, uninstallToken str cfg, err := operations.LoadFullAgentConfig(ctx, log, cfgFile, false) if err != nil { - return err + return fmt.Errorf("error loading agent config: %w", err) } cfg, err = applyDynamics(ctx, log, cfg) if err != nil { - return err + return fmt.Errorf("error applying dynamic inputs: %w", err) } comps, err := serviceComponentsFromConfig(specs, cfg) if err != nil { - return err + return fmt.Errorf("error creating service components: %w", err) } // nothing to remove @@ -222,7 +224,7 @@ func uninstallComponents(ctx context.Context, cfgFile string, uninstallToken str // prevented from installing caps, err := capabilities.LoadFile(paths.AgentCapabilitiesPath(), log) if err != nil { - return err + return fmt.Errorf("error checking capabilities: %w", err) } // remove each service component @@ -236,24 +238,24 @@ func uninstallComponents(ctx context.Context, cfgFile string, uninstallToken str // The decision was made to change the behaviour and leave the Agent installed if Endpoint uninstall fails // https://github.com/elastic/elastic-agent/pull/2708#issuecomment-1574251911 // Thus returning error here. - return err + return fmt.Errorf("error uninstalling component: %w", err) } } return nil } -func uninstallServiceComponent(ctx context.Context, log *logp.Logger, comp component.Component, uninstallToken string, pt ProgressTrackerStep) error { +func uninstallServiceComponent(ctx context.Context, log *logp.Logger, comp component.Component, uninstallToken string, pt *progressbar.ProgressBar) error { // Do not use infinite retries when uninstalling from the command line. If the uninstall needs to be // retried the entire uninstall command can be retried. Retries may complete asynchronously with the // execution of the uninstall command, leading to bugs like https://github.com/elastic/elastic-agent/issues/3060. - s := pt.StepStart(fmt.Sprintf("Uninstalling service component %s", comp.InputType)) + pt.Describe(fmt.Sprintf("Uninstalling service component %s", comp.InputType)) err := comprt.UninstallService(ctx, log, comp, uninstallToken) if err != nil { - s.Failed() - return err + pt.Describe("Failed to uninstall service") + return fmt.Errorf("error uninstalling service: %w", err) } - s.Succeeded() + pt.Describe("Uninstalled service") return nil } @@ -314,37 +316,27 @@ func applyDynamics(ctx context.Context, log *logger.Logger, cfg *config.Config) } // killWatcher finds and kills any running Elastic Agent watcher. -func killWatcher(pt ProgressTrackerStep) error { - var s ProgressTrackerStep +func killWatcher(pt *progressbar.ProgressBar) error { for { // finding and killing watchers is performed in a loop until no // more watchers are existing, this ensures that during uninstall // that no matter what the watchers are dead before going any further pids, err := utils.GetWatcherPIDs() if err != nil { - if s != nil { - s.Failed() - } - return err + pt.Describe("Failed to get watcher PID") + return fmt.Errorf("error fetching watcher PIDs: %w", err) } if len(pids) == 0 { - if s != nil { - s.Succeeded() - } else { - // step was never started so no watcher was found on first loop - s = pt.StepStart("Stopping upgrade watcher; none found") - s.Succeeded() - } + // step was never started so no watcher was found on first loop + pt.Describe("Stopping upgrade watcher; none found") return nil } - if s == nil { - var pidsStr []string - for _, pid := range pids { - pidsStr = append(pidsStr, fmt.Sprintf("%d", pid)) - } - s = pt.StepStart(fmt.Sprintf("Stopping upgrade watcher (%s)", strings.Join(pidsStr, ", "))) + var pidsStr []string + for _, pid := range pids { + pidsStr = append(pidsStr, fmt.Sprintf("%d", pid)) } + pt.Describe(fmt.Sprintf("Stopping upgrade watcher (%s)", strings.Join(pidsStr, ", "))) var errs error for _, pid := range pids { @@ -360,9 +352,7 @@ func killWatcher(pt ProgressTrackerStep) error { } } if errs != nil { - if s != nil { - s.Failed() - } + pt.Describe("Failed to find and stop watcher processes") return errs } // wait 1 second before performing the loop again