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
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
c24329a
Add pipectl init command but not implemented
t-kikuc Dec 22, 2023
6170b7d
Modify init command
t-kikuc Dec 24, 2023
30f7554
Rename init command to initialize
t-kikuc Dec 24, 2023
419aa6b
Create pipectl int for ECS
t-kikuc Dec 25, 2023
546e2cd
Add init for ECS
t-kikuc Dec 25, 2023
4141bbf
Convert json.RawMessage to struct
t-kikuc Dec 26, 2023
d877787
Use original structs of ECS for pipectl init
t-kikuc Dec 26, 2023
4097fb7
Init k8s app for pipectl init, but not implemented
t-kikuc Dec 26, 2023
e4d71d2
Fix init input(wip)
t-kikuc Dec 27, 2023
608c91e
Add mock prompt reader
t-kikuc Dec 27, 2023
a819e97
Reractor prompt reader
t-kikuc Dec 27, 2023
3e813b7
Modify mock reader to public
t-kikuc Dec 27, 2023
c9ef764
Refactored prompt reader
t-kikuc Dec 27, 2023
6b18136
Add yaml snapshot test
t-kikuc Dec 27, 2023
757b637
Revert "Init k8s app for pipectl init, but not implemented"
t-kikuc Dec 27, 2023
299c464
Refactor pipectl init
t-kikuc Dec 27, 2023
85184b2
Remove go-yaml from go.mod
t-kikuc Dec 27, 2023
0efa6ea
Modify tests to Parallel
t-kikuc Dec 27, 2023
32c41e2
Fix reference of loop variable in tests
t-kikuc Dec 27, 2023
f3f121f
Enable interruption signal for pipectl init
t-kikuc Dec 27, 2023
6c1182f
Add LoadBalancerName to ECS TargetGroup
t-kikuc Dec 28, 2023
9fb9056
Merge branch 'master' into pipectl-init-ecs
t-kikuc Jan 4, 2024
02cd0bf
Use Reader in tests
t-kikuc Jan 9, 2024
cb6dab2
Remove stdinReader
t-kikuc Jan 9, 2024
bd35685
Remove mockReader
t-kikuc Jan 9, 2024
4813398
Add Prompt and PromptInput types
t-kikuc Jan 10, 2024
4800de6
Remove promptReader
t-kikuc Jan 10, 2024
8081826
Refactored init,exporter
t-kikuc Jan 11, 2024
77c8c92
Update Copyright
t-kikuc Jan 11, 2024
76f89d0
Refactored nits
t-kikuc Jan 11, 2024
cf7dfa4
Add reference to the blog that shows how to install control plane on …
ffjlabo Jan 10, 2024
8b95253
Bump follow-redirects from 1.15.2 to 1.15.4 in /web (#4747)
dependabot[bot] Jan 15, 2024
e797d9b
Add RC Release Procedure (#4749)
t-kikuc Jan 15, 2024
2714de4
Merge branch 'master' of https://github.com/pipe-cd/pipecd into pipec…
t-kikuc Jan 19, 2024
85814be
Fix nits
t-kikuc Jan 19, 2024
2a4339a
Fix error variable name
t-kikuc Jan 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/pipectl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/pipe-cd/pipecd/pkg/app/pipectl/cmd/deployment"
"github.com/pipe-cd/pipecd/pkg/app/pipectl/cmd/encrypt"
"github.com/pipe-cd/pipecd/pkg/app/pipectl/cmd/event"
"github.com/pipe-cd/pipecd/pkg/app/pipectl/cmd/initialize"
"github.com/pipe-cd/pipecd/pkg/app/pipectl/cmd/piped"
"github.com/pipe-cd/pipecd/pkg/app/pipectl/cmd/planpreview"
"github.com/pipe-cd/pipecd/pkg/app/pipectl/cmd/quickstart"
Expand All @@ -41,6 +42,7 @@ func main() {
piped.NewCommand(),
encrypt.NewCommand(),
quickstart.NewCommand(),
initialize.NewCommand(),
)

if err := app.Run(); err != nil {
Expand Down
90 changes: 90 additions & 0 deletions pkg/app/pipectl/cmd/initialize/ecs.go
Original file line number Diff line number Diff line change
@@ -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

//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package initialize

import (
"fmt"

"github.com/pipe-cd/pipecd/pkg/app/pipectl/prompt"
"github.com/pipe-cd/pipecd/pkg/config"
)

// Use genericConfigs in order to simplify using the GenericApplicationSpec and keep the order as we want.
type genericECSApplicationSpec struct {
Name string `json:"name"`
Input config.ECSDeploymentInput `json:"input"`
Description string `json:"description,omitempty"`
}

func generateECSConfig(reader prompt.Reader) (*genericConfig, error) {
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


return &genericConfig{
Kind: config.KindECSApp,
APIVersion: config.VersionV1Beta1,
ApplicationSpec: spec,
}, nil
}

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

if e != nil {
return nil, fmt.Errorf("invalid input for application name(string): %v", e)
}
serviceDefFile, e := reader.ReadStringRequired("Name of the service definition file (e.g. serviceDef.yaml): ")
if e != nil {
return nil, fmt.Errorf("invalid input for service definition file(string): %v", e)
}

taskDefFile, e := reader.ReadStringRequired("Name of the task definition file (e.g. taskDef.yaml): ")
if e != nil {
return nil, fmt.Errorf("invalid input for task definition file(string): %v", e)
}

// target groups
targetGroupArn, e := reader.ReadString("ARN of the target group to the service: ")
if e != nil {
return nil, fmt.Errorf("invalid input for targetGroupArn(string): %v", e)
}
containerName, e := reader.ReadString("Name of the container of the target group: ")
if e != nil {
return nil, fmt.Errorf("invalid input for containerName(string): %v", e)
}
containerPort, e := reader.ReadInt("Port of the container of the target group [int]: ")
if e != nil {
return nil, fmt.Errorf("invalid input for containerPort(int): %v", e)
}

cfg := &genericECSApplicationSpec{
Name: appName,
Input: config.ECSDeploymentInput{
ServiceDefinitionFile: serviceDefFile,
TaskDefinitionFile: taskDefFile,
TargetGroups: config.ECSTargetGroups{
Primary: &config.ECSTargetGroup{
TargetGroupArn: targetGroupArn,
ContainerName: containerName,
ContainerPort: containerPort,
},
},
},
Description: "Generated by `pipectl init`. See https://pipecd.dev/docs/user-guide/configuration-reference/ for more.",
}

return cfg, nil
}
73 changes: 73 additions & 0 deletions pkg/app/pipectl/cmd/initialize/ecs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright 2023 The PipeCD Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package initialize

import (
"os"
"strings"
"testing"

"github.com/goccy/go-yaml"
"github.com/stretchr/testify/assert"

"github.com/pipe-cd/pipecd/pkg/app/pipectl/prompt"
"github.com/pipe-cd/pipecd/pkg/config"
)

func TestGenerateECSConfig(t *testing.T) {
testcases := []struct {
name string
inputs string // mock for user's input
expectedFile string
expectedErr error
}{
{
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.

}

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()

// Mock user's input.
strReader := strings.NewReader(tc.inputs)
// reader := prompt.NewMockReader(tc.inputs)
reader := prompt.NewReader(strReader)

// Generate the config.
cfg, err := generateECSConfig(reader)
assert.Equal(t, tc.expectedErr, err)

// Compare the YAML output
yml, err := yaml.Marshal(cfg)
assert.NoError(t, err)
file, err := os.ReadFile(tc.expectedFile)
assert.NoError(t, err)
assert.Equal(t, string(file), string(yml))

// Check if the YAML output is compatible with the original Config model
_, err = config.DecodeYAML(yml)
assert.NoError(t, err)
})
}
}
163 changes: 163 additions & 0 deletions pkg/app/pipectl/cmd/initialize/initialize.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// Copyright 2023 The PipeCD Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package initialize

import (
"context"
"fmt"
"os"
"os/signal"
"syscall"

"github.com/goccy/go-yaml"
"github.com/spf13/cobra"

"github.com/pipe-cd/pipecd/pkg/app/pipectl/prompt"
"github.com/pipe-cd/pipecd/pkg/cli"
"github.com/pipe-cd/pipecd/pkg/config"
)

type command struct {
// Add flags if needed.
}

// Use genericConfigs in order to simplify using the spec.
type genericConfig struct {
APIVersion string `json:"apiVersion"`
Kind config.Kind `json:"kind"`
ApplicationSpec interface{} `json:"spec"`
}

func NewCommand() *cobra.Command {
c := &command{}
cmd := &cobra.Command{
Use: "init",
Short: "Generate a app.pipecd.yaml easily and interactively",
Example: ` pipectl init`,
Long: "Generate a app.pipecd.yaml easily, interactively selecting options.",
RunE: cli.WithContext(c.run),
}

return cmd
}

func (c *command) run(ctx context.Context, input cli.Input) error {
// Enable interrupt signal.
ctx, cancel := context.WithCancel(ctx)
signals := make(chan os.Signal, 1)
signal.Notify(signals, os.Interrupt, syscall.SIGHUP, syscall.SIGTERM)

defer func() {
signal.Stop(signals)
cancel()
}()

go func() {
select {
case s := <-signals:
fmt.Printf(" Interrupted by signal: %v\n", s)
cancel()
os.Exit(1)
case <-ctx.Done():
}
}()

reader := prompt.NewReader(os.Stdin)
return generateConfig(ctx, input, reader)
}

func generateConfig(ctx context.Context, input cli.Input, reader prompt.Reader) error {
platform, err := reader.ReadString("Which platform? Enter the number [0]Kubernetes [1]ECS : ")
if err != nil {
return fmt.Errorf("invalid input: %v`", err)
}

var cfg *genericConfig
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

default:
return fmt.Errorf("invalid platform number: %s", platform)
}

if err != nil {
return err
}

cfgBytes, err := yaml.Marshal(cfg)
if err != nil {
return err
}

fmt.Println("### The config model was successfully prepared. Move on to exporting. ###")
exportConfig(cfgBytes, reader)

return nil
}

func exportConfig(configBytes []byte, reader prompt.Reader) {
path, err := reader.ReadString("Path to save the config (if not specified, it goes to stdout) : ")
if err != nil {
fmt.Printf("Failed to read path %s \n", path)
printConfig(configBytes)
return
}
if len(path) == 0 {
// If the target path is not specified, print to stdout.
printConfig(configBytes)
return
}

// Check if the file/directory already exists and ask if overwrite it.
if fInfo, err := os.Stat(path); err == nil {
if fInfo.IsDir() {
fmt.Printf("The path %s is a directory. Please specify a file path.\n", path)
printConfig(configBytes)
return
}

// If the file exists, ask if overwrite it.
overwrite, err := reader.ReadStringRequired(fmt.Sprintf("The file %s already exists. Overwrite it? [y/n] : ", path))
if err != nil {
fmt.Printf("Invalid input for overwrite(string): %v\n", err)
printConfig(configBytes)
return
}
if overwrite != "y" && overwrite != "Y" {
fmt.Println("Cancelled exporting the config.")
printConfig(configBytes)
return
}
}

// If the file does not exist or overwrite, write to the path, including validating.
fmt.Printf("Start exporting the config to %s\n", path)
err = os.WriteFile(path, configBytes, 0644)
if err != nil {
fmt.Printf("Failed to export the config to %s: %v\n", path, err)
// If failed, print the config to avoid losing it.
printConfig(configBytes)
} else {
fmt.Printf("Successfully exported the config to %s\n", path)
}
}

// Print the config to stdout.
func printConfig(configBytes []byte) {
fmt.Printf("\n### Generated Config is below ###\n%s\n", string(configBytes))
}
13 changes: 13 additions & 0 deletions pkg/app/pipectl/cmd/initialize/testdata/ecs-app.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
apiVersion: pipecd.dev/v1beta1
kind: ECSApp
spec:
name: myApp
input:
serviceDefinitionFile: serviceDef.yaml
taskDefinitionFile: taskDef.yaml
targetGroups:
primary:
targetGroupArn: arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:targetgroup/xxx/xxx
containerName: web
containerPort: 80
description: Generated by `pipectl init`. See https://pipecd.dev/docs/user-guide/configuration-reference/ for more.
Loading