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

Create pipectl init for ECS #4741

Merged
merged 36 commits into from
Jan 19, 2024
Merged

Create pipectl init for ECS #4741

merged 36 commits into from
Jan 19, 2024

Conversation

t-kikuc
Copy link
Member

@t-kikuc t-kikuc commented Dec 26, 2023

What this PR does / why we need it:

Create pipectl init command for ECS's simplest use case.
I'll add Kubernetes next.

pipectl-init-ecs-1.mov

(x1.5 pace)

NOTE: This PR depends on #4737

Which issue(s) this PR fixes:

Part of #4677

Does this PR introduce a user-facing change?: no

  • How are users affected by this change: no
  • Is this breaking change: no
  • How to migrate (if breaking change): no

@t-kikuc
Copy link
Member Author

t-kikuc commented Dec 26, 2023

I'll clean unnecessary commits

Copy link

codecov bot commented Dec 26, 2023

Codecov Report

Attention: 104 lines in your changes are missing coverage. Please review.

Comparison is base (522a13b) 30.87% compared to head (2a4339a) 31.03%.

Files Patch % Lines
pkg/app/pipectl/cmd/initialize/initialize.go 0.00% 92 Missing ⚠️
pkg/app/pipectl/cmd/initialize/prompt/prompt.go 81.35% 10 Missing and 1 partial ⚠️
pkg/config/config.go 0.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #4741      +/-   ##
==========================================
+ Coverage   30.87%   31.03%   +0.15%     
==========================================
  Files         222      225       +3     
  Lines       26037    26257     +220     
==========================================
+ Hits         8039     8148     +109     
- Misses      17349    17458     +109     
- Partials      649      651       +2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>
Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>
Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>
Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>
Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>
Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>
Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>
@t-kikuc
Copy link
Member Author

t-kikuc commented Dec 26, 2023

I'll clean unnecessary commits

Done

@t-kikuc
Copy link
Member Author

t-kikuc commented Dec 26, 2023

/review

Copy link

PR Analysis

Main theme

Enhancement

PR summary

This PR introduces initialization commands to assist users in generating application configuration files for ECS applications in an interactive manner.

Type of PR

Enhancement

PR Feedback:

General suggestions

  • The new code implements functionality that helps users generate application configurations through an interactive command-line interface. This is a user-friendly feature, which assists in setting up application configurations without needing in-depth knowledge about the required fields or the structure of the configuration itself.
  • It is crucial to consider input validation and error handling to ensure that the tool provides meaningful feedback for incorrect inputs and to prevent any potential issues from misconfiguration.
  • Refactoring some duplicated logic into reusable functions could improve maintainability and testability.

Code feedback

  • relevant file: pkg/app/pipectl/cmd/initialize/ecs.go
    suggestion: |-
    It would be beneficial to encapsulate the user input prompts into separate functions to promote code reuse and improve testability. For example, prompting and reading the service definition file can be refactored into a reusable function. Since this is a non-critical enhancement, it can be treated as a 'medium' priority.

    func promptServiceDefinitionFile(in io.Reader) string {
      return promptStringRequired("Name of the service definition file (e.g. serviceDef.yaml): ", in)
    }

    relevant line: + serviceDefFile := promptStringRequired("Name of the service definition file (e.g. serviceDef.yaml): ", in)

  • relevant file: pkg/app/piped/platformprovider/ecs/target_groups.go
    suggestion: |-
    Consider adding more comprehensive validation of the TargetGroupArn, ContainerName, and ContainerPort values to ensure that they meet AWS's requirements. This could help in catching misconfigurations early in the setup process, rather than at runtime. Tagging with 'important' due to the potential impact on deployment reliability.

    if err := validateTargetGroup(primary); err != nil {
      return nil, nil, err
    }

    relevant line: + primary := &types.LoadBalancer{

Security concerns:

No

The changes seem to focus on configuration generation and input handling, which does not introduce security concerns such as SQL injection, XSS, CSRF, etc.

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>
Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>
Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>
Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>
Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>
Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>
Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>
Copy link
Member

@ffjlabo ffjlabo left a comment

Choose a reason for hiding this comment

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

Thank you for your great contribution!
I left a comment at first 👍

Copy link
Member

@ffjlabo ffjlabo left a comment

Choose a reason for hiding this comment

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

oops... I forgot to add review comment.
re: comment🙏

Comment on lines 30 to 44
// Reader is an interface that reads input from user or mock.
type Reader interface {
// ReadString reads a string with showing message. If the input is empty, it returns an empty string.
// If the input is more than one word, it returns an error.
ReadString(message string) (string, error)
// ReadStrings reads a string slice separated by blank space with showing message. If the input is empty, it returns an empty slice.
ReadStrings(message string) ([]string, error)
// ReadInt reads an integer with showing message. If the input is empty, it returns 0.
// If the input is not an integer, it returns an error.
ReadInt(message string) (int, error)
// ReadStringRequired reads a string with showing message. If the input is empty, it asks again.
// If number of retry exceeds maximum_retry, it returns an error.
// If the input is more than one word, it returns an error.
ReadStringRequired(message string) (string, error)
}
Copy link
Member

Choose a reason for hiding this comment

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

[ask] What's the purpose of the Interface?
I think reading data from stdin is a concrete usecase, so it might not be necessary abstraction 👀
WDYT?

Copy link
Member Author

Choose a reason for hiding this comment

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

The purpose is to use mock in initialize/ecs_test.go.
In tests, stdin is difficult to use but we need to pass values instead of users.
So in tests I use mockReader(in prompt/reader_mock.go), which implements Reader.

Copy link
Member

@ffjlabo ffjlabo Jan 8, 2024

Choose a reason for hiding this comment

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

Thx, I got it!
So you can create prompt.NewReader(input io.Reader) instead of NewStdinReader and mock the input 👍
You can create the implementation of io.Reader from a string using strings.NewReader https://pkg.go.dev/strings#NewReader

Like this ↓

// default
r := prompt.NewReader(os.Stdin)

// when test
mockStr := `foo
foo bar
`
mockStdin := strings.NewReader(mockStr)
r := prompt.NewReader(mockStdin)

Copy link
Member Author

Choose a reason for hiding this comment

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

@ffjlabo
great, amazing, thank you so much!
It worked well, and I could remove the Reader interface!
strings.NewReader is what i wanted.

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>
Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>
Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>
Copy link
Member

@ffjlabo ffjlabo left a comment

Choose a reason for hiding this comment

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

Thank you for the fixes!
Added some more comments 🙏

// Automatically reverts all changes from all stages when one of them failed.
// Default is true.
AutoRollback *bool `json:"autoRollback,omitempty" default:"true"`
// Run standalone task during deployment.
// Default is true.
RunStandaloneTask *bool `json:"runStandaloneTask" default:"true"`
RunStandaloneTask *bool `json:"runStandaloneTask,omitempty" default:"true"`
Copy link
Member

Choose a reason for hiding this comment

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

[must] look back whether to check the reference of the field is nil before using it for avoiding nil pointer exception.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks, I confirmed there's no preblem.
omitempty affects only encoding to yaml/json, not decoding from yaml/json.
And when encoding,

  • If RunStandaloneTask is not defined in genericECSApplicationSpec.Input, runStandaloneTask won't appear in the pipectl init output.
  • If RunStandaloneTask has null value, runStandaloneTask: false will appear in the pipectl init output.

@@ -0,0 +1,90 @@
// Copyright 2023 The PipeCD Authors.
Copy link
Member

Choose a reason for hiding this comment

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

[must] plz update the copyright to the one of 2024

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks, I updated all

Comment on lines 50 to 51
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
Copy link
Member

Choose a reason for hiding this comment

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

[nit] add tc := tc

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks, I fixed with adding T.Parallel()

Comment on lines 88 to 93
switch platform {
case "0": // Kubernetes
// cfg, err = generateKubernetesConfig(in)
panic("not implemented")
case "1": // ECS
cfg, err = generateECSConfig(reader)
Copy link
Member

Choose a reason for hiding this comment

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

[imo] Define identifier for the platform number as the const value for the readability of the code👍

const (
	PlatformKubernetes string = "0"
	PlatformECS        string = "1"
)

switch platform {
case PlatformKubernetes:
case PlatformECS:
}

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks, that's better

Comment on lines 88 to 99
func (r *Reader) ReadStringRequired(message string) (string, error) {
for i := 0; i < maximum_retry; i++ {
s, e := r.ReadString(message)
if e != nil {
return "", e
}
if len(s) > 0 {
return s, nil
}
fmt.Printf("[WARN] This field is required. \n")
}
return "", fmt.Errorf("this field is required. Maximum retry(%d) exceeded", maximum_retry)
Copy link
Member

Choose a reason for hiding this comment

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

[imo] How about adding the argument required bool to each method like ReadStrings and ReadString instead of preparing the method for required input.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks, I created prompt.Input.Required

Comment on lines 32 to 35
spec, e := generateECSSpec(reader)
if e != nil {
return nil, e
}
Copy link
Member

Choose a reason for hiding this comment

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

[imo] It would be nice to define necessary values first, and pass them to generateECSSpec 👀 WDYT?

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks, I refactored

}

func generateECSSpec(reader prompt.Reader) (*genericECSApplicationSpec, error) {
appName, e := reader.ReadStringRequired("Name of the application: ")
Copy link
Member

Choose a reason for hiding this comment

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

[must] Use err as the variable for error instead of e. Go often uses err 👍

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks, I fixed all

Comment on lines 29 to 56
// Reader reads input from user or mock.
type Reader struct {
bufReader bufio.Reader
}

func NewReader(in io.Reader) Reader {
return Reader{
bufReader: *bufio.NewReader(in),
}
}

// ReadString reads a string with showing message. If the input is empty, it returns an empty string.
// If the input is more than one word, it returns an error.
func (r *Reader) ReadString(message string) (string, error) {
s, e := r.ReadStrings(message)
if e != nil {
return "", e
}

if len(s) == 0 {
return "", nil
}
if len(s) > 1 {
return "", fmt.Errorf("too many arguments")
}

return s[0], nil
}
Copy link
Member

Choose a reason for hiding this comment

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

[imo] How about rename like below? WDYT?

  • Reader -> Prompt
  • ReadXX -> InputXX

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks, I renamed

  • Reader -> Prompt
  • ReadXX -> removed and created prompt.Input struct.

Comment on lines 36 to 47
{
name: "valid config for ECSApp",
inputs: `myApp
serviceDef.yaml
taskDef.yaml
arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:targetgroup/xxx/xxx
web
80
`,
expectedFile: "testdata/ecs-app.yaml",
expectedErr: nil,
},
Copy link
Member

Choose a reason for hiding this comment

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

[imo] Add testcase for failed case 👀

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks, I added a failure case.

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>
Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>
Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>
Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>
Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>
@t-kikuc
Copy link
Member Author

t-kikuc commented Jan 11, 2024

@ffjlabo Thank you for great reviewing. I fixed all

Comment on lines 25 to 33
type Prompt struct {
bufReader bufio.Reader
}

type Input struct {
Message string
TargetPointer any
Required bool
}
Copy link
Member

Choose a reason for hiding this comment

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

[nit] Add comments for these structs and fields 👍

Comment on lines 83 to 85
t.Run(tc.name, func(t *testing.T) {
strReader := strings.NewReader(tc.str)
p := NewPrompt(strReader)
Copy link
Member

Choose a reason for hiding this comment

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

[nit] Add t.Parallel here :)

Comment on lines 140 to 142
t.Run(tc.name, func(t *testing.T) {
strReader := strings.NewReader(tc.str)
p := NewPrompt(strReader)
Copy link
Member

Choose a reason for hiding this comment

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

[nit] Add t.Parallel here :)

Same as above.

Comment on lines 219 to 221
t.Run(tc.name, func(t *testing.T) {
strReader := strings.NewReader(tc.str)
p := NewPrompt(strReader)
Copy link
Member

Choose a reason for hiding this comment

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

[nit] Add t.Parallel here :)

Same as above.

Comment on lines 320 to 322
t.Run(tc.name, func(t *testing.T) {
strReader := strings.NewReader(tc.str)
p := NewPrompt(strReader)
Copy link
Member

Choose a reason for hiding this comment

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

[nit] Add t.Parallel here :)

Same as above.

}

// RunSlice sequentially asks for inputs and set the values to the target pointers.
func (prompt *Prompt) RunSlice(inputs []Input) error {
Copy link
Member

Choose a reason for hiding this comment

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

[imo] It should change prompt to p to distinguish package name and receiver name 👍

Comment on lines 37 to 40
const (
platformKubernetes string = "0"
platformECS string = "1"
)
Copy link
Member

Choose a reason for hiding this comment

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

[nit] Add comments :)

@ffjlabo
Copy link
Member

ffjlabo commented Jan 18, 2024

@t-kikuc
[imo] As for package prompt and exporter, just add prompt.go and exporter.go into pkg/app/pipectl/cmd/initialize 👀
This is because these functionalities are only for pipectl init at this time :)

ffjlabo and others added 5 commits January 19, 2024 12:40
…ECS (#4746)

Signed-off-by: Yoshiki Fujikane <ffjlabo@gmail.com>
Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>
Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>
Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>
…tl-init-ecs

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>
Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>
@t-kikuc
Copy link
Member Author

t-kikuc commented Jan 19, 2024

@t-kikuc [imo] As for package prompt and exporter, just add prompt.go and exporter.go into pkg/app/pipectl/cmd/initialize 👀 This is because these functionalities are only for pipectl init at this time :)

I got it.
I'd use them in pipectl migrate command but that is not definitive.
So they should be in pkg/app/pipectl/cmd/initialize for now.

@t-kikuc
Copy link
Member Author

t-kikuc commented Jan 19, 2024

@ffjlabo
Thank you so much, I fixed.

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>
Copy link
Member

@ffjlabo ffjlabo left a comment

Choose a reason for hiding this comment

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

Thank you! 🚀

Copy link
Member

@khanhtc1202 khanhtc1202 left a comment

Choose a reason for hiding this comment

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

Let's go 🚀

@t-kikuc t-kikuc merged commit 0189b18 into master Jan 19, 2024
14 checks passed
@t-kikuc t-kikuc deleted the pipectl-init branch January 19, 2024 06:08
@t-kikuc t-kikuc self-assigned this Jan 19, 2024
@github-actions github-actions bot mentioned this pull request Feb 6, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
No open projects
Status: Done
Development

Successfully merging this pull request may close these issues.

None yet

3 participants