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

feat: Add support for different output formats on project view and project variables view command #328

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ If successful, the go compiler does not output anything. You should now have an

**Makefile**

If you are using a sytem that has `make` installed, then you can also simpl run `make` in the cli root folder.
If you are using a system that has `make` installed, then you can also simply run `make` in the cli root folder.
The default action for the `Makefile` is to run `go build`, as above.

## Running the CLI
Expand Down
216 changes: 168 additions & 48 deletions pkg/cmd/project/variables/view/view.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package view

import (
"encoding/json"
"fmt"
"strconv"
"strings"

"github.com/MakeNowJust/heredoc/v2"
"github.com/OctopusDeploy/cli/pkg/apiclient"
variableShared "github.com/OctopusDeploy/cli/pkg/cmd/project/variables/shared"
Expand All @@ -14,9 +18,6 @@ import (
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/resources"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/variables"
"github.com/spf13/cobra"
"io"
"strconv"
"strings"
)

const (
Expand All @@ -34,9 +35,35 @@ type ViewFlags struct {
type ViewOptions struct {
Client *client.Client
Host string
out io.Writer
name string
*ViewFlags
cmd *cobra.Command
}

type VarScopedItem struct {
Id string `json:"id"`
Name string `json:"name"`
}

type VarProcessScopedItem struct {
VarScopedItem
ProcessType string `json:"processtype"`
}

type VariableViewData struct {
Id string `json:"id"`
Value string `json:"value"`
Description string `json:"description"`
EnvironmentScope []*VarScopedItem `json:"environmentscope"`
RoleScope []*VarScopedItem `json:"rolescope"`
MachineScope []*VarScopedItem `json:"machinescope"`
ProcessScope []*VarProcessScopedItem `json:"processscope"`
StepScope []*VarScopedItem `json:"stepscope"`
ChannelScope []*VarScopedItem `json:"channelscope"`
Prompted bool `json:"prompted,omitempty"`
PromptLabel string `json:"promptlabel,omitempty"`
PromptLabelDescription string `json:"promptlabeldescription,omitempty"`
PromptRequired string `json:"promptrequired,omitempty"`
}

func NewViewFlags() *ViewFlags {
Expand Down Expand Up @@ -71,9 +98,9 @@ func NewCmdView(f factory.Factory) *cobra.Command {
opts := &ViewOptions{
client,
f.GetCurrentHost(),
cmd.OutOrStdout(),
args[0],
viewFlags,
cmd,
}

return viewRun(opts)
Expand Down Expand Up @@ -113,55 +140,148 @@ func viewRun(opts *ViewOptions) error {
return fmt.Errorf("cannot find variable '%s'", opts.name)
}

fmt.Fprintln(opts.out, output.Bold(filteredVars[0].Name))
outputFormat, err := opts.cmd.Flags().GetString(constants.FlagOutputFormat)

for _, v := range filteredVars {
data := []*output.DataRow{}
if err != nil { // should never happen, but fallback if it does
outputFormat = constants.OutputFormatTable
}

data = append(data, output.NewDataRow("Id", output.Dim(v.GetID())))
if v.IsSensitive {
data = append(data, output.NewDataRow("Value", output.Bold("*** (sensitive)")))
} else {
data = append(data, output.NewDataRow("Value", output.Bold(v.Value)))
}
out := opts.cmd.OutOrStdout()

if v.Description == "" {
v.Description = constants.NoDescription
}
data = append(data, output.NewDataRow("Description", output.Dim(v.Description)))
switch strings.ToLower(outputFormat) {
case constants.OutputFormatBasic, constants.OutputFormatTable:
fmt.Fprintln(out, output.Bold(filteredVars[0].Name))
for _, v := range filteredVars {
data := []*output.DataRow{}

Copy link

Choose a reason for hiding this comment

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

Should this also print out the KEY VALUE heading like the project view does?

Suggested change
data = append(data, output.NewDataRow(output.Bold("KEY"), output.Bold("VALUE")))

Copy link
Author

Choose a reason for hiding this comment

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

Not against that change - although it perhaps should only apply to the table format (as it is with project view) ?

Copy link

Choose a reason for hiding this comment

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

Yeah definitely, it would probably be a bit confusing in the basic format

Copy link
Author

Choose a reason for hiding this comment

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

I've made a change in 98078bf but wanted to mention that Ben P mentioned he was going to review the view command output format holistically. See this thread (internal link) for more details.

scopeValues, err := variableShared.ToScopeValues(v, allVars.ScopeValues)
if err != nil {
return err
}
data = addScope(scopeValues.Environments, "Environment scope", data, nil)
data = addScope(scopeValues.Roles, "Role scope", data, nil)
data = addScope(scopeValues.Channels, "Channel scope", data, nil)
data = addScope(scopeValues.Machines, "Machine scope", data, nil)
data = addScope(scopeValues.TenantTags, "Tenant tag scope", data, func(item *resources.ReferenceDataItem) string {
return item.ID
})
data = addScope(scopeValues.Actions, "Step scope", data, nil)
data = addScope(
util.SliceTransform(scopeValues.Processes, func(item *resources.ProcessReferenceDataItem) *resources.ReferenceDataItem {
return &resources.ReferenceDataItem{
ID: item.ID,
Name: item.Name,
}
}),
"Process scope",
data,
nil)

if v.Prompt != nil {
data = append(data, output.NewDataRow("Prompted", "true"))
data = append(data, output.NewDataRow("Prompt Label", v.Prompt.Label))
data = append(data, output.NewDataRow("Prompt Description", output.Dim(v.Prompt.Description)))
data = append(data, output.NewDataRow("Prompt Required", strconv.FormatBool(v.Prompt.IsRequired)))
data = append(data, output.NewDataRow("Id", output.Dim(v.GetID())))
if v.IsSensitive {
data = append(data, output.NewDataRow("Value", output.Bold("*** (sensitive)")))
} else {
data = append(data, output.NewDataRow("Value", output.Bold(v.Value)))
}

if v.Description == "" {
v.Description = constants.NoDescription
}
data = append(data, output.NewDataRow("Description", output.Dim(v.Description)))

scopeValues, err := variableShared.ToScopeValues(v, allVars.ScopeValues)
if err != nil {
return err
}

data = addScope(scopeValues.Environments, "Environment scope", data, nil)
data = addScope(scopeValues.Roles, "Role scope", data, nil)
data = addScope(scopeValues.Channels, "Channel scope", data, nil)
data = addScope(scopeValues.Machines, "Machine scope", data, nil)
data = addScope(scopeValues.TenantTags, "Tenant tag scope", data, func(item *resources.ReferenceDataItem) string {
return item.ID
})
data = addScope(scopeValues.Actions, "Step scope", data, nil)
data = addScope(
util.SliceTransform(scopeValues.Processes, func(item *resources.ProcessReferenceDataItem) *resources.ReferenceDataItem {
return &resources.ReferenceDataItem{
ID: item.ID,
Name: item.Name,
}
}),
"Process scope",
data,
nil)

if v.Prompt != nil {
data = append(data, output.NewDataRow("Prompted", "true"))
data = append(data, output.NewDataRow("Prompt Label", v.Prompt.Label))
data = append(data, output.NewDataRow("Prompt Description", output.Dim(v.Prompt.Description)))
data = append(data, output.NewDataRow("Prompt Required", strconv.FormatBool(v.Prompt.IsRequired)))
}

fmt.Fprintln(out)
output.PrintRows(data, out)
}
case constants.OutputFormatJson:

viewItems := make([]VariableViewData, 0, len(filteredVars))
for _, v := range filteredVars {

scopeValues, err := variableShared.ToScopeValues(v, allVars.ScopeValues)
if err != nil {
return err
}

fmt.Fprintln(opts.out)
output.PrintRows(data, opts.out)
vd := VariableViewData{}
vd.Id = v.ID
if v.IsSensitive {
vd.Value = "*** (sensitive)"
} else {
vd.Value = v.Value
}
vd.Description = v.Description

if util.Any(scopeValues.Environments) {
vd.EnvironmentScope = util.SliceTransform(scopeValues.Environments, func(e *resources.ReferenceDataItem) *VarScopedItem {
return &VarScopedItem{
Id: e.ID,
Name: e.Name,
}
})
}
if util.Any(scopeValues.Roles) {
vd.RoleScope = util.SliceTransform(scopeValues.Roles, func(e *resources.ReferenceDataItem) *VarScopedItem {
return &VarScopedItem{
Id: e.ID,
Name: e.Name,
}
})
}
if util.Any(scopeValues.Machines) {
vd.MachineScope = util.SliceTransform(scopeValues.Machines, func(e *resources.ReferenceDataItem) *VarScopedItem {
return &VarScopedItem{
Id: e.ID,
Name: e.Name,
}
})
}
if util.Any(scopeValues.Processes) {
vd.ProcessScope = util.SliceTransform(scopeValues.Processes, func(e *resources.ProcessReferenceDataItem) *VarProcessScopedItem {
return &VarProcessScopedItem{
VarScopedItem: VarScopedItem{
Id: e.ID,
Name: e.Name,
},
ProcessType: e.ProcessType,
}
})
}
if util.Any(scopeValues.Actions) {
vd.StepScope = util.SliceTransform(scopeValues.Actions, func(e *resources.ReferenceDataItem) *VarScopedItem {
return &VarScopedItem{
Id: e.ID,
Name: e.Name,
}
})
}
if util.Any(scopeValues.Channels) {
vd.ChannelScope = util.SliceTransform(scopeValues.Channels, func(e *resources.ReferenceDataItem) *VarScopedItem {
return &VarScopedItem{
Id: e.ID,
Name: e.Name,
}
})
}
if v.Prompt != nil {
vd.Prompted = true
vd.PromptLabel = v.Prompt.Label
vd.PromptLabelDescription = v.Prompt.Description
vd.PromptRequired = strconv.FormatBool(v.Prompt.IsRequired)
} else {
vd.Prompted = false
}
viewItems = append(viewItems, vd)
}
data, _ := json.MarshalIndent(viewItems, "", " ")
opts.cmd.Println(string(data))
}

return nil
Expand Down
67 changes: 54 additions & 13 deletions pkg/cmd/project/view/view.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package view

import (
"encoding/json"
"fmt"
"strings"

"github.com/OctopusDeploy/cli/pkg/apiclient"
"io"

"github.com/MakeNowJust/heredoc/v2"
"github.com/OctopusDeploy/cli/pkg/constants"
Expand Down Expand Up @@ -34,9 +36,9 @@ func NewViewFlags() *ViewFlags {
type ViewOptions struct {
Client *client.Client
Host string
out io.Writer
idOrName string
flags *ViewFlags
cmd *cobra.Command
}

func NewCmdView(f factory.Factory) *cobra.Command {
Expand All @@ -60,9 +62,9 @@ func NewCmdView(f factory.Factory) *cobra.Command {
opts := &ViewOptions{
client,
f.GetCurrentHost(),
cmd.OutOrStdout(),
args[0],
viewFlags,
cmd,
}

return viewRun(opts)
Expand All @@ -81,23 +83,62 @@ func viewRun(opts *ViewOptions) error {
return err
}

fmt.Fprintf(opts.out, "%s %s\n", output.Bold(project.Name), output.Dimf("(%s)", project.Slug))
outputFormat, err := opts.cmd.Flags().GetString(constants.FlagOutputFormat)

if err != nil { // should never happen, but fallback if it does
outputFormat = constants.OutputFormatTable
}
out := opts.cmd.OutOrStdout()

cacBranch := "Not version controlled"
projectVcsBranch := "N/A"
if project.IsVersionControlled {
cacBranch = project.PersistenceSettings.(projects.GitPersistenceSettings).DefaultBranch()
projectVcsBranch = project.PersistenceSettings.(projects.GitPersistenceSettings).DefaultBranch()
}
fmt.Fprintf(opts.out, "Version control branch: %s\n", output.Cyan(cacBranch))
if project.Description == "" {
fmt.Fprintln(opts.out, output.Dim(constants.NoDescription))
} else {
fmt.Fprintln(opts.out, output.Dim(project.Description))

projectDescription := project.Description
if projectDescription == "" {
projectDescription = constants.NoDescription
}

url := opts.Host + project.Links["Web"]

// footer
fmt.Fprintf(opts.out, "View this project in Octopus Deploy: %s\n", output.Blue(url))
type ViewData struct {
Name string `json:"name"`
Slug string `json:"slug"`
Description string `json:"description"`
IsVersionControlled bool `json:"isversioncontrolled"`
Branch string `json:"branch"`
Url string `json:"url"`
}

switch strings.ToLower(outputFormat) {
case constants.OutputFormatBasic:
fmt.Fprintf(out, "Name: %s %s\n", output.Bold(project.Name), output.Dimf("(%s)", project.Slug))
fmt.Fprintf(out, "Description: %s\n", output.Dim(projectDescription))
fmt.Fprintf(out, "Is version controlled: %s\n", output.Cyanf("%t", project.IsVersionControlled))
fmt.Fprintf(out, "Branch: %s\n", output.Cyan(projectVcsBranch))
fmt.Fprintf(out, "View this project in Octopus Deploy: %s\n", output.Blue(url))
case constants.OutputFormatTable:
t := output.NewTable(out)
t.AddRow(output.Bold("KEY"), output.Bold("VALUE"))
t.AddRow("Name", project.Name)
t.AddRow("Slug", project.Slug)
t.AddRow("Description", project.Description)
t.AddRow("IsVersionControlled", fmt.Sprintf("%t", project.IsVersionControlled))
t.AddRow("Branch", projectVcsBranch)
t.AddRow("Url", fmt.Sprintf("%s", output.Blue(url)))
t.Print()
case constants.OutputFormatJson:
viewData := &ViewData{}
viewData.Name = project.Name
viewData.Slug = project.Slug
viewData.Description = project.Description
viewData.IsVersionControlled = project.IsVersionControlled
viewData.Branch = projectVcsBranch
viewData.Url = url
data, _ := json.MarshalIndent(viewData, "", " ")
opts.cmd.Println(string(data))
}

if opts.flags.Web.Value {
browser.OpenURL(url)
Expand Down
Loading