diff --git a/cmd/pipectl/main.go b/cmd/pipectl/main.go index 9b08e19993..0aaf9449f5 100644 --- a/cmd/pipectl/main.go +++ b/cmd/pipectl/main.go @@ -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" @@ -41,6 +42,7 @@ func main() { piped.NewCommand(), encrypt.NewCommand(), quickstart.NewCommand(), + initialize.NewCommand(), ) if err := app.Run(); err != nil { diff --git a/pkg/app/pipectl/cmd/initialize/ecs.go b/pkg/app/pipectl/cmd/initialize/ecs.go new file mode 100644 index 0000000000..9625b6d0e8 --- /dev/null +++ b/pkg/app/pipectl/cmd/initialize/ecs.go @@ -0,0 +1,99 @@ +// Copyright 2024 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 ( + "github.com/pipe-cd/pipecd/pkg/app/pipectl/cmd/initialize/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(p prompt.Prompt) (*genericConfig, error) { + // inputs + var ( + appName string + serviceDefFile string + taskDefFile string + targetGroupArn string + containerName string + containerPort int + ) + inputs := []prompt.Input{ + { + Message: "Name of the application", + TargetPointer: &appName, + Required: true, + }, + { + Message: "Name of the service definition file (e.g. serviceDef.yaml)", + TargetPointer: &serviceDefFile, + Required: true, + }, + { + Message: "Name of the task definition file (e.g. taskDef.yaml)", + TargetPointer: &taskDefFile, + Required: true, + }, + // target group inputs + { + Message: "ARN of the target group to the service", + TargetPointer: &targetGroupArn, + Required: false, + }, + { + Message: "Name of the container of the target group", + TargetPointer: &containerName, + Required: false, + }, + { + Message: "Port number of the container of the target group", + TargetPointer: &containerPort, + Required: false, + }, + } + + err := p.RunSlice(inputs) + if err != nil { + return nil, err + } + + spec := &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 &genericConfig{ + Kind: config.KindECSApp, + APIVersion: config.VersionV1Beta1, + ApplicationSpec: spec, + }, nil +} diff --git a/pkg/app/pipectl/cmd/initialize/ecs_test.go b/pkg/app/pipectl/cmd/initialize/ecs_test.go new file mode 100644 index 0000000000..35c41e64a0 --- /dev/null +++ b/pkg/app/pipectl/cmd/initialize/ecs_test.go @@ -0,0 +1,85 @@ +// Copyright 2024 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/cmd/initialize/prompt" + "github.com/pipe-cd/pipecd/pkg/config" +) + +func TestGenerateECSConfig(t *testing.T) { + t.Parallel() + + testcases := []struct { + name string + inputs string // mock for user's input + expectedFile string + expectedErr bool + }{ + { + name: "valid inputs", + inputs: `myApp + serviceDef.yaml + taskDef.yaml + arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:targetgroup/xxx/xxx + web + 80 + `, + expectedFile: "testdata/ecs-app.yaml", + expectedErr: false, + }, + { + name: "missing required", + inputs: `myApp + serviceDef.yaml + `, + expectedFile: "", + expectedErr: true, + }, + } + + for _, tc := range testcases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + reader := strings.NewReader(tc.inputs) + prompt := prompt.NewPrompt(reader) + + // Generate the config + cfg, err := generateECSConfig(prompt) + assert.Equal(t, tc.expectedErr, err != nil) + + if err == nil { + // 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) + } + }) + } +} diff --git a/pkg/app/pipectl/cmd/initialize/exporter/exporter.go b/pkg/app/pipectl/cmd/initialize/exporter/exporter.go new file mode 100644 index 0000000000..f26fc7b3a3 --- /dev/null +++ b/pkg/app/pipectl/cmd/initialize/exporter/exporter.go @@ -0,0 +1,58 @@ +package exporter + +import ( + "fmt" + "os" + + "github.com/pipe-cd/pipecd/pkg/app/pipectl/cmd/initialize/prompt" +) + +// Export the bytes to the path. +// If the path is empty or a directory, return an error. +// If the file already exists, ask if overwrite it. +func Export(bytes []byte, path string) error { + if len(path) == 0 { + return fmt.Errorf("path is not specified. Please specify a file path") + } + + // Check if the file/directory already exists + if fInfo, err := os.Stat(path); err == nil { + if fInfo.IsDir() { + // When the target is a directory. + return fmt.Errorf("the path %s is a directory. Please specify a file path", path) + } + + // When the file exists, ask if overwrite it. + overwrite, err := askOverwrite() + if err != nil { + return fmt.Errorf("invalid input for overwrite(y/n): %v", err) + } + + if !overwrite { + return fmt.Errorf("cancelled exporting") + } + } + + fmt.Printf("Start exporting to %s\n", path) + err := os.WriteFile(path, bytes, 0644) + if err != nil { + return fmt.Errorf("failed to export to %s: %v", path, err) + } else { + fmt.Printf("Successfully exported to %s\n", path) + } + return nil +} + +func askOverwrite() (overwrite bool, err error) { + overwriteInput := prompt.Input{ + Message: "The file already exists. Overwrite it? [y/n]", + TargetPointer: &overwrite, + Required: false, + } + p := prompt.NewPrompt(os.Stdin) + err = p.Run(overwriteInput) + if err != nil { + return false, err + } + return overwrite, nil +} diff --git a/pkg/app/pipectl/cmd/initialize/initialize.go b/pkg/app/pipectl/cmd/initialize/initialize.go new file mode 100644 index 0000000000..34b3a37e07 --- /dev/null +++ b/pkg/app/pipectl/cmd/initialize/initialize.go @@ -0,0 +1,161 @@ +// Copyright 2024 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/cmd/initialize/exporter" + "github.com/pipe-cd/pipecd/pkg/app/pipectl/cmd/initialize/prompt" + "github.com/pipe-cd/pipecd/pkg/cli" + "github.com/pipe-cd/pipecd/pkg/config" +) + +type command struct { + // Add flags if needed. +} + +const ( + // platform numbers to select which platform to use. + platformKubernetes string = "0" // for KubernetesApp + platformECS string = "1" // for ECSApp +) + +// 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(): + } + }() + + p := prompt.NewPrompt(os.Stdin) + return generateConfig(ctx, input, p) +} + +func generateConfig(ctx context.Context, input cli.Input, p prompt.Prompt) error { + // user's inputs + var ( + platform string + exportPath string + ) + + platformInput := prompt.Input{ + Message: fmt.Sprintf("Which platform? Enter the number [%s]Kubernetes [%s]ECS", platformKubernetes, platformECS), + TargetPointer: &platform, + Required: true, + } + exportPathInput := prompt.Input{ + Message: "Path to save the config (if not specified, it goes to stdout)", + TargetPointer: &exportPath, + Required: false, + } + + err := p.Run(platformInput) + if err != nil { + return fmt.Errorf("invalid platform number: %v", err) + } + + var cfg *genericConfig + switch platform { + case platformKubernetes: + panic("not implemented") + case platformECS: + cfg, err = generateECSConfig(p) + 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. ###") + err = p.Run(exportPathInput) + if err != nil { + printConfig(cfgBytes) + return err + } + err = export(cfgBytes, exportPath) + if err != nil { + return nil + } + + return nil +} + +func export(cfgBytes []byte, exportPath string) error { + if len(exportPath) == 0 { + // if the path is not specified, print to stdout + printConfig(cfgBytes) + return nil + } + err := exporter.Export(cfgBytes, exportPath) + if err != nil { + printConfig(cfgBytes) + return err + } + return nil +} + +// Print the config to stdout. +func printConfig(configBytes []byte) { + fmt.Printf("\n### Generated Config is below ###\n%s\n", string(configBytes)) +} diff --git a/pkg/app/pipectl/cmd/initialize/prompt/prompt.go b/pkg/app/pipectl/cmd/initialize/prompt/prompt.go new file mode 100644 index 0000000000..9e77b922d8 --- /dev/null +++ b/pkg/app/pipectl/cmd/initialize/prompt/prompt.go @@ -0,0 +1,112 @@ +// Copyright 2024 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 prompt + +import ( + "bufio" + "fmt" + "io" + "strconv" + "strings" +) + +// Prompt is a helper for asking inputs from users. +type Prompt struct { + bufReader bufio.Reader +} + +// Input represents an query to ask from users. +type Input struct { + // Message is a message to show when asking for an input. e.g. "Which platform to use?" + Message string + // TargetPointer is a pointer to the target variable to set the value of the input. + TargetPointer any + // Required indicates whether this input is required or not. + Required bool +} + +func NewPrompt(in io.Reader) Prompt { + return Prompt{ + bufReader: *bufio.NewReader(in), + } +} + +// RunSlice sequentially asks for inputs and set the values to the target pointers. +func (p *Prompt) RunSlice(inputs []Input) error { + for _, in := range inputs { + if err := p.Run(in); err != nil { + return err + } + } + return nil +} + +// Run asks for an input and set the value to the target pointer. +func (p *Prompt) Run(input Input) error { + fmt.Printf("%s: ", input.Message) + line, err := p.bufReader.ReadString('\n') + if err != nil { + return err + } + // split by spaces + fields := strings.Fields(line) + + if len(fields) == 0 { + if input.Required { + return fmt.Errorf("this field is required") + } else { + return nil + } + } + + switch tp := input.TargetPointer.(type) { + case *string: + if len(fields) > 1 { + return fmt.Errorf("too many arguments") + } + *tp = fields[0] + case *[]string: + *tp = fields + case *int: + if len(fields) > 1 { + return fmt.Errorf("too many arguments") + } + n, err := strconv.Atoi(fields[0]) + if err != nil { + return fmt.Errorf("this field should be an int value") + } + *tp = n + case *bool: + if len(fields) > 1 { + return fmt.Errorf("too many arguments") + } + + switch fields[0] { + case "y", "Y": + *tp = true + case "n", "N": + *tp = false + default: + b, err := strconv.ParseBool(fields[0]) + if err != nil { + return fmt.Errorf("this field should be a bool value") + } + *tp = b + } + default: + return fmt.Errorf("unsupported type: %T", tp) + } + return nil +} diff --git a/pkg/app/pipectl/cmd/initialize/prompt/prompt_test.go b/pkg/app/pipectl/cmd/initialize/prompt/prompt_test.go new file mode 100644 index 0000000000..bf90515a4b --- /dev/null +++ b/pkg/app/pipectl/cmd/initialize/prompt/prompt_test.go @@ -0,0 +1,332 @@ +// Copyright 2024 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 prompt + +import ( + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRunString(t *testing.T) { + t.Parallel() + + testcases := []struct { + name string + in Input + str string // user's input + expectedValue string + expectedErr error + }{ + { + name: "valid string", + in: Input{ + Message: "anyPrompt", + TargetPointer: new(string), + Required: false, + }, + str: "foo\n", + expectedValue: "foo", + expectedErr: nil, + }, + { + name: "empty but not required", + in: Input{ + Message: "anyPrompt", + TargetPointer: new(string), + Required: false, + }, + str: "\n", + expectedValue: "", + expectedErr: nil, + }, + { + name: "missing required", + in: Input{ + Message: "anyPrompt", + TargetPointer: new(string), + Required: true, + }, + str: "\n", + expectedValue: "", + expectedErr: fmt.Errorf("this field is required"), + }, + { + name: "two many arguments", + in: Input{ + Message: "anyPrompt", + TargetPointer: new(string), + Required: false, + }, + str: "foo bar\n", + expectedValue: "", + expectedErr: fmt.Errorf("too many arguments"), + }, + } + + for _, tc := range testcases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + strReader := strings.NewReader(tc.str) + p := NewPrompt(strReader) + err := p.Run(tc.in) + assert.Equal(t, tc.expectedErr, err) + assert.Equal(t, tc.expectedValue, *tc.in.TargetPointer.(*string)) + }) + } +} + +func TestRunStringSlice(t *testing.T) { + t.Parallel() + + testcases := []struct { + name string + in Input + str string // user's input + expectedValue []string + expectedErr error + }{ + { + name: "valid string slice", + in: Input{ + Message: "anyPrompt", + TargetPointer: new([]string), + Required: false, + }, + str: "foo bar\n", + expectedValue: []string{"foo", "bar"}, + expectedErr: nil, + }, + { + name: "empty but not required", + in: Input{ + Message: "anyPrompt", + TargetPointer: new([]string), + Required: false, + }, + str: "\n", + expectedValue: nil, + expectedErr: nil, + }, + { + name: "missing required", + in: Input{ + Message: "anyPrompt", + TargetPointer: new([]string), + Required: true, + }, + str: "\n", + expectedValue: nil, + expectedErr: fmt.Errorf("this field is required"), + }, + } + + for _, tc := range testcases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + strReader := strings.NewReader(tc.str) + p := NewPrompt(strReader) + err := p.Run(tc.in) + assert.Equal(t, tc.expectedErr, err) + assert.Equal(t, tc.expectedValue, *tc.in.TargetPointer.(*[]string)) + }) + } +} + +func TestRunInt(t *testing.T) { + t.Parallel() + + testcases := []struct { + name string + in Input + str string // user's input + expectedValue int + expectedErr error + }{ + { + name: "valid int", + in: Input{ + Message: "anyPrompt", + TargetPointer: new(int), + Required: false, + }, + str: "123\n", + expectedValue: 123, + expectedErr: nil, + }, + { + name: "invalid int", + in: Input{ + Message: "anyPrompt", + TargetPointer: new(int), + Required: false, + }, + str: "abc\n", + expectedValue: 0, + expectedErr: fmt.Errorf("this field should be an int value"), + }, + { + name: "empty but not required", + in: Input{ + Message: "anyPrompt", + TargetPointer: new(int), + Required: false, + }, + str: "\n", + expectedValue: 0, + expectedErr: nil, + }, + { + name: "missing required", + in: Input{ + Message: "anyPrompt", + TargetPointer: new(int), + Required: true, + }, + str: "\n", + expectedValue: 0, + expectedErr: fmt.Errorf("this field is required"), + }, + { + name: "too many arguments", + in: Input{ + Message: "anyPrompt", + TargetPointer: new(int), + Required: true, + }, + str: "12 34\n", + expectedValue: 0, + expectedErr: fmt.Errorf("too many arguments"), + }, + } + + for _, tc := range testcases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + strReader := strings.NewReader(tc.str) + p := NewPrompt(strReader) + err := p.Run(tc.in) + assert.Equal(t, tc.expectedErr, err) + assert.Equal(t, tc.expectedValue, *tc.in.TargetPointer.(*int)) + }) + } +} + +func TestRunBool(t *testing.T) { + t.Parallel() + + testcases := []struct { + name string + in Input + str string // user's input + expectedValue bool + expectedErr error + }{ + { + name: "valid bool", + in: Input{ + Message: "anyPrompt", + TargetPointer: new(bool), + Required: false, + }, + str: "true\n", + expectedValue: true, + expectedErr: nil, + }, + { + name: "y means true", + in: Input{ + Message: "anyPrompt", + TargetPointer: new(bool), + Required: false, + }, + str: "y\n", + expectedValue: true, + expectedErr: nil, + }, + { + name: "n means false", + in: Input{ + Message: "anyPrompt", + TargetPointer: new(bool), + Required: false, + }, + str: "n\n", + expectedValue: false, + expectedErr: nil, + }, + { + name: "invalid bool", + in: Input{ + Message: "anyPrompt", + TargetPointer: new(bool), + Required: false, + }, + str: "abc\n", + expectedValue: false, + expectedErr: fmt.Errorf("this field should be a bool value"), + }, + { + name: "empty but not required", + in: Input{ + Message: "anyPrompt", + TargetPointer: new(bool), + Required: false, + }, + str: "\n", + expectedValue: false, + expectedErr: nil, + }, + { + name: "missing required", + in: Input{ + Message: "anyPrompt", + TargetPointer: new(bool), + Required: true, + }, + str: "\n", + expectedValue: false, + expectedErr: fmt.Errorf("this field is required"), + }, + { + name: "too many arguments", + in: Input{ + Message: "anyPrompt", + TargetPointer: new(bool), + Required: true, + }, + str: "true false\n", + expectedValue: false, + expectedErr: fmt.Errorf("too many arguments"), + }, + } + + for _, tc := range testcases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + strReader := strings.NewReader(tc.str) + p := NewPrompt(strReader) + err := p.Run(tc.in) + assert.Equal(t, tc.expectedErr, err) + assert.Equal(t, tc.expectedValue, *tc.in.TargetPointer.(*bool)) + }) + } +} diff --git a/pkg/app/pipectl/cmd/initialize/testdata/ecs-app.yaml b/pkg/app/pipectl/cmd/initialize/testdata/ecs-app.yaml new file mode 100644 index 0000000000..14b86020e3 --- /dev/null +++ b/pkg/app/pipectl/cmd/initialize/testdata/ecs-app.yaml @@ -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. diff --git a/pkg/config/application_ecs.go b/pkg/config/application_ecs.go index 0c739ba84b..3355fa7e09 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,21 +84,21 @@ func (in *ECSDeploymentInput) IsAccessedViaELB() bool { } type ECSVpcConfiguration struct { - Subnets []string - AssignPublicIP string - SecurityGroups []string + 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"` - LoadBalancerName string `json:"loadBalancerName"` + TargetGroupArn string `json:"targetGroupArn,omitempty"` + ContainerName string `json:"containerName,omitempty"` + ContainerPort int `json:"containerPort,omitempty"` + LoadBalancerName string `json:"loadBalancerName,omitempty"` } // ECSSyncStageOptions contains all configurable values for a ECS_SYNC stage. diff --git a/pkg/config/config.go b/pkg/config/config.go index 37d1f857a9..495330b7a7 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -29,7 +29,7 @@ import ( const ( SharedConfigurationDirName = ".pipe" - versionV1Beta1 = "pipecd.dev/v1beta1" + VersionV1Beta1 = "pipecd.dev/v1beta1" ) // Kind represents the kind of configuration the data contains. @@ -172,7 +172,7 @@ type validator interface { // Validate validates the value of all fields. func (c *Config) Validate() error { - if c.APIVersion != versionV1Beta1 { + if c.APIVersion != VersionV1Beta1 { return fmt.Errorf("unsupported version: %s", c.APIVersion) } if c.Kind == "" {