Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show installation progress #3338

Merged
merged 12 commits into from
Sep 8, 2023
74 changes: 37 additions & 37 deletions NOTICE.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6193,6 +6193,43 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


--------------------------------------------------------------------------------
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/[email protected]/LICENSE:

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
met:

* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


--------------------------------------------------------------------------------
Dependency : golang.org/x/lint
Version: v0.0.0-20210508222113-6edffad5e616
Expand Down Expand Up @@ -17161,43 +17198,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


--------------------------------------------------------------------------------
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/[email protected]/LICENSE:

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
met:

* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


--------------------------------------------------------------------------------
Dependency : golang.org/x/mod
Version: v0.9.0
Expand Down
32 changes: 32 additions & 0 deletions changelog/fragments/1693427183-install-progress.yaml
Original file line number Diff line number Diff line change
@@ -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: feature

# Change summary; a 80ish characters long description of the change.
summary: Print out Elastic Agent installation steps to show progress

# 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: elastic-agent

# 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/3338

# 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/owner/repo/1234
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ require (
go.elastic.co/go-licence-detector v0.5.0
go.uber.org/zap v1.25.0
golang.org/x/crypto v0.7.0
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616
golang.org/x/sync v0.1.0
golang.org/x/sys v0.9.0
Expand Down Expand Up @@ -141,7 +142,6 @@ 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.9.0 // indirect
golang.org/x/oauth2 v0.4.0 // indirect
Expand Down
16 changes: 15 additions & 1 deletion internal/pkg/agent/cmd/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,13 @@ func installCmd(streams *cli.IOStreams, cmd *cobra.Command) error {
}
}

pt := install.NewProgressTracker(streams.Out)
pt.Start()
defer pt.Stop()

cfgFile := paths.ConfigFile()
if status != install.PackageInstall {
err = install.Install(cfgFile, topPath)
err = install.Install(cfgFile, topPath, pt)
if err != nil {
return err
}
Expand All @@ -193,15 +197,20 @@ func installCmd(streams *cli.IOStreams, cmd *cobra.Command) error {
}()

if !delayEnroll {
pt.StepStart("Starting service")
err = install.StartService(topPath)
if err != nil {
pt.StepFailed()
fmt.Fprintf(streams.Out, "Installation failed to start Elastic Agent service.\n")
return err
}
pt.StepSucceeded()

defer func() {
if err != nil {
fmt.Fprint(streams.Out, "Stopping service... ")
_ = install.StopService(topPath)
pt.StepSucceeded()
}
}()
}
Expand All @@ -214,12 +223,16 @@ func installCmd(streams *cli.IOStreams, cmd *cobra.Command) error {
enrollCmd.Stdin = os.Stdin
enrollCmd.Stdout = os.Stdout
enrollCmd.Stderr = os.Stderr

fmt.Fprint(streams.Out, "Enrolling Elastic Agent with Fleet... ")
err = enrollCmd.Start()
if err != nil {
pt.StepFailed()
return fmt.Errorf("failed to execute enroll command: %w", err)
}
err = enrollCmd.Wait()
if err != nil {
pt.StepFailed()
if status != install.PackageInstall {
var exitErr *exec.ExitError
_ = install.Uninstall(cfgFile, topPath, "")
Expand All @@ -229,6 +242,7 @@ func installCmd(streams *cli.IOStreams, cmd *cobra.Command) error {
}
return fmt.Errorf("enroll command failed for unknown reason: %w", err)
}
pt.StepSucceeded()
}

if err := info.CreateInstallMarker(topPath); err != nil {
Expand Down
17 changes: 15 additions & 2 deletions internal/pkg/agent/install/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const (
)

// Install installs Elastic Agent persistently on the system including creating and starting its service.
func Install(cfgFile, topPath string) error {
func Install(cfgFile, topPath string, pt *ProgressTracker) error {
dir, err := findDirectory()
if err != nil {
return errors.New(err, "failed to discover the source directory for installation", errors.TypeFilesystem)
Expand All @@ -33,34 +33,42 @@ func Install(cfgFile, topPath string) 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.
pt.StepStart("Uninstalling current Elastic Agent")
err = Uninstall(cfgFile, topPath, "")
if err != nil {
pt.StepFailed()
return errors.New(
err,
fmt.Sprintf("failed to uninstall Agent at (%s)", filepath.Dir(topPath)),
errors.M("directory", filepath.Dir(topPath)))
}
pt.StepSucceeded()

// ensure parent directory exists, copy source into install path
// ensure parent directory exists
err = os.MkdirAll(filepath.Dir(topPath), 0755)
if err != nil {
return errors.New(
err,
fmt.Sprintf("failed to create installation parent directory (%s)", filepath.Dir(topPath)),
errors.M("directory", filepath.Dir(topPath)))
}

// copy source into install path
pt.StepStart("Copying files")
err = copy.Copy(dir, topPath, copy.Options{
OnSymlink: func(_ string) copy.SymlinkAction {
return copy.Shallow
},
Sync: true,
})
if err != nil {
pt.StepFailed()
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))
}
pt.StepSucceeded()

// place shell wrapper, if present on platform
if paths.ShellWrapperPath != "" {
Expand Down Expand Up @@ -124,17 +132,22 @@ func Install(cfgFile, topPath string) error {
}

// install service
pt.StepStart("Installing service")
svc, err := newService(topPath)
if err != nil {
pt.StepFailed()
return err
}
err = svc.Install()
if err != nil {
pt.StepFailed()
return errors.New(
err,
fmt.Sprintf("failed to install service (%s)", paths.ServiceName),
errors.M("service", paths.ServiceName))
}
pt.StepSucceeded()

return nil
}

Expand Down
111 changes: 111 additions & 0 deletions internal/pkg/agent/install/progress.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.

package install

import (
"fmt"
"io"
"math"
"strings"
"sync"
"time"

"golang.org/x/exp/rand"
)

type ProgressTracker struct {
writer io.Writer

tickInterval time.Duration
randomizeTickInterval bool

stepInProgress bool
mu sync.RWMutex
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() {
timer := time.NewTimer(pt.calculateTickInterval())
go func() {
defer timer.Stop()
for {
select {
case <-pt.stop:
return
case <-timer.C:
pt.mu.RLock()
if pt.stepInProgress {
_, _ = pt.writer.Write([]byte("."))
}
pt.mu.RUnlock()

timer = time.NewTimer(pt.calculateTickInterval())
}
}
}()
}

func (pt *ProgressTracker) StepStart(msg string) {
pt.mu.Lock()
defer pt.mu.Unlock()

pt.stepInProgress = true
fmt.Fprintf(pt.writer, strings.TrimSpace(msg)+"...")
}

func (pt *ProgressTracker) StepSucceeded() {
pt.mu.Lock()
defer pt.mu.Unlock()

fmt.Fprintln(pt.writer, " DONE")
pt.stepInProgress = false
}

func (pt *ProgressTracker) StepFailed() {
pt.mu.Lock()
defer pt.mu.Unlock()

fmt.Fprintln(pt.writer, " FAILED")
pt.stepInProgress = false
}

func (pt *ProgressTracker) Stop() {
pt.stop <- struct{}{}
}

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))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha, I don't know if this is really needed but okay! Why not.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole deal is just to show some random progress to make things appear more realistic ;)


randomDuration := rand.Int63() % ceiling
if randomDuration < floor {
randomDuration = floor
}

return time.Duration(randomDuration) * time.Millisecond
}
Loading
Loading