cmd/status.go (394 lines of code) (raw):

// 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 ( "encoding/json" "errors" "fmt" "io" "net/http" "os" "slices" "strings" "github.com/Masterminds/semver/v3" "github.com/fatih/color" "github.com/olekukonko/tablewriter" "github.com/spf13/cobra" "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/internal/packages/status" "github.com/elastic/elastic-package/internal/registry" ) const statusLongDescription = `Use this command to display the current deployment status of a package. If a package name is specified, then information about that package is returned, otherwise this command checks if the current directory is a package directory and reports its status.` const ( kibanaVersionParameter = "kibana.version" categoriesParameter = "categories" elasticsearchSubscriptionParameter = "elastic.subscription" serverlessProjectTypesParameter = "serverless.project_types" statusTableFormat = "table" statusJSONFormat = "json" ) var ( bold = color.New(color.Bold) red = color.New(color.FgRed, color.Bold) cyan = color.New(color.FgCyan, color.Bold) availableExtraInfoParameters = []string{ kibanaVersionParameter, categoriesParameter, elasticsearchSubscriptionParameter, serverlessProjectTypesParameter, } availableFormatsParameters = []string{ statusTableFormat, statusJSONFormat, } ) func setupStatusCommand() *cobraext.Command { cmd := &cobra.Command{ Use: "status [package]", Short: "Show package status", Args: cobra.MaximumNArgs(1), Long: statusLongDescription, RunE: statusCommandAction, } cmd.Flags().BoolP(cobraext.ShowAllFlagName, "a", false, cobraext.ShowAllFlagDescription) cmd.Flags().String(cobraext.StatusKibanaVersionFlagName, "", cobraext.StatusKibanaVersionFlagDescription) cmd.Flags().StringSlice(cobraext.StatusExtraInfoFlagName, nil, fmt.Sprintf(cobraext.StatusExtraInfoFlagDescription, strings.Join(availableExtraInfoParameters, ","))) cmd.Flags().String(cobraext.StatusFormatFlagName, "table", fmt.Sprintf(cobraext.StatusFormatFlagDescription, strings.Join(availableFormatsParameters, ","))) return cobraext.NewCommand(cmd, cobraext.ContextPackage) } func statusCommandAction(cmd *cobra.Command, args []string) error { var packageName string showAll, err := cmd.Flags().GetBool(cobraext.ShowAllFlagName) if err != nil { return cobraext.FlagParsingError(err, cobraext.ShowAllFlagName) } if len(args) > 0 { packageName = args[0] } kibanaVersion, err := cmd.Flags().GetString(cobraext.StatusKibanaVersionFlagName) if err != nil { return cobraext.FlagParsingError(err, cobraext.StatusKibanaVersionFlagName) } extraParameters, err := cmd.Flags().GetStringSlice(cobraext.StatusExtraInfoFlagName) if err != nil { return cobraext.FlagParsingError(err, cobraext.StatusExtraInfoFlagName) } format, err := cmd.Flags().GetString(cobraext.StatusFormatFlagName) if err != nil { return cobraext.FlagParsingError(err, cobraext.StatusFormatFlagName) } if !slices.Contains(availableFormatsParameters, format) { return cobraext.FlagParsingError(fmt.Errorf("unsupported format %q, supported formats: %s", format, strings.Join(availableFormatsParameters, ",")), cobraext.StatusFormatFlagName) } err = validateExtraInfoParameters(extraParameters) if err != nil { return fmt.Errorf("validating info paramaters failed: %w", err) } options := registry.SearchOptions{ All: showAll, KibanaVersion: kibanaVersion, Prerelease: true, // Deprecated, keeping for compatibility with older versions of the registry. Experimental: true, } packageStatus, err := getPackageStatus(packageName, options) if err != nil { return err } if slices.Contains(extraParameters, serverlessProjectTypesParameter) { if packageName == "" && packageStatus.Local != nil { packageName = packageStatus.Local.Name } packageStatus.Serverless, err = getServerlessManifests(packageName, options) if err != nil { return err } } switch format { case "table": return print(packageStatus, os.Stdout, extraParameters) case "json": return printJSON(packageStatus, os.Stdout, extraParameters) default: return errors.New("unknown format") } } func validateExtraInfoParameters(extraParameters []string) error { for _, param := range extraParameters { found := false for _, validParam := range availableExtraInfoParameters { if param == validParam { found = true } } if !found { return fmt.Errorf("parameter \"%s\" is not available (available ones: \"%s\")", param, strings.Join(availableExtraInfoParameters, ",")) } } return nil } func getPackageStatus(packageName string, options registry.SearchOptions) (*status.PackageStatus, error) { if packageName != "" { return status.RemotePackage(packageName, options) } packageRootPath, found, err := packages.FindPackageRoot() if !found { return nil, errors.New("no package specified and package root not found") } if err != nil { return nil, fmt.Errorf("locating package root failed: %w", err) } return status.LocalPackage(packageRootPath, options) } func getServerlessManifests(packageName string, options registry.SearchOptions) ([]status.ServerlessManifests, error) { if packageName == "" { return nil, nil } var serverless []status.ServerlessManifests projectTypes := status.GetServerlessProjectTypes(http.DefaultClient) for _, projectType := range projectTypes { if slices.Contains(projectType.ExcludePackages, packageName) { continue } options := options options.Capabilities = projectType.Capabilities options.SpecMax = projectType.SpecMax options.SpecMin = projectType.SpecMin manifests, err := registry.Production.Revisions(packageName, options) if err != nil { return nil, fmt.Errorf("failed to get packages available for serverless projects of type %s: %w", projectType.Name, err) } serverless = append(serverless, status.ServerlessManifests{ Name: projectType.Name, Manifests: manifests, }) } return serverless, nil } // print formats and prints package information into a table func print(p *status.PackageStatus, w io.Writer, extraParameters []string) error { bold.Fprint(w, "Package: ") cyan.Fprintln(w, p.Name) if p.Local != nil { bold.Fprint(w, "Owner: ") cyan.Fprintln(w, formatOwner(p)) } if p.PendingChanges != nil { renderPendingChanges(p, w) } renderPackageVersions(p, w, extraParameters) return nil } // renderPendingChanges formats and prints pending changes in the package into a table func renderPendingChanges(p *status.PackageStatus, w io.Writer) { bold.Fprint(w, "Next Version: ") red.Fprintln(w, p.PendingChanges.Version) bold.Fprintln(w, "Pending Changes:") var changelogTable [][]string for _, change := range p.PendingChanges.Changes { changelogTable = append(changelogTable, formatChangelogEntry(change)) } table := tablewriter.NewWriter(w) table.SetHeader([]string{"Type", "Description", "Link"}) table.SetHeaderColor( twColor(tablewriter.Colors{tablewriter.Bold}), twColor(tablewriter.Colors{tablewriter.Bold}), twColor(tablewriter.Colors{tablewriter.Bold}), ) table.SetColumnColor( twColor(tablewriter.Colors{tablewriter.Bold, tablewriter.FgCyanColor}), tablewriter.Colors{}, tablewriter.Colors{}, ) table.SetRowLine(true) table.AppendBulk(changelogTable) table.Render() } // renderPackageVersions formats and prints local and production versions of the package into a table func renderPackageVersions(p *status.PackageStatus, w io.Writer, extraParameters []string) { var environmentTable [][]string if p.Local != nil { environmentTable = append(environmentTable, formatManifest("Local", "-", *p.Local, nil, extraParameters)) } data := formatManifests("Production", "-", p.Production, extraParameters) environmentTable = append(environmentTable, data) for _, projectType := range p.Serverless { data := formatManifests("Production", projectType.Name, projectType.Manifests, extraParameters) environmentTable = append(environmentTable, data) } bold.Fprintln(w, "Package Versions:") table := tablewriter.NewWriter(w) headers := []string{"Environment", "Version", "Release", "Title", "Description"} headers = append(headers, extraParameters...) table.SetHeader(headers) headerColors := []tablewriter.Colors{} for i := 0; i < len(headers); i++ { headerColors = append(headerColors, twColor(tablewriter.Colors{tablewriter.Bold})) } table.SetHeaderColor(headerColors...) columnColors := []tablewriter.Colors{ twColor(tablewriter.Colors{tablewriter.Bold, tablewriter.FgCyanColor}), twColor(tablewriter.Colors{tablewriter.Bold, tablewriter.FgRedColor}), tablewriter.Colors{}, tablewriter.Colors{}, tablewriter.Colors{}, } for i := 0; i < len(extraParameters); i++ { columnColors = append(columnColors, tablewriter.Colors{}) } table.SetColumnColor(columnColors...) table.SetRowLine(true) table.AppendBulk(environmentTable) table.Render() } // formatOwner returns the name of the package owner func formatOwner(p *status.PackageStatus) string { if p.Local != nil && p.Local.Owner.Github != "" { return p.Local.Owner.Github } return "-" } // formatChangelogEntry returns a row of changelog data func formatChangelogEntry(change changelog.Entry) []string { return []string{change.Type, change.Description, change.Link} } // formatManifests returns a row of data ffor a set of versioned packaged manifests func formatManifests(environment string, serverless string, manifests []packages.PackageManifest, extraParameters []string) []string { if len(manifests) == 0 { return []string{environment, "-", "-", "-", "-"} } var extraVersions []string for i, m := range manifests { if i != len(manifests)-1 { extraVersions = append(extraVersions, m.Version) } } return formatManifest(environment, serverless, manifests[len(manifests)-1], extraVersions, extraParameters) } // formatManifest returns a row of data for a given package manifest func formatManifest(environment string, serverless string, manifest packages.PackageManifest, extraVersions []string, extraParameters []string) []string { version := manifest.Version if len(extraVersions) > 0 { version = fmt.Sprintf("%s (%s)", version, strings.Join(extraVersions, ", ")) } data := []string{environment, version, releaseFromVersion(manifest.Version), manifest.Title, manifest.Description} for _, param := range extraParameters { switch param { case kibanaVersionParameter: data = append(data, manifest.Conditions.Kibana.Version) case categoriesParameter: data = append(data, strings.Join(manifest.Categories, ",")) case elasticsearchSubscriptionParameter: data = append(data, manifest.Conditions.Elastic.Subscription) case serverlessProjectTypesParameter: data = append(data, serverless) } } return data } // twColor no-ops the color setting if we don't want to colorize the output func twColor(colors tablewriter.Colors) tablewriter.Colors { if color.NoColor { return tablewriter.Colors{} } return colors } // releaseFromVersion returns the human-friendly release level based on semantic versioning conventions. // It does a best-effort mapping, it doesn't do validation. func releaseFromVersion(version string) string { const ( previewVersionText = "Technical Preview" betaVersionText = "Beta" releaseCandidateText = "Release Candidate" gaVersion = "GA" defaultText = betaVersionText ) conventionPrereleasePrefixes := []struct { prefix string text string }{ {"beta", betaVersionText}, {"rc", releaseCandidateText}, {"preview", previewVersionText}, } sv, err := semver.NewVersion(version) if err != nil { // Ignoring errors on version parsing here, use best-effort defaults. if strings.HasPrefix(version, "0.") { return previewVersionText } return defaultText } if sv.Major() == 0 { return previewVersionText } if sv.Prerelease() == "" { return gaVersion } for _, convention := range conventionPrereleasePrefixes { if strings.HasPrefix(sv.Prerelease(), convention.prefix) { return convention.text } } return defaultText } type statusJSON struct { Package string `json:"package"` Owner string `json:"owner,omitempty"` Versions []statusJSONVersion `json:"versions,omitempty"` PendingChanges *changelog.Revision `json:"pending_changes,omitempty"` } type statusJSONVersion struct { Environment string `json:"environment,omitempty"` Version string `json:"version"` Release string `json:"release,omitempty"` Title string `json:"title,omitempty"` Description string `json:"description,omitempty"` // Extra parameters KibanaVersion string `json:"kibana_version,omitempty"` Subscription string `json:"subscription,omitempty"` Categories []string `json:"categories,omitempty"` ServerlessProjectType string `json:"serverless_project_type,omitempty"` } func newStatusJSONVersion(environment string, manifest packages.PackageManifest, extraParameters []string) statusJSONVersion { version := statusJSONVersion{ Environment: environment, Version: manifest.Version, Release: strings.ToLower(releaseFromVersion(manifest.Version)), Title: manifest.Title, Description: manifest.Description, } for _, param := range extraParameters { switch param { case kibanaVersionParameter: version.KibanaVersion = manifest.Conditions.Kibana.Version case categoriesParameter: version.Categories = manifest.Categories case elasticsearchSubscriptionParameter: version.Subscription = manifest.Conditions.Elastic.Subscription } } return version } func printJSON(p *status.PackageStatus, w io.Writer, extraParameters []string) error { enc := json.NewEncoder(w) enc.SetEscapeHTML(false) enc.SetIndent("", " ") owner := formatOwner(p) if owner == "-" { owner = "" } info := statusJSON{ Package: p.Name, Owner: owner, } if manifest := p.Local; manifest != nil { version := newStatusJSONVersion("local", *manifest, extraParameters) info.Versions = append(info.Versions, version) info.PendingChanges = p.PendingChanges } for _, manifest := range p.Production { version := newStatusJSONVersion("production", manifest, extraParameters) info.Versions = append(info.Versions, version) } if slices.Contains(extraParameters, serverlessProjectTypesParameter) { for _, projectType := range p.Serverless { for _, manifest := range projectType.Manifests { version := newStatusJSONVersion("production", manifest, extraParameters) version.ServerlessProjectType = projectType.Name info.Versions = append(info.Versions, version) } } } return enc.Encode(info) }