Skip to content

Commit 0fc0cdd

Browse files
feat: Add ephemeral environment create command (#559)
* Add command to create an ephemeral environment * Add tests of the new command
1 parent e044a1d commit 0fc0cdd

6 files changed

Lines changed: 259 additions & 3 deletions

File tree

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ require (
66
github.com/AlecAivazis/survey/v2 v2.3.7
77
github.com/MakeNowJust/heredoc/v2 v2.0.1
88
github.com/OctopusDeploy/go-octodiff v1.0.0
9-
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.84.2
9+
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.86.0
1010
github.com/bmatcuk/doublestar/v4 v4.4.0
1111
github.com/briandowns/spinner v1.19.0
1212
github.com/google/uuid v1.3.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63n
4646
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
4747
github.com/OctopusDeploy/go-octodiff v1.0.0 h1:U+ORg6azniwwYo+O44giOw6TiD5USk8S4VDhOQ0Ven0=
4848
github.com/OctopusDeploy/go-octodiff v1.0.0/go.mod h1:Mze0+EkOWTgTmi8++fyUc6r0aLZT7qD9gX+31t8MmIU=
49-
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.84.2 h1:dBTkP2Uxxn/gGOC0i0RPQzyJOqmL1Sv/ntja2u6Jhwo=
50-
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.84.2/go.mod h1:VkTXDoIPbwGFi5+goo1VSwFNdMVo784cVtJdKIEvfus=
49+
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.86.0 h1:yGoohDFkruQ13mxoK21J+U+IUkxoU5jSxGEpknW/E5Y=
50+
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.86.0/go.mod h1:VkTXDoIPbwGFi5+goo1VSwFNdMVo784cVtJdKIEvfus=
5151
github.com/bmatcuk/doublestar/v4 v4.4.0 h1:LmAwNwhjEbYtyVLzjcP/XeVw4nhuScHGkF/XWXnvIic=
5252
github.com/bmatcuk/doublestar/v4 v4.4.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
5353
github.com/briandowns/spinner v1.19.0 h1:s8aq38H+Qju89yhp89b4iIiMzMm8YN3p6vGpwyh/a8E=
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package create
2+
3+
import (
4+
"fmt"
5+
"slices"
6+
7+
"github.com/MakeNowJust/heredoc/v2"
8+
"github.com/OctopusDeploy/cli/pkg/cmd"
9+
"github.com/OctopusDeploy/cli/pkg/cmd/runbook/shared"
10+
"github.com/OctopusDeploy/cli/pkg/constants"
11+
"github.com/OctopusDeploy/cli/pkg/factory"
12+
"github.com/OctopusDeploy/cli/pkg/output"
13+
"github.com/OctopusDeploy/cli/pkg/question"
14+
"github.com/OctopusDeploy/cli/pkg/question/selectors"
15+
"github.com/OctopusDeploy/cli/pkg/util/flag"
16+
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/channels"
17+
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/environments/v2/ephemeralenvironments"
18+
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/projects"
19+
"github.com/spf13/cobra"
20+
)
21+
22+
const (
23+
FlagName = "name"
24+
FlagProject = "project"
25+
)
26+
27+
type CreateFlags struct {
28+
Name *flag.Flag[string]
29+
Project *flag.Flag[string]
30+
}
31+
32+
func NewCreateFlags() *CreateFlags {
33+
return &CreateFlags{
34+
Name: flag.New[string](FlagName, false),
35+
Project: flag.New[string](FlagProject, false),
36+
}
37+
}
38+
39+
type CreateOptions struct {
40+
*CreateFlags
41+
*cmd.Dependencies
42+
GetConfiguredProjectsCallback func() ([]*projects.Project, error)
43+
}
44+
45+
func NewCreateOptions(createFlags *CreateFlags, dependencies *cmd.Dependencies) *CreateOptions {
46+
return &CreateOptions{
47+
CreateFlags: createFlags,
48+
Dependencies: dependencies,
49+
GetConfiguredProjectsCallback: func() ([]*projects.Project, error) {
50+
return getConfiguredProjects(dependencies)
51+
},
52+
}
53+
}
54+
55+
func getConfiguredProjects(dependencies *cmd.Dependencies) ([]*projects.Project, error) {
56+
allProjects, err := shared.GetAllProjects(dependencies.Client)
57+
if err != nil {
58+
return nil, err
59+
}
60+
61+
var filteredProjects []*projects.Project
62+
63+
for _, project := range allProjects {
64+
projectChannels, err := dependencies.Client.Projects.GetChannels(project)
65+
if err != nil {
66+
return nil, fmt.Errorf("failed to get channels for project '%s': %w", project.GetName(), err)
67+
}
68+
69+
if slices.ContainsFunc(projectChannels, func(channel *channels.Channel) bool {
70+
return channel.Type == "EphemeralEnvironment"
71+
}) {
72+
filteredProjects = append(filteredProjects, project)
73+
}
74+
}
75+
76+
if len(filteredProjects) == 0 {
77+
return nil, fmt.Errorf("no configured projects - configure a project with an ephemeral environment channel before creating an ephemeral environment")
78+
}
79+
80+
return filteredProjects, nil
81+
}
82+
83+
func NewCmdCreate(f factory.Factory) *cobra.Command {
84+
createFlags := NewCreateFlags()
85+
86+
cmd := &cobra.Command{
87+
Use: "create",
88+
Short: "Create an ephemeral environment",
89+
Long: "Create an ephemeral environment in Octopus Deploy",
90+
Example: heredoc.Docf("$ %s ephemeral-environment create", constants.ExecutableName),
91+
Aliases: []string{"new"},
92+
RunE: func(c *cobra.Command, _ []string) error {
93+
opts := NewCreateOptions(createFlags, cmd.NewDependencies(f, c))
94+
95+
return createRun(opts)
96+
},
97+
}
98+
99+
flags := cmd.Flags()
100+
flags.StringVarP(&createFlags.Name.Value, createFlags.Name.Name, "n", "", "Name of the environment")
101+
flags.StringVarP(&createFlags.Project.Value, createFlags.Project.Name, "p", "", "Name of the project")
102+
103+
return cmd
104+
}
105+
106+
func createRun(opts *CreateOptions) error {
107+
108+
if !opts.NoPrompt {
109+
err := PromptMissing(opts)
110+
if err != nil {
111+
return err
112+
}
113+
}
114+
115+
projectResource, err := projects.GetByName(opts.Client, opts.Space.ID, opts.Project.Value)
116+
if err != nil {
117+
return fmt.Errorf("failed to find project '%s': %w", opts.Project.Value, err)
118+
}
119+
projectId := projectResource.GetID()
120+
121+
createEnv, err := ephemeralenvironments.Add(opts.Client, opts.Space.ID, projectId, opts.Name.Value)
122+
if err != nil {
123+
return err
124+
}
125+
126+
_, err = fmt.Fprintf(opts.Out, "\nSuccessfully created ephemeral environment '%s' with id '%s'.\n", opts.Name.Value, createEnv.Id)
127+
if err != nil {
128+
return err
129+
}
130+
131+
link := output.Bluef("%s/app#/%s/projects/%s/ephemeral-environments", opts.Host, opts.Space.GetID(), projectId)
132+
fmt.Fprintf(opts.Out, "View this ephemeral environment for project `%s` on Octopus Deploy: %s\n", opts.Project.Value, link)
133+
134+
if !opts.NoPrompt {
135+
autoCmd := flag.GenerateAutomationCmd(opts.CmdPath, opts.Name, opts.Project)
136+
fmt.Fprintf(opts.Out, "%s\n", autoCmd)
137+
}
138+
139+
return nil
140+
}
141+
142+
func PromptMissing(opts *CreateOptions) error {
143+
144+
err := question.AskName(opts.Ask, "", "ephemeral environment", &opts.Name.Value)
145+
if err != nil {
146+
return err
147+
}
148+
149+
if opts.Project.Value == "" {
150+
fmt.Fprintf(opts.Out, " Choose from projects configured with an ephemeral environment channel.\n")
151+
project, err := selectors.Select(opts.Ask, "Select a project:", opts.GetConfiguredProjectsCallback, func(project *projects.Project) string { return project.GetName() })
152+
if err != nil {
153+
return err
154+
}
155+
opts.Project.Value = project.GetName()
156+
}
157+
158+
return nil
159+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package create_test
2+
3+
import (
4+
"bytes"
5+
"testing"
6+
7+
"github.com/OctopusDeploy/cli/pkg/cmd"
8+
"github.com/OctopusDeploy/cli/pkg/cmd/ephemeralenvironment/create"
9+
"github.com/OctopusDeploy/cli/test/fixtures"
10+
"github.com/OctopusDeploy/cli/test/testutil"
11+
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/projects"
12+
"github.com/stretchr/testify/assert"
13+
)
14+
15+
func TestPromptMissing_AllOptionsSupplied(t *testing.T) {
16+
17+
project1 := fixtures.NewProject("Spaces-1", "Projects-1", "Test1", "Lifecycles-1", "ProjectGroups-1", "DeploymentProcesses-1")
18+
project2 := fixtures.NewProject("Spaces-1", "Projects-2", "Test2", "Lifecycles-1", "ProjectGroups-1", "DeploymentProcesses-2")
19+
20+
pa := []*testutil.PA{}
21+
22+
asker, checkRemainingPrompts := testutil.NewMockAsker(t, pa)
23+
24+
flags := create.NewCreateFlags()
25+
flags.Name.Value = "Hello Ephemeral Environment"
26+
flags.Project.Value = "Hello Project"
27+
28+
opts := &create.CreateOptions{
29+
CreateFlags: flags,
30+
Dependencies: &cmd.Dependencies{Ask: asker},
31+
}
32+
opts.GetConfiguredProjectsCallback = func() ([]*projects.Project, error) {
33+
return []*projects.Project{project1, project2}, nil
34+
}
35+
36+
// Check that no unexpected prompts were triggered
37+
create.PromptMissing(opts)
38+
checkRemainingPrompts()
39+
}
40+
41+
func TestPromptMissing_NoOptionsSupplied(t *testing.T) {
42+
project1 := fixtures.NewProject("Spaces-1", "Projects-1", "Hello Project 1", "Lifecycles-1", "ProjectGroups-1", "DeploymentProcesses-1")
43+
project2 := fixtures.NewProject("Spaces-1", "Projects-2", "Hello Project 2", "Lifecycles-1", "ProjectGroups-1", "DeploymentProcesses-2")
44+
45+
pa := []*testutil.PA{
46+
testutil.NewInputPrompt("Name", "A short, memorable, unique name for this ephemeral environment.", "Hello Ephemeral Environment"),
47+
testutil.NewSelectPrompt("Select a project:", "", []string{project1.Name, project2.Name}, project1.Name),
48+
}
49+
50+
asker, checkRemainingPrompts := testutil.NewMockAsker(t, pa)
51+
52+
flags := create.NewCreateFlags()
53+
54+
opts := &create.CreateOptions{
55+
CreateFlags: flags,
56+
Dependencies: &cmd.Dependencies{Ask: asker, Out: &bytes.Buffer{}},
57+
GetConfiguredProjectsCallback: func() ([]*projects.Project, error) {
58+
return []*projects.Project{project1, project2}, nil
59+
},
60+
}
61+
62+
create.PromptMissing(opts)
63+
64+
// Check that all expected prompts were called
65+
checkRemainingPrompts()
66+
assert.Equal(t, "Hello Ephemeral Environment", flags.Name.Value)
67+
assert.Equal(t, project1.Name, flags.Project.Value)
68+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package ephemeralenvironment
2+
3+
import (
4+
"github.com/MakeNowJust/heredoc/v2"
5+
cmdCreate "github.com/OctopusDeploy/cli/pkg/cmd/ephemeralenvironment/create"
6+
"github.com/OctopusDeploy/cli/pkg/constants"
7+
"github.com/OctopusDeploy/cli/pkg/constants/annotations"
8+
"github.com/OctopusDeploy/cli/pkg/factory"
9+
"github.com/spf13/cobra"
10+
)
11+
12+
func NewCmdEphemeralEnvironment(f factory.Factory) *cobra.Command {
13+
cmd := &cobra.Command{
14+
Use: "ephemeral-environment <command>",
15+
Short: "Manage ephemeral environments",
16+
Long: "Manage ephemeral environments in Octopus Deploy",
17+
Example: heredoc.Docf(`
18+
$ %[1]s ephemeral-environment create --name "MyEphemeralEnvironment" --project "MyProject"
19+
`, constants.ExecutableName),
20+
Annotations: map[string]string{
21+
annotations.IsInfrastructure: "true",
22+
},
23+
}
24+
25+
cmd.AddCommand(cmdCreate.NewCmdCreate(f))
26+
return cmd
27+
}

pkg/cmd/root/root.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
channelCmd "github.com/OctopusDeploy/cli/pkg/cmd/channel"
88
configCmd "github.com/OctopusDeploy/cli/pkg/cmd/config"
99
environmentCmd "github.com/OctopusDeploy/cli/pkg/cmd/environment"
10+
ephemeralEnvironmentCmd "github.com/OctopusDeploy/cli/pkg/cmd/ephemeralenvironment"
1011
loginCmd "github.com/OctopusDeploy/cli/pkg/cmd/login"
1112
logoutCmd "github.com/OctopusDeploy/cli/pkg/cmd/logout"
1213
packageCmd "github.com/OctopusDeploy/cli/pkg/cmd/package"
@@ -51,6 +52,7 @@ func NewCmdRoot(f factory.Factory, clientFactory apiclient.ClientFactory, askPro
5152
// infrastructure
5253
cmd.AddCommand(accountCmd.NewCmdAccount(f))
5354
cmd.AddCommand(environmentCmd.NewCmdEnvironment(f))
55+
cmd.AddCommand(ephemeralEnvironmentCmd.NewCmdEphemeralEnvironment(f))
5456
cmd.AddCommand(packageCmd.NewCmdPackage(f))
5557
cmd.AddCommand(buildInfoCmd.NewCmdBuildInformation(f))
5658
cmd.AddCommand(deploymentTargetCmd.NewCmdDeploymentTarget(f))

0 commit comments

Comments
 (0)