diff --git a/README.md b/README.md index 53d17daff7..76e597f3f2 100644 --- a/README.md +++ b/README.md @@ -409,6 +409,12 @@ _Context: package_ Boot up the stack. +### `elastic-package shell` + +_Context: global_ + + + ### `elastic-package stack` _Context: global_ diff --git a/cmd/root.go b/cmd/root.go index 58c181d330..3b7016325d 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -31,6 +31,7 @@ var commands = []*cobraext.Command{ setupPublishCommand(), setupReportsCommand(), setupServiceCommand(), + setupShellCommand(), setupStackCommand(), setupStatusCommand(), setupTestCommand(), diff --git a/cmd/shell.go b/cmd/shell.go new file mode 100644 index 0000000000..ff81c5dfc0 --- /dev/null +++ b/cmd/shell.go @@ -0,0 +1,29 @@ +// 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 cmd + +import ( + cshell "github.com/brianstrauch/cobra-shell" + "github.com/spf13/cobra" + + "github.com/elastic/elastic-package/internal/cobraext" + "github.com/elastic/elastic-package/pkg/shell" +) + +func setupShellCommand() *cobraext.Command { + cmd := &cobra.Command{ + Use: "shell", + Hidden: true, + SilenceUsage: true, + } + cmd.CompletionOptions.DisableDefaultCmd = true + cmd.CompletionOptions.HiddenDefaultCmd = true + + shell.AttachCommands(cmd) + + shellCmd := cshell.New(cmd, nil) + + return cobraext.NewCommand(shellCmd, cobraext.ContextGlobal) +} diff --git a/go.mod b/go.mod index 5396e1455c..0922b85bf2 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/ProtonMail/gopenpgp/v2 v2.7.3 github.com/aymerick/raymond v2.0.2+incompatible github.com/boumenot/gocover-cobertura v1.2.0 + github.com/brianstrauch/cobra-shell v0.4.0 github.com/cbroglie/mustache v1.4.0 github.com/cespare/xxhash/v2 v2.2.0 github.com/dustin/go-humanize v1.0.1 @@ -26,11 +27,13 @@ require ( github.com/google/uuid v1.3.1 github.com/jedib0t/go-pretty v4.3.0+incompatible github.com/magefile/mage v1.15.0 + github.com/mattn/go-sqlite3 v1.14.17 github.com/mholt/archiver/v3 v3.5.1 github.com/olekukonko/tablewriter v0.0.5 github.com/pmezard/go-difflib v1.0.0 github.com/shirou/gopsutil/v3 v3.23.7 github.com/spf13/cobra v1.7.0 + github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 golang.org/x/tools v0.12.0 gopkg.in/yaml.v3 v3.0.1 @@ -58,6 +61,7 @@ require ( github.com/acomagu/bufpipe v1.0.4 // indirect github.com/andybalholm/brotli v1.0.4 // indirect github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect + github.com/c-bata/go-prompt v0.2.6 // indirect github.com/chai2010/gettext-go v1.0.2 // indirect github.com/cloudflare/circl v1.3.3 // indirect github.com/creasty/defaults v1.7.0 // indirect @@ -109,6 +113,7 @@ require ( 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-tty v0.0.3 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect @@ -126,6 +131,7 @@ require ( github.com/pierrec/lz4/v4 v4.1.17 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pkg/term v1.2.0-beta.2 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/rivo/uniseg v0.4.3 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect @@ -134,7 +140,6 @@ require ( github.com/shopspring/decimal v1.3.1 // indirect github.com/skeema/knownhosts v1.2.0 // indirect github.com/spf13/cast v1.5.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect github.com/tklauser/go-sysconf v0.3.11 // indirect github.com/tklauser/numcpus v0.6.0 // indirect github.com/ulikunitz/xz v0.5.11 // indirect diff --git a/go.sum b/go.sum index 36d3e19b14..2682af8ea2 100644 --- a/go.sum +++ b/go.sum @@ -50,9 +50,13 @@ github.com/aymerick/raymond v2.0.2+incompatible h1:VEp3GpgdAnv9B2GFyTvqgcKvY+mfK github.com/aymerick/raymond v2.0.2+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= github.com/boumenot/gocover-cobertura v1.2.0 h1:g+VROIASoEHBrEilIyaCmgo7HGm+AV5yKEPLk0qIY+s= github.com/boumenot/gocover-cobertura v1.2.0/go.mod h1:fz7ly8dslE42VRR5ZWLt2OHGDHjkTiA2oNvKgJEjLT0= +github.com/brianstrauch/cobra-shell v0.4.0 h1:oPWTBqPPbE8Vd/i3WRvQd8XWTrevIwR0bFBIS7X6gWk= +github.com/brianstrauch/cobra-shell v0.4.0/go.mod h1:QkRKnD1+MfpITTqYDE8Yp8zALgsHdeYur/qAw4kNr8c= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/cbroglie/mustache v1.4.0 h1:Azg0dVhxTml5me+7PsZ7WPrQq1Gkf3WApcHMjMprYoU= github.com/cbroglie/mustache v1.4.0/go.mod h1:SS1FTIghy0sjse4DUVGV1k/40B1qE1XkD9DtDsHo9iM= +github.com/c-bata/go-prompt v0.2.6 h1:POP+nrHE+DfLYx370bedwNhsqmpCUynWPxuHi0C5vZI= +github.com/c-bata/go-prompt v0.2.6/go.mod h1:/LMAke8wD2FsNu9EXNdHxNLbd9MedkPnCdfpU9wwHfY= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -257,19 +261,27 @@ github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJ github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-runewidth v0.0.6/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.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= +github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-tty v0.0.3 h1:5OfyWorkyO7xP52Mq7tB36ajHDG5OHrmBGIS/DtakQI= +github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= @@ -321,6 +333,8 @@ github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFz github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/term v1.2.0-beta.2 h1:L3y/h2jkuBVFdWiJvNfYfKmzcCnILw7mJWm2JQuMppw= +github.com/pkg/term v1.2.0-beta.2/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= @@ -470,9 +484,13 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/internal/configuration/locations/locations.go b/internal/configuration/locations/locations.go index 18888f7e6e..2d7e82edae 100644 --- a/internal/configuration/locations/locations.go +++ b/internal/configuration/locations/locations.go @@ -18,6 +18,7 @@ const ( stackDir = "stack" packagesDir = "development" profilesDir = "profiles" + pluginsDir = "shell_plugins" temporaryDir = "tmp" deployerDir = "deployer" @@ -100,6 +101,11 @@ func (loc LocationManager) FieldsCacheDir() string { return filepath.Join(loc.stackPath, fieldsCachedDir) } +// ShellPluginsDir returns the directory where the shell plugins will be stored. +func (loc LocationManager) ShellPluginsDir() string { + return filepath.Join(loc.stackPath, pluginsDir) +} + // configurationDir returns the configuration directory location // If a environment variable named as in elasticPackageDataHome is present, // the value is used as is, overriding the value of this function. diff --git a/internal/install/install.go b/internal/install/install.go index 92eddb5e6e..4b7b707cf6 100644 --- a/internal/install/install.go +++ b/internal/install/install.go @@ -73,6 +73,10 @@ func EnsureInstalled() error { return fmt.Errorf("creating service logs directory failed: %w", err) } + if err := createShellPluginsDir(elasticPackagePath); err != nil { + return fmt.Errorf("creating shell plugins directory failed: %w", err) + } + fmt.Fprintln(os.Stderr, "elastic-package has been installed.") return nil } @@ -149,3 +153,12 @@ func createServiceLogsDir(elasticPackagePath *locations.LocationManager) error { } return nil } + +func createShellPluginsDir(elasticPackagePath *locations.LocationManager) error { + dirPath := elasticPackagePath.ShellPluginsDir() + err := os.MkdirAll(dirPath, 0755) + if err != nil { + return fmt.Errorf("mkdir failed (path: %s): %w", dirPath, err) + } + return nil +} diff --git a/pkg/shell/plugins/changelog.go b/pkg/shell/plugins/changelog.go new file mode 100644 index 0000000000..37eeff69c3 --- /dev/null +++ b/pkg/shell/plugins/changelog.go @@ -0,0 +1,166 @@ +// 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 main + +import ( + "context" + "errors" + "fmt" + "io" + "os" + "path/filepath" + + "github.com/Masterminds/semver/v3" + "github.com/spf13/pflag" + + "github.com/elastic/elastic-package/internal/cobraext" + "github.com/elastic/elastic-package/internal/packages" + "github.com/elastic/elastic-package/internal/packages/changelog" + "github.com/elastic/elastic-package/pkg/shell" +) + +var _ shell.Command = changelogCmd{} + +type changelogCmd struct{} + +func (changelogCmd) Usage() string { + return "changelog --next {major|minor|patch} --description desc --type {bugfix|enhancement|breaking-change} --link link" +} + +func (changelogCmd) Desc() string { + return "Add an entry to the changelog file in each of the packages in context 'Shell.Packages'." +} + +func (changelogCmd) Flags() *pflag.FlagSet { + flags := pflag.NewFlagSet("", pflag.ContinueOnError) + flags.String(cobraext.ChangelogAddNextFlagName, "", cobraext.ChangelogAddNextFlagDescription) + flags.String(cobraext.ChangelogAddDescriptionFlagName, "", cobraext.ChangelogAddDescriptionFlagDescription) + flags.String(cobraext.ChangelogAddTypeFlagName, "", cobraext.ChangelogAddTypeFlagDescription) + flags.String(cobraext.ChangelogAddLinkFlagName, "", cobraext.ChangelogAddLinkFlagDescription) + return flags +} + +func (changelogCmd) Exec(ctx context.Context, flags *pflag.FlagSet, args []string, _, stderr io.Writer) (context.Context, error) { + packages, ok := ctx.Value(ctxKeyPackages).([]string) + if !ok { + fmt.Fprintln(stderr, "no packages found in the context") + return ctx, nil + } + for _, pkg := range packages { + packageRoot := pkg + // check if we are in packages folder + if _, err := os.Stat(filepath.Join(".", pkg)); err != nil { + // check if we are in integrations root folder + packageRoot = filepath.Join(".", "packages", pkg) + if _, err := os.Stat(packageRoot); err != nil { + return ctx, errors.New("you need to be in intgerations root folder or in the packages folder") + } + } + if err := changelogAddCmdForRoot(packageRoot, flags, args); err != nil { + return ctx, err + } + } + return ctx, nil +} + +func changelogAddCmdForRoot(packageRoot string, flags *pflag.FlagSet, args []string) error { + nextMode, _ := flags.GetString(cobraext.ChangelogAddNextFlagName) + v, err := changelogCmdVersion(nextMode, packageRoot) + if err != nil { + return err + } + version := v.String() + + description, _ := flags.GetString(cobraext.ChangelogAddDescriptionFlagName) + changeType, _ := flags.GetString(cobraext.ChangelogAddTypeFlagName) + link, _ := flags.GetString(cobraext.ChangelogAddLinkFlagName) + + entry := changelog.Revision{ + Version: version, + Changes: []changelog.Entry{ + { + Description: description, + Type: changeType, + Link: link, + }, + }, + } + + if err := patchChangelogFile(packageRoot, entry); err != nil { + return err + } + + if err := setManifestVersion(packageRoot, version); err != nil { + return err + } + + return nil +} + +func changelogCmdVersion(nextMode, packageRoot string) (*semver.Version, error) { + revisions, err := changelog.ReadChangelogFromPackageRoot(packageRoot) + if err != nil { + return nil, fmt.Errorf("failed to read current changelog: %w", err) + } + if len(revisions) == 0 { + return semver.MustParse("0.0.0"), nil + } + + version, err := semver.NewVersion(revisions[0].Version) + if err != nil { + return nil, fmt.Errorf("invalid version in changelog %q: %w", revisions[0].Version, err) + } + + switch nextMode { + case "": + break + case "major": + v := version.IncMajor() + version = &v + case "minor": + v := version.IncMinor() + version = &v + case "patch": + v := version.IncPatch() + version = &v + default: + return nil, fmt.Errorf("invalid value for %q: %s", + cobraext.ChangelogAddNextFlagName, nextMode) + } + + return version, nil +} + +// patchChangelogFile looks for the proper place to add the new revision in the changelog, +// trying to conserve original format and comments. +func patchChangelogFile(packageRoot string, patch changelog.Revision) error { + changelogPath := filepath.Join(packageRoot, changelog.PackageChangelogFile) + d, err := os.ReadFile(changelogPath) + if err != nil { + return err + } + + d, err = changelog.PatchYAML(d, patch) + if err != nil { + return err + } + + return os.WriteFile(changelogPath, d, 0644) +} + +func setManifestVersion(packageRoot string, version string) error { + manifestPath := filepath.Join(packageRoot, packages.PackageManifestFile) + d, err := os.ReadFile(manifestPath) + if err != nil { + return err + } + + d, err = changelog.SetManifestVersion(d, version) + if err != nil { + return err + } + + return os.WriteFile(manifestPath, d, 0644) +} diff --git a/pkg/shell/plugins/registry.go b/pkg/shell/plugins/registry.go new file mode 100644 index 0000000000..e330a86f6a --- /dev/null +++ b/pkg/shell/plugins/registry.go @@ -0,0 +1,38 @@ +// 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 main + +import ( + "github.com/elastic/elastic-package/pkg/shell" +) + +func init() { + Registry.commands = append( + []shell.Command{}, + changelogCmd{}, + writefileCmd{}, + whereCmd{}, + initdbCmd{}, + ) +} + +type ctxKey string + +const ( + ctxKeyPackages ctxKey = "Shell.Packages" + ctxKeyDB ctxKey = "Shell.DB" +) + +var Registry = registry{} + +var _ shell.Plugin = registry{} + +type registry struct { + commands []shell.Command +} + +func (r registry) Commands() []shell.Command { + return r.commands +} diff --git a/pkg/shell/plugins/sql.go b/pkg/shell/plugins/sql.go new file mode 100644 index 0000000000..0aacc75e01 --- /dev/null +++ b/pkg/shell/plugins/sql.go @@ -0,0 +1,140 @@ +// 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 main + +import ( + "context" + "database/sql" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + _ "github.com/mattn/go-sqlite3" + "github.com/spf13/pflag" + + "github.com/elastic/elastic-package/internal/packages" + "github.com/elastic/elastic-package/pkg/shell" +) + +const ( + createPackagesQuery = "CREATE TABLE `packages` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` VARCHAR(256) NOT NULL, `manifest` TEXT NOT NULL)" + insertPackageQuery = "INSERT INTO `packages` (`name`, `manifest`) VALUES (?,?)" +) + +var _ shell.Command = whereCmd{} + +type whereCmd struct{} + +func (whereCmd) Usage() string { + return `where "query"` +} + +func (whereCmd) Desc() string { + return "Select a list of packages based on some conditions. Reads from context 'Shell.DB' and updates context 'Shell.Packages'." +} + +func (whereCmd) Flags() *pflag.FlagSet { + return nil +} + +func (whereCmd) Exec(ctx context.Context, flags *pflag.FlagSet, args []string, _, stderr io.Writer) (context.Context, error) { + db, ok := ctx.Value(ctxKeyDB).(*sql.DB) + if !ok { + return ctx, errors.New("db connection not found in context") + } + conditions := strings.Join(args, " ") + query := `SELECT name FROM packages` + if conditions != "" { + query = fmt.Sprintf("%s WHERE %s", query, conditions) + } + + rows, err := db.Query(query) + if err != nil { + return nil, err + } + defer rows.Close() + + var pkgs []string + var pkg string + for rows.Next() { + if err := rows.Scan(&pkg); err != nil { + return nil, err + } + pkgs = append(pkgs, pkg) + } + + ctx = context.WithValue(ctx, ctxKeyPackages, pkgs) + fmt.Fprintf(stderr, "Found %d packages\n", len(pkgs)) + return ctx, nil +} + +var _ shell.Command = initdbCmd{} + +type initdbCmd struct{} + +func (initdbCmd) Usage() string { + return "initdb" +} + +func (initdbCmd) Desc() string { + return "Initializes the packages database. Sets context 'Shell.DB'." +} + +func (initdbCmd) Flags() *pflag.FlagSet { + return nil +} + +func (initdbCmd) Exec(ctx context.Context, flags *pflag.FlagSet, args []string, _, stderr io.Writer) (context.Context, error) { + fmt.Fprintln(stderr, "Initializing database...") + + db, err := sql.Open("sqlite3", ":memory:") + if err != nil { + return ctx, err + } + + if _, err := db.Exec(createPackagesQuery); err != nil { + return ctx, err + } + + packagesPath := filepath.Join(".", "packages") + if _, err := os.Stat(packagesPath); err != nil { + return ctx, err + } + + entries, err := os.ReadDir(packagesPath) + if err != nil { + return ctx, err + } + + var c int + for _, e := range entries { + if !e.IsDir() || strings.HasPrefix(e.Name(), ".") { + continue + } + root := filepath.Join(packagesPath, e.Name()) + manifest, err := packages.ReadPackageManifestFromPackageRoot(root) + if err != nil { + return ctx, err + } + p, err := json.Marshal(manifest) + if err != nil { + return ctx, err + } + if _, err := db.Exec(insertPackageQuery, e.Name(), string(p)); err != nil { + return ctx, err + } + c++ + } + + fmt.Fprintf(stderr, "Loaded %d packages\n", c) + + ctx = context.WithValue(ctx, ctxKeyDB, db) + + return ctx, nil +} diff --git a/pkg/shell/plugins/writefile.go b/pkg/shell/plugins/writefile.go new file mode 100644 index 0000000000..358c6b1d61 --- /dev/null +++ b/pkg/shell/plugins/writefile.go @@ -0,0 +1,79 @@ +// 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 main + +import ( + "context" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/spf13/pflag" + + "github.com/elastic/elastic-package/pkg/shell" +) + +var _ shell.Command = writefileCmd{} + +type writefileCmd struct{} + +func (writefileCmd) Usage() string { + return "write-file --path path --contents contents" +} + +func (writefileCmd) Desc() string { + return "Writes a file in each of the packages in context 'Shell.Packages'." +} + +func (writefileCmd) Flags() *pflag.FlagSet { + flags := pflag.NewFlagSet("", pflag.ContinueOnError) + flags.String("path", "", "Path to the file (relative to the package root).") + flags.String("contents", "", "Contents of the file") + return flags +} + +func (writefileCmd) Exec(ctx context.Context, flags *pflag.FlagSet, args []string, _, stderr io.Writer) (context.Context, error) { + packages, ok := ctx.Value(ctxKeyPackages).([]string) + if !ok { + fmt.Fprintln(stderr, "no packages found in the context") + return ctx, nil + } + for _, pkg := range packages { + packageRoot := pkg + // check if we are in packages folder + if _, err := os.Stat(filepath.Join(".", pkg)); err != nil { + // check if we are in integrations root folder + packageRoot = filepath.Join(".", "packages", pkg) + if _, err := os.Stat(packageRoot); err != nil { + return ctx, errors.New("you need to be in intgerations root folder or in the packages folder") + } + } + + path, _ := flags.GetString("path") + path = filepath.Join(packageRoot, path) + + contents, _ := flags.GetString("contents") + + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + return ctx, err + } + + f, err := os.Create(path) + if err != nil { + return ctx, err + } + + if _, err := f.WriteString(strings.ReplaceAll(contents, `\n`, "\n")); err != nil { + f.Close() + return ctx, err + } + + f.Close() + } + return ctx, nil +} diff --git a/pkg/shell/shell.go b/pkg/shell/shell.go new file mode 100644 index 0000000000..c4f00296d1 --- /dev/null +++ b/pkg/shell/shell.go @@ -0,0 +1,119 @@ +// 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 shell + +import ( + "context" + "fmt" + "io" + "os" + "path/filepath" + "plugin" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/elastic/elastic-package/internal/configuration/locations" + "github.com/elastic/elastic-package/internal/logger" +) + +var ( + commands = []Command{} + + // globalCtx is updated through all the run time of the shell and + // it is used to pass information between plugins + globalCtx = context.Background() +) + +type Command interface { + // Usage is the one-line usage message. + // Recommended syntax is as follows: + // [ ] identifies an optional argument. Arguments that are not enclosed in brackets are required. + // ... indicates that you can specify multiple values for the previous argument. + // | indicates mutually exclusive information. You can use the argument to the left of the separator or the + // argument to the right of the separator. You cannot use both arguments in a single use of the command. + // { } delimits a set of mutually exclusive arguments when one of the arguments is required. If the arguments are + // optional, they are enclosed in brackets ([ ]). + // Example: add [-F file | -D dir]... [-f format] profile + Usage() string + Desc() string + Flags() *pflag.FlagSet + Exec(ctx context.Context, flags *pflag.FlagSet, args []string, stdout, stderr io.Writer) (context.Context, error) +} + +type Plugin interface { + Commands() []Command +} + +func initCommands() error { + lm, err := locations.NewLocationManager() + if err != nil { + return err + } + + pluginsDir := lm.ShellPluginsDir() + entries, err := os.ReadDir(pluginsDir) + if err != nil { + return err + } + + for _, e := range entries { + if e.IsDir() || filepath.Ext(e.Name()) != ".so" { + continue + } + + pluginPath := filepath.Join(pluginsDir, e.Name()) + + p, err := plugin.Open(pluginPath) + if err != nil { + return err + } + + regSymbol, err := p.Lookup("Registry") + if err != nil { + return err + } + + registry, ok := regSymbol.(Plugin) + if !ok { + return fmt.Errorf("registry in plugin %s does not implement the Plugin interface", pluginPath) + } + + commands = append(commands, registry.Commands()...) + } + + return nil +} + +func AttachCommands(parent *cobra.Command) { + if err := initCommands(); err != nil { + logger.Error(err) + } + for _, command := range commands { + cmd := &cobra.Command{ + Use: command.Usage(), + Short: command.Desc(), + RunE: commandRunE(command), + } + if command.Flags() != nil { + command.Flags().VisitAll(func(f *pflag.Flag) { + cmd.Flags().AddFlag(f) + }) + } + parent.AddCommand(cmd) + } +} + +func commandRunE(command Command) func(*cobra.Command, []string) error { + return func(cmd *cobra.Command, args []string) error { + flags := cmd.Flags() + ctx, err := command.Exec(globalCtx, flags, args, cmd.OutOrStdout(), cmd.OutOrStderr()) + if err != nil { + return err + } + globalCtx = ctx + return nil + } +}