diff --git a/go.mod b/go.mod index a81211a33d..9e59348c7f 100644 --- a/go.mod +++ b/go.mod @@ -112,6 +112,7 @@ require ( github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.5 // indirect github.com/go-openapi/swag v0.19.14 // indirect + github.com/go-yaml/yaml v2.1.0+incompatible github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/gnostic v0.5.7-v3refs // indirect diff --git a/go.sum b/go.sum index 333ba40b69..7688bb3d13 100644 --- a/go.sum +++ b/go.sum @@ -293,6 +293,8 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho= github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o= +github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= github.com/goccy/go-yaml v1.9.8 h1:5gMyLUeU1/6zl+WFfR1hN7D2kf+1/eRGa7DFtToiBvQ= github.com/goccy/go-yaml v1.9.8/go.mod h1:JubOolP3gh0HpiBc4BLRD4YmjEjHAmIIB2aaXKkTfoE= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= diff --git a/pkg/app/pipectl/cmd/initialize/ecs.go b/pkg/app/pipectl/cmd/initialize/ecs.go index a80d23bdd1..e9d421d81e 100644 --- a/pkg/app/pipectl/cmd/initialize/ecs.go +++ b/pkg/app/pipectl/cmd/initialize/ecs.go @@ -22,27 +22,9 @@ import ( ) type genericECSApplicationSpec struct { - Name string `yaml:"name"` - Input genericECSDeploymentInput `yaml:"input"` - Description string `yaml:"description,omitempty"` -} - -// genericECSDeploymentInput represents an generic input for config.ECSDeploymentInput. -type genericECSDeploymentInput struct { - ServiceDefinitionFile string `yaml:"serviceDefinitionFile"` - TaskDefinitionFile string `yaml:"taskDefinitionFile"` - TargetGroups genericECSTargetGroups `yaml:"targetGroups,omitempty"` -} - -type genericECSTargetGroups struct { - Primary genericECSTargetGroup `yaml:"primary,omitempty"` - Canary genericECSTargetGroup `yaml:"canary,omitempty"` -} - -type genericECSTargetGroup struct { - TargetGroupArn string `yaml:"targetGroupArn"` - ContainerName string `yaml:"containerName"` - ContainerPort int `yaml:"containerPort"` + Name string `json:"name"` + Input config.ECSDeploymentInput `json:"input"` + Description string `json:"description,omitempty"` } func generateECSConfig(in io.Reader) (*genericConfig, error) { @@ -50,6 +32,7 @@ func generateECSConfig(in io.Reader) (*genericConfig, error) { if e != nil { return nil, e } + return &genericConfig{ Kind: config.KindECSApp, APIVersion: config.VersionV1Beta1, @@ -73,11 +56,11 @@ func generateECSSpec(in io.Reader) (*genericECSApplicationSpec, error) { cfg := &genericECSApplicationSpec{ Name: appName, - Input: genericECSDeploymentInput{ + Input: config.ECSDeploymentInput{ ServiceDefinitionFile: serviceDefFile, TaskDefinitionFile: taskDefFile, - TargetGroups: genericECSTargetGroups{ - Primary: genericECSTargetGroup{ + TargetGroups: config.ECSTargetGroups{ + Primary: &config.ECSTargetGroup{ TargetGroupArn: targetGroupArn, ContainerName: containerName, ContainerPort: containerPort, diff --git a/pkg/app/pipectl/cmd/initialize/ecs_test.go b/pkg/app/pipectl/cmd/initialize/ecs_test.go index e7c7735479..5ef8cd3158 100644 --- a/pkg/app/pipectl/cmd/initialize/ecs_test.go +++ b/pkg/app/pipectl/cmd/initialize/ecs_test.go @@ -18,9 +18,9 @@ import ( "bytes" "testing" + "github.com/goccy/go-yaml" "github.com/pipe-cd/pipecd/pkg/config" "github.com/stretchr/testify/assert" - "gopkg.in/yaml.v3" ) func TestGenerateECSConfig(t *testing.T) { @@ -31,7 +31,7 @@ func TestGenerateECSConfig(t *testing.T) { expectedErr error }{ { - name: "correct config for ECSApp", + name: "valid config for ECSApp", inputs: []string{ "myApp", "service-definition.json", @@ -45,11 +45,11 @@ func TestGenerateECSConfig(t *testing.T) { APIVersion: config.VersionV1Beta1, ApplicationSpec: &genericECSApplicationSpec{ Name: "myApp", - Input: genericECSDeploymentInput{ + Input: config.ECSDeploymentInput{ ServiceDefinitionFile: "service-definition.json", TaskDefinitionFile: "task-definition.json", - TargetGroups: genericECSTargetGroups{ - Primary: genericECSTargetGroup{ + TargetGroups: config.ECSTargetGroups{ + Primary: &config.ECSTargetGroup{ TargetGroupArn: "arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:targetgroup/xxx/xxx", ContainerName: "test-container", ContainerPort: 80, @@ -65,6 +65,7 @@ func TestGenerateECSConfig(t *testing.T) { for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { + // Mock user's input. in := bytes.NewBufferString("") for _, word := range tc.inputs { in.WriteString(word + "\n") diff --git a/pkg/app/pipectl/cmd/initialize/initialize.go b/pkg/app/pipectl/cmd/initialize/initialize.go index 86e5109bb5..7bd3513658 100644 --- a/pkg/app/pipectl/cmd/initialize/initialize.go +++ b/pkg/app/pipectl/cmd/initialize/initialize.go @@ -20,11 +20,8 @@ import ( "io" "os" + "github.com/goccy/go-yaml" "github.com/spf13/cobra" - "gopkg.in/yaml.v3" - - // "github.com/go-yaml/yaml" - // "sigs.k8s.io/yaml" "github.com/pipe-cd/pipecd/pkg/cli" "github.com/pipe-cd/pipecd/pkg/config" @@ -34,14 +31,11 @@ type command struct { someTextOption string } -// Use genericConfigs in order to -// - keep the order as we want -// - use only simple fields, without attatching `omitempty` to all fields -// - enable modifying the original configs isolately from init command +// Use genericConfigs in order to simplify using the spec. type genericConfig struct { - APIVersion string `yaml:"apiVersion"` - Kind config.Kind `yaml:"kind"` - ApplicationSpec interface{} `yaml:"spec"` + APIVersion string `json:"apiVersion"` + Kind config.Kind `json:"kind"` + ApplicationSpec interface{} `json:"spec"` } func NewCommand() *cobra.Command { @@ -50,9 +44,9 @@ func NewCommand() *cobra.Command { } cmd := &cobra.Command{ Use: "init", - Short: "Create a app.pipecd.yaml easily (interactively)", + Short: "Generate a app.pipecd.yaml easily and interactively", Example: ` pipectl init`, - Long: "Create a app.pipecd.yaml easily, interactively selecting options.", + Long: "Generate a app.pipecd.yaml easily, interactively selecting options.", RunE: cli.WithContext(c.run), } @@ -89,38 +83,43 @@ func generateConfig(ctx context.Context, input cli.Input, in io.Reader) error { return err } - fmt.Println("### The config model was successfully generated.") + fmt.Println("### The config model was successfully generated. Move on to exporting. ###") targetPath := promptString("Path to save the generated config (if not specified, it goes to stdout) : ", in) if len(targetPath) == 0 { - // If the target path is not specified, print the config to stdout. + // If the target path is not specified, print to stdout. printConfig(cfgBytes) } else { - if _, err := os.Stat(targetPath); err == nil { - // If the file exists, ask if overwrite it. - overwrite := promptStringRequired(fmt.Sprintf("The file %s already exists. Overwrite it? [y/n] : ", targetPath), in) - if overwrite == "y" || overwrite == "Y" { - exportConfig(cfgBytes, targetPath) - } else { - fmt.Println("Cancelled exporting the config.") - printConfig(cfgBytes) - } - } else { - // If the file does not exist, simply write to the new file, including validating the path. - exportConfig(cfgBytes, targetPath) - } + exportConfig(cfgBytes, targetPath, in) } return nil } -// Write the config to the specified path file. -func exportConfig(configBytes []byte, path string) { +// Write the config to the specified path. +func exportConfig(configBytes []byte, path string, in io.Reader) { + 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 := promptStringRequired(fmt.Sprintf("The file %s already exists. Overwrite it? [y/n] : ", path), in) + 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 prevent losing it. + // If failed, print the config to avoid losing it. printConfig(configBytes) } else { fmt.Printf("Successfully exported the config to %s\n", path) @@ -153,13 +152,11 @@ func promptInt(message string, in io.Reader) (int, error) { // Read a string value from stdin, and validate it is not empty. func promptStringRequired(message string, in io.Reader) string { - // Limit for avoiding infinite loops in tests. - for i := 0; i < 30; i++ { + for { in := promptString(message, in) if in != "" { return in } fmt.Printf("[WARN] This field is required. \n") } - return "[not specified]" } diff --git a/pkg/config/application_ecs.go b/pkg/config/application_ecs.go index d78bd10931..0f5f67a425 100644 --- a/pkg/config/application_ecs.go +++ b/pkg/config/application_ecs.go @@ -47,32 +47,32 @@ func (s *ECSApplicationSpec) Validate() error { type ECSDeploymentInput struct { // The Amazon Resource Name (ARN) that identifies the cluster. - ClusterArn string `json:"clusterArn"` + ClusterArn string `json:"clusterArn,omitempty"` // The launch type on which to run your task. // https://docs.aws.amazon.com/AmazonECS/latest/developerguide/launch_types.html // Default is FARGATE - LaunchType string `json:"launchType" default:"FARGATE"` + LaunchType string `json:"launchType,omitempty" default:"FARGATE"` // VpcConfiguration ECSVpcConfiguration `json:"awsvpcConfiguration"` - AwsVpcConfiguration ECSVpcConfiguration `json:"awsvpcConfiguration"` + AwsVpcConfiguration ECSVpcConfiguration `json:"awsvpcConfiguration,omitempty" default:""` // The name of service definition file placing in application directory. ServiceDefinitionFile string `json:"serviceDefinitionFile"` // The name of task definition file placing in application directory. // Default is taskdef.json TaskDefinitionFile string `json:"taskDefinitionFile" default:"taskdef.json"` // ECSTargetGroups - TargetGroups ECSTargetGroups `json:"targetGroups"` + TargetGroups ECSTargetGroups `json:"targetGroups,omitempty"` // 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"` // How the ECS service is accessed. // Possible values are: // - ELB - The service is accessed via ELB and target groups. // - SERVICE_DISCOVERY - The service is accessed via ECS Service Discovery. // Default is ELB. - AccessType string `json:"accessType" default:"ELB"` + AccessType string `json:"accessType,omitempty" default:"ELB"` } func (in *ECSDeploymentInput) IsStandaloneTask() bool { @@ -84,20 +84,20 @@ func (in *ECSDeploymentInput) IsAccessedViaELB() bool { } type ECSVpcConfiguration struct { - Subnets []string `json:"subnets"` - AssignPublicIP string `json:"assignPublicIp"` - SecurityGroups []string `json:"securityGroups"` + Subnets []string `json:"subnets,omitempty"` + AssignPublicIP string `json:"assignPublicIp,omitempty"` + SecurityGroups []string `json:"securityGroups,omitempty"` } type ECSTargetGroups struct { - Primary *ECSTargetGroup `json:"primary"` - Canary *ECSTargetGroup `json:"canary"` + Primary *ECSTargetGroup `json:"primary,omitempty"` + Canary *ECSTargetGroup `json:"canary,omitempty"` } type ECSTargetGroup struct { - TargetGroupArn string `json:"targetGroupArn"` - ContainerName string `json:"containerName"` - ContainerPort int `json:"containerPort"` + TargetGroupArn string `json:"targetGroupArn,omitempty"` + ContainerName string `json:"containerName,omitempty"` + ContainerPort int `json:"containerPort,omitempty"` } // ECSSyncStageOptions contains all configurable values for a ECS_SYNC stage.