Skip to content

Commit 0189b18

Browse files
t-kikucffjlabodependabot[bot]
authored
Create pipectl init for ECS (#4741)
* Add pipectl init command but not implemented Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> * Modify init command Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> * Rename init command to initialize Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> * Create pipectl int for ECS Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> * Add init for ECS Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> * Convert json.RawMessage to struct Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> * Use original structs of ECS for pipectl init Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> * Init k8s app for pipectl init, but not implemented Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> * Fix init input(wip) Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> * Add mock prompt reader Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> * Reractor prompt reader Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> * Modify mock reader to public Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> * Refactored prompt reader Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> * Add yaml snapshot test Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> * Revert "Init k8s app for pipectl init, but not implemented" This reverts commit 4097fb7. Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> * Refactor pipectl init Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> * Remove go-yaml from go.mod Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> * Modify tests to Parallel Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> * Fix reference of loop variable in tests Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> * Enable interruption signal for pipectl init Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> * Add LoadBalancerName to ECS TargetGroup Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> * Use Reader in tests Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> * Remove stdinReader Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> * Remove mockReader Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> * Add Prompt and PromptInput types Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> * Remove promptReader Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> * Refactored init,exporter Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> * Update Copyright Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> * Refactored nits Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> * Add reference to the blog that shows how to install control plane on ECS (#4746) Signed-off-by: Yoshiki Fujikane <ffjlabo@gmail.com> Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> * Bump follow-redirects from 1.15.2 to 1.15.4 in /web (#4747) Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> * Add RC Release Procedure (#4749) Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> * Fix nits Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> * Fix error variable name Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> --------- Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> Signed-off-by: Yoshiki Fujikane <ffjlabo@gmail.com> Co-authored-by: Yoshiki Fujikane <40124947+ffjlabo@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 parent 522a13b commit 0189b18

10 files changed

Lines changed: 879 additions & 17 deletions

File tree

cmd/pipectl/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/pipe-cd/pipecd/pkg/app/pipectl/cmd/deployment"
2222
"github.com/pipe-cd/pipecd/pkg/app/pipectl/cmd/encrypt"
2323
"github.com/pipe-cd/pipecd/pkg/app/pipectl/cmd/event"
24+
"github.com/pipe-cd/pipecd/pkg/app/pipectl/cmd/initialize"
2425
"github.com/pipe-cd/pipecd/pkg/app/pipectl/cmd/piped"
2526
"github.com/pipe-cd/pipecd/pkg/app/pipectl/cmd/planpreview"
2627
"github.com/pipe-cd/pipecd/pkg/app/pipectl/cmd/quickstart"
@@ -41,6 +42,7 @@ func main() {
4142
piped.NewCommand(),
4243
encrypt.NewCommand(),
4344
quickstart.NewCommand(),
45+
initialize.NewCommand(),
4446
)
4547

4648
if err := app.Run(); err != nil {
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// Copyright 2024 The PipeCD Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package initialize
16+
17+
import (
18+
"github.com/pipe-cd/pipecd/pkg/app/pipectl/cmd/initialize/prompt"
19+
"github.com/pipe-cd/pipecd/pkg/config"
20+
)
21+
22+
// Use genericConfigs in order to simplify using the GenericApplicationSpec and keep the order as we want.
23+
type genericECSApplicationSpec struct {
24+
Name string `json:"name"`
25+
Input config.ECSDeploymentInput `json:"input"`
26+
Description string `json:"description,omitempty"`
27+
}
28+
29+
func generateECSConfig(p prompt.Prompt) (*genericConfig, error) {
30+
// inputs
31+
var (
32+
appName string
33+
serviceDefFile string
34+
taskDefFile string
35+
targetGroupArn string
36+
containerName string
37+
containerPort int
38+
)
39+
inputs := []prompt.Input{
40+
{
41+
Message: "Name of the application",
42+
TargetPointer: &appName,
43+
Required: true,
44+
},
45+
{
46+
Message: "Name of the service definition file (e.g. serviceDef.yaml)",
47+
TargetPointer: &serviceDefFile,
48+
Required: true,
49+
},
50+
{
51+
Message: "Name of the task definition file (e.g. taskDef.yaml)",
52+
TargetPointer: &taskDefFile,
53+
Required: true,
54+
},
55+
// target group inputs
56+
{
57+
Message: "ARN of the target group to the service",
58+
TargetPointer: &targetGroupArn,
59+
Required: false,
60+
},
61+
{
62+
Message: "Name of the container of the target group",
63+
TargetPointer: &containerName,
64+
Required: false,
65+
},
66+
{
67+
Message: "Port number of the container of the target group",
68+
TargetPointer: &containerPort,
69+
Required: false,
70+
},
71+
}
72+
73+
err := p.RunSlice(inputs)
74+
if err != nil {
75+
return nil, err
76+
}
77+
78+
spec := &genericECSApplicationSpec{
79+
Name: appName,
80+
Input: config.ECSDeploymentInput{
81+
ServiceDefinitionFile: serviceDefFile,
82+
TaskDefinitionFile: taskDefFile,
83+
TargetGroups: config.ECSTargetGroups{
84+
Primary: &config.ECSTargetGroup{
85+
TargetGroupArn: targetGroupArn,
86+
ContainerName: containerName,
87+
ContainerPort: containerPort,
88+
},
89+
},
90+
},
91+
Description: "Generated by `pipectl init`. See https://pipecd.dev/docs/user-guide/configuration-reference/ for more.",
92+
}
93+
94+
return &genericConfig{
95+
Kind: config.KindECSApp,
96+
APIVersion: config.VersionV1Beta1,
97+
ApplicationSpec: spec,
98+
}, nil
99+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright 2024 The PipeCD Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package initialize
16+
17+
import (
18+
"os"
19+
"strings"
20+
"testing"
21+
22+
"github.com/goccy/go-yaml"
23+
"github.com/stretchr/testify/assert"
24+
25+
"github.com/pipe-cd/pipecd/pkg/app/pipectl/cmd/initialize/prompt"
26+
"github.com/pipe-cd/pipecd/pkg/config"
27+
)
28+
29+
func TestGenerateECSConfig(t *testing.T) {
30+
t.Parallel()
31+
32+
testcases := []struct {
33+
name string
34+
inputs string // mock for user's input
35+
expectedFile string
36+
expectedErr bool
37+
}{
38+
{
39+
name: "valid inputs",
40+
inputs: `myApp
41+
serviceDef.yaml
42+
taskDef.yaml
43+
arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:targetgroup/xxx/xxx
44+
web
45+
80
46+
`,
47+
expectedFile: "testdata/ecs-app.yaml",
48+
expectedErr: false,
49+
},
50+
{
51+
name: "missing required",
52+
inputs: `myApp
53+
serviceDef.yaml
54+
`,
55+
expectedFile: "",
56+
expectedErr: true,
57+
},
58+
}
59+
60+
for _, tc := range testcases {
61+
tc := tc
62+
t.Run(tc.name, func(t *testing.T) {
63+
t.Parallel()
64+
reader := strings.NewReader(tc.inputs)
65+
prompt := prompt.NewPrompt(reader)
66+
67+
// Generate the config
68+
cfg, err := generateECSConfig(prompt)
69+
assert.Equal(t, tc.expectedErr, err != nil)
70+
71+
if err == nil {
72+
// Compare the YAML output
73+
yml, err := yaml.Marshal(cfg)
74+
assert.NoError(t, err)
75+
file, err := os.ReadFile(tc.expectedFile)
76+
assert.NoError(t, err)
77+
assert.Equal(t, string(file), string(yml))
78+
79+
// Check if the YAML output is compatible with the original Config model
80+
_, err = config.DecodeYAML(yml)
81+
assert.NoError(t, err)
82+
}
83+
})
84+
}
85+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package exporter
2+
3+
import (
4+
"fmt"
5+
"os"
6+
7+
"github.com/pipe-cd/pipecd/pkg/app/pipectl/cmd/initialize/prompt"
8+
)
9+
10+
// Export the bytes to the path.
11+
// If the path is empty or a directory, return an error.
12+
// If the file already exists, ask if overwrite it.
13+
func Export(bytes []byte, path string) error {
14+
if len(path) == 0 {
15+
return fmt.Errorf("path is not specified. Please specify a file path")
16+
}
17+
18+
// Check if the file/directory already exists
19+
if fInfo, err := os.Stat(path); err == nil {
20+
if fInfo.IsDir() {
21+
// When the target is a directory.
22+
return fmt.Errorf("the path %s is a directory. Please specify a file path", path)
23+
}
24+
25+
// When the file exists, ask if overwrite it.
26+
overwrite, err := askOverwrite()
27+
if err != nil {
28+
return fmt.Errorf("invalid input for overwrite(y/n): %v", err)
29+
}
30+
31+
if !overwrite {
32+
return fmt.Errorf("cancelled exporting")
33+
}
34+
}
35+
36+
fmt.Printf("Start exporting to %s\n", path)
37+
err := os.WriteFile(path, bytes, 0644)
38+
if err != nil {
39+
return fmt.Errorf("failed to export to %s: %v", path, err)
40+
} else {
41+
fmt.Printf("Successfully exported to %s\n", path)
42+
}
43+
return nil
44+
}
45+
46+
func askOverwrite() (overwrite bool, err error) {
47+
overwriteInput := prompt.Input{
48+
Message: "The file already exists. Overwrite it? [y/n]",
49+
TargetPointer: &overwrite,
50+
Required: false,
51+
}
52+
p := prompt.NewPrompt(os.Stdin)
53+
err = p.Run(overwriteInput)
54+
if err != nil {
55+
return false, err
56+
}
57+
return overwrite, nil
58+
}

0 commit comments

Comments
 (0)