Skip to content

Commit

Permalink
upgrade(installer): Return specific error codes (#31619)
Browse files Browse the repository at this point in the history
Co-authored-by: arbll <[email protected]>
  • Loading branch information
BaptisteFoy and arbll authored Dec 1, 2024
1 parent 8a9c5b5 commit c6d0c1e
Show file tree
Hide file tree
Showing 7 changed files with 170 additions and 43 deletions.
24 changes: 22 additions & 2 deletions cmd/installer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,33 @@
package main

import (
"fmt"
"os"

"github.com/DataDog/datadog-agent/cmd/installer/command"
"github.com/DataDog/datadog-agent/cmd/installer/subcommands"
"github.com/DataDog/datadog-agent/cmd/internal/runcmd"
"github.com/spf13/cobra"
"go.uber.org/dig"

installerErrors "github.com/DataDog/datadog-agent/pkg/fleet/installer/errors"
)

func main() {
os.Exit(runcmd.Run(command.MakeCommand(subcommands.InstallerSubcommands())))
os.Exit(runCmd(command.MakeCommand(subcommands.InstallerSubcommands())))
}

func runCmd(cmd *cobra.Command) int {
// always silence errors, since they are handled here
cmd.SilenceErrors = true

err := cmd.Execute()
if err != nil {
if rootCauseErr := dig.RootCause(err); rootCauseErr != err {
fmt.Fprintln(cmd.ErrOrStderr(), installerErrors.FromErr(rootCauseErr).ToJSON())
} else {
fmt.Fprintln(cmd.ErrOrStderr(), installerErrors.FromErr(err).ToJSON())
}
return -1
}
return 0
}
2 changes: 1 addition & 1 deletion pkg/fleet/daemon/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,7 @@ func setRequestDone(ctx context.Context, err error) {
state.State = pbgo.TaskState_DONE
if err != nil {
state.State = pbgo.TaskState_ERROR
state.Err = installerErrors.From(err)
state.Err = installerErrors.FromErr(err)
}
}

Expand Down
71 changes: 53 additions & 18 deletions pkg/fleet/installer/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,23 @@
package errors

import (
"encoding/json"
"errors"
)

// InstallerErrorCode is an error code used by the installer.
type InstallerErrorCode uint64

const (
errUnknown InstallerErrorCode = iota // This error code is purposefully not exported
// ErrInstallFailed is the code for an install failure.
ErrInstallFailed
errUnknown InstallerErrorCode = 0 // This error code is purposefully not exported
// ErrDownloadFailed is the code for a download failure.
ErrDownloadFailed
// ErrInvalidHash is the code for an invalid hash.
ErrInvalidHash
// ErrInvalidState is the code for an invalid state.
ErrInvalidState
ErrDownloadFailed InstallerErrorCode = 1
// ErrNotEnoughDiskSpace is the code for not enough disk space.
ErrNotEnoughDiskSpace InstallerErrorCode = 2
// ErrPackageNotFound is the code for a package not found.
ErrPackageNotFound
// ErrUpdateExperimentFailed is the code for an update experiment failure.
ErrUpdateExperimentFailed
ErrPackageNotFound InstallerErrorCode = 3
// ErrFilesystemIssue is the code for a filesystem issue (e.g. permission issue).
ErrFilesystemIssue InstallerErrorCode = 4
)

// InstallerError is an error type used by the installer.
Expand All @@ -35,6 +32,11 @@ type InstallerError struct {
code InstallerErrorCode
}

type installerErrorJSON struct {
Error string `json:"error"`
Code int `json:"code"`
}

// Error returns the error message.
func (e InstallerError) Error() string {
return e.err.Error()
Expand All @@ -60,7 +62,7 @@ func (e InstallerError) Code() InstallerErrorCode {
// If the given error is already an installer error, it is not wrapped and
// left as it is. Only the deepest InstallerError remains.
func Wrap(errCode InstallerErrorCode, err error) error {
if errors.Is(err, &InstallerError{}) {
if FromErr(err).code != errUnknown {
return err
}
return &InstallerError{
Expand All @@ -69,18 +71,51 @@ func Wrap(errCode InstallerErrorCode, err error) error {
}
}

// From returns a new InstallerError from the given error.
func From(err error) *InstallerError {
// FromErr returns a new InstallerError from the given error.
// Unwraps the error until it finds an InstallerError and return unknown error code if not found.
func FromErr(err error) *InstallerError {
if err == nil {
return nil
}

e, ok := err.(*InstallerError)
if !ok {
return &InstallerError{
err: err,
code: errUnknown,
unwrappedErr := errors.Unwrap(err)
if unwrappedErr == nil {
return &InstallerError{
err: err,
code: errUnknown,
}
}
return FromErr(unwrappedErr)
}
return e
}

// ToJSON returns the error as a JSON string.
func (e InstallerError) ToJSON() string {
tmp := installerErrorJSON{
Error: e.err.Error(),
Code: int(e.code),
}
jsonErr, err := json.Marshal(tmp)
if err != nil {
return e.err.Error()
}
return string(jsonErr)
}

// FromJSON returns an InstallerError from a JSON string.
func FromJSON(errStr string) InstallerError {
var jsonError installerErrorJSON
err := json.Unmarshal([]byte(errStr), &jsonError)
if err != nil {
return InstallerError{
err: errors.New(errStr),
code: errUnknown,
}
}
return InstallerError{
err: errors.New(jsonError.Error),
code: InstallerErrorCode(jsonError.Code),
}
}
27 changes: 23 additions & 4 deletions pkg/fleet/installer/errors/errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,34 @@ import (
"github.com/stretchr/testify/assert"
)

func TestFrom(t *testing.T) {
func TestFromErr(t *testing.T) {
var err error = &InstallerError{
err: fmt.Errorf("test: test"),
code: ErrDownloadFailed,
}
taskErr := From(err)
taskErr := FromErr(err)
assert.Equal(t, taskErr, &InstallerError{
err: fmt.Errorf("test: test"),
code: ErrDownloadFailed,
})

assert.Nil(t, From(nil))
assert.Nil(t, FromErr(nil))
}

func TestFromErrWithWrap(t *testing.T) {
err := fmt.Errorf("test: %w", &InstallerError{
err: fmt.Errorf("test: test"),
code: ErrDownloadFailed,
})
taskErr := FromErr(err)
assert.Equal(t, taskErr, &InstallerError{
err: fmt.Errorf("test: test"),
code: ErrDownloadFailed,
})

taskErr2 := fmt.Errorf("Wrap 2: %w", fmt.Errorf("Wrap 1: %w", taskErr))
assert.Equal(t, FromErr(taskErr2).Code(), ErrDownloadFailed)
assert.Nil(t, FromErr(nil))
}

func TestWrap(t *testing.T) {
Expand All @@ -36,9 +52,12 @@ func TestWrap(t *testing.T) {

// Check that Wrap doesn't change anything if the error
// is already an InstallerError
taskErr2 := Wrap(ErrInstallFailed, taskErr)
taskErr2 := Wrap(ErrNotEnoughDiskSpace, taskErr)
assert.Equal(t, taskErr2, &InstallerError{
err: err,
code: ErrDownloadFailed,
})

taskErr3 := Wrap(ErrFilesystemIssue, fmt.Errorf("Wrap 2: %w", fmt.Errorf("Wrap 1: %w", taskErr2)))
assert.Equal(t, FromErr(taskErr3).Code(), ErrDownloadFailed)
}
76 changes: 61 additions & 15 deletions pkg/fleet/installer/installer.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/DataDog/datadog-agent/pkg/fleet/internal/paths"

fleetEnv "github.com/DataDog/datadog-agent/pkg/fleet/env"
installerErrors "github.com/DataDog/datadog-agent/pkg/fleet/installer/errors"
"github.com/DataDog/datadog-agent/pkg/fleet/installer/packages"
"github.com/DataDog/datadog-agent/pkg/fleet/installer/repository"
"github.com/DataDog/datadog-agent/pkg/fleet/internal/db"
Expand Down Expand Up @@ -227,30 +228,48 @@ func (i *installerImpl) InstallExperiment(ctx context.Context, url string) error
defer i.m.Unlock()
pkg, err := i.downloader.Download(ctx, url)
if err != nil {
return fmt.Errorf("could not download package: %w", err)
return installerErrors.Wrap(
installerErrors.ErrDownloadFailed,
fmt.Errorf("could not download package: %w", err),
)
}
err = checkAvailableDiskSpace(i.packages, pkg)
if err != nil {
return fmt.Errorf("not enough disk space: %w", err)
return installerErrors.Wrap(
installerErrors.ErrNotEnoughDiskSpace,
fmt.Errorf("not enough disk space: %w", err),
)
}
tmpDir, err := i.packages.MkdirTemp()
if err != nil {
return fmt.Errorf("could not create temporary directory: %w", err)
return installerErrors.Wrap(
installerErrors.ErrFilesystemIssue,
fmt.Errorf("could create temporary directory: %w", err),
)
}
defer os.RemoveAll(tmpDir)
configDir := filepath.Join(i.userConfigsDir, pkg.Name)
err = pkg.ExtractLayers(oci.DatadogPackageLayerMediaType, tmpDir)
if err != nil {
return fmt.Errorf("could not extract package layers: %w", err)
return installerErrors.Wrap(
installerErrors.ErrDownloadFailed,
fmt.Errorf("could not extract package layer: %w", err),
)
}
err = pkg.ExtractLayers(oci.DatadogPackageConfigLayerMediaType, configDir)
if err != nil {
return fmt.Errorf("could not extract package config layer: %w", err)
return installerErrors.Wrap(
installerErrors.ErrDownloadFailed,
fmt.Errorf("could not extract package config layer: %w", err),
)
}
repository := i.packages.Get(pkg.Name)
err = repository.SetExperiment(pkg.Version, tmpDir)
if err != nil {
return fmt.Errorf("could not set experiment: %w", err)
return installerErrors.Wrap(
installerErrors.ErrFilesystemIssue,
fmt.Errorf("could not set experiment: %w", err),
)
}

return i.startExperiment(ctx, pkg.Name)
Expand All @@ -267,7 +286,10 @@ func (i *installerImpl) RemoveExperiment(ctx context.Context, pkg string) error
// will kill the current process, delete the experiment first.
err := repository.DeleteExperiment()
if err != nil {
return fmt.Errorf("could not delete experiment: %w", err)
return installerErrors.Wrap(
installerErrors.ErrFilesystemIssue,
fmt.Errorf("could not delete experiment: %w", err),
)
}
err = i.stopExperiment(ctx, pkg)
if err != nil {
Expand All @@ -280,7 +302,10 @@ func (i *installerImpl) RemoveExperiment(ctx context.Context, pkg string) error
}
err = repository.DeleteExperiment()
if err != nil {
return fmt.Errorf("could not delete experiment: %w", err)
return installerErrors.Wrap(
installerErrors.ErrFilesystemIssue,
fmt.Errorf("could not delete experiment: %w", err),
)
}
}
return nil
Expand All @@ -306,27 +331,42 @@ func (i *installerImpl) InstallConfigExperiment(ctx context.Context, pkg string,

config, err := i.cdn.Get(ctx, pkg)
if err != nil {
return fmt.Errorf("could not get cdn config: %w", err)
return installerErrors.Wrap(
installerErrors.ErrDownloadFailed,
fmt.Errorf("could not get cdn config: %w", err),
)
}
if config.State().GetVersion() != version {
return fmt.Errorf("version mismatch: expected %s, got %s", config.State().GetVersion(), version)
return installerErrors.Wrap(
installerErrors.ErrDownloadFailed,
fmt.Errorf("version mismatch: expected %s, got %s", config.State().GetVersion(), version),
)
}

tmpDir, err := i.packages.MkdirTemp()
if err != nil {
return fmt.Errorf("could not create temporary directory: %w", err)
return installerErrors.Wrap(
installerErrors.ErrFilesystemIssue,
fmt.Errorf("could not create temporary directory: %w", err),
)
}
defer os.RemoveAll(tmpDir)

err = config.Write(tmpDir)
if err != nil {
return fmt.Errorf("could not write agent config: %w", err)
return installerErrors.Wrap(
installerErrors.ErrFilesystemIssue,
fmt.Errorf("could not write agent config: %w", err),
)
}

configRepo := i.configs.Get(pkg)
err = configRepo.SetExperiment(version, tmpDir)
if err != nil {
return fmt.Errorf("could not set experiment: %w", err)
return installerErrors.Wrap(
installerErrors.ErrFilesystemIssue,
fmt.Errorf("could not set experiment: %w", err),
)
}

switch runtime.GOOS {
Expand All @@ -349,7 +389,10 @@ func (i *installerImpl) RemoveConfigExperiment(ctx context.Context, pkg string)
repository := i.configs.Get(pkg)
err = repository.DeleteExperiment()
if err != nil {
return fmt.Errorf("could not delete experiment: %w", err)
return installerErrors.Wrap(
installerErrors.ErrFilesystemIssue,
fmt.Errorf("could not delete experiment: %w", err),
)
}
return nil
}
Expand All @@ -362,7 +405,10 @@ func (i *installerImpl) PromoteConfigExperiment(ctx context.Context, pkg string)
repository := i.configs.Get(pkg)
err := repository.PromoteExperiment()
if err != nil {
return fmt.Errorf("could not promote experiment: %w", err)
return installerErrors.Wrap(
installerErrors.ErrFilesystemIssue,
fmt.Errorf("could not promote experiment: %w", err),
)
}
return i.promoteExperiment(ctx, pkg)
}
Expand Down
6 changes: 4 additions & 2 deletions pkg/fleet/internal/exec/installer_exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/DataDog/datadog-agent/pkg/util/log"

"github.com/DataDog/datadog-agent/pkg/fleet/env"
installerErrors "github.com/DataDog/datadog-agent/pkg/fleet/installer/errors"
"github.com/DataDog/datadog-agent/pkg/fleet/installer/repository"
"github.com/DataDog/datadog-agent/pkg/fleet/telemetry"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
Expand Down Expand Up @@ -247,8 +248,9 @@ func (iCmd *installerCmd) Run() error {
}

if len(errBuf.Bytes()) == 0 {
return fmt.Errorf("run failed: %s", err.Error())
return fmt.Errorf("run failed: %w", err)
}

return fmt.Errorf("run failed: %s \n%s", strings.TrimSpace(errBuf.String()), err.Error())
installerError := installerErrors.FromJSON(strings.TrimSpace(errBuf.String()))
return fmt.Errorf("run failed: %v \n%s", installerError, err.Error())
}
Loading

0 comments on commit c6d0c1e

Please sign in to comment.