Skip to content

Commit 4b9ee59

Browse files
authored
chore(manifest): move platform validation and container dependencies (#2869)
<!-- Provide summary of changes --> part of #2818 <!-- Issue number, if available. E.g. "Fixes #31", "Addresses #42, 77" --> By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the Apache 2.0 License.
1 parent e6adb90 commit 4b9ee59

File tree

14 files changed

+805
-117
lines changed

14 files changed

+805
-117
lines changed

internal/pkg/deploy/cloudformation/stack/transformers_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1490,7 +1490,7 @@ func Test_convertImageDependsOn(t *testing.T) {
14901490
Essential: aws.Bool(false),
14911491
},
14921492
},
1493-
wantedError: errInvalidSidecarDependsOnStatus,
1493+
wantedError: errInvalidDependsOnStatus,
14941494
},
14951495
"invalid implied essential container depdendency": {
14961496
inImage: &manifest.Image{
@@ -1501,7 +1501,7 @@ func Test_convertImageDependsOn(t *testing.T) {
15011501
inSidecars: map[string]*manifest.SidecarConfig{
15021502
"sidecar": {},
15031503
},
1504-
wantedError: errEssentialSidecarStatus,
1504+
wantedError: errEssentialContainerStatus,
15051505
},
15061506
"invalid set essential container depdendency": {
15071507
inImage: &manifest.Image{
@@ -1514,7 +1514,7 @@ func Test_convertImageDependsOn(t *testing.T) {
15141514
Essential: aws.Bool(true),
15151515
},
15161516
},
1517-
wantedError: errEssentialSidecarStatus,
1517+
wantedError: errEssentialContainerStatus,
15181518
},
15191519
"good essential container dependency": {
15201520
inImage: &manifest.Image{

internal/pkg/deploy/cloudformation/stack/validate.go

Lines changed: 14 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -32,28 +32,25 @@ var (
3232

3333
// Conditional errors.
3434
var (
35-
errAccessPointWithRootDirectory = errors.New("`root_directory` must be empty or \"/\" when `access_point` is specified")
36-
errAccessPointWithoutIAM = errors.New("`iam` must be true when `access_point` is specified")
37-
errUIDWithNonManagedFS = errors.New("UID and GID cannot be specified with non-managed EFS")
38-
errInvalidUIDGIDConfig = errors.New("must specify both UID and GID, or neither")
39-
errInvalidEFSConfig = errors.New("bad EFS configuration: cannot specify both bool and config")
40-
errReservedUID = errors.New("UID must not be 0")
41-
errInvalidContainer = errors.New("container dependency does not exist")
42-
errInvalidDependsOnStatus = fmt.Errorf("container dependency status must be one of < %s | %s | %s | %s >", dependsOnStart, dependsOnComplete, dependsOnSuccess, dependsOnHealthy)
43-
errInvalidSidecarDependsOnStatus = fmt.Errorf("sidecar container dependency status must be one of < %s | %s | %s >", dependsOnStart, dependsOnComplete, dependsOnSuccess)
44-
errEssentialContainerStatus = fmt.Errorf("essential container dependencies can only have status < %s | %s >", dependsOnStart, dependsOnHealthy)
45-
errEssentialSidecarStatus = fmt.Errorf("essential sidecar container dependencies can only have status < %s >", dependsOnStart)
46-
errInvalidPubSubTopicName = errors.New("topic names can only contain letters, numbers, underscores, and hypthens")
47-
errInvalidSvcName = errors.New("service names cannot be empty")
48-
errSvcNameTooLong = errors.New("service names must not exceed 255 characters")
49-
errSvcNameBadFormat = errors.New("service names must start with a letter, contain only lower-case letters, numbers, and hyphens, and have no consecutive or trailing hyphen")
35+
errAccessPointWithRootDirectory = errors.New("`root_directory` must be empty or \"/\" when `access_point` is specified")
36+
errAccessPointWithoutIAM = errors.New("`iam` must be true when `access_point` is specified")
37+
errUIDWithNonManagedFS = errors.New("UID and GID cannot be specified with non-managed EFS")
38+
errInvalidUIDGIDConfig = errors.New("must specify both UID and GID, or neither")
39+
errInvalidEFSConfig = errors.New("bad EFS configuration: cannot specify both bool and config")
40+
errReservedUID = errors.New("UID must not be 0")
41+
errInvalidContainer = errors.New("container dependency does not exist")
42+
errInvalidDependsOnStatus = fmt.Errorf("container dependency status must be one of < %s | %s | %s | %s >", dependsOnStart, dependsOnComplete, dependsOnSuccess, dependsOnHealthy)
43+
errEssentialContainerStatus = fmt.Errorf("essential container dependencies can only have status < %s | %s >", dependsOnStart, dependsOnHealthy)
44+
errInvalidPubSubTopicName = errors.New("topic names can only contain letters, numbers, underscores, and hypthens")
45+
errInvalidSvcName = errors.New("service names cannot be empty")
46+
errSvcNameTooLong = errors.New("service names must not exceed 255 characters")
47+
errSvcNameBadFormat = errors.New("service names must start with a letter, contain only lower-case letters, numbers, and hyphens, and have no consecutive or trailing hyphen")
5048
)
5149

5250
// Container dependency status options.
5351
var (
5452
essentialContainerValidStatuses = []string{dependsOnStart, dependsOnHealthy}
5553
dependsOnValidStatuses = []string{dependsOnStart, dependsOnComplete, dependsOnSuccess, dependsOnHealthy}
56-
sidecarDependsOnValidStatuses = []string{dependsOnStart, dependsOnComplete, dependsOnSuccess}
5754
)
5855

5956
// Regex options.
@@ -167,15 +164,6 @@ func isSidecar(container string, sidecars map[string]*manifest.SidecarConfig) bo
167164
}
168165

169166
func isValidStatusForContainer(status string, container string, c convertSidecarOpts) (bool, error) {
170-
if isSidecar(container, c.sidecarConfig) {
171-
for _, allowed := range sidecarDependsOnValidStatuses {
172-
if status == allowed {
173-
return true, nil
174-
}
175-
}
176-
return false, errInvalidSidecarDependsOnStatus
177-
}
178-
179167
for _, allowed := range dependsOnValidStatuses {
180168
if status == allowed {
181169
return true, nil
@@ -200,7 +188,7 @@ func isEssentialStatus(status string, container string, c convertSidecarOpts) (b
200188
if status == dependsOnStart {
201189
return true, nil
202190
}
203-
return false, errEssentialSidecarStatus
191+
return false, errEssentialContainerStatus
204192
}
205193

206194
for _, allowed := range essentialContainerValidStatuses {

internal/pkg/deploy/cloudformation/stack/validate_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ func TestValidateSidecarDependsOn(t *testing.T) {
213213
Essential: aws.Bool(false),
214214
},
215215
},
216-
wantErr: errInvalidSidecarDependsOnStatus,
216+
wantErr: errInvalidDependsOnStatus,
217217
},
218218
"error when container dependency status is invalid": {
219219
inSidecar: &manifest.SidecarConfig{
@@ -238,7 +238,7 @@ func TestValidateSidecarDependsOn(t *testing.T) {
238238
Essential: aws.Bool(true),
239239
},
240240
},
241-
wantErr: errEssentialSidecarStatus,
241+
wantErr: errEssentialContainerStatus,
242242
},
243243
"error when implied essential sidecar has a status besides start": {
244244
inSidecar: &manifest.SidecarConfig{
@@ -250,7 +250,7 @@ func TestValidateSidecarDependsOn(t *testing.T) {
250250
"sidecar": {},
251251
"sidecar2": {},
252252
},
253-
wantErr: errEssentialSidecarStatus,
253+
wantErr: errEssentialContainerStatus,
254254
},
255255
"error when essential container dependency status is invalid": {
256256
inSidecar: &manifest.SidecarConfig{
@@ -452,7 +452,7 @@ func TestValidateImageDependsOn(t *testing.T) {
452452
inSidecars: map[string]*manifest.SidecarConfig{
453453
"sidecar": {},
454454
},
455-
wantErr: errInvalidSidecarDependsOnStatus,
455+
wantErr: errInvalidDependsOnStatus,
456456
},
457457
"error when set essential sidecar container has a status besides start": {
458458
inImage: &manifest.Image{
@@ -465,7 +465,7 @@ func TestValidateImageDependsOn(t *testing.T) {
465465
Essential: aws.Bool(true),
466466
},
467467
},
468-
wantErr: errEssentialSidecarStatus,
468+
wantErr: errEssentialContainerStatus,
469469
},
470470
"error when implied essential sidecar container has a status besides start": {
471471
inImage: &manifest.Image{
@@ -476,7 +476,7 @@ func TestValidateImageDependsOn(t *testing.T) {
476476
inSidecars: map[string]*manifest.SidecarConfig{
477477
"sidecar": {},
478478
},
479-
wantErr: errEssentialSidecarStatus,
479+
wantErr: errEssentialContainerStatus,
480480
},
481481
}
482482
for name, tc := range testCases {

internal/pkg/graph/graph.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
// Package graph provides functionality for directed graphs.
5+
package graph
6+
7+
// nodeStatus denotes the visiting status of a node when running DFS in a graph.
8+
type nodeStatus int
9+
10+
const (
11+
unvisited nodeStatus = iota + 1
12+
visiting
13+
visited
14+
)
15+
16+
// Graph represents a directed graph.
17+
type Graph struct {
18+
nodes map[string]neighbors
19+
}
20+
21+
// Edge represents one edge of a directed graph.
22+
type Edge struct {
23+
From string
24+
To string
25+
}
26+
27+
type neighbors map[string]bool
28+
29+
// New initiates a new Graph.
30+
func New() *Graph {
31+
return &Graph{
32+
nodes: make(map[string]neighbors),
33+
}
34+
}
35+
36+
// Add adds a connection between two Nodes.
37+
func (g *Graph) Add(edge Edge) {
38+
fromNode, toNode := edge.From, edge.To
39+
// Add origin node if doesn't exist.
40+
if _, ok := g.nodes[fromNode]; !ok {
41+
g.nodes[fromNode] = make(neighbors)
42+
}
43+
// Add edge.
44+
g.nodes[fromNode][toNode] = true
45+
}
46+
47+
type findCycleTempVars struct {
48+
status map[string]nodeStatus
49+
nodeParent map[string]string
50+
cycleStart string
51+
cycleEnd string
52+
}
53+
54+
// IsAcyclic checks if the graph is acyclic. If not, return the first detected cycle.
55+
func (g *Graph) IsAcyclic() ([]string, bool) {
56+
var cycle []string
57+
status := make(map[string]nodeStatus)
58+
for node := range g.nodes {
59+
status[node] = unvisited
60+
}
61+
temp := findCycleTempVars{
62+
status: status,
63+
nodeParent: make(map[string]string),
64+
}
65+
// We will run a series of DFS in the graph. Initially all vertices are marked unvisited.
66+
// From each unvisited node, start the DFS, mark it visiting while entering and mark it visited on exit.
67+
// If DFS moves to a visiting node, then we have found a cycle. The cycle itself can be reconstructed using parent map.
68+
// See https://cp-algorithms.com/graph/finding-cycle.html
69+
for node := range g.nodes {
70+
if status[node] == unvisited && g.hasCycles(&temp, node) {
71+
for n := temp.cycleStart; n != temp.cycleEnd; n = temp.nodeParent[n] {
72+
cycle = append(cycle, n)
73+
}
74+
cycle = append(cycle, temp.cycleEnd)
75+
return cycle, false
76+
}
77+
}
78+
return nil, true
79+
}
80+
81+
func (g *Graph) hasCycles(temp *findCycleTempVars, currNode string) bool {
82+
temp.status[currNode] = visiting
83+
for node := range g.nodes[currNode] {
84+
if temp.status[node] == unvisited {
85+
temp.nodeParent[node] = currNode
86+
if g.hasCycles(temp, node) {
87+
return true
88+
}
89+
} else if temp.status[node] == visiting {
90+
temp.cycleStart = currNode
91+
temp.cycleEnd = node
92+
return true
93+
}
94+
}
95+
temp.status[currNode] = visited
96+
return false
97+
}

internal/pkg/graph/graph_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package graph
5+
6+
import (
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestGraph_Add(t *testing.T) {
13+
t.Run("success", func(t *testing.T) {
14+
// GIVEN
15+
graph := New()
16+
17+
// WHEN
18+
graph.Add(Edge{
19+
From: "A",
20+
To: "B",
21+
})
22+
graph.Add(Edge{
23+
From: "B",
24+
To: "A",
25+
})
26+
graph.Add(Edge{
27+
From: "A",
28+
To: "C",
29+
})
30+
31+
// THEN
32+
require.Equal(t, graph.nodes["A"], neighbors{"B": true, "C": true})
33+
require.Equal(t, graph.nodes["B"], neighbors{"A": true})
34+
})
35+
}
36+
37+
func TestGraph_IsAcyclic(t *testing.T) {
38+
testCases := map[string]struct {
39+
graph Graph
40+
41+
isAcyclic bool
42+
cycle []string
43+
}{
44+
"small non acyclic graph": {
45+
graph: Graph{
46+
nodes: map[string]neighbors{
47+
"A": {"B": true, "C": true},
48+
"B": {"A": true},
49+
},
50+
},
51+
52+
isAcyclic: false,
53+
cycle: []string{"A", "B"},
54+
},
55+
"non acyclic": {
56+
graph: Graph{
57+
nodes: map[string]neighbors{
58+
"K": {"F": true},
59+
"A": {"B": true, "C": true},
60+
"B": {"D": true, "E": true},
61+
"E": {"G": true},
62+
"F": {"G": true},
63+
"G": {"A": true},
64+
},
65+
},
66+
67+
isAcyclic: false,
68+
cycle: []string{"A", "G", "E", "B"},
69+
},
70+
"acyclic": {
71+
graph: Graph{
72+
nodes: map[string]neighbors{
73+
"A": {"B": true, "C": true},
74+
"B": {"D": true},
75+
"E": {"G": true},
76+
"F": {"G": true},
77+
},
78+
},
79+
80+
isAcyclic: true,
81+
},
82+
}
83+
for name, tc := range testCases {
84+
t.Run(name, func(t *testing.T) {
85+
// WHEN
86+
gotCycle, gotAcyclic := tc.graph.IsAcyclic()
87+
88+
// THEN
89+
require.Equal(t, tc.isAcyclic, gotAcyclic)
90+
require.ElementsMatch(t, tc.cycle, gotCycle)
91+
})
92+
}
93+
}

internal/pkg/manifest/backend_svc.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type BackendService struct {
2525

2626
// BackendServiceConfig holds the configuration that can be overridden per environments.
2727
type BackendServiceConfig struct {
28+
name string
2829
ImageConfig ImageWithPortAndHealthcheck `yaml:"image,flow"`
2930
ImageOverride `yaml:",inline"`
3031
TaskConfig `yaml:",inline"`

internal/pkg/manifest/job.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ type ScheduledJob struct {
3636

3737
// ScheduledJobConfig holds the configuration for a scheduled job
3838
type ScheduledJobConfig struct {
39+
name string
3940
ImageConfig ImageWithHealthcheck `yaml:"image,flow"`
4041
ImageOverride `yaml:",inline"`
4142
TaskConfig `yaml:",inline"`

internal/pkg/manifest/lb_web_svc.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ type LoadBalancedWebService struct {
4646

4747
// LoadBalancedWebServiceConfig holds the configuration for a load balanced web service.
4848
type LoadBalancedWebServiceConfig struct {
49+
name string
4950
ImageConfig ImageWithPortAndHealthcheck `yaml:"image,flow"`
5051
ImageOverride `yaml:",inline"`
5152
RoutingRule `yaml:"http,flow"`

0 commit comments

Comments
 (0)