internal/pkg/manifest/validate.go (2,153 lines of code) (raw):
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package manifest
import (
"errors"
"fmt"
"net"
"path/filepath"
"regexp"
"slices"
"sort"
"strconv"
"strings"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/arn"
"github.com/aws/copilot-cli/internal/pkg/aws/cloudfront"
"github.com/aws/copilot-cli/internal/pkg/graph"
"github.com/aws/copilot-cli/internal/pkg/manifest/manifestinfo"
"github.com/aws/copilot-cli/internal/pkg/term/color"
"github.com/dustin/go-humanize/english"
)
const (
// Container dependency status constants.
dependsOnStart = "START"
dependsOnComplete = "COMPLETE"
dependsOnSuccess = "SUCCESS"
dependsOnHealthy = "HEALTHY"
// Min and Max values for task ephemeral storage in GiB.
ephemeralMinValueGiB = 20
ephemeralMaxValueGiB = 200
envFileExt = ".env"
)
const (
// TCP is the tcp protocol for NLB.
TCP = "TCP"
// TLS is the tls protocol for NLB.
TLS = "TLS"
// UDP is the udp protocol for NLB.
UDP = "UDP"
// Tracing vendors.
awsXRAY = "awsxray"
)
const (
defaultProtocol = TCP
)
const (
// Listener rules have a quota of five condition values per rule.
// Please refer to https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-limits.html.
maxConditionsPerRule = 5
rootPath = "/"
)
var (
intRangeBandRegexp = regexp.MustCompile(`^(\d+)-(\d+)$`)
volumesPathRegexp = regexp.MustCompile(`^[a-zA-Z0-9\-\.\_/]+$`)
awsSNSTopicRegexp = regexp.MustCompile(`^[a-zA-Z0-9_-]*$`) // Validates that an expression contains only letters, numbers, underscores, and hyphens.
awsNameRegexp = regexp.MustCompile(`^[a-z][a-z0-9\-]+$`) // Validates that an expression starts with a letter and only contains letters, numbers, and hyphens.
punctuationRegExp = regexp.MustCompile(`[\.\-]{2,}`) // Check for consecutive periods or dashes.
trailingPunctRegExp = regexp.MustCompile(`[\-\.]$`) // Check for trailing dash or dot.
essentialContainerDependsOnValidStatuses = []string{dependsOnStart, dependsOnHealthy}
dependsOnValidStatuses = []string{dependsOnStart, dependsOnComplete, dependsOnSuccess, dependsOnHealthy}
nlbValidProtocols = []string{TCP, UDP, TLS}
validContainerProtocols = []string{TCP, UDP}
validHealthCheckProtocols = []string{TCP}
tracingValidVendors = []string{awsXRAY}
ecsRollingUpdateStrategies = []string{ECSDefaultRollingUpdateStrategy, ECSRecreateRollingUpdateStrategy}
httpProtocolVersions = []string{"GRPC", "HTTP1", "HTTP2"}
invalidTaskDefOverridePathRegexp = []string{`Family`, `ContainerDefinitions\[\d+\].Name`}
validSQSDeduplicationScopeValues = []string{sqsDeduplicationScopeMessageGroup, sqsDeduplicationScopeQueue}
validSQSFIFOThroughputLimitValues = []string{sqsFIFOThroughputLimitPerMessageGroupID, sqsFIFOThroughputLimitPerQueue}
)
// Validate returns nil if DynamicLoadBalancedWebService is configured correctly.
func (l *DynamicWorkloadManifest) Validate() error {
return l.mft.validate()
}
// validate returns nil if LoadBalancedWebService is configured correctly.
func (l LoadBalancedWebService) validate() error {
var err error
if err = l.LoadBalancedWebServiceConfig.validate(); err != nil {
return err
}
if err = l.Workload.validate(); err != nil {
return err
}
if err = validateTargetContainer(validateTargetContainerOpts{
mainContainerName: aws.StringValue(l.Name),
mainContainerPort: l.ImageConfig.Port,
targetContainer: l.HTTPOrBool.Main.TargetContainer,
sidecarConfig: l.Sidecars,
}); err != nil {
return fmt.Errorf(`validate load balancer target for "http": %w`, err)
}
for idx, rule := range l.HTTPOrBool.AdditionalRoutingRules {
if err = validateTargetContainer(validateTargetContainerOpts{
mainContainerName: aws.StringValue(l.Name),
mainContainerPort: l.ImageConfig.Port,
targetContainer: rule.TargetContainer,
sidecarConfig: l.Sidecars,
}); err != nil {
return fmt.Errorf(`validate load balancer target for "http.additional_rules[%d]": %w`, idx, err)
}
}
if err = validateTargetContainer(validateTargetContainerOpts{
mainContainerName: aws.StringValue(l.Name),
mainContainerPort: l.ImageConfig.Port,
targetContainer: l.NLBConfig.Listener.TargetContainer,
sidecarConfig: l.Sidecars,
}); err != nil {
return fmt.Errorf(`validate target for "nlb": %w`, err)
}
for idx, listener := range l.NLBConfig.AdditionalListeners {
if err = validateTargetContainer(validateTargetContainerOpts{
mainContainerName: aws.StringValue(l.Name),
mainContainerPort: l.ImageConfig.Port,
targetContainer: listener.TargetContainer,
sidecarConfig: l.Sidecars,
}); err != nil {
return fmt.Errorf(`validate target for "nlb.additional_listeners[%d]": %w`, idx, err)
}
}
if err = validateContainerDeps(validateDependenciesOpts{
sidecarConfig: l.Sidecars,
imageConfig: l.ImageConfig.Image,
mainContainerName: aws.StringValue(l.Name),
logging: l.Logging,
}); err != nil {
return fmt.Errorf("validate container dependencies: %w", err)
}
if err = validateExposedPorts(validateExposedPortsOpts{
mainContainerName: aws.StringValue(l.Name),
mainContainerPort: l.ImageConfig.Port,
sidecarConfig: l.Sidecars,
alb: &l.HTTPOrBool.HTTP,
nlb: &l.NLBConfig,
}); err != nil {
return fmt.Errorf("validate unique exposed ports: %w", err)
}
ports, err := l.ExposedPorts()
if err != nil {
return err
}
if err = validateHealthCheckPorts(validateHealthCheckPortsOpts{
exposedPorts: ports,
mainContainerPort: l.ImageConfig.Port,
alb: l.HTTPOrBool.HTTP,
nlb: l.NLBConfig,
}); err != nil {
return fmt.Errorf("validate load balancer health check ports: %w", err)
}
return nil
}
func (d DeploymentConfig) validate() error {
if d.isEmpty() {
return nil
}
if err := d.RollbackAlarms.validate(); err != nil {
return fmt.Errorf(`validate "rollback_alarms": %w`, err)
}
if err := d.DeploymentControllerConfig.validate(); err != nil {
return fmt.Errorf(`validate "rolling": %w`, err)
}
return nil
}
func (w WorkerDeploymentConfig) validate() error {
if w.isEmpty() {
return nil
}
if err := w.WorkerRollbackAlarms.validate(); err != nil {
return fmt.Errorf(`validate "rollback_alarms": %w`, err)
}
if err := w.DeploymentControllerConfig.validate(); err != nil {
return fmt.Errorf(`validate "deployment controller strategy": %w`, err)
}
return nil
}
func (d DeploymentControllerConfig) validate() error {
if d.Rolling != nil {
for _, validStrategy := range ecsRollingUpdateStrategies {
if strings.EqualFold(aws.StringValue(d.Rolling), validStrategy) {
return nil
}
}
return fmt.Errorf("invalid rolling deployment strategy %q, must be one of %s",
aws.StringValue(d.Rolling),
english.WordSeries(ecsRollingUpdateStrategies, "or"))
}
return nil
}
func (a AlarmArgs) validate() error {
return nil
}
func (w WorkerAlarmArgs) validate() error {
return nil
}
// validate returns nil if LoadBalancedWebServiceConfig is configured correctly.
func (l LoadBalancedWebServiceConfig) validate() error {
var err error
if l.HTTPOrBool.Disabled() && l.NLBConfig.IsEmpty() {
return &errAtLeastOneFieldMustBeSpecified{
missingFields: []string{"http", "nlb"},
}
}
if err = l.validateGracePeriod(); err != nil {
return fmt.Errorf(`validate "grace_period": %w`, err)
}
if l.HTTPOrBool.Disabled() && (!l.Count.AdvancedCount.Requests.IsEmpty() || !l.Count.AdvancedCount.ResponseTime.IsEmpty()) {
return errors.New(`scaling based on "nlb" requests or response time is not supported`)
}
if err = l.ImageConfig.validate(); err != nil {
return fmt.Errorf(`validate "image": %w`, err)
}
if err = l.ImageOverride.validate(); err != nil {
return err
}
if l.HTTPOrBool.isEmpty() {
return &errFieldMustBeSpecified{
missingField: "http",
}
}
if err = l.HTTPOrBool.validate(); err != nil {
return fmt.Errorf(`validate "http": %w`, err)
}
if err = l.TaskConfig.validate(); err != nil {
return err
}
if err = l.Logging.validate(); err != nil {
return fmt.Errorf(`validate "logging": %w`, err)
}
for k, v := range l.Sidecars {
if err = v.validate(); err != nil {
return fmt.Errorf(`validate "sidecars[%s]": %w`, k, err)
}
}
if err = l.Network.validate(); err != nil {
return fmt.Errorf(`validate "network": %w`, err)
}
if err = l.PublishConfig.validate(); err != nil {
return fmt.Errorf(`validate "publish": %w`, err)
}
for ind, taskDefOverride := range l.TaskDefOverrides {
if err = taskDefOverride.validate(); err != nil {
return fmt.Errorf(`validate "taskdef_overrides[%d]": %w`, ind, err)
}
}
if l.TaskConfig.IsWindows() {
if err = validateWindows(validateWindowsOpts{
efsVolumes: l.Storage.Volumes,
readOnlyFS: l.Storage.ReadonlyRootFS,
}); err != nil {
return fmt.Errorf("validate Windows: %w", err)
}
if l.Network.Connect.Enabled() {
return fmt.Errorf("validate Windows: service connect (`network.connect`) is not supported for Windows")
}
}
if err = l.NLBConfig.validate(); err != nil {
return fmt.Errorf(`validate "nlb": %w`, err)
}
if err = l.DeployConfig.validate(); err != nil {
return fmt.Errorf(`validate "deployment": %w`, err)
}
return nil
}
// validate returns nil if BackendService is configured correctly.
func (b BackendService) validate() error {
var err error
if err = b.DeployConfig.validate(); err != nil {
return fmt.Errorf(`validate "deployment": %w`, err)
}
if err = b.BackendServiceConfig.validate(); err != nil {
return err
}
if err = b.Workload.validate(); err != nil {
return err
}
if err = validateTargetContainer(validateTargetContainerOpts{
mainContainerName: aws.StringValue(b.Name),
mainContainerPort: b.ImageConfig.Port,
targetContainer: b.HTTP.Main.TargetContainer,
sidecarConfig: b.Sidecars,
}); err != nil {
return fmt.Errorf(`validate load balancer target for "http": %w`, err)
}
for idx, rule := range b.HTTP.AdditionalRoutingRules {
if err = validateTargetContainer(validateTargetContainerOpts{
mainContainerName: aws.StringValue(b.Name),
mainContainerPort: b.ImageConfig.Port,
targetContainer: rule.TargetContainer,
sidecarConfig: b.Sidecars,
}); err != nil {
return fmt.Errorf(`validate load balancer target for "http.additional_rules[%d]": %w`, idx, err)
}
}
if err = validateContainerDeps(validateDependenciesOpts{
sidecarConfig: b.Sidecars,
imageConfig: b.ImageConfig.Image,
mainContainerName: aws.StringValue(b.Name),
logging: b.Logging,
}); err != nil {
return fmt.Errorf("validate container dependencies: %w", err)
}
if err = validateExposedPorts(validateExposedPortsOpts{
mainContainerName: aws.StringValue(b.Name),
mainContainerPort: b.ImageConfig.Port,
sidecarConfig: b.Sidecars,
alb: &b.HTTP,
}); err != nil {
return fmt.Errorf("validate unique exposed ports: %w", err)
}
exposedPortsIndex, err := b.ExposedPorts()
if err != nil {
return err
}
if err = validateHealthCheckPorts(validateHealthCheckPortsOpts{
exposedPorts: exposedPortsIndex,
mainContainerPort: b.ImageConfig.Port,
alb: b.HTTP,
}); err != nil {
return fmt.Errorf("validate load balancer health check ports: %w", err)
}
return nil
}
// validate returns nil if BackendServiceConfig is configured correctly.
func (b BackendServiceConfig) validate() error {
var err error
if err = b.ImageConfig.validate(); err != nil {
return fmt.Errorf(`validate "image": %w`, err)
}
if err = b.ImageOverride.validate(); err != nil {
return err
}
if err = b.HTTP.validate(); err != nil {
return fmt.Errorf(`validate "http": %w`, err)
}
if b.HTTP.IsEmpty() && (!b.Count.AdvancedCount.Requests.IsEmpty() || !b.Count.AdvancedCount.ResponseTime.IsEmpty()) {
return &errFieldMustBeSpecified{
missingField: "http",
conditionalFields: []string{"count.requests", "count.response_time"},
}
}
if err = b.TaskConfig.validate(); err != nil {
return err
}
if err = b.Logging.validate(); err != nil {
return fmt.Errorf(`validate "logging": %w`, err)
}
for k, v := range b.Sidecars {
if err = v.validate(); err != nil {
return fmt.Errorf(`validate "sidecars[%s]": %w`, k, err)
}
}
if err = b.Network.validate(); err != nil {
return fmt.Errorf(`validate "network": %w`, err)
}
if b.Network.Connect.Alias != nil {
if b.HTTP.Main.TargetContainer == nil && b.ImageConfig.Port == nil {
return fmt.Errorf(`cannot set "network.connect.alias" when no ports are exposed`)
}
}
if err = b.PublishConfig.validate(); err != nil {
return fmt.Errorf(`validate "publish": %w`, err)
}
for ind, taskDefOverride := range b.TaskDefOverrides {
if err = taskDefOverride.validate(); err != nil {
return fmt.Errorf(`validate "taskdef_overrides[%d]": %w`, ind, err)
}
}
if b.TaskConfig.IsWindows() {
if err = validateWindows(validateWindowsOpts{
efsVolumes: b.Storage.Volumes,
readOnlyFS: b.Storage.ReadonlyRootFS,
}); err != nil {
return fmt.Errorf("validate Windows: %w", err)
}
if b.Network.Connect.Enabled() {
return fmt.Errorf("validate Windows: service connect (`network.connect`) is not supported for Windows")
}
}
return nil
}
// validate returns nil if RequestDrivenWebService is configured correctly.
func (r RequestDrivenWebService) validate() error {
if err := r.RequestDrivenWebServiceConfig.validate(); err != nil {
return err
}
return r.Workload.validate()
}
// validate returns nil if RequestDrivenWebServiceConfig is configured correctly.
func (r RequestDrivenWebServiceConfig) validate() error {
var err error
if err = r.ImageConfig.validate(); err != nil {
return fmt.Errorf(`validate "image": %w`, err)
}
if err = r.InstanceConfig.validate(); err != nil {
return err
}
if err = r.RequestDrivenWebServiceHttpConfig.validate(); err != nil {
return fmt.Errorf(`validate "http": %w`, err)
}
if err = r.PublishConfig.validate(); err != nil {
return fmt.Errorf(`validate "publish": %w`, err)
}
if err = r.Network.validate(); err != nil {
return fmt.Errorf(`validate "network": %w`, err)
}
if r.Network.VPC.Placement.PlacementString != nil &&
*r.Network.VPC.Placement.PlacementString != PrivateSubnetPlacement {
return fmt.Errorf(`placement %q is not supported for %s`,
*r.Network.VPC.Placement.PlacementString, manifestinfo.RequestDrivenWebServiceType)
}
if err = r.Observability.validate(); err != nil {
return fmt.Errorf(`validate "observability": %w`, err)
}
return nil
}
// validate returns nil if WorkerService is configured correctly.
func (w WorkerService) validate() error {
var err error
if err = w.WorkerServiceConfig.validate(); err != nil {
return err
}
if err = w.Workload.validate(); err != nil {
return err
}
if err = validateContainerDeps(validateDependenciesOpts{
sidecarConfig: w.Sidecars,
imageConfig: w.ImageConfig.Image,
mainContainerName: aws.StringValue(w.Name),
logging: w.Logging,
}); err != nil {
return fmt.Errorf("validate container dependencies: %w", err)
}
if err = validateExposedPorts(validateExposedPortsOpts{
sidecarConfig: w.Sidecars,
}); err != nil {
return fmt.Errorf("validate unique exposed ports: %w", err)
}
return nil
}
// validate returns nil if WorkerServiceConfig is configured correctly.
func (w WorkerServiceConfig) validate() error {
var err error
if err = w.DeployConfig.validate(); err != nil {
return fmt.Errorf(`validate "deployment": %w`, err)
}
if err = w.ImageConfig.validate(); err != nil {
return fmt.Errorf(`validate "image": %w`, err)
}
if err = w.ImageOverride.validate(); err != nil {
return err
}
if err = w.TaskConfig.validate(); err != nil {
return err
}
if err = w.Logging.validate(); err != nil {
return fmt.Errorf(`validate "logging": %w`, err)
}
for k, v := range w.Sidecars {
if err = v.validate(); err != nil {
return fmt.Errorf(`validate "sidecars[%s]": %w`, k, err)
}
}
if err = w.Network.validate(); err != nil {
return fmt.Errorf(`validate "network": %w`, err)
}
if w.Network.Connect.Alias != nil {
return fmt.Errorf(`cannot set "network.connect.alias" when no ports are exposed`)
}
if err = w.Subscribe.validate(); err != nil {
return fmt.Errorf(`validate "subscribe": %w`, err)
}
if err = w.PublishConfig.validate(); err != nil {
return fmt.Errorf(`validate "publish": %w`, err)
}
for ind, taskDefOverride := range w.TaskDefOverrides {
if err = taskDefOverride.validate(); err != nil {
return fmt.Errorf(`validate "taskdef_overrides[%d]": %w`, ind, err)
}
}
if w.TaskConfig.IsWindows() {
if err = validateWindows(validateWindowsOpts{
efsVolumes: w.Storage.Volumes,
readOnlyFS: w.Storage.ReadonlyRootFS,
}); err != nil {
return fmt.Errorf(`validate Windows: %w`, err)
}
}
return nil
}
// validate returns nil if ScheduledJob is configured correctly.
func (s ScheduledJob) validate() error {
var err error
if err = s.ScheduledJobConfig.validate(); err != nil {
return err
}
if err = s.Workload.validate(); err != nil {
return err
}
if err = validateContainerDeps(validateDependenciesOpts{
sidecarConfig: s.Sidecars,
imageConfig: s.ImageConfig.Image,
mainContainerName: aws.StringValue(s.Name),
logging: s.Logging,
}); err != nil {
return fmt.Errorf("validate container dependencies: %w", err)
}
if err = validateExposedPorts(validateExposedPortsOpts{
sidecarConfig: s.Sidecars,
}); err != nil {
return fmt.Errorf("validate unique exposed ports: %w", err)
}
return nil
}
// validate returns nil if ScheduledJobConfig is configured correctly.
func (s ScheduledJobConfig) validate() error {
var err error
if err = s.ImageConfig.validate(); err != nil {
return fmt.Errorf(`validate "image": %w`, err)
}
if err = s.ImageOverride.validate(); err != nil {
return err
}
if err = s.TaskConfig.validate(); err != nil {
return err
}
if err = s.Logging.validate(); err != nil {
return fmt.Errorf(`validate "logging": %w`, err)
}
for k, v := range s.Sidecars {
if err = v.validate(); err != nil {
return fmt.Errorf(`validate "sidecars[%s]": %w`, k, err)
}
}
if err = s.Network.validate(); err != nil {
return fmt.Errorf(`validate "network": %w`, err)
}
if err = s.On.validate(); err != nil {
return fmt.Errorf(`validate "on": %w`, err)
}
if err = s.JobFailureHandlerConfig.validate(); err != nil {
return err
}
if err = s.PublishConfig.validate(); err != nil {
return fmt.Errorf(`validate "publish": %w`, err)
}
for ind, taskDefOverride := range s.TaskDefOverrides {
if err = taskDefOverride.validate(); err != nil {
return fmt.Errorf(`validate "taskdef_overrides[%d]": %w`, ind, err)
}
}
if s.TaskConfig.IsWindows() {
if err = validateWindows(validateWindowsOpts{
efsVolumes: s.Storage.Volumes,
readOnlyFS: s.Storage.ReadonlyRootFS,
}); err != nil {
return fmt.Errorf(`validate Windows: %w`, err)
}
}
return nil
}
// validate returns nil if StaticSite is configured correctly.
func (s StaticSite) validate() error {
if err := s.StaticSiteConfig.validate(); err != nil {
return err
}
return s.Workload.validate()
}
func (s StaticSiteConfig) validate() error {
if err := s.HTTP.validate(); err != nil {
return fmt.Errorf(`validate "http": %w`, err)
}
for idx, fileupload := range s.FileUploads {
if err := fileupload.validate(); err != nil {
return fmt.Errorf(`validate "files[%d]": %w`, idx, err)
}
}
return nil
}
func (f FileUpload) validate() error {
return f.validateSource()
}
func (s StaticSiteHTTP) validate() error {
if s.Certificate != "" {
if s.Alias == "" {
return &errFieldMustBeSpecified{
missingField: "alias",
conditionalFields: []string{"certificate"},
}
}
certARN, err := arn.Parse(s.Certificate)
if err != nil {
return fmt.Errorf(`parse cdn certificate: %w`, err)
}
if certARN.Region != cloudfront.CertRegion {
return &errInvalidCloudFrontRegion{}
}
}
return nil
}
// validateSource returns nil if Source is configured correctly.
func (f FileUpload) validateSource() error {
if f.Source == "" {
return &errFieldMustBeSpecified{
missingField: "source",
}
}
return nil
}
// Validate returns nil if the pipeline manifest is configured correctly.
func (p Pipeline) Validate() error {
if len(p.Name) > 100 {
return fmt.Errorf(`pipeline name '%s' must be shorter than 100 characters`, p.Name)
}
for _, stg := range p.Stages {
if err := stg.validate(); err != nil {
return fmt.Errorf(`validate stage %q for pipeline %q: %w`, stg.Name, p.Name, err)
}
if err := stg.Deployments.validate(); err != nil {
return fmt.Errorf(`validate "deployments" for pipeline stage %s: %w`, stg.Name, err)
}
}
return nil
}
// validate returns nil if stages are configured correctly.
func (s PipelineStage) validate() error {
if len(s.TestCommands) != 0 && s.PostDeployments != nil {
return &errFieldMutualExclusive{
firstField: "post_deployments",
secondField: "test_commands",
mustExist: false,
}
}
if s.PreDeployments != nil {
for _, preDep := range s.PreDeployments {
if preDep.BuildspecPath == "" {
return &errFieldMustBeSpecified{
missingField: "buildspec",
}
}
}
}
if s.PostDeployments != nil {
for _, postDep := range s.PostDeployments {
if postDep.BuildspecPath == "" {
return &errFieldMustBeSpecified{
missingField: "buildspec",
}
}
}
}
return nil
}
// validate returns nil if deployments are configured correctly.
func (d Deployments) validate() error {
names := make(map[string]bool)
for name := range d {
names[name] = true
}
for name, conf := range d {
if conf == nil {
continue
}
for _, dependency := range conf.DependsOn {
if _, ok := names[dependency]; !ok {
return fmt.Errorf("dependency deployment named '%s' of '%s' does not exist", dependency, name)
}
}
}
return nil
}
// validate returns nil if Workload is configured correctly.
func (w Workload) validate() error {
if w.Name == nil {
return &errFieldMustBeSpecified{
missingField: "name",
}
}
return nil
}
// validate returns nil if ImageWithPortAndHealthcheck is configured correctly.
func (i ImageWithPortAndHealthcheck) validate() error {
var err error
if err = i.ImageWithPort.validate(); err != nil {
return err
}
if err = i.HealthCheck.validate(); err != nil {
return fmt.Errorf(`validate "healthcheck": %w`, err)
}
return nil
}
// validate returns nil if ImageWithHealthcheckAndOptionalPort is configured correctly.
func (i ImageWithHealthcheckAndOptionalPort) validate() error {
var err error
if err = i.ImageWithOptionalPort.validate(); err != nil {
return err
}
if err = i.HealthCheck.validate(); err != nil {
return fmt.Errorf(`validate "healthcheck": %w`, err)
}
return nil
}
// validate returns nil if ImageWithHealthcheck is configured correctly.
func (i ImageWithHealthcheck) validate() error {
if err := i.Image.validate(); err != nil {
return err
}
return nil
}
// validate returns nil if ImageWithOptionalPort is configured correctly.
func (i ImageWithOptionalPort) validate() error {
if err := i.Image.validate(); err != nil {
return err
}
return nil
}
// validate returns nil if ImageWithPort is configured correctly.
func (i ImageWithPort) validate() error {
if err := i.Image.validate(); err != nil {
return err
}
if i.Port == nil {
return &errFieldMustBeSpecified{
missingField: "port",
}
}
return nil
}
// validate returns nil if Image is configured correctly.
func (i Image) validate() error {
var err error
if err := i.ImageLocationOrBuild.validate(); err != nil {
return err
}
if err = i.DependsOn.validate(); err != nil {
return fmt.Errorf(`validate "depends_on": %w`, err)
}
return nil
}
// validate returns nil if DependsOn is configured correctly.
func (d DependsOn) validate() error {
if d == nil {
return nil
}
for _, v := range d {
status := strings.ToUpper(v)
var isValid bool
for _, allowed := range dependsOnValidStatuses {
if status == allowed {
isValid = true
break
}
}
if !isValid {
return fmt.Errorf("container dependency status must be one of %s", english.WordSeries([]string{dependsOnStart, dependsOnComplete, dependsOnSuccess, dependsOnHealthy}, "or"))
}
}
return nil
}
// validate returns nil if BuildArgsOrString is configured correctly.
func (b BuildArgsOrString) validate() error {
if b.isEmpty() {
return nil
}
if !b.BuildArgs.isEmpty() {
return b.BuildArgs.validate()
}
return nil
}
// validate returns nil if DockerBuildArgs is configured correctly.
func (DockerBuildArgs) validate() error {
return nil
}
// validate returns nil if ContainerHealthCheck is configured correctly.
func (ContainerHealthCheck) validate() error {
return nil
}
// validate returns nil if ImageOverride is configured correctly.
func (i ImageOverride) validate() error {
var err error
if err = i.EntryPoint.validate(); err != nil {
return fmt.Errorf(`validate "entrypoint": %w`, err)
}
if err = i.Command.validate(); err != nil {
return fmt.Errorf(`validate "command": %w`, err)
}
return nil
}
// validate returns nil if EntryPointOverride is configured correctly.
func (EntryPointOverride) validate() error {
return nil
}
// validate returns nil if CommandOverride is configured correctly.
func (CommandOverride) validate() error {
return nil
}
// validate returns nil if HTTP is configured correctly.
func (r HTTP) validate() error {
if r.IsEmpty() {
return nil
}
// we consider the fact that primary routing rule is mandatory before you write any additional routing rules.
if err := r.Main.validate(); err != nil {
return err
}
if r.Main.TargetContainer != nil && r.TargetContainerCamelCase != nil {
return &errFieldMutualExclusive{
firstField: "target_container",
secondField: "targetContainer",
}
}
for idx, rule := range r.AdditionalRoutingRules {
if err := rule.validate(); err != nil {
return fmt.Errorf(`validate "additional_rules[%d]": %w`, idx, err)
}
}
return nil
}
// validate returns nil if HTTPOrBool is configured correctly.
func (r HTTPOrBool) validate() error {
if r.Disabled() {
return nil
}
return r.HTTP.validate()
}
func (l LoadBalancedWebServiceConfig) validateGracePeriod() error {
gracePeriodForALB, err := l.validateGracePeriodForALB()
if err != nil {
return err
}
gracePeriodForNLB, err := l.validateGracePeriodForNLB()
if err != nil {
return err
}
if gracePeriodForALB && gracePeriodForNLB {
return &errGracePeriodsInBothALBAndNLB{
errFieldMutualExclusive: errFieldMutualExclusive{
firstField: "http.healthcheck.grace_period",
secondField: "nlb.healthcheck.grace_period",
},
}
}
return nil
}
// validateGracePeriodForALB validates if ALB has grace period mentioned in their additional listeners rules.
func (cfg *LoadBalancedWebServiceConfig) validateGracePeriodForALB() (bool, error) {
var exist bool
if cfg.HTTPOrBool.Main.HealthCheck.Advanced.GracePeriod != nil {
exist = true
}
for idx, rule := range cfg.HTTPOrBool.AdditionalRoutingRules {
if rule.HealthCheck.Advanced.GracePeriod != nil {
return exist, &errGracePeriodSpecifiedInAdditionalRule{
index: idx,
}
}
}
return exist, nil
}
// validateGracePeriodForNLB validates if NLB has grace period mentioned in their additional listeners.
func (cfg *LoadBalancedWebServiceConfig) validateGracePeriodForNLB() (bool, error) {
var exist bool
if cfg.NLBConfig.Listener.HealthCheck.GracePeriod != nil {
exist = true
}
for idx, listener := range cfg.NLBConfig.AdditionalListeners {
if listener.HealthCheck.GracePeriod != nil {
return exist, &errGracePeriodSpecifiedInAdditionalListener{
index: idx,
}
}
}
return exist, nil
}
// validate returns nil if HTTP is configured correctly.
func (r RoutingRule) validate() error {
if r.Path == nil {
return &errFieldMustBeSpecified{
missingField: "path",
}
}
if err := r.HealthCheck.validate(); err != nil {
return fmt.Errorf(`validate "healthcheck": %w`, err)
}
if err := r.Alias.validate(); err != nil {
return fmt.Errorf(`validate "alias": %w`, err)
}
for ind, ip := range r.AllowedSourceIps {
if err := ip.validate(); err != nil {
return fmt.Errorf(`validate "allowed_source_ips[%d]": %w`, ind, err)
}
}
if r.ProtocolVersion != nil {
if !slices.Contains(httpProtocolVersions, strings.ToUpper(*r.ProtocolVersion)) {
return fmt.Errorf(`"version" field value '%s' must be one of %s`, *r.ProtocolVersion, english.WordSeries(httpProtocolVersions, "or"))
}
}
if r.HostedZone != nil && r.Alias.IsEmpty() {
return &errFieldMustBeSpecified{
missingField: "alias",
conditionalFields: []string{"hosted_zone"},
}
}
if err := r.validateConditionValuesPerRule(); err != nil {
return fmt.Errorf("validate condition values per listener rule: %w", err)
}
return nil
}
// validate returns nil if HTTPHealthCheckArgs is configured correctly.
func (h HTTPHealthCheckArgs) validate() error {
return nil
}
// validate returns nil if NLBHealthCheckArgs is configured correctly.
func (h NLBHealthCheckArgs) validate() error {
if h.isEmpty() {
return nil
}
return nil
}
// validate returns nil if Alias is configured correctly.
func (a Alias) validate() error {
if a.IsEmpty() {
return nil
}
if err := a.StringSliceOrString.validate(); err != nil {
return err
}
for _, alias := range a.AdvancedAliases {
if err := alias.validate(); err != nil {
return err
}
}
return nil
}
// validate returns nil if AdvancedAlias is configured correctly.
func (a AdvancedAlias) validate() error {
if a.Alias == nil {
return &errFieldMustBeSpecified{
missingField: "name",
}
}
return nil
}
// validate is a no-op for StringSliceOrString.
func (StringSliceOrString) validate() error {
return nil
}
// validate returns nil if IPNet is configured correctly.
func (ip IPNet) validate() error {
if _, _, err := net.ParseCIDR(string(ip)); err != nil {
return fmt.Errorf("parse IPNet %s: %w", string(ip), err)
}
return nil
}
// validate returns nil if NetworkLoadBalancerConfiguration is configured correctly.
func (c NetworkLoadBalancerConfiguration) validate() error {
if c.IsEmpty() {
return nil
}
if err := c.Listener.validate(); err != nil {
return err
}
if err := c.Aliases.validate(); err != nil {
return fmt.Errorf(`validate "alias": %w`, err)
}
if !c.Aliases.IsEmpty() {
for _, advancedAlias := range c.Aliases.AdvancedAliases {
if advancedAlias.HostedZone != nil {
return fmt.Errorf(`"hosted_zone" is not supported for Network Load Balancer`)
}
}
}
for idx, listener := range c.AdditionalListeners {
if err := listener.validate(); err != nil {
return fmt.Errorf(`validate "additional_listeners[%d]": %w`, idx, err)
}
}
return nil
}
func (c NetworkLoadBalancerListener) validate() error {
if aws.StringValue(c.Port) == "" {
return &errFieldMustBeSpecified{
missingField: "port",
}
}
if err := validateNLBPort(c.Port); err != nil {
return fmt.Errorf(`validate "port": %w`, err)
}
if err := c.HealthCheck.validate(); err != nil {
return fmt.Errorf(`validate "healthcheck": %w`, err)
}
return nil
}
func validateNLBPort(port *string) error {
_, protocol, err := ParsePortMapping(port)
if err != nil {
return err
}
if protocol == nil {
return nil
}
protocolVal := aws.StringValue(protocol)
var isValidProtocol bool
for _, valid := range nlbValidProtocols {
if strings.EqualFold(protocolVal, valid) {
isValidProtocol = true
break
}
}
if !isValidProtocol {
return fmt.Errorf(`invalid protocol %s; valid protocols include %s`, protocolVal, english.WordSeries(nlbValidProtocols, "and"))
}
return nil
}
// validate returns nil if TaskConfig is configured correctly.
func (t TaskConfig) validate() error {
var err error
if err = t.Platform.validate(); err != nil {
return fmt.Errorf(`validate "platform": %w`, err)
}
if err = t.Count.validate(); err != nil {
return fmt.Errorf(`validate "count": %w`, err)
}
if err = t.ExecuteCommand.validate(); err != nil {
return fmt.Errorf(`validate "exec": %w`, err)
}
if err = t.Storage.validate(); err != nil {
return fmt.Errorf(`validate "storage": %w`, err)
}
for n, v := range t.Variables {
if err := v.validate(); err != nil {
return fmt.Errorf(`validate %q "variables": %w`, n, err)
}
}
for _, v := range t.Secrets {
if err := v.validate(); err != nil {
return fmt.Errorf(`validate "secret": %w`, err)
}
}
if t.EnvFile != nil {
envFile := aws.StringValue(t.EnvFile)
if filepath.Ext(envFile) != envFileExt {
return fmt.Errorf("environment file %s must have a %s file extension", envFile, envFileExt)
}
}
return nil
}
// validate returns nil if PlatformArgsOrString is configured correctly.
func (p PlatformArgsOrString) validate() error {
if p.IsEmpty() {
return nil
}
if !p.PlatformArgs.isEmpty() {
return p.PlatformArgs.validate()
}
if p.PlatformString != nil {
return p.PlatformString.validate()
}
return nil
}
// validate returns nil if PlatformArgs is configured correctly.
func (p PlatformArgs) validate() error {
if !p.bothSpecified() {
return errors.New(`fields "osfamily" and "architecture" must either both be specified or both be empty`)
}
var ss []string
for _, p := range validAdvancedPlatforms {
ss = append(ss, p.String())
}
prettyValidPlatforms := strings.Join(ss, ", ")
os := strings.ToLower(aws.StringValue(p.OSFamily))
arch := strings.ToLower(aws.StringValue(p.Arch))
for _, vap := range validAdvancedPlatforms {
if os == aws.StringValue(vap.OSFamily) && arch == aws.StringValue(vap.Arch) {
return nil
}
}
return fmt.Errorf("platform pair %s is invalid: fields ('osfamily', 'architecture') must be one of %s", p.String(), prettyValidPlatforms)
}
// validate returns nil if PlatformString is configured correctly.
func (p PlatformString) validate() error {
args := strings.Split(string(p), "/")
if len(args) != 2 {
return fmt.Errorf("platform '%s' must be in the format [OS]/[Arch]", string(p))
}
for _, validPlatform := range validShortPlatforms {
if strings.ToLower(string(p)) == validPlatform {
return nil
}
}
return fmt.Errorf("platform '%s' is invalid; %s: %s", p, english.PluralWord(len(validShortPlatforms), "the valid platform is", "valid platforms are"), english.WordSeries(validShortPlatforms, "and"))
}
// validate returns nil if Count is configured correctly.
func (c Count) validate() error {
return c.AdvancedCount.validate()
}
// validate returns nil if AdvancedCount is configured correctly.
func (a AdvancedCount) validate() error {
if a.IsEmpty() {
return nil
}
if len(a.validScalingFields()) == 0 {
return fmt.Errorf("cannot have autoscaling options for workloads of type '%s'", a.workloadType)
}
// validate if incorrect autoscaling fields are set
if fields := a.getInvalidFieldsSet(); fields != nil {
return &errInvalidAutoscalingFieldsWithWkldType{
invalidFields: fields,
workloadType: a.workloadType,
}
}
// validate spot and remaining autoscaling fields.
if a.Spot != nil && a.hasAutoscaling() {
return &errFieldMutualExclusive{
firstField: "spot",
secondField: fmt.Sprintf("range/%s", strings.Join(a.validScalingFields(), "/")),
}
}
if err := a.Range.validate(); err != nil {
return fmt.Errorf(`validate "range": %w`, err)
}
// validate combinations with "range".
if a.Range.IsEmpty() && a.hasScalingFieldsSet() {
return &errFieldMustBeSpecified{
missingField: "range",
conditionalFields: a.validScalingFields(),
}
}
if !a.Range.IsEmpty() && !a.hasScalingFieldsSet() {
return &errAtLeastOneFieldMustBeSpecified{
missingFields: a.validScalingFields(),
conditionalField: "range",
}
}
// validate combinations with cooldown
if !a.Cooldown.IsEmpty() && !a.hasScalingFieldsSet() {
return &errAtLeastOneFieldMustBeSpecified{
missingFields: a.validScalingFields(),
conditionalField: "cooldown",
}
}
// validate individual custom autoscaling options.
if err := a.QueueScaling.validate(); err != nil {
return fmt.Errorf(`validate "queue_delay": %w`, err)
}
if err := a.CPU.validate(); err != nil {
return fmt.Errorf(`validate "cpu_percentage": %w`, err)
}
if err := a.Memory.validate(); err != nil {
return fmt.Errorf(`validate "memory_percentage": %w`, err)
}
return nil
}
// validate returns nil if Percentage is configured correctly.
func (p Percentage) validate() error {
if val := int(p); val < 0 || val > 100 {
return fmt.Errorf("percentage value %v must be an integer from 0 to 100", val)
}
return nil
}
// validate returns nil if ScalingConfigOrT is configured correctly.
func (r ScalingConfigOrT[_]) validate() error {
if r.IsEmpty() {
return nil
}
if r.Value != nil {
switch any(r.Value).(type) {
case *Percentage:
return any(r.Value).(*Percentage).validate()
default:
return nil
}
}
return r.ScalingConfig.validate()
}
// validate returns nil if AdvancedScalingConfig is configured correctly.
func (r AdvancedScalingConfig[_]) validate() error {
if r.IsEmpty() {
return nil
}
switch any(r.Value).(type) {
case *Percentage:
if err := any(r.Value).(*Percentage).validate(); err != nil {
return err
}
}
return r.Cooldown.validate()
}
// Validation is a no-op for Cooldown.
func (c Cooldown) validate() error {
return nil
}
// validate returns nil if QueueScaling is configured correctly.
func (qs QueueScaling) validate() error {
if qs.IsEmpty() {
return nil
}
if qs.AcceptableLatency == nil && qs.AvgProcessingTime != nil {
return &errFieldMustBeSpecified{
missingField: "acceptable_latency",
conditionalFields: []string{"msg_processing_time"},
}
}
if qs.AvgProcessingTime == nil && qs.AcceptableLatency != nil {
return &errFieldMustBeSpecified{
missingField: "msg_processing_time",
conditionalFields: []string{"acceptable_latency"},
}
}
latency, process := *qs.AcceptableLatency, *qs.AvgProcessingTime
if latency == 0 {
return errors.New(`"acceptable_latency" cannot be 0`)
}
if process == 0 {
return errors.New(`"msg_processing_time" cannot be 0`)
}
if process > latency {
return errors.New(`"msg_processing_time" cannot be longer than "acceptable_latency"`)
}
return qs.Cooldown.validate()
}
// validate returns nil if Range is configured correctly.
func (r Range) validate() error {
if r.IsEmpty() {
return nil
}
if !r.RangeConfig.IsEmpty() {
return r.RangeConfig.validate()
}
return r.Value.validate()
}
type errInvalidRange struct {
value string
validFormat string
}
func (e *errInvalidRange) Error() string {
return fmt.Sprintf("invalid range value %s: valid format is %s", e.value, e.validFormat)
}
// validate returns nil if IntRangeBand is configured correctly.
func (r IntRangeBand) validate() error {
str := string(r)
minMax := intRangeBandRegexp.FindStringSubmatch(str)
// Valid minMax example: ["1-2", "1", "2"]
if len(minMax) != 3 {
return &errInvalidRange{
value: str,
validFormat: "${min}-${max}",
}
}
// Guaranteed by intRangeBandRegexp.
min, err := strconv.Atoi(minMax[1])
if err != nil {
return err
}
max, err := strconv.Atoi(minMax[2])
if err != nil {
return err
}
if min <= max {
return nil
}
return &errMinGreaterThanMax{
min: min,
max: max,
}
}
// validate returns nil if RangeConfig is configured correctly.
func (r RangeConfig) validate() error {
if r.Min == nil || r.Max == nil {
return &errFieldMustBeSpecified{
missingField: "min/max",
}
}
min, max, spotFrom := aws.IntValue(r.Min), aws.IntValue(r.Max), aws.IntValue(r.SpotFrom)
if min < 0 || max < 0 || spotFrom < 0 {
return &errRangeValueLessThanZero{
min: min,
max: max,
spotFrom: spotFrom,
}
}
if min <= max {
return nil
}
return &errMinGreaterThanMax{
min: min,
max: max,
}
}
// validate returns nil if ExecuteCommand is configured correctly.
func (e ExecuteCommand) validate() error {
if !e.Config.IsEmpty() {
return e.Config.validate()
}
return nil
}
// validate returns nil if ExecuteCommandConfig is configured correctly.
func (ExecuteCommandConfig) validate() error {
return nil
}
// validate returns nil if Storage is configured correctly.
func (s Storage) validate() error {
if s.IsEmpty() {
return nil
}
if s.Ephemeral != nil {
ephemeral := aws.IntValue(s.Ephemeral)
if ephemeral < ephemeralMinValueGiB || ephemeral > ephemeralMaxValueGiB {
return fmt.Errorf(`validate "ephemeral": ephemeral storage must be between 20 GiB and 200 GiB`)
}
}
var hasManagedVolume bool
for k, v := range s.Volumes {
if err := v.validate(); err != nil {
return fmt.Errorf(`validate "volumes[%s]": %w`, k, err)
}
if !v.EmptyVolume() && v.EFS.UseManagedFS() {
if hasManagedVolume {
return fmt.Errorf("cannot specify more than one managed volume per service")
}
hasManagedVolume = true
}
}
return nil
}
// validate returns nil if Volume is configured correctly.
func (v Volume) validate() error {
if err := v.EFS.validate(); err != nil {
return fmt.Errorf(`validate "efs": %w`, err)
}
return v.MountPointOpts.validate()
}
// validate returns nil if MountPointOpts is configured correctly.
func (m MountPointOpts) validate() error {
path := aws.StringValue(m.ContainerPath)
if path == "" {
return &errFieldMustBeSpecified{
missingField: "path",
}
}
if err := validateVolumePath(path); err != nil {
return fmt.Errorf(`validate "path": %w`, err)
}
return nil
}
// validate returns nil if EFSConfigOrBool is configured correctly.
func (e EFSConfigOrBool) validate() error {
if e.IsEmpty() {
return nil
}
return e.Advanced.validate()
}
// validate returns nil if EFSVolumeConfiguration is configured correctly.
func (e EFSVolumeConfiguration) validate() error {
if e.IsEmpty() {
return nil
}
if !e.EmptyBYOConfig() && !e.EmptyUIDConfig() {
return &errFieldMutualExclusive{
firstField: "uid/gid",
secondField: "id/root_dir/auth",
}
}
if e.UID != nil && e.GID == nil {
return &errFieldMustBeSpecified{
missingField: "gid",
conditionalFields: []string{"uid"},
}
}
if e.UID == nil && e.GID != nil {
return &errFieldMustBeSpecified{
missingField: "uid",
conditionalFields: []string{"gid"},
}
}
if e.UID != nil && *e.UID == 0 {
return fmt.Errorf(`"uid" must not be 0`)
}
if err := e.AuthConfig.validate(); err != nil {
return fmt.Errorf(`validate "auth": %w`, err)
}
if e.AuthConfig.AccessPointID != nil {
if (aws.StringValue(e.RootDirectory) == "" || aws.StringValue(e.RootDirectory) == "/") &&
(e.AuthConfig.IAM == nil || aws.BoolValue(e.AuthConfig.IAM)) {
return nil
}
return fmt.Errorf(`"root_dir" must be either empty or "/" and "auth.iam" must be true when "access_point_id" is used`)
}
if e.RootDirectory != nil {
if err := validateVolumePath(aws.StringValue(e.RootDirectory)); err != nil {
return fmt.Errorf(`validate "root_dir": %w`, err)
}
}
return nil
}
// validate returns nil if AuthorizationConfig is configured correctly.
func (a AuthorizationConfig) validate() error {
if a.IsEmpty() {
return nil
}
return nil
}
// validate returns nil if Logging is configured correctly.
func (l Logging) validate() error {
if l.IsEmpty() {
return nil
}
if l.EnvFile != nil {
envFile := aws.StringValue(l.EnvFile)
if filepath.Ext(envFile) != envFileExt {
return fmt.Errorf("environment file %s must have a %s file extension", envFile, envFileExt)
}
}
return nil
}
// validate returns nil if SidecarConfig is configured correctly.
func (s SidecarConfig) validate() error {
if err := s.validateImage(); err != nil {
return err
}
for ind, mp := range s.MountPoints {
if err := mp.validate(); err != nil {
return fmt.Errorf(`validate "mount_points[%d]": %w`, ind, err)
}
}
_, protocol, err := ParsePortMapping(s.Port)
if err != nil {
return err
}
if protocol != nil {
protocolVal := aws.StringValue(protocol)
var isValidProtocol bool
for _, valid := range validContainerProtocols {
if strings.EqualFold(protocolVal, valid) {
isValidProtocol = true
break
}
}
if !isValidProtocol {
return fmt.Errorf(`invalid protocol %s; valid protocols include %s`, protocolVal, english.WordSeries(validContainerProtocols, "and"))
}
}
if err := s.HealthCheck.validate(); err != nil {
return fmt.Errorf(`validate "healthcheck": %w`, err)
}
if err := s.DependsOn.validate(); err != nil {
return fmt.Errorf(`validate "depends_on": %w`, err)
}
if s.EnvFile != nil {
envFile := aws.StringValue(s.EnvFile)
if filepath.Ext(envFile) != envFileExt {
return fmt.Errorf("environment file %s must have a %s file extension", envFile, envFileExt)
}
}
return s.ImageOverride.validate()
}
func (s SidecarConfig) validateImage() error {
if s.Image.IsZero() {
return fmt.Errorf(`must specify one of "image", "image.build, or "image.location"`)
}
if err := s.Image.validate(); err != nil {
return fmt.Errorf(`validate "image": %w`, err)
}
return nil
}
// validate returns nil if SidecarMountPoint is configured correctly.
func (s SidecarMountPoint) validate() error {
if aws.StringValue(s.SourceVolume) == "" {
return &errFieldMustBeSpecified{
missingField: "source_volume",
}
}
return s.MountPointOpts.validate()
}
// validate returns nil if NetworkConfig is configured correctly.
func (n NetworkConfig) validate() error {
if n.IsEmpty() {
return nil
}
if err := n.VPC.validate(); err != nil {
return fmt.Errorf(`validate "vpc": %w`, err)
}
if err := n.Connect.validate(); err != nil {
return fmt.Errorf(`validate "connect": %w`, err)
}
return nil
}
// validate returns nil if ServiceConnectBoolOrArgs is configured correctly.
func (s ServiceConnectBoolOrArgs) validate() error {
return s.ServiceConnectArgs.validate()
}
// validate is a no-op for ServiceConnectArgs.
func (ServiceConnectArgs) validate() error {
return nil
}
// validate returns nil if RequestDrivenWebServiceNetworkConfig is configured correctly.
func (n RequestDrivenWebServiceNetworkConfig) validate() error {
if n.IsEmpty() {
return nil
}
if err := n.VPC.validate(); err != nil {
return fmt.Errorf(`validate "vpc": %w`, err)
}
return nil
}
// validate returns nil if rdwsVpcConfig is configured correctly.
func (v rdwsVpcConfig) validate() error {
if v.isEmpty() {
return nil
}
if err := v.Placement.validate(); err != nil {
return fmt.Errorf(`validate "placement": %w`, err)
}
return nil
}
// validate returns nil if vpcConfig is configured correctly.
func (v vpcConfig) validate() error {
if v.isEmpty() {
return nil
}
if err := v.Placement.validate(); err != nil {
return fmt.Errorf(`validate "placement": %w`, err)
}
if err := v.SecurityGroups.validate(); err != nil {
return fmt.Errorf(`validate "security_groups": %w`, err)
}
return nil
}
// validate returns nil if PlacementArgOrString is configured correctly.
func (p PlacementArgOrString) validate() error {
if p.IsEmpty() {
return nil
}
if p.PlacementString != nil {
return p.PlacementString.validate()
}
return p.PlacementArgs.validate()
}
// validate returns nil if PlacementArgs is configured correctly.
func (p PlacementArgs) validate() error {
if !p.Subnets.isEmpty() {
return p.Subnets.validate()
}
return nil
}
// validate returns nil if SubnetArgs is configured correctly.
func (s SubnetArgs) validate() error {
if s.isEmpty() {
return nil
}
return s.FromTags.validate()
}
// validate returns nil if Tags is configured correctly.
func (t Tags) validate() error {
for _, v := range t {
if err := v.validate(); err != nil {
return err
}
}
return nil
}
// validate returns nil if PlacementString is configured correctly.
func (p PlacementString) validate() error {
if string(p) == "" {
return fmt.Errorf(`"placement" cannot be empty`)
}
for _, allowed := range subnetPlacements {
if string(p) == allowed {
return nil
}
}
return fmt.Errorf(`"placement" %s must be one of %s`, string(p), strings.Join(subnetPlacements, ", "))
}
// validate is a no-op for SecurityGroupsIDsOrConfig.
func (s SecurityGroupsIDsOrConfig) validate() error {
if s.isEmpty() {
return nil
}
return s.AdvancedConfig.validate()
}
// validate is a no-op for SecurityGroupsConfig.
func (SecurityGroupsConfig) validate() error {
return nil
}
// validate returns nil if AppRunnerInstanceConfig is configured correctly.
func (r AppRunnerInstanceConfig) validate() error {
if err := r.Platform.validate(); err != nil {
return fmt.Errorf(`validate "platform": %w`, err)
}
// Error out if user added Windows as platform in manifest.
if isWindowsPlatform(r.Platform) {
return ErrAppRunnerInvalidPlatformWindows
}
// This extra check is because ARM architectures won't work for App Runner services.
if !r.Platform.IsEmpty() {
if r.Platform.Arch() != ArchAMD64 || r.Platform.Arch() != ArchX86 {
return fmt.Errorf("App Runner services can only build on %s and %s architectures", ArchAMD64, ArchX86)
}
}
return nil
}
// validate returns nil if RequestDrivenWebServiceHttpConfig is configured correctly.
func (r RequestDrivenWebServiceHttpConfig) validate() error {
if err := r.HealthCheckConfiguration.validate(); err != nil {
return err
}
return r.Private.validate()
}
func (v VPCEndpoint) validate() error {
return nil
}
// validate returns nil if Observability is configured correctly.
func (o Observability) validate() error {
if o.isEmpty() {
return nil
}
for _, validVendor := range tracingValidVendors {
if strings.EqualFold(aws.StringValue(o.Tracing), validVendor) {
return nil
}
}
return fmt.Errorf("invalid tracing vendor %s: %s %s",
aws.StringValue(o.Tracing),
english.PluralWord(len(tracingValidVendors), "the valid vendor is", "valid vendors are"),
english.WordSeries(tracingValidVendors, "and"))
}
// validate returns nil if JobTriggerConfig is configured correctly.
func (c JobTriggerConfig) validate() error {
if c.Schedule == nil {
return &errFieldMustBeSpecified{
missingField: "schedule",
}
}
return nil
}
// validate returns nil if JobFailureHandlerConfig is configured correctly.
func (JobFailureHandlerConfig) validate() error {
return nil
}
// validate returns nil if PublishConfig is configured correctly.
func (p PublishConfig) validate() error {
for ind, topic := range p.Topics {
if err := topic.validate(); err != nil {
return fmt.Errorf(`validate "topics[%d]": %w`, ind, err)
}
}
return nil
}
// validate returns nil if Topic is configured correctly.
func (t Topic) validate() error {
if err := validatePubSubName(aws.StringValue(t.Name)); err != nil {
return err
}
return t.FIFO.validate()
}
// validate returns nil if FIFOTopicAdvanceConfigOrBool is configured correctly.
func (f FIFOTopicAdvanceConfigOrBool) validate() error {
if f.IsEmpty() {
return nil
}
return f.Advanced.validate()
}
// validate returns nil if FIFOTopicAdvanceConfig is configured correctly.
func (a FIFOTopicAdvanceConfig) validate() error {
return nil
}
// validate returns nil if SubscribeConfig is configured correctly.
func (s SubscribeConfig) validate() error {
if s.IsEmpty() {
return nil
}
for ind, topic := range s.Topics {
if err := topic.validate(); err != nil {
return fmt.Errorf(`validate "topics[%d]": %w`, ind, err)
}
}
if err := s.Queue.validate(); err != nil {
return fmt.Errorf(`validate "queue": %w`, err)
}
return nil
}
// validate returns nil if TopicSubscription is configured correctly.
func (t TopicSubscription) validate() error {
if err := validatePubSubName(aws.StringValue(t.Name)); err != nil {
return err
}
svcName := aws.StringValue(t.Service)
if svcName == "" {
return &errFieldMustBeSpecified{
missingField: "service",
}
}
if !isValidSubSvcName(svcName) {
return fmt.Errorf("service name must start with a letter, contain only lower-case letters, numbers, and hyphens, and have no consecutive or trailing hyphen")
}
if err := t.Queue.validate(); err != nil {
return fmt.Errorf(`validate "queue": %w`, err)
}
return nil
}
// validate returns nil if SQSQueue is configured correctly.
func (q SQSQueueOrBool) validate() error {
if q.IsEmpty() {
return nil
}
return q.Advanced.validate()
}
// validate returns nil if SQSQueue is configured correctly.
func (q SQSQueue) validate() error {
if q.IsEmpty() {
return nil
}
if err := q.DeadLetter.validate(); err != nil {
return fmt.Errorf(`validate "dead_letter": %w`, err)
}
return q.FIFO.validate()
}
// validate returns nil if FIFOAdvanceConfig is configured correctly.
func (q FIFOAdvanceConfig) validate() error {
if q.IsEmpty() {
return nil
}
if err := q.validateHighThroughputFIFO(); err != nil {
return err
}
if err := q.validateDeduplicationScope(); err != nil {
return err
}
if err := q.validateFIFOThroughputLimit(); err != nil {
return err
}
if aws.StringValue(q.FIFOThroughputLimit) == sqsFIFOThroughputLimitPerMessageGroupID && aws.StringValue(q.DeduplicationScope) == sqsDeduplicationScopeQueue {
return fmt.Errorf(`"throughput_limit" must be set to "perQueue" when "deduplication_scope" is set to "queue"`)
}
return nil
}
// validateFIFO returns nil if FIFOAdvanceConfigOrBool is configured correctly.
func (q FIFOAdvanceConfigOrBool) validate() error {
if q.IsEmpty() {
return nil
}
return q.Advanced.validate()
}
func (q FIFOAdvanceConfig) validateHighThroughputFIFO() error {
if q.HighThroughputFifo == nil {
return nil
}
if q.FIFOThroughputLimit != nil {
return &errFieldMutualExclusive{
firstField: "high_throughput",
secondField: "throughput_limit",
mustExist: false,
}
}
if q.DeduplicationScope != nil {
return &errFieldMutualExclusive{
firstField: "high_throughput",
secondField: "deduplication_scope",
mustExist: false,
}
}
return nil
}
func (q FIFOAdvanceConfig) validateDeduplicationScope() error {
if q.DeduplicationScope != nil && !slices.Contains(validSQSDeduplicationScopeValues, aws.StringValue(q.DeduplicationScope)) {
return fmt.Errorf(`validate "deduplication_scope": deduplication scope value must be one of %s`, english.WordSeries(validSQSDeduplicationScopeValues, "or"))
}
return nil
}
func (q FIFOAdvanceConfig) validateFIFOThroughputLimit() error {
if q.FIFOThroughputLimit != nil && !slices.Contains(validSQSFIFOThroughputLimitValues, aws.StringValue(q.FIFOThroughputLimit)) {
return fmt.Errorf(`validate "throughput_limit": fifo throughput limit value must be one of %s`, english.WordSeries(validSQSFIFOThroughputLimitValues, "or"))
}
return nil
}
// validate returns nil if DeadLetterQueue is configured correctly.
func (d DeadLetterQueue) validate() error {
if d.IsEmpty() {
return nil
}
return nil
}
// validate returns nil if OverrideRule is configured correctly.
func (r OverrideRule) validate() error {
for _, s := range invalidTaskDefOverridePathRegexp {
re := regexp.MustCompile(fmt.Sprintf(`^%s$`, s))
if re.MatchString(r.Path) {
return fmt.Errorf(`"%s" cannot be overridden with a custom value`, s)
}
}
return nil
}
// validate returns nil if Variable is configured correctly.
func (v Variable) validate() error {
if err := v.FromCFN.validate(); err != nil {
return fmt.Errorf(`validate "from_cfn": %w`, err)
}
return nil
}
// validate returns nil if StringOrFromCFN is configured correctly.
func (s StringOrFromCFN) validate() error {
if s.isEmpty() {
return nil
}
return s.FromCFN.validate()
}
// validate returns nil if fromCFN is configured correctly.
func (cfg fromCFN) validate() error {
if cfg.isEmpty() {
return nil
}
if len(aws.StringValue(cfg.Name)) == 0 {
return errors.New("name cannot be an empty string")
}
return nil
}
// validate is a no-op for Secrets.
func (s Secret) validate() error {
return nil
}
type validateHealthCheckPortsOpts struct {
exposedPorts ExposedPortsIndex
mainContainerPort *uint16
alb HTTP
nlb NetworkLoadBalancerConfiguration
}
type validateExposedPortsOpts struct {
mainContainerName string
mainContainerPort *uint16
alb *HTTP
nlb *NetworkLoadBalancerConfiguration
sidecarConfig map[string]*SidecarConfig
}
type validateDependenciesOpts struct {
mainContainerName string
sidecarConfig map[string]*SidecarConfig
imageConfig Image
logging Logging
}
type validateTargetContainerOpts struct {
mainContainerName string
mainContainerPort *uint16
targetContainer *string
sidecarConfig map[string]*SidecarConfig
}
type validateWindowsOpts struct {
readOnlyFS *bool
efsVolumes map[string]*Volume
}
func validateHealthCheckPorts(opts validateHealthCheckPortsOpts) error {
for _, rule := range opts.alb.RoutingRules() {
healthCheckPort := rule.HealthCheckPort(opts.mainContainerPort)
if err := validateHealthCheckPort(healthCheckPort, opts.exposedPorts); err != nil {
return err
}
}
for _, listener := range opts.nlb.NLBListeners() {
healthCheckPort, err := listener.HealthCheckPort(opts.mainContainerPort)
if err != nil {
return err
}
if err := validateHealthCheckPort(healthCheckPort, opts.exposedPorts); err != nil {
return err
}
}
return nil
}
func validateHealthCheckPort(port uint16, ports ExposedPortsIndex) error {
container := ports.ContainerForPort[port]
containerPorts := ports.PortsForContainer[container]
for _, exposedPort := range containerPorts {
if exposedPort.Port != port {
continue
}
if !slices.Contains(validHealthCheckProtocols, strings.ToUpper(exposedPort.Protocol)) {
return &errHealthCheckPortExposedWithInvalidProtocol{
healthCheckPort: port,
container: container,
protocol: exposedPort.Protocol,
}
}
}
return nil
}
func validateTargetContainer(opts validateTargetContainerOpts) error {
if opts.targetContainer == nil {
return nil
}
targetContainer := aws.StringValue(opts.targetContainer)
if targetContainer == opts.mainContainerName {
if opts.mainContainerPort == nil {
return fmt.Errorf("target container %q doesn't expose a port", targetContainer)
}
return nil
}
sidecar, ok := opts.sidecarConfig[targetContainer]
if !ok {
return fmt.Errorf("target container %q doesn't exist", targetContainer)
}
if sidecar.Port == nil {
return fmt.Errorf("target container %q doesn't expose a port", targetContainer)
}
return nil
}
func validateContainerDeps(opts validateDependenciesOpts) error {
containerDependencies := containerDependencies(opts.mainContainerName, opts.imageConfig, opts.logging, opts.sidecarConfig)
if err := validateDepsForEssentialContainers(containerDependencies); err != nil {
return err
}
return validateNoCircularDependencies(containerDependencies)
}
func validateDepsForEssentialContainers(deps map[string]ContainerDependency) error {
for name, containerDep := range deps {
for dep, status := range containerDep.DependsOn {
if !deps[dep].IsEssential {
continue
}
if err := validateEssentialContainerDependency(dep, strings.ToUpper(status)); err != nil {
return fmt.Errorf("validate %s container dependencies status: %w", name, err)
}
}
}
return nil
}
type containerNameAndProtocol struct {
containerName string
containerProtocol string
}
func validateExposedPorts(opts validateExposedPortsOpts) error {
portExposedTo := make(map[uint16]containerNameAndProtocol)
if err := validateAndPopulateSidecarContainerPorts(portExposedTo, opts); err != nil {
return err
}
if err := validateAndPopulateALBPorts(portExposedTo, opts); err != nil {
return err
}
if err := validateAndPopulateNLBPorts(portExposedTo, opts); err != nil {
return err
}
if err := validateAndPopulateMainContainerPort(portExposedTo, opts); err != nil {
return err
}
return nil
}
func validateAndPopulateMainContainerPort(portExposedTo map[uint16]containerNameAndProtocol, opts validateExposedPortsOpts) error {
if opts.mainContainerPort == nil {
return nil
}
targetPort := aws.Uint16Value(opts.mainContainerPort)
targetProtocol := defaultProtocol
if existingContainerNameAndProtocol, ok := portExposedTo[targetPort]; ok {
targetProtocol = existingContainerNameAndProtocol.containerProtocol
}
return validateAndPopulateExposedPortMapping(portExposedTo, targetPort, targetProtocol, opts.mainContainerName)
}
func validateAndPopulateSidecarContainerPorts(portExposedTo map[uint16]containerNameAndProtocol, opts validateExposedPortsOpts) error {
for name, sidecar := range opts.sidecarConfig {
if sidecar.Port == nil {
continue
}
sidecarPort, sidecarProtocol, err := ParsePortMapping(sidecar.Port)
if err != nil {
return err
}
parsedPort, err := strconv.ParseUint(aws.StringValue(sidecarPort), 10, 16)
if err != nil {
return err
}
protocol := defaultProtocol
if sidecarProtocol != nil {
protocol = aws.StringValue(sidecarProtocol)
}
if err = validateAndPopulateExposedPortMapping(portExposedTo, uint16(parsedPort), protocol, name); err != nil {
return err
}
}
return nil
}
func validateAndPopulateALBPorts(portExposedTo map[uint16]containerNameAndProtocol, opts validateExposedPortsOpts) error {
if opts.alb == nil || opts.alb.IsEmpty() {
return nil
}
alb := opts.alb
for _, rule := range alb.RoutingRules() {
if rule.TargetPort == nil {
continue
}
targetPort := aws.Uint16Value(rule.TargetPort)
// Prefer `http.target_container`, then existing exposed port mapping, then fallback on name of main container
targetContainer := opts.mainContainerName
if existingContainerNameAndProtocol, ok := portExposedTo[targetPort]; ok {
targetContainer = existingContainerNameAndProtocol.containerName
}
if rule.TargetContainer != nil {
targetContainer = aws.StringValue(rule.TargetContainer)
}
if err := validateAndPopulateExposedPortMapping(portExposedTo, targetPort, TCP, targetContainer); err != nil {
return err
}
}
return nil
}
func validateAndPopulateNLBPorts(portExposedTo map[uint16]containerNameAndProtocol, opts validateExposedPortsOpts) error {
if opts.nlb == nil || opts.nlb.IsEmpty() {
return nil
}
nlb := opts.nlb
if err := validateAndPopulateNLBListenerPorts(nlb.Listener, portExposedTo, opts.mainContainerName); err != nil {
return fmt.Errorf(`validate "nlb": %w`, err)
}
for idx, listener := range nlb.AdditionalListeners {
if err := validateAndPopulateNLBListenerPorts(listener, portExposedTo, opts.mainContainerName); err != nil {
return fmt.Errorf(`validate "nlb.additional_listeners[%d]": %w`, idx, err)
}
}
return nil
}
func validateAndPopulateNLBListenerPorts(listener NetworkLoadBalancerListener, portExposedTo map[uint16]containerNameAndProtocol, mainContainerName string) error {
nlbReceiverPort, nlbProtocol, err := ParsePortMapping(listener.Port)
if err != nil {
return err
}
port, err := strconv.ParseUint(aws.StringValue(nlbReceiverPort), 10, 16)
if err != nil {
return err
}
targetPort := uint16(port)
if listener.TargetPort != nil {
targetPort = uint16(aws.IntValue(listener.TargetPort))
}
// Prefer `nlb.port`, then fallback on default protocol
targetProtocol := defaultProtocol
if nlbProtocol != nil {
targetProtocol = strings.ToUpper(aws.StringValue(nlbProtocol))
}
// Handle TLS termination of container exposed port protocol
if targetProtocol == TLS {
targetProtocol = TCP
}
// Prefer `nlb.target_container`, then existing exposed port mapping, then fallback on name of main container
targetContainer := mainContainerName
if existingContainerNameAndProtocol, ok := portExposedTo[targetPort]; ok {
targetContainer = existingContainerNameAndProtocol.containerName
}
if listener.TargetContainer != nil {
targetContainer = aws.StringValue(listener.TargetContainer)
}
return validateAndPopulateExposedPortMapping(portExposedTo, targetPort, targetProtocol, targetContainer)
}
func validateAndPopulateExposedPortMapping(portExposedTo map[uint16]containerNameAndProtocol, targetPort uint16, targetProtocol string, targetContainer string) error {
exposedContainerAndProtocol, alreadyExposed := portExposedTo[targetPort]
targetProtocol = strings.ToUpper(targetProtocol)
// Port is not associated with container and protocol, populate map
if !alreadyExposed {
portExposedTo[targetPort] = containerNameAndProtocol{
containerName: targetContainer,
containerProtocol: targetProtocol,
}
return nil
}
exposedContainer := exposedContainerAndProtocol.containerName
exposedProtocol := exposedContainerAndProtocol.containerProtocol
if exposedContainer != targetContainer {
return &errContainersExposingSamePort{
firstContainer: targetContainer,
secondContainer: exposedContainer,
port: targetPort,
}
}
if exposedProtocol != targetProtocol {
return &errContainerPortExposedWithMultipleProtocol{
container: exposedContainer,
port: targetPort,
firstProtocol: targetProtocol,
secondProtocol: exposedProtocol,
}
}
return nil
}
func validateEssentialContainerDependency(name, status string) error {
for _, allowed := range essentialContainerDependsOnValidStatuses {
if status == allowed {
return nil
}
}
return fmt.Errorf("essential container %s can only have status %s", name, english.WordSeries([]string{dependsOnStart, dependsOnHealthy}, "or"))
}
func validateNoCircularDependencies(deps map[string]ContainerDependency) error {
dependencies, err := buildDependencyGraph(deps)
if err != nil {
return err
}
cycle, ok := dependencies.IsAcyclic()
if ok {
return nil
}
if len(cycle) == 1 {
return fmt.Errorf("container %s cannot depend on itself", cycle[0])
}
// Stabilize unit tests.
sort.SliceStable(cycle, func(i, j int) bool { return cycle[i] < cycle[j] })
return fmt.Errorf("circular container dependency chain includes the following containers: %s", cycle)
}
func buildDependencyGraph(deps map[string]ContainerDependency) (*graph.Graph[string], error) {
dependencyGraph := graph.New[string]()
for name, containerDep := range deps {
for dep := range containerDep.DependsOn {
if _, ok := deps[dep]; !ok {
return nil, fmt.Errorf("container %s does not exist", dep)
}
dependencyGraph.Add(graph.Edge[string]{
From: name,
To: dep,
})
}
}
return dependencyGraph, nil
}
// validate that paths contain only an approved set of characters to guard against command injection.
// We can accept 0-9A-Za-z-_.
func validateVolumePath(input string) error {
if len(input) == 0 {
return nil
}
m := volumesPathRegexp.FindStringSubmatch(input)
if len(m) == 0 {
return fmt.Errorf("path can only contain the characters a-zA-Z0-9.-_/")
}
return nil
}
func validatePubSubName(name string) error {
if name == "" {
return &errFieldMustBeSpecified{
missingField: "name",
}
}
// Name must contain letters, numbers, and can't use special characters besides underscores, and hyphens.
if !awsSNSTopicRegexp.MatchString(name) {
return fmt.Errorf(`"name" can only contain letters, numbers, underscores, and hyphens`)
}
return nil
}
func isValidSubSvcName(name string) bool {
if !awsNameRegexp.MatchString(name) {
return false
}
// Check for bad punctuation (no consecutive dashes or dots)
formatMatch := punctuationRegExp.FindStringSubmatch(name)
if len(formatMatch) != 0 {
return false
}
trailingMatch := trailingPunctRegExp.FindStringSubmatch(name)
return len(trailingMatch) == 0
}
func validateWindows(opts validateWindowsOpts) error {
if aws.BoolValue(opts.readOnlyFS) {
return fmt.Errorf(`%q can not be set to 'true' when deploying a Windows container`, "readonly_fs")
}
for _, volume := range opts.efsVolumes {
if !volume.EmptyVolume() {
return errors.New(`'EFS' is not supported when deploying a Windows container`)
}
}
return nil
}
// validate returns nil if ImageLocationOrBuild is configured correctly.
func (i ImageLocationOrBuild) validate() error {
if err := i.Build.validate(); err != nil {
return fmt.Errorf(`validate "build": %w`, err)
}
if i.Build.isEmpty() == (i.Location == nil) {
return &errFieldMutualExclusive{
firstField: "build",
secondField: "location",
mustExist: true,
}
}
return nil
}
func (r *RoutingRule) validateConditionValuesPerRule() error {
aliases, err := r.Alias.ToStringSlice()
if err != nil {
return fmt.Errorf("convert aliases to string slice: %w", err)
}
allowedSourceIps := make([]string, len(r.AllowedSourceIps))
for idx, ip := range r.AllowedSourceIps {
allowedSourceIps[idx] = string(ip)
}
if len(aliases)+len(allowedSourceIps) >= maxConditionsPerRule {
return &errMaxConditionValuesPerRule{
path: aws.StringValue(r.Path),
aliases: aliases,
allowedSourceIps: allowedSourceIps,
}
}
return nil
}
type errMaxConditionValuesPerRule struct {
path string
aliases []string
allowedSourceIps []string
}
func (e *errMaxConditionValuesPerRule) Error() string {
return fmt.Sprintf("listener rule has more than five conditions %s %s", english.WordSeries(e.aliases, "and"),
english.WordSeries(e.allowedSourceIps, "and"))
}
func (e *errMaxConditionValuesPerRule) RecommendActions() string {
cgList := e.generateConditionGroups()
var fmtListenerRules strings.Builder
fmtListenerRules.WriteString(fmt.Sprintf(`http:
path: %s
alias: %s
allowed_source_ips: %s
additional_rules:`, e.path, fmtStringArray(cgList[0].aliases), fmtStringArray(cgList[0].allowedSourceIps)))
for i := 1; i < len(cgList); i++ {
fmtListenerRules.WriteString(fmt.Sprintf(`
- path: %s
alias: %s
allowed_source_ips: %s`, e.path, fmtStringArray(cgList[i].aliases), fmtStringArray(cgList[i].allowedSourceIps)))
}
return fmt.Sprintf(`You can split the "alias" and "allowed_source_ips" field into separate rules, so that each rule contains up to 5 values:
%s`, color.HighlightCodeBlock(fmtListenerRules.String()))
}
func fmtStringArray(arr []string) string {
return fmt.Sprintf("[%s]", strings.Join(arr, ","))
}
// conditionGroup represents groups of conditions per listener rule.
type conditionGroup struct {
allowedSourceIps []string
aliases []string
}
func (e *errMaxConditionValuesPerRule) generateConditionGroups() []conditionGroup {
remaining := calculateRemainingConditions(e.path)
if len(e.aliases) != 0 && len(e.allowedSourceIps) != 0 {
return e.generateConditionsWithSourceIPsAndAlias(remaining)
}
if len(e.aliases) != 0 {
return e.generateConditionsWithAliasOnly(remaining)
}
return e.generateConditionWithSourceIPsOnly(remaining)
}
func calculateRemainingConditions(path string) int {
rcPerRule := maxConditionsPerRule
if path != rootPath {
return rcPerRule - 2
}
return rcPerRule - 1
}
func (e *errMaxConditionValuesPerRule) generateConditionsWithSourceIPsAndAlias(remaining int) []conditionGroup {
var groups []conditionGroup
for i := 0; i < len(e.allowedSourceIps); i++ {
var group conditionGroup
group.allowedSourceIps = []string{e.allowedSourceIps[i]}
groups = append(groups, e.generateConditionsGroups(remaining-1, true, group)...)
}
return groups
}
func (e *errMaxConditionValuesPerRule) generateConditionsWithAliasOnly(remaining int) []conditionGroup {
var group conditionGroup
return e.generateConditionsGroups(remaining, true, group)
}
func (e *errMaxConditionValuesPerRule) generateConditionWithSourceIPsOnly(remaining int) []conditionGroup {
var group conditionGroup
return e.generateConditionsGroups(remaining, false, group)
}
func (e *errMaxConditionValuesPerRule) generateConditionsGroups(remaining int, isAlias bool, group conditionGroup) []conditionGroup {
var groups []conditionGroup
var conditions []string
if isAlias {
conditions = e.aliases
} else {
conditions = e.allowedSourceIps
}
for i := 0; i < len(conditions); i += remaining {
end := i + remaining
if end > len(conditions) {
end = len(conditions)
}
if isAlias {
group.aliases = conditions[i:end]
groups = append(groups, group)
continue
}
group.allowedSourceIps = conditions[i:end]
groups = append(groups, group)
}
return groups
}