mirror of
https://github.com/argoproj/argo-cd.git
synced 2026-03-23 16:58:47 +01:00
Compare commits
8 Commits
v3.4.0-rc1
...
release-3.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea71adbae5 | ||
|
|
5ed403cf60 | ||
|
|
9044c6c0ff | ||
|
|
3157fb15a4 | ||
|
|
e70034a44b | ||
|
|
5deef68eaf | ||
|
|
21e13a621e | ||
|
|
226178c1a5 |
@@ -90,6 +90,241 @@ source:
|
||||
ignoreMissingValueFiles: true
|
||||
```
|
||||
|
||||
## Glob Patterns in Value Files
|
||||
|
||||
Glob patterns can be used in `valueFiles` entries to match multiple files at once. This is useful
|
||||
when the set of environment-specific override files is not known in advance, or when you want to
|
||||
pick up new files automatically without updating the Application spec.
|
||||
|
||||
```bash
|
||||
# Single quotes prevent the shell from expanding the glob before Argo CD receives it
|
||||
argocd app set helm-guestbook --values 'envs/*.yaml'
|
||||
```
|
||||
|
||||
In the declarative syntax:
|
||||
|
||||
```yaml
|
||||
source:
|
||||
helm:
|
||||
valueFiles:
|
||||
- envs/*.yaml
|
||||
```
|
||||
|
||||
### Supported pattern syntax
|
||||
|
||||
Glob expansion uses the [doublestar](https://github.com/bmatcuk/doublestar) library.
|
||||
|
||||
| Pattern | Description |
|
||||
|---------|-------------|
|
||||
| `*` | Matches any sequence of non-separator characters within a single directory level |
|
||||
| `?` | Matches any single non-separator character |
|
||||
| `[abc]` | Matches one of the characters listed inside the brackets |
|
||||
| `[a-z]` | Matches any character in the given range |
|
||||
| `**` | Matches any sequence of characters including `/` (recursive across directory levels) |
|
||||
|
||||
### How files are passed to Helm
|
||||
|
||||
Each matched file is passed to `helm template` as a separate `--values <path>` flag, in the same
|
||||
order they appear after expansion. This is identical to listing each file individually in
|
||||
`valueFiles`. Argo CD does the expansion before invoking Helm.
|
||||
|
||||
Matched files are expanded **in-place** within the `valueFiles` list and sorted in **lexical
|
||||
(alphabetical) order**. Because Helm gives higher precedence to later `--values` flags, lexical
|
||||
order determines which file wins when the same key appears in multiple files.
|
||||
|
||||
```
|
||||
envs/
|
||||
a.yaml # sets foo: a-value
|
||||
b.yaml # sets foo: b-value
|
||||
```
|
||||
|
||||
```yaml
|
||||
# envs/*.yaml expands to: envs/a.yaml, envs/b.yaml (lexical order)
|
||||
# b.yaml is last → foo = "b-value"
|
||||
source:
|
||||
helm:
|
||||
valueFiles:
|
||||
- envs/*.yaml
|
||||
```
|
||||
|
||||
When you have multiple entries in `valueFiles`, the relative order between entries is preserved.
|
||||
Glob expansion only reorders files within a single pattern:
|
||||
|
||||
```yaml
|
||||
valueFiles:
|
||||
- base.yaml # passed first
|
||||
- overrides/*.yaml # expanded in lexical order, passed after base.yaml
|
||||
- final.yaml # passed last, highest precedence
|
||||
```
|
||||
|
||||
### Recursive matching with `**`
|
||||
|
||||
Use `**` to match files at any depth below a directory:
|
||||
|
||||
```yaml
|
||||
# envs/**/*.yaml processes each directory's own files before descending into subdirectories,
|
||||
# with directories and files sorted alphabetically at each level.
|
||||
#
|
||||
# envs/a.yaml ← 'a' (flat file in envs/)
|
||||
# envs/z.yaml ← 'z' (flat file in envs/, processed before descending)
|
||||
# envs/nested/c.yaml ← inside envs/nested/, processed after envs/ flat files
|
||||
#
|
||||
# nested/c.yaml is last → foo = "nested-value"
|
||||
source:
|
||||
helm:
|
||||
valueFiles:
|
||||
- envs/**/*.yaml
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> `**` matches zero or more path segments, so `envs/**/*.yaml` also matches files directly
|
||||
> inside `envs/` (not just subdirectories). doublestar traverses directories in lexical order
|
||||
> and processes each directory's own files (alphabetically) before descending into its
|
||||
> subdirectories. This means `envs/z.yaml` always comes before `envs/nested/c.yaml`, even
|
||||
> though `'n' < 'z'` alphabetically. To make ordering fully explicit and predictable,
|
||||
> use numeric prefixes (see [Naming conventions](#naming-conventions)).
|
||||
|
||||
### Using environment variables in glob patterns
|
||||
|
||||
[Build environment variables](./build-environment.md) are substituted **before** the glob is
|
||||
evaluated, so you can construct patterns dynamically:
|
||||
|
||||
```yaml
|
||||
source:
|
||||
helm:
|
||||
valueFiles:
|
||||
- envs/$ARGOCD_APP_NAME/*.yaml
|
||||
```
|
||||
|
||||
This lets a single Application template expand to the right set of files per app name.
|
||||
|
||||
### Glob patterns with multiple sources
|
||||
|
||||
Glob patterns work with [value files from an external repository](./multiple_sources.md#helm-value-files-from-external-git-repository).
|
||||
The `$ref` variable is resolved first to the external repo's root, and the rest of the pattern is
|
||||
evaluated within that repo's directory tree:
|
||||
|
||||
```yaml
|
||||
sources:
|
||||
- repoURL: https://git.example.com/my-configs.git
|
||||
ref: configs
|
||||
- repoURL: https://git.example.com/my-chart.git
|
||||
path: chart
|
||||
helm:
|
||||
valueFiles:
|
||||
- $configs/envs/*.yaml # matches files in the 'my-configs' repo under envs/
|
||||
```
|
||||
|
||||
### Naming conventions
|
||||
|
||||
Because files are sorted lexically, the sort order controls merge precedence. A common pattern is
|
||||
to use a numeric prefix to make the intended order explicit:
|
||||
|
||||
```
|
||||
values/
|
||||
00-defaults.yaml
|
||||
10-region.yaml
|
||||
20-env.yaml
|
||||
30-override.yaml
|
||||
```
|
||||
|
||||
```yaml
|
||||
valueFiles:
|
||||
- values/*.yaml
|
||||
# expands to: 00-defaults.yaml, 10-region.yaml, 20-env.yaml, 30-override.yaml
|
||||
# 30-override.yaml has the highest precedence
|
||||
```
|
||||
|
||||
Without a prefix, pure alphabetical ordering applies. Be careful with names that sort
|
||||
unexpectedly, for example `values-10.yaml` sorts before `values-9.yaml` because `"1"` < `"9"`
|
||||
lexically.
|
||||
|
||||
### Constraints and limitations
|
||||
|
||||
**Path boundary**: Glob patterns cannot match files outside the repository root, even with
|
||||
patterns like `../../secrets/*.yaml`. Argo CD resolves the pattern's base path against the
|
||||
repository root before expanding it, and any match that would escape the root is rejected.
|
||||
|
||||
**Symlinks**: Argo CD follows symlinks when checking the path boundary. A symlink that lives
|
||||
inside the repository but points to a target outside the repository root is rejected, even though
|
||||
the symlink's own path is within the repo. This check applies to every file produced by glob
|
||||
expansion, including multi-hop symlink chains. Symlinks that resolve to a target still inside the
|
||||
repository are allowed.
|
||||
|
||||
**Absolute paths**: A path starting with `/` is treated as relative to the **repository root**,
|
||||
not the filesystem root. The pattern `/configs/*.yaml` matches files in the `configs/` directory
|
||||
at the top of the repository.
|
||||
|
||||
**Remote URLs are not glob-expanded**: Entries that are remote URLs (e.g.
|
||||
`https://raw.githubusercontent.com/.../values.yaml`) are passed to Helm as-is. Glob characters
|
||||
in a URL have no special meaning and will cause the URL to fail if the literal characters are not
|
||||
part of the URL.
|
||||
|
||||
**Shell quoting on the CLI**: Shells expand glob patterns before passing arguments to programs.
|
||||
Always quote patterns to prevent unintended shell expansion:
|
||||
|
||||
```bash
|
||||
# Correct: single quotes pass the literal pattern to Argo CD
|
||||
argocd app set myapp --values 'envs/*.yaml'
|
||||
|
||||
# Incorrect: the shell expands *.yaml against the current directory first
|
||||
argocd app set myapp --values envs/*.yaml
|
||||
```
|
||||
|
||||
### Deduplication
|
||||
|
||||
Each file is included only once, but **explicit entries take priority over glob matches** when
|
||||
determining position. If a file appears both in a glob pattern and as an explicit entry, the glob
|
||||
skips it and the explicit entry places it at its declared position.
|
||||
|
||||
```yaml
|
||||
valueFiles:
|
||||
- envs/*.yaml # expands to base.yaml, prod.yaml — but prod.yaml is listed explicitly below,
|
||||
# so the glob skips it: only base.yaml is added here
|
||||
- envs/prod.yaml # placed here at the end, giving it highest Helm precedence
|
||||
```
|
||||
|
||||
This means you can use a glob to pick up all files in a directory and then pin a specific file to
|
||||
the end (highest precedence) by listing it explicitly after the glob.
|
||||
|
||||
If the same file (same absolute path) is matched by two glob patterns, it is included at the
|
||||
position of the first match. Subsequent glob matches for that exact path are silently dropped.
|
||||
Files with the same name but at different paths are treated as distinct files and are always included.
|
||||
|
||||
```yaml
|
||||
valueFiles:
|
||||
- envs/*.yaml # matches envs/base.yaml, envs/prod.yaml
|
||||
- envs/**/*.yaml # envs/prod.yaml already matched above and is skipped;
|
||||
# envs/nested/prod.yaml is a different path and is still included
|
||||
```
|
||||
|
||||
### No-match behavior
|
||||
|
||||
If a glob pattern matches no files, Argo CD saves the Application spec (the spec is not invalid and
|
||||
the files may be added to the repository later) and surfaces a `ComparisonError` condition on the
|
||||
Application:
|
||||
|
||||
```
|
||||
values file glob "nonexistent/*.yaml" matched no files
|
||||
```
|
||||
|
||||
The app will remain in a degraded state until the pattern matches at least one file or the pattern
|
||||
is removed. No spec update is required once the files are added to the repository.
|
||||
|
||||
To silently skip a pattern that matches no files instead of raising an error, combine the glob with
|
||||
`ignoreMissingValueFiles`:
|
||||
|
||||
```yaml
|
||||
source:
|
||||
helm:
|
||||
valueFiles:
|
||||
- envs/*.yaml
|
||||
ignoreMissingValueFiles: true
|
||||
```
|
||||
|
||||
This is useful for implementing a default/override pattern where override files may not exist in
|
||||
every environment.
|
||||
|
||||
## Values
|
||||
|
||||
Argo CD supports the equivalent of a values file directly in the Application manifest using the `source.helm.valuesObject` key.
|
||||
|
||||
81
gitops-engine/pkg/cache/cluster.go
vendored
81
gitops-engine/pkg/cache/cluster.go
vendored
@@ -92,6 +92,15 @@ const (
|
||||
RespectRbacStrict
|
||||
)
|
||||
|
||||
// callState tracks whether action() has been called on a resource during hierarchy iteration.
|
||||
type callState int
|
||||
|
||||
const (
|
||||
notCalled callState = iota // action() has not been called yet
|
||||
inProgress // action() is currently being processed (in call stack)
|
||||
completed // action() has been called and processing is complete
|
||||
)
|
||||
|
||||
type apiMeta struct {
|
||||
namespaced bool
|
||||
// watchCancel stops the watch of all resources for this API. This gets called when the cache is invalidated or when
|
||||
@@ -1186,8 +1195,11 @@ func (c *clusterCache) IterateHierarchyV2(keys []kube.ResourceKey, action func(r
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
|
||||
// Track visited resources to avoid cycles
|
||||
visited := make(map[kube.ResourceKey]int)
|
||||
// Track whether action() has been called on each resource (notCalled/inProgress/completed).
|
||||
// This is shared across processNamespaceHierarchy and processCrossNamespaceChildren.
|
||||
// Note: This is distinct from 'crossNSTraversed' in processCrossNamespaceChildren, which tracks
|
||||
// whether we've traversed a cluster-scoped key's cross-namespace children.
|
||||
actionCallState := make(map[kube.ResourceKey]callState)
|
||||
|
||||
// Group keys by namespace for efficient processing
|
||||
keysPerNamespace := make(map[string][]kube.ResourceKey)
|
||||
@@ -1203,12 +1215,18 @@ func (c *clusterCache) IterateHierarchyV2(keys []kube.ResourceKey, action func(r
|
||||
for namespace, namespaceKeys := range keysPerNamespace {
|
||||
nsNodes := c.nsIndex[namespace]
|
||||
graph := buildGraph(nsNodes)
|
||||
c.processNamespaceHierarchy(namespaceKeys, nsNodes, graph, visited, action)
|
||||
c.processNamespaceHierarchy(namespaceKeys, nsNodes, graph, actionCallState, action)
|
||||
}
|
||||
|
||||
// Process pre-computed cross-namespace children
|
||||
if clusterKeys, ok := keysPerNamespace[""]; ok {
|
||||
c.processCrossNamespaceChildren(clusterKeys, visited, action)
|
||||
// Track which cluster-scoped keys have had their cross-namespace children traversed.
|
||||
// This is distinct from 'actionCallState' - a resource may have had action() called
|
||||
// (i.e., its actionCallState is in the completed state) but not yet had its cross-namespace
|
||||
// children traversed. This prevents infinite recursion when resources have circular
|
||||
// ownerReferences.
|
||||
crossNSTraversed := make(map[kube.ResourceKey]bool)
|
||||
c.processCrossNamespaceChildren(clusterKeys, actionCallState, crossNSTraversed, action)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1216,12 +1234,21 @@ func (c *clusterCache) IterateHierarchyV2(keys []kube.ResourceKey, action func(r
|
||||
// This enables traversing from cluster-scoped parents to their namespaced children across namespace boundaries.
|
||||
// It also handles multi-level hierarchies where cluster-scoped resources own other cluster-scoped resources
|
||||
// that in turn own namespaced resources (e.g., Provider -> ProviderRevision -> Deployment in Crossplane).
|
||||
// The crossNSTraversed map tracks which keys have already been processed to prevent infinite recursion
|
||||
// from circular ownerReferences (e.g., a resource that owns itself).
|
||||
func (c *clusterCache) processCrossNamespaceChildren(
|
||||
clusterScopedKeys []kube.ResourceKey,
|
||||
visited map[kube.ResourceKey]int,
|
||||
actionCallState map[kube.ResourceKey]callState,
|
||||
crossNSTraversed map[kube.ResourceKey]bool,
|
||||
action func(resource *Resource, namespaceResources map[kube.ResourceKey]*Resource) bool,
|
||||
) {
|
||||
for _, clusterKey := range clusterScopedKeys {
|
||||
// Skip if already processed (cycle detection)
|
||||
if crossNSTraversed[clusterKey] {
|
||||
continue
|
||||
}
|
||||
crossNSTraversed[clusterKey] = true
|
||||
|
||||
// Get cluster-scoped resource to access its UID
|
||||
clusterResource := c.resources[clusterKey]
|
||||
if clusterResource == nil {
|
||||
@@ -1236,16 +1263,17 @@ func (c *clusterCache) processCrossNamespaceChildren(
|
||||
continue
|
||||
}
|
||||
|
||||
alreadyVisited := visited[childKey] != 0
|
||||
alreadyProcessed := actionCallState[childKey] != notCalled
|
||||
|
||||
// If child is cluster-scoped and was already visited by processNamespaceHierarchy,
|
||||
// If child is cluster-scoped and action() was already called by processNamespaceHierarchy,
|
||||
// we still need to recursively check for its cross-namespace children.
|
||||
// This handles multi-level hierarchies like: ClusterScoped -> ClusterScoped -> Namespaced
|
||||
// (e.g., Crossplane's Provider -> ProviderRevision -> Deployment)
|
||||
if alreadyVisited {
|
||||
if alreadyProcessed {
|
||||
if childKey.Namespace == "" {
|
||||
// Recursively process cross-namespace children of this cluster-scoped child
|
||||
c.processCrossNamespaceChildren([]kube.ResourceKey{childKey}, visited, action)
|
||||
// The crossNSTraversed map prevents infinite recursion on circular ownerReferences
|
||||
c.processCrossNamespaceChildren([]kube.ResourceKey{childKey}, actionCallState, crossNSTraversed, action)
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -1258,16 +1286,16 @@ func (c *clusterCache) processCrossNamespaceChildren(
|
||||
|
||||
// Process this child
|
||||
if action(child, nsNodes) {
|
||||
visited[childKey] = 1
|
||||
actionCallState[childKey] = inProgress
|
||||
// Recursively process descendants using index-based traversal
|
||||
c.iterateChildrenUsingIndex(child, nsNodes, visited, action)
|
||||
c.iterateChildrenUsingIndex(child, nsNodes, actionCallState, action)
|
||||
|
||||
// If this child is also cluster-scoped, recursively process its cross-namespace children
|
||||
if childKey.Namespace == "" {
|
||||
c.processCrossNamespaceChildren([]kube.ResourceKey{childKey}, visited, action)
|
||||
c.processCrossNamespaceChildren([]kube.ResourceKey{childKey}, actionCallState, crossNSTraversed, action)
|
||||
}
|
||||
|
||||
visited[childKey] = 2
|
||||
actionCallState[childKey] = completed
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1278,14 +1306,14 @@ func (c *clusterCache) processCrossNamespaceChildren(
|
||||
func (c *clusterCache) iterateChildrenUsingIndex(
|
||||
parent *Resource,
|
||||
nsNodes map[kube.ResourceKey]*Resource,
|
||||
visited map[kube.ResourceKey]int,
|
||||
actionCallState map[kube.ResourceKey]callState,
|
||||
action func(resource *Resource, namespaceResources map[kube.ResourceKey]*Resource) bool,
|
||||
) {
|
||||
// Look up direct children of this parent using the index
|
||||
childKeys := c.parentUIDToChildren[parent.Ref.UID]
|
||||
for _, childKey := range childKeys {
|
||||
if visited[childKey] != 0 {
|
||||
continue // Already visited or in progress
|
||||
if actionCallState[childKey] != notCalled {
|
||||
continue // action() already called or in progress
|
||||
}
|
||||
|
||||
child := c.resources[childKey]
|
||||
@@ -1300,10 +1328,10 @@ func (c *clusterCache) iterateChildrenUsingIndex(
|
||||
}
|
||||
|
||||
if action(child, nsNodes) {
|
||||
visited[childKey] = 1
|
||||
actionCallState[childKey] = inProgress
|
||||
// Recursively process this child's descendants
|
||||
c.iterateChildrenUsingIndex(child, nsNodes, visited, action)
|
||||
visited[childKey] = 2
|
||||
c.iterateChildrenUsingIndex(child, nsNodes, actionCallState, action)
|
||||
actionCallState[childKey] = completed
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1313,22 +1341,19 @@ func (c *clusterCache) processNamespaceHierarchy(
|
||||
namespaceKeys []kube.ResourceKey,
|
||||
nsNodes map[kube.ResourceKey]*Resource,
|
||||
graph map[kube.ResourceKey]map[types.UID]*Resource,
|
||||
visited map[kube.ResourceKey]int,
|
||||
actionCallState map[kube.ResourceKey]callState,
|
||||
action func(resource *Resource, namespaceResources map[kube.ResourceKey]*Resource) bool,
|
||||
) {
|
||||
for _, key := range namespaceKeys {
|
||||
visited[key] = 0
|
||||
}
|
||||
for _, key := range namespaceKeys {
|
||||
res := c.resources[key]
|
||||
if visited[key] == 2 || !action(res, nsNodes) {
|
||||
if actionCallState[key] == completed || !action(res, nsNodes) {
|
||||
continue
|
||||
}
|
||||
visited[key] = 1
|
||||
actionCallState[key] = inProgress
|
||||
if _, ok := graph[key]; ok {
|
||||
for _, child := range graph[key] {
|
||||
if visited[child.ResourceKey()] == 0 && action(child, nsNodes) {
|
||||
child.iterateChildrenV2(graph, nsNodes, visited, func(err error, child *Resource, namespaceResources map[kube.ResourceKey]*Resource) bool {
|
||||
if actionCallState[child.ResourceKey()] == notCalled && action(child, nsNodes) {
|
||||
child.iterateChildrenV2(graph, nsNodes, actionCallState, func(err error, child *Resource, namespaceResources map[kube.ResourceKey]*Resource) bool {
|
||||
if err != nil {
|
||||
c.log.V(2).Info(err.Error())
|
||||
return false
|
||||
@@ -1338,7 +1363,7 @@ func (c *clusterCache) processNamespaceHierarchy(
|
||||
}
|
||||
}
|
||||
}
|
||||
visited[key] = 2
|
||||
actionCallState[key] = completed
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
109
gitops-engine/pkg/cache/cluster_test.go
vendored
109
gitops-engine/pkg/cache/cluster_test.go
vendored
@@ -2189,3 +2189,112 @@ func TestIterateHierarchyV2_NoDuplicatesCrossNamespace(t *testing.T) {
|
||||
assert.Equal(t, 1, visitCount["namespaced-child"], "namespaced child should be visited once")
|
||||
assert.Equal(t, 1, visitCount["cluster-child"], "cluster child should be visited once")
|
||||
}
|
||||
|
||||
func TestIterateHierarchyV2_CircularOwnerReference_NoStackOverflow(t *testing.T) {
|
||||
// Test that self-referencing resources (circular ownerReferences) don't cause stack overflow.
|
||||
// This reproduces the bug reported in https://github.com/argoproj/argo-cd/issues/26783
|
||||
// where a resource with an ownerReference pointing to itself caused infinite recursion.
|
||||
|
||||
// Create a cluster-scoped resource that owns itself (self-referencing)
|
||||
selfReferencingResource := &corev1.Namespace{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "Namespace",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "self-referencing",
|
||||
UID: "self-ref-uid",
|
||||
ResourceVersion: "1",
|
||||
OwnerReferences: []metav1.OwnerReference{{
|
||||
APIVersion: "v1",
|
||||
Kind: "Namespace",
|
||||
Name: "self-referencing",
|
||||
UID: "self-ref-uid", // Points to itself
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
cluster := newCluster(t, selfReferencingResource).WithAPIResources([]kube.APIResourceInfo{{
|
||||
GroupKind: schema.GroupKind{Group: "", Kind: "Namespace"},
|
||||
GroupVersionResource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "namespaces"},
|
||||
Meta: metav1.APIResource{Namespaced: false},
|
||||
}})
|
||||
err := cluster.EnsureSynced()
|
||||
require.NoError(t, err)
|
||||
|
||||
visitCount := 0
|
||||
// This should complete without stack overflow
|
||||
cluster.IterateHierarchyV2(
|
||||
[]kube.ResourceKey{kube.GetResourceKey(mustToUnstructured(selfReferencingResource))},
|
||||
func(resource *Resource, _ map[kube.ResourceKey]*Resource) bool {
|
||||
visitCount++
|
||||
return true
|
||||
},
|
||||
)
|
||||
|
||||
// The self-referencing resource should be visited exactly once
|
||||
assert.Equal(t, 1, visitCount, "self-referencing resource should be visited exactly once")
|
||||
}
|
||||
|
||||
func TestIterateHierarchyV2_CircularOwnerChain_NoStackOverflow(t *testing.T) {
|
||||
// Test that circular ownership chains (A -> B -> A) don't cause stack overflow.
|
||||
// This is a more complex case where two resources own each other.
|
||||
|
||||
resourceA := &corev1.Namespace{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "Namespace",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "resource-a",
|
||||
UID: "uid-a",
|
||||
ResourceVersion: "1",
|
||||
OwnerReferences: []metav1.OwnerReference{{
|
||||
APIVersion: "v1",
|
||||
Kind: "Namespace",
|
||||
Name: "resource-b",
|
||||
UID: "uid-b", // A is owned by B
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
resourceB := &corev1.Namespace{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "Namespace",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "resource-b",
|
||||
UID: "uid-b",
|
||||
ResourceVersion: "1",
|
||||
OwnerReferences: []metav1.OwnerReference{{
|
||||
APIVersion: "v1",
|
||||
Kind: "Namespace",
|
||||
Name: "resource-a",
|
||||
UID: "uid-a", // B is owned by A
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
cluster := newCluster(t, resourceA, resourceB).WithAPIResources([]kube.APIResourceInfo{{
|
||||
GroupKind: schema.GroupKind{Group: "", Kind: "Namespace"},
|
||||
GroupVersionResource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "namespaces"},
|
||||
Meta: metav1.APIResource{Namespaced: false},
|
||||
}})
|
||||
err := cluster.EnsureSynced()
|
||||
require.NoError(t, err)
|
||||
|
||||
visitCount := make(map[string]int)
|
||||
// This should complete without stack overflow
|
||||
cluster.IterateHierarchyV2(
|
||||
[]kube.ResourceKey{kube.GetResourceKey(mustToUnstructured(resourceA))},
|
||||
func(resource *Resource, _ map[kube.ResourceKey]*Resource) bool {
|
||||
visitCount[resource.Ref.Name]++
|
||||
return true
|
||||
},
|
||||
)
|
||||
|
||||
// Each resource in the circular chain should be visited exactly once
|
||||
assert.Equal(t, 1, visitCount["resource-a"], "resource-a should be visited exactly once")
|
||||
assert.Equal(t, 1, visitCount["resource-b"], "resource-b should be visited exactly once")
|
||||
}
|
||||
|
||||
16
gitops-engine/pkg/cache/resource.go
vendored
16
gitops-engine/pkg/cache/resource.go
vendored
@@ -76,16 +76,16 @@ func (r *Resource) toOwnerRef() metav1.OwnerReference {
|
||||
}
|
||||
|
||||
// iterateChildrenV2 is a depth-first traversal of the graph of resources starting from the current resource.
|
||||
func (r *Resource) iterateChildrenV2(graph map[kube.ResourceKey]map[types.UID]*Resource, ns map[kube.ResourceKey]*Resource, visited map[kube.ResourceKey]int, action func(err error, child *Resource, namespaceResources map[kube.ResourceKey]*Resource) bool) {
|
||||
func (r *Resource) iterateChildrenV2(graph map[kube.ResourceKey]map[types.UID]*Resource, ns map[kube.ResourceKey]*Resource, actionCallState map[kube.ResourceKey]callState, action func(err error, child *Resource, namespaceResources map[kube.ResourceKey]*Resource) bool) {
|
||||
key := r.ResourceKey()
|
||||
if visited[key] == 2 {
|
||||
if actionCallState[key] == completed {
|
||||
return
|
||||
}
|
||||
// this indicates that we've started processing this node's children
|
||||
visited[key] = 1
|
||||
actionCallState[key] = inProgress
|
||||
defer func() {
|
||||
// this indicates that we've finished processing this node's children
|
||||
visited[key] = 2
|
||||
actionCallState[key] = completed
|
||||
}()
|
||||
children, ok := graph[key]
|
||||
if !ok || children == nil {
|
||||
@@ -94,13 +94,13 @@ func (r *Resource) iterateChildrenV2(graph map[kube.ResourceKey]map[types.UID]*R
|
||||
for _, child := range children {
|
||||
childKey := child.ResourceKey()
|
||||
// For cross-namespace relationships, child might not be in ns, so use it directly from graph
|
||||
switch visited[childKey] {
|
||||
case 1:
|
||||
switch actionCallState[childKey] {
|
||||
case inProgress:
|
||||
// Since we encountered a node that we're currently processing, we know we have a circular dependency.
|
||||
_ = action(fmt.Errorf("circular dependency detected. %s is child and parent of %s", childKey.String(), key.String()), child, ns)
|
||||
case 0:
|
||||
case notCalled:
|
||||
if action(nil, child, ns) {
|
||||
child.iterateChildrenV2(graph, ns, visited, action)
|
||||
child.iterateChildrenV2(graph, ns, actionCallState, action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2
go.mod
2
go.mod
@@ -102,7 +102,7 @@ require (
|
||||
golang.org/x/term v0.41.0
|
||||
golang.org/x/time v0.15.0
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57
|
||||
google.golang.org/grpc v1.79.2
|
||||
google.golang.org/grpc v1.79.3
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
|
||||
4
go.sum
4
go.sum
@@ -1404,8 +1404,8 @@ google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
||||
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
|
||||
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
|
||||
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
|
||||
@@ -12,4 +12,4 @@ resources:
|
||||
images:
|
||||
- name: quay.io/argoproj/argocd
|
||||
newName: quay.io/argoproj/argocd
|
||||
newTag: v3.4.0-rc1
|
||||
newTag: v3.4.0-rc2
|
||||
|
||||
@@ -5,7 +5,7 @@ kind: Kustomization
|
||||
images:
|
||||
- name: quay.io/argoproj/argocd
|
||||
newName: quay.io/argoproj/argocd
|
||||
newTag: v3.4.0-rc1
|
||||
newTag: v3.4.0-rc2
|
||||
resources:
|
||||
- ./application-controller
|
||||
- ./dex
|
||||
|
||||
12
manifests/core-install-with-hydrator.yaml
generated
12
manifests/core-install-with-hydrator.yaml
generated
@@ -31332,7 +31332,7 @@ spec:
|
||||
key: applicationsetcontroller.status.max.resources.count
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
name: argocd-applicationset-controller
|
||||
ports:
|
||||
@@ -31473,7 +31473,7 @@ spec:
|
||||
key: log.format.timestamp
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
failureThreshold: 3
|
||||
@@ -31601,7 +31601,7 @@ spec:
|
||||
- argocd
|
||||
- admin
|
||||
- redis-initial-password
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: IfNotPresent
|
||||
name: secret-init
|
||||
securityContext:
|
||||
@@ -31910,7 +31910,7 @@ spec:
|
||||
value: /helm-working-dir
|
||||
- name: HELM_DATA_HOME
|
||||
value: /helm-working-dir
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
failureThreshold: 3
|
||||
@@ -31963,7 +31963,7 @@ spec:
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
name: copyutil
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
@@ -32366,7 +32366,7 @@ spec:
|
||||
optional: true
|
||||
- name: KUBECACHEDIR
|
||||
value: /tmp/kubecache
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
name: argocd-application-controller
|
||||
ports:
|
||||
|
||||
10
manifests/core-install.yaml
generated
10
manifests/core-install.yaml
generated
@@ -31300,7 +31300,7 @@ spec:
|
||||
key: applicationsetcontroller.status.max.resources.count
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
name: argocd-applicationset-controller
|
||||
ports:
|
||||
@@ -31429,7 +31429,7 @@ spec:
|
||||
- argocd
|
||||
- admin
|
||||
- redis-initial-password
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: IfNotPresent
|
||||
name: secret-init
|
||||
securityContext:
|
||||
@@ -31738,7 +31738,7 @@ spec:
|
||||
value: /helm-working-dir
|
||||
- name: HELM_DATA_HOME
|
||||
value: /helm-working-dir
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
failureThreshold: 3
|
||||
@@ -31791,7 +31791,7 @@ spec:
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
name: copyutil
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
@@ -32194,7 +32194,7 @@ spec:
|
||||
optional: true
|
||||
- name: KUBECACHEDIR
|
||||
value: /tmp/kubecache
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
name: argocd-application-controller
|
||||
ports:
|
||||
|
||||
@@ -12,4 +12,4 @@ resources:
|
||||
images:
|
||||
- name: quay.io/argoproj/argocd
|
||||
newName: quay.io/argoproj/argocd
|
||||
newTag: v3.4.0-rc1
|
||||
newTag: v3.4.0-rc2
|
||||
|
||||
@@ -12,7 +12,7 @@ patches:
|
||||
images:
|
||||
- name: quay.io/argoproj/argocd
|
||||
newName: quay.io/argoproj/argocd
|
||||
newTag: v3.4.0-rc1
|
||||
newTag: v3.4.0-rc2
|
||||
resources:
|
||||
- ../../base/application-controller
|
||||
- ../../base/applicationset-controller
|
||||
|
||||
18
manifests/ha/install-with-hydrator.yaml
generated
18
manifests/ha/install-with-hydrator.yaml
generated
@@ -32758,7 +32758,7 @@ spec:
|
||||
key: applicationsetcontroller.status.max.resources.count
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
name: argocd-applicationset-controller
|
||||
ports:
|
||||
@@ -32899,7 +32899,7 @@ spec:
|
||||
key: log.format.timestamp
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
failureThreshold: 3
|
||||
@@ -33057,7 +33057,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /shared/argocd-dex
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
name: copyutil
|
||||
securityContext:
|
||||
@@ -33159,7 +33159,7 @@ spec:
|
||||
key: notificationscontroller.repo.server.plaintext
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
@@ -33283,7 +33283,7 @@ spec:
|
||||
- argocd
|
||||
- admin
|
||||
- redis-initial-password
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: IfNotPresent
|
||||
name: secret-init
|
||||
securityContext:
|
||||
@@ -33618,7 +33618,7 @@ spec:
|
||||
value: /helm-working-dir
|
||||
- name: HELM_DATA_HOME
|
||||
value: /helm-working-dir
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
failureThreshold: 3
|
||||
@@ -33671,7 +33671,7 @@ spec:
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
name: copyutil
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
@@ -34100,7 +34100,7 @@ spec:
|
||||
key: server.sync.replace.allowed
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
@@ -34532,7 +34532,7 @@ spec:
|
||||
optional: true
|
||||
- name: KUBECACHEDIR
|
||||
value: /tmp/kubecache
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
name: argocd-application-controller
|
||||
ports:
|
||||
|
||||
16
manifests/ha/install.yaml
generated
16
manifests/ha/install.yaml
generated
@@ -32728,7 +32728,7 @@ spec:
|
||||
key: applicationsetcontroller.status.max.resources.count
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
name: argocd-applicationset-controller
|
||||
ports:
|
||||
@@ -32887,7 +32887,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /shared/argocd-dex
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
name: copyutil
|
||||
securityContext:
|
||||
@@ -32989,7 +32989,7 @@ spec:
|
||||
key: notificationscontroller.repo.server.plaintext
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
@@ -33113,7 +33113,7 @@ spec:
|
||||
- argocd
|
||||
- admin
|
||||
- redis-initial-password
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: IfNotPresent
|
||||
name: secret-init
|
||||
securityContext:
|
||||
@@ -33448,7 +33448,7 @@ spec:
|
||||
value: /helm-working-dir
|
||||
- name: HELM_DATA_HOME
|
||||
value: /helm-working-dir
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
failureThreshold: 3
|
||||
@@ -33501,7 +33501,7 @@ spec:
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
name: copyutil
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
@@ -33930,7 +33930,7 @@ spec:
|
||||
key: server.sync.replace.allowed
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
@@ -34362,7 +34362,7 @@ spec:
|
||||
optional: true
|
||||
- name: KUBECACHEDIR
|
||||
value: /tmp/kubecache
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
name: argocd-application-controller
|
||||
ports:
|
||||
|
||||
18
manifests/ha/namespace-install-with-hydrator.yaml
generated
18
manifests/ha/namespace-install-with-hydrator.yaml
generated
@@ -2005,7 +2005,7 @@ spec:
|
||||
key: applicationsetcontroller.status.max.resources.count
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
name: argocd-applicationset-controller
|
||||
ports:
|
||||
@@ -2146,7 +2146,7 @@ spec:
|
||||
key: log.format.timestamp
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
failureThreshold: 3
|
||||
@@ -2304,7 +2304,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /shared/argocd-dex
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
name: copyutil
|
||||
securityContext:
|
||||
@@ -2406,7 +2406,7 @@ spec:
|
||||
key: notificationscontroller.repo.server.plaintext
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
@@ -2530,7 +2530,7 @@ spec:
|
||||
- argocd
|
||||
- admin
|
||||
- redis-initial-password
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: IfNotPresent
|
||||
name: secret-init
|
||||
securityContext:
|
||||
@@ -2865,7 +2865,7 @@ spec:
|
||||
value: /helm-working-dir
|
||||
- name: HELM_DATA_HOME
|
||||
value: /helm-working-dir
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
failureThreshold: 3
|
||||
@@ -2918,7 +2918,7 @@ spec:
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
name: copyutil
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
@@ -3347,7 +3347,7 @@ spec:
|
||||
key: server.sync.replace.allowed
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
@@ -3779,7 +3779,7 @@ spec:
|
||||
optional: true
|
||||
- name: KUBECACHEDIR
|
||||
value: /tmp/kubecache
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
name: argocd-application-controller
|
||||
ports:
|
||||
|
||||
16
manifests/ha/namespace-install.yaml
generated
16
manifests/ha/namespace-install.yaml
generated
@@ -1975,7 +1975,7 @@ spec:
|
||||
key: applicationsetcontroller.status.max.resources.count
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
name: argocd-applicationset-controller
|
||||
ports:
|
||||
@@ -2134,7 +2134,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /shared/argocd-dex
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
name: copyutil
|
||||
securityContext:
|
||||
@@ -2236,7 +2236,7 @@ spec:
|
||||
key: notificationscontroller.repo.server.plaintext
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
@@ -2360,7 +2360,7 @@ spec:
|
||||
- argocd
|
||||
- admin
|
||||
- redis-initial-password
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: IfNotPresent
|
||||
name: secret-init
|
||||
securityContext:
|
||||
@@ -2695,7 +2695,7 @@ spec:
|
||||
value: /helm-working-dir
|
||||
- name: HELM_DATA_HOME
|
||||
value: /helm-working-dir
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
failureThreshold: 3
|
||||
@@ -2748,7 +2748,7 @@ spec:
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
name: copyutil
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
@@ -3177,7 +3177,7 @@ spec:
|
||||
key: server.sync.replace.allowed
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
@@ -3609,7 +3609,7 @@ spec:
|
||||
optional: true
|
||||
- name: KUBECACHEDIR
|
||||
value: /tmp/kubecache
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
name: argocd-application-controller
|
||||
ports:
|
||||
|
||||
18
manifests/install-with-hydrator.yaml
generated
18
manifests/install-with-hydrator.yaml
generated
@@ -31776,7 +31776,7 @@ spec:
|
||||
key: applicationsetcontroller.status.max.resources.count
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
name: argocd-applicationset-controller
|
||||
ports:
|
||||
@@ -31917,7 +31917,7 @@ spec:
|
||||
key: log.format.timestamp
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
failureThreshold: 3
|
||||
@@ -32075,7 +32075,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /shared/argocd-dex
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
name: copyutil
|
||||
securityContext:
|
||||
@@ -32177,7 +32177,7 @@ spec:
|
||||
key: notificationscontroller.repo.server.plaintext
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
@@ -32279,7 +32279,7 @@ spec:
|
||||
- argocd
|
||||
- admin
|
||||
- redis-initial-password
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: IfNotPresent
|
||||
name: secret-init
|
||||
securityContext:
|
||||
@@ -32588,7 +32588,7 @@ spec:
|
||||
value: /helm-working-dir
|
||||
- name: HELM_DATA_HOME
|
||||
value: /helm-working-dir
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
failureThreshold: 3
|
||||
@@ -32641,7 +32641,7 @@ spec:
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
name: copyutil
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
@@ -33068,7 +33068,7 @@ spec:
|
||||
key: server.sync.replace.allowed
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
@@ -33500,7 +33500,7 @@ spec:
|
||||
optional: true
|
||||
- name: KUBECACHEDIR
|
||||
value: /tmp/kubecache
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
name: argocd-application-controller
|
||||
ports:
|
||||
|
||||
16
manifests/install.yaml
generated
16
manifests/install.yaml
generated
@@ -31744,7 +31744,7 @@ spec:
|
||||
key: applicationsetcontroller.status.max.resources.count
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
name: argocd-applicationset-controller
|
||||
ports:
|
||||
@@ -31903,7 +31903,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /shared/argocd-dex
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
name: copyutil
|
||||
securityContext:
|
||||
@@ -32005,7 +32005,7 @@ spec:
|
||||
key: notificationscontroller.repo.server.plaintext
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
@@ -32107,7 +32107,7 @@ spec:
|
||||
- argocd
|
||||
- admin
|
||||
- redis-initial-password
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: IfNotPresent
|
||||
name: secret-init
|
||||
securityContext:
|
||||
@@ -32416,7 +32416,7 @@ spec:
|
||||
value: /helm-working-dir
|
||||
- name: HELM_DATA_HOME
|
||||
value: /helm-working-dir
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
failureThreshold: 3
|
||||
@@ -32469,7 +32469,7 @@ spec:
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
name: copyutil
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
@@ -32896,7 +32896,7 @@ spec:
|
||||
key: server.sync.replace.allowed
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
@@ -33328,7 +33328,7 @@ spec:
|
||||
optional: true
|
||||
- name: KUBECACHEDIR
|
||||
value: /tmp/kubecache
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
name: argocd-application-controller
|
||||
ports:
|
||||
|
||||
18
manifests/namespace-install-with-hydrator.yaml
generated
18
manifests/namespace-install-with-hydrator.yaml
generated
@@ -1023,7 +1023,7 @@ spec:
|
||||
key: applicationsetcontroller.status.max.resources.count
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
name: argocd-applicationset-controller
|
||||
ports:
|
||||
@@ -1164,7 +1164,7 @@ spec:
|
||||
key: log.format.timestamp
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
failureThreshold: 3
|
||||
@@ -1322,7 +1322,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /shared/argocd-dex
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
name: copyutil
|
||||
securityContext:
|
||||
@@ -1424,7 +1424,7 @@ spec:
|
||||
key: notificationscontroller.repo.server.plaintext
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
@@ -1526,7 +1526,7 @@ spec:
|
||||
- argocd
|
||||
- admin
|
||||
- redis-initial-password
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: IfNotPresent
|
||||
name: secret-init
|
||||
securityContext:
|
||||
@@ -1835,7 +1835,7 @@ spec:
|
||||
value: /helm-working-dir
|
||||
- name: HELM_DATA_HOME
|
||||
value: /helm-working-dir
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
failureThreshold: 3
|
||||
@@ -1888,7 +1888,7 @@ spec:
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
name: copyutil
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
@@ -2315,7 +2315,7 @@ spec:
|
||||
key: server.sync.replace.allowed
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
@@ -2747,7 +2747,7 @@ spec:
|
||||
optional: true
|
||||
- name: KUBECACHEDIR
|
||||
value: /tmp/kubecache
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
name: argocd-application-controller
|
||||
ports:
|
||||
|
||||
16
manifests/namespace-install.yaml
generated
16
manifests/namespace-install.yaml
generated
@@ -991,7 +991,7 @@ spec:
|
||||
key: applicationsetcontroller.status.max.resources.count
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
name: argocd-applicationset-controller
|
||||
ports:
|
||||
@@ -1150,7 +1150,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /shared/argocd-dex
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
name: copyutil
|
||||
securityContext:
|
||||
@@ -1252,7 +1252,7 @@ spec:
|
||||
key: notificationscontroller.repo.server.plaintext
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
@@ -1354,7 +1354,7 @@ spec:
|
||||
- argocd
|
||||
- admin
|
||||
- redis-initial-password
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: IfNotPresent
|
||||
name: secret-init
|
||||
securityContext:
|
||||
@@ -1663,7 +1663,7 @@ spec:
|
||||
value: /helm-working-dir
|
||||
- name: HELM_DATA_HOME
|
||||
value: /helm-working-dir
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
failureThreshold: 3
|
||||
@@ -1716,7 +1716,7 @@ spec:
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
name: copyutil
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
@@ -2143,7 +2143,7 @@ spec:
|
||||
key: server.sync.replace.allowed
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
@@ -2575,7 +2575,7 @@ spec:
|
||||
optional: true
|
||||
- name: KUBECACHEDIR
|
||||
value: /tmp/kubecache
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc1
|
||||
image: quay.io/argoproj/argocd:v3.4.0-rc2
|
||||
imagePullPolicy: Always
|
||||
name: argocd-application-controller
|
||||
ports:
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/TomOnTime/utfutil"
|
||||
"github.com/bmatcuk/doublestar/v4"
|
||||
imagev1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
@@ -656,6 +657,13 @@ func (s *Service) GenerateManifest(ctx context.Context, q *apiclient.ManifestReq
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Convert typed errors to gRPC status codes so callers can use status.Code()
|
||||
// rather than string matching.
|
||||
var globNoMatch *GlobNoMatchError
|
||||
if errors.As(err, &globNoMatch) {
|
||||
return nil, status.Error(codes.NotFound, err.Error())
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
|
||||
@@ -1376,19 +1384,55 @@ func getResolvedValueFiles(
|
||||
gitRepoPaths utilio.TempPaths,
|
||||
ignoreMissingValueFiles bool,
|
||||
) ([]pathutil.ResolvedFilePath, error) {
|
||||
// Pre-collect resolved paths for all explicit (non-glob) entries. This allows glob
|
||||
// expansion to skip files that also appear explicitly, so the explicit entry controls
|
||||
// the final position. For example, with ["*.yaml", "c.yaml"], c.yaml is excluded from
|
||||
// the glob expansion and placed at the end where it was explicitly listed.
|
||||
explicitPaths := make(map[pathutil.ResolvedFilePath]struct{})
|
||||
for _, rawValueFile := range rawValueFiles {
|
||||
referencedSource := getReferencedSource(rawValueFile, refSources)
|
||||
var resolved pathutil.ResolvedFilePath
|
||||
var err error
|
||||
if referencedSource != nil {
|
||||
resolved, err = getResolvedRefValueFile(rawValueFile, env, allowedValueFilesSchemas, referencedSource.Repo.Repo, gitRepoPaths)
|
||||
} else {
|
||||
resolved, _, err = pathutil.ResolveValueFilePathOrUrl(appPath, repoRoot, env.Envsubst(rawValueFile), allowedValueFilesSchemas)
|
||||
}
|
||||
if err != nil {
|
||||
continue // resolution errors will be surfaced in the main loop below
|
||||
}
|
||||
if !isGlobPath(string(resolved)) {
|
||||
explicitPaths[resolved] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
var resolvedValueFiles []pathutil.ResolvedFilePath
|
||||
seen := make(map[pathutil.ResolvedFilePath]struct{})
|
||||
appendUnique := func(p pathutil.ResolvedFilePath) {
|
||||
if _, ok := seen[p]; !ok {
|
||||
seen[p] = struct{}{}
|
||||
resolvedValueFiles = append(resolvedValueFiles, p)
|
||||
}
|
||||
}
|
||||
for _, rawValueFile := range rawValueFiles {
|
||||
isRemote := false
|
||||
var resolvedPath pathutil.ResolvedFilePath
|
||||
var err error
|
||||
|
||||
referencedSource := getReferencedSource(rawValueFile, refSources)
|
||||
// effectiveRoot is the repository root used for the symlink boundary check
|
||||
// on glob matches. For ref-source paths this is the external repo's checkout
|
||||
// directory; for local paths it is the main repo root.
|
||||
effectiveRoot := repoRoot
|
||||
if referencedSource != nil {
|
||||
// If the $-prefixed path appears to reference another source, do env substitution _after_ resolving that source.
|
||||
resolvedPath, err = getResolvedRefValueFile(rawValueFile, env, allowedValueFilesSchemas, referencedSource.Repo.Repo, gitRepoPaths)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error resolving value file path: %w", err)
|
||||
}
|
||||
if refRepoPath := gitRepoPaths.GetPathIfExists(git.NormalizeGitURL(referencedSource.Repo.Repo)); refRepoPath != "" {
|
||||
effectiveRoot = refRepoPath
|
||||
}
|
||||
} else {
|
||||
// This will resolve val to an absolute path (or a URL)
|
||||
resolvedPath, isRemote, err = pathutil.ResolveValueFilePathOrUrl(appPath, repoRoot, env.Envsubst(rawValueFile), allowedValueFilesSchemas)
|
||||
@@ -1397,6 +1441,38 @@ func getResolvedValueFiles(
|
||||
}
|
||||
}
|
||||
|
||||
// If the resolved path contains a glob pattern, expand it to all matching files.
|
||||
// doublestar.FilepathGlob is used (consistent with AppSet generators) because it supports
|
||||
// ** for recursive matching in addition to all standard glob patterns (*,?,[).
|
||||
// Matches are returned in lexical order, which determines helm's merge precedence
|
||||
// (later files override earlier ones). Glob patterns are only expanded for local files;
|
||||
// remote value file URLs (e.g. https://...) are passed through as-is.
|
||||
// If the glob matches no files and ignoreMissingValueFiles is true, skip it silently.
|
||||
// Otherwise, return an error — consistent with how missing non-glob value files are handled.
|
||||
if !isRemote && isGlobPath(string(resolvedPath)) {
|
||||
matches, err := doublestar.FilepathGlob(string(resolvedPath))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error expanding glob pattern %q: %w", rawValueFile, err)
|
||||
}
|
||||
if len(matches) == 0 {
|
||||
if ignoreMissingValueFiles {
|
||||
log.Debugf(" %s values file glob matched no files", rawValueFile)
|
||||
continue
|
||||
}
|
||||
return nil, &GlobNoMatchError{Pattern: rawValueFile}
|
||||
}
|
||||
if err := verifyGlobMatchesWithinRoot(matches, effectiveRoot); err != nil {
|
||||
return nil, fmt.Errorf("glob pattern %q: %w", rawValueFile, err)
|
||||
}
|
||||
for _, match := range matches {
|
||||
// Skip files that are also listed explicitly - they will be placed
|
||||
// at their explicit position rather than the glob's position.
|
||||
if _, isExplicit := explicitPaths[pathutil.ResolvedFilePath(match)]; !isExplicit {
|
||||
appendUnique(pathutil.ResolvedFilePath(match))
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !isRemote {
|
||||
_, err = os.Stat(string(resolvedPath))
|
||||
if os.IsNotExist(err) {
|
||||
@@ -1407,8 +1483,9 @@ func getResolvedValueFiles(
|
||||
}
|
||||
}
|
||||
|
||||
resolvedValueFiles = append(resolvedValueFiles, resolvedPath)
|
||||
appendUnique(resolvedPath)
|
||||
}
|
||||
log.Infof("resolved value files: %v", resolvedValueFiles)
|
||||
return resolvedValueFiles, nil
|
||||
}
|
||||
|
||||
@@ -1478,6 +1555,61 @@ func getRepoCredential(repoCredentials []*v1alpha1.RepoCreds, repoURL string) *v
|
||||
return nil
|
||||
}
|
||||
|
||||
// GlobNoMatchError is returned when a glob pattern in valueFiles matches no files.
|
||||
// It is a runtime condition (the files may be added later), not a spec error.
|
||||
type GlobNoMatchError struct {
|
||||
Pattern string
|
||||
}
|
||||
|
||||
func (e *GlobNoMatchError) Error() string {
|
||||
return fmt.Sprintf("values file glob %q matched no files", e.Pattern)
|
||||
}
|
||||
|
||||
// isGlobPath reports whether path contains any glob metacharacters
|
||||
// supported by doublestar: *, ?, or [. The ** pattern is covered by *.
|
||||
func isGlobPath(path string) bool {
|
||||
return strings.ContainsAny(path, "*?[")
|
||||
}
|
||||
|
||||
// verifyGlobMatchesWithinRoot resolves symlinks for each glob match and verifies
|
||||
// that the resolved target is within effectiveRoot. It protects against symlinks
|
||||
// inside the repository that point to targets outside it.
|
||||
//
|
||||
// doublestar.FilepathGlob uses os.Lstat, so it returns the path of the symlink
|
||||
// itself (which lives inside the repo) rather than the symlink target. If the
|
||||
// target is outside the repo, Helm would still follow the link and read the
|
||||
// external file. This function catches that case before the paths reach Helm.
|
||||
//
|
||||
// Both effectiveRoot and each match are canonicalized via filepath.EvalSymlinks
|
||||
// so the prefix comparison is correct on systems where the working directory is
|
||||
// itself under a symlink chain (e.g. /var -> /private/var on macOS).
|
||||
func verifyGlobMatchesWithinRoot(matches []string, effectiveRoot string) error {
|
||||
absRoot, err := filepath.Abs(effectiveRoot)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error resolving repo root: %w", err)
|
||||
}
|
||||
canonicalRoot, err := filepath.EvalSymlinks(absRoot)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error resolving symlinks in repo root: %w", err)
|
||||
}
|
||||
requiredRootPath := canonicalRoot
|
||||
if !strings.HasSuffix(requiredRootPath, string(os.PathSeparator)) {
|
||||
requiredRootPath += string(os.PathSeparator)
|
||||
}
|
||||
for _, match := range matches {
|
||||
realMatch, err := filepath.EvalSymlinks(match)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error resolving symlink for glob match %q: %w", match, err)
|
||||
}
|
||||
// Allow the match to resolve exactly to the root (realMatch+sep == requiredRootPath)
|
||||
// or to any path beneath it (HasPrefix).
|
||||
if realMatch+string(os.PathSeparator) != requiredRootPath && !strings.HasPrefix(realMatch, requiredRootPath) {
|
||||
return fmt.Errorf("glob match %q resolved to outside repository root", match)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type (
|
||||
GenerateManifestOpt func(*generateManifestOpt)
|
||||
generateManifestOpt struct {
|
||||
|
||||
@@ -3895,6 +3895,567 @@ func Test_getResolvedValueFiles(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getResolvedValueFiles_glob(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tempDir := t.TempDir()
|
||||
paths := utilio.NewRandomizedTempPaths(tempDir)
|
||||
paths.Add(git.NormalizeGitURL("https://github.com/org/repo1"), path.Join(tempDir, "repo1"))
|
||||
|
||||
// main-repo files
|
||||
require.NoError(t, os.MkdirAll(path.Join(tempDir, "main-repo", "prod", "nested"), 0o755))
|
||||
require.NoError(t, os.MkdirAll(path.Join(tempDir, "main-repo", "staging"), 0o755))
|
||||
require.NoError(t, os.WriteFile(path.Join(tempDir, "main-repo", "prod", "a.yaml"), []byte{}, 0o644))
|
||||
require.NoError(t, os.WriteFile(path.Join(tempDir, "main-repo", "prod", "b.yaml"), []byte{}, 0o644))
|
||||
require.NoError(t, os.WriteFile(path.Join(tempDir, "main-repo", "prod", "nested", "c.yaml"), []byte{}, 0o644))
|
||||
require.NoError(t, os.WriteFile(path.Join(tempDir, "main-repo", "prod", "nested", "d.yaml"), []byte{}, 0o644))
|
||||
require.NoError(t, os.WriteFile(path.Join(tempDir, "main-repo", "staging", "e.yaml"), []byte{}, 0o644))
|
||||
|
||||
// main-repo envs: used to verify depth-order with ** (z.yaml sorts after nested/ alphabetically
|
||||
// but is still returned before nested/c.yaml because doublestar matches depth-0 files first).
|
||||
require.NoError(t, os.MkdirAll(path.Join(tempDir, "main-repo", "envs", "nested"), 0o755))
|
||||
require.NoError(t, os.WriteFile(path.Join(tempDir, "main-repo", "envs", "a.yaml"), []byte{}, 0o644))
|
||||
require.NoError(t, os.WriteFile(path.Join(tempDir, "main-repo", "envs", "z.yaml"), []byte{}, 0o644))
|
||||
require.NoError(t, os.WriteFile(path.Join(tempDir, "main-repo", "envs", "nested", "c.yaml"), []byte{}, 0o644))
|
||||
|
||||
// repo1 files
|
||||
require.NoError(t, os.MkdirAll(path.Join(tempDir, "repo1", "prod", "nested"), 0o755))
|
||||
require.NoError(t, os.WriteFile(path.Join(tempDir, "repo1", "prod", "x.yaml"), []byte{}, 0o644))
|
||||
require.NoError(t, os.WriteFile(path.Join(tempDir, "repo1", "prod", "y.yaml"), []byte{}, 0o644))
|
||||
require.NoError(t, os.WriteFile(path.Join(tempDir, "repo1", "prod", "nested", "z.yaml"), []byte{}, 0o644))
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
rawPath string
|
||||
env *v1alpha1.Env
|
||||
refSources map[string]*v1alpha1.RefTarget
|
||||
expectedPaths []string
|
||||
ignoreMissingValueFiles bool
|
||||
expectedErr bool
|
||||
}{
|
||||
{
|
||||
name: "local glob matches multiple files",
|
||||
rawPath: "prod/*.yaml",
|
||||
env: &v1alpha1.Env{},
|
||||
refSources: map[string]*v1alpha1.RefTarget{},
|
||||
expectedPaths: []string{
|
||||
// the order is a.yaml before b.yaml
|
||||
// since doublestar.FilepathGlob returns lexical order
|
||||
path.Join(tempDir, "main-repo", "prod", "a.yaml"),
|
||||
path.Join(tempDir, "main-repo", "prod", "b.yaml"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "local glob matches no files returns error",
|
||||
rawPath: "dev/*.yaml",
|
||||
env: &v1alpha1.Env{},
|
||||
refSources: map[string]*v1alpha1.RefTarget{},
|
||||
expectedPaths: nil,
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
name: "local glob matches no files with ignoreMissingValueFiles set to true",
|
||||
rawPath: "dev/*.yaml",
|
||||
env: &v1alpha1.Env{},
|
||||
refSources: map[string]*v1alpha1.RefTarget{},
|
||||
ignoreMissingValueFiles: true,
|
||||
expectedPaths: nil,
|
||||
},
|
||||
{
|
||||
name: "referenced glob matches multiple files in external repo",
|
||||
rawPath: "$ref/prod/*.yaml",
|
||||
env: &v1alpha1.Env{},
|
||||
refSources: map[string]*v1alpha1.RefTarget{
|
||||
"$ref": {
|
||||
Repo: v1alpha1.Repository{
|
||||
Repo: "https://github.com/org/repo1",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedPaths: []string{
|
||||
path.Join(tempDir, "repo1", "prod", "x.yaml"),
|
||||
path.Join(tempDir, "repo1", "prod", "y.yaml"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ref glob with env var in path",
|
||||
rawPath: "$ref/$ENV/*.yaml",
|
||||
env: &v1alpha1.Env{
|
||||
&v1alpha1.EnvEntry{
|
||||
Name: "ENV",
|
||||
Value: "prod",
|
||||
},
|
||||
},
|
||||
refSources: map[string]*v1alpha1.RefTarget{
|
||||
"$ref": {
|
||||
Repo: v1alpha1.Repository{
|
||||
Repo: "https://github.com/org/repo1",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedPaths: []string{
|
||||
path.Join(tempDir, "repo1", "prod", "x.yaml"),
|
||||
path.Join(tempDir, "repo1", "prod", "y.yaml"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "local glob single match",
|
||||
rawPath: "prod/a*.yaml",
|
||||
env: &v1alpha1.Env{},
|
||||
refSources: map[string]*v1alpha1.RefTarget{},
|
||||
expectedPaths: []string{path.Join(tempDir, "main-repo", "prod", "a.yaml")},
|
||||
},
|
||||
{
|
||||
name: "recursive glob matches files at all depths under a subdirectory",
|
||||
// ** matches zero or more path segments, so prod/**/*.yaml covers both
|
||||
// prod/*.yaml (zero intermediate segments) and prod/nested/*.yaml (one segment), etc.
|
||||
rawPath: "prod/**/*.yaml",
|
||||
env: &v1alpha1.Env{},
|
||||
refSources: map[string]*v1alpha1.RefTarget{},
|
||||
// lexical order: prod/a.yaml, prod/b.yaml, prod/nested/c.yaml, prod/nested/d.yaml
|
||||
expectedPaths: []string{
|
||||
path.Join(tempDir, "main-repo", "prod", "a.yaml"),
|
||||
path.Join(tempDir, "main-repo", "prod", "b.yaml"),
|
||||
path.Join(tempDir, "main-repo", "prod", "nested", "c.yaml"),
|
||||
path.Join(tempDir, "main-repo", "prod", "nested", "d.yaml"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "recursive glob from repo root matches yaml files across all directories",
|
||||
rawPath: "**/*.yaml",
|
||||
env: &v1alpha1.Env{},
|
||||
refSources: map[string]*v1alpha1.RefTarget{},
|
||||
// doublestar traverses directories in lexical order, processing each directory's
|
||||
// own files before its subdirectories. So the order is:
|
||||
// envs/ flat files → envs/nested/ files → prod/ flat files → prod/nested/ files → staging/ files
|
||||
expectedPaths: []string{
|
||||
path.Join(tempDir, "main-repo", "envs", "a.yaml"),
|
||||
path.Join(tempDir, "main-repo", "envs", "z.yaml"),
|
||||
path.Join(tempDir, "main-repo", "envs", "nested", "c.yaml"),
|
||||
path.Join(tempDir, "main-repo", "prod", "a.yaml"),
|
||||
path.Join(tempDir, "main-repo", "prod", "b.yaml"),
|
||||
path.Join(tempDir, "main-repo", "prod", "nested", "c.yaml"),
|
||||
path.Join(tempDir, "main-repo", "prod", "nested", "d.yaml"),
|
||||
path.Join(tempDir, "main-repo", "staging", "e.yaml"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "recursive glob anchored to a named subdirectory matches at any depth",
|
||||
rawPath: "**/nested/*.yaml",
|
||||
env: &v1alpha1.Env{},
|
||||
refSources: map[string]*v1alpha1.RefTarget{},
|
||||
expectedPaths: []string{
|
||||
path.Join(tempDir, "main-repo", "envs", "nested", "c.yaml"),
|
||||
path.Join(tempDir, "main-repo", "prod", "nested", "c.yaml"),
|
||||
path.Join(tempDir, "main-repo", "prod", "nested", "d.yaml"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "recursive glob with no matches and ignoreMissingValueFiles skips silently",
|
||||
rawPath: "**/nonexistent/*.yaml",
|
||||
env: &v1alpha1.Env{},
|
||||
refSources: map[string]*v1alpha1.RefTarget{},
|
||||
ignoreMissingValueFiles: true,
|
||||
expectedPaths: nil,
|
||||
},
|
||||
{
|
||||
name: "recursive glob with no matches returns error",
|
||||
rawPath: "**/nonexistent/*.yaml",
|
||||
env: &v1alpha1.Env{},
|
||||
refSources: map[string]*v1alpha1.RefTarget{},
|
||||
expectedPaths: nil,
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
// z.yaml sorts after "nested/" alphabetically by full path, but doublestar processes
|
||||
// each directory's own files before descending into subdirectories. So for envs/**/*.yaml:
|
||||
// envs/ flat files (a, z) come before envs/nested/ files (c), giving:
|
||||
// a.yaml, z.yaml, nested/c.yaml — not a.yaml, nested/c.yaml, z.yaml.
|
||||
name: "** depth-order: flat files before nested even when flat file sorts after nested/ alphabetically",
|
||||
rawPath: "envs/**/*.yaml",
|
||||
env: &v1alpha1.Env{},
|
||||
refSources: map[string]*v1alpha1.RefTarget{},
|
||||
expectedPaths: []string{
|
||||
path.Join(tempDir, "main-repo", "envs", "a.yaml"),
|
||||
path.Join(tempDir, "main-repo", "envs", "z.yaml"),
|
||||
path.Join(tempDir, "main-repo", "envs", "nested", "c.yaml"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "recursive glob in external ref repo",
|
||||
rawPath: "$ref/prod/**/*.yaml",
|
||||
env: &v1alpha1.Env{},
|
||||
refSources: map[string]*v1alpha1.RefTarget{
|
||||
"$ref": {
|
||||
Repo: v1alpha1.Repository{
|
||||
Repo: "https://github.com/org/repo1",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedPaths: []string{
|
||||
// doublestar matches zero path segments before recursing into subdirectories,
|
||||
// so flat files (x, y) come before nested ones (nested/z).
|
||||
path.Join(tempDir, "repo1", "prod", "x.yaml"),
|
||||
path.Join(tempDir, "repo1", "prod", "y.yaml"),
|
||||
path.Join(tempDir, "repo1", "prod", "nested", "z.yaml"),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
repoPath := path.Join(tempDir, "main-repo")
|
||||
resolvedPaths, err := getResolvedValueFiles(repoPath, repoPath, tt.env, []string{}, []string{tt.rawPath}, tt.refSources, paths, tt.ignoreMissingValueFiles)
|
||||
if tt.expectedErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resolvedPaths, len(tt.expectedPaths))
|
||||
|
||||
for i, p := range tt.expectedPaths {
|
||||
assert.Equal(t, p, string(resolvedPaths[i]))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Deduplication: first occurrence of a resolved path wins. Subsequent references to the
|
||||
// same file, whether explicit or via glob are silently dropped. This preserves the
|
||||
// merge-precedence position set by the first mention of each file.
|
||||
t.Run("glob then explicit: explicit entry placed at end, giving it highest Helm precedence", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
repoPath := path.Join(tempDir, "main-repo")
|
||||
resolvedPaths, err := getResolvedValueFiles(
|
||||
repoPath, repoPath,
|
||||
&v1alpha1.Env{}, []string{},
|
||||
[]string{
|
||||
"envs/*.yaml", // glob - z.yaml is explicit so skipped; only a.yaml added
|
||||
"envs/z.yaml", // explicit - placed last, highest precedence
|
||||
},
|
||||
map[string]*v1alpha1.RefTarget{}, paths, false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resolvedPaths, 2)
|
||||
assert.Equal(t, path.Join(tempDir, "main-repo", "envs", "a.yaml"), string(resolvedPaths[0]))
|
||||
assert.Equal(t, path.Join(tempDir, "main-repo", "envs", "z.yaml"), string(resolvedPaths[1]))
|
||||
})
|
||||
|
||||
t.Run("explicit path before glob: explicit position is kept, glob re-match is dropped", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
repoPath := path.Join(tempDir, "main-repo")
|
||||
resolvedPaths, err := getResolvedValueFiles(
|
||||
repoPath, repoPath,
|
||||
&v1alpha1.Env{}, []string{},
|
||||
[]string{
|
||||
"prod/a.yaml", // explicit locks in position 0
|
||||
"prod/*.yaml", // glob - a.yaml already seen, only b.yaml is new
|
||||
},
|
||||
map[string]*v1alpha1.RefTarget{}, paths, false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resolvedPaths, 2)
|
||||
assert.Equal(t, path.Join(tempDir, "main-repo", "prod", "a.yaml"), string(resolvedPaths[0]))
|
||||
assert.Equal(t, path.Join(tempDir, "main-repo", "prod", "b.yaml"), string(resolvedPaths[1]))
|
||||
})
|
||||
|
||||
t.Run("glob before explicit path: explicit position wins, glob skips the explicitly listed file", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
repoPath := path.Join(tempDir, "main-repo")
|
||||
resolvedPaths, err := getResolvedValueFiles(
|
||||
repoPath, repoPath,
|
||||
&v1alpha1.Env{}, []string{},
|
||||
[]string{
|
||||
"prod/*.yaml", // glob - a.yaml is explicit so skipped; only b.yaml added (pos 0)
|
||||
"prod/a.yaml", // explicit - placed here at pos 1 (highest precedence)
|
||||
},
|
||||
map[string]*v1alpha1.RefTarget{}, paths, false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resolvedPaths, 2)
|
||||
assert.Equal(t, path.Join(tempDir, "main-repo", "prod", "b.yaml"), string(resolvedPaths[0]))
|
||||
assert.Equal(t, path.Join(tempDir, "main-repo", "prod", "a.yaml"), string(resolvedPaths[1]))
|
||||
})
|
||||
|
||||
t.Run("two overlapping globs: second glob only adds files not matched by first", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
repoPath := path.Join(tempDir, "main-repo")
|
||||
resolvedPaths, err := getResolvedValueFiles(
|
||||
repoPath, repoPath,
|
||||
&v1alpha1.Env{}, []string{},
|
||||
[]string{
|
||||
"prod/*.yaml", // adds a.yaml, b.yaml
|
||||
"prod/**/*.yaml", // a.yaml, b.yaml already seen; adds nested/c.yaml, nested/d.yaml
|
||||
},
|
||||
map[string]*v1alpha1.RefTarget{}, paths, false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resolvedPaths, 4)
|
||||
assert.Equal(t, path.Join(tempDir, "main-repo", "prod", "a.yaml"), string(resolvedPaths[0]))
|
||||
assert.Equal(t, path.Join(tempDir, "main-repo", "prod", "b.yaml"), string(resolvedPaths[1]))
|
||||
assert.Equal(t, path.Join(tempDir, "main-repo", "prod", "nested", "c.yaml"), string(resolvedPaths[2]))
|
||||
assert.Equal(t, path.Join(tempDir, "main-repo", "prod", "nested", "d.yaml"), string(resolvedPaths[3]))
|
||||
})
|
||||
|
||||
t.Run("explicit paths take priority: globs skip explicitly listed files, which are placed at their explicit positions", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
repoPath := path.Join(tempDir, "main-repo")
|
||||
resolvedPaths, err := getResolvedValueFiles(
|
||||
repoPath, repoPath,
|
||||
&v1alpha1.Env{}, []string{},
|
||||
[]string{
|
||||
"prod/a.yaml", // explicit - pos 0
|
||||
"prod/*.yaml", // a.yaml and b.yaml are both explicit, skipped entirely
|
||||
"prod/b.yaml", // explicit - pos 1
|
||||
"prod/**/*.yaml", // a.yaml, b.yaml, nested/c.yaml all explicit and skipped; nested/d.yaml added - pos 2
|
||||
"prod/nested/c.yaml", // explicit - pos 3
|
||||
},
|
||||
map[string]*v1alpha1.RefTarget{}, paths, false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resolvedPaths, 4)
|
||||
assert.Equal(t, path.Join(tempDir, "main-repo", "prod", "a.yaml"), string(resolvedPaths[0]))
|
||||
assert.Equal(t, path.Join(tempDir, "main-repo", "prod", "b.yaml"), string(resolvedPaths[1]))
|
||||
assert.Equal(t, path.Join(tempDir, "main-repo", "prod", "nested", "d.yaml"), string(resolvedPaths[2]))
|
||||
assert.Equal(t, path.Join(tempDir, "main-repo", "prod", "nested", "c.yaml"), string(resolvedPaths[3]))
|
||||
})
|
||||
}
|
||||
|
||||
func Test_verifyGlobMatchesWithinRoot(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tempDir := t.TempDir()
|
||||
repoDir := filepath.Join(tempDir, "repo")
|
||||
outsideDir := filepath.Join(tempDir, "outside")
|
||||
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(repoDir, "values", "sub"), 0o755))
|
||||
require.NoError(t, os.MkdirAll(outsideDir, 0o755))
|
||||
|
||||
// Files used as symlink targets
|
||||
inRepoFile := filepath.Join(repoDir, "values", "real.yaml")
|
||||
outsideFile := filepath.Join(outsideDir, "secret.yaml")
|
||||
require.NoError(t, os.WriteFile(inRepoFile, []byte{}, 0o644))
|
||||
require.NoError(t, os.WriteFile(outsideFile, []byte("password: hunter2"), 0o644))
|
||||
|
||||
// Symlink inside repo → file inside repo (safe)
|
||||
inRepoLink := filepath.Join(repoDir, "values", "inrepo-link.yaml")
|
||||
require.NoError(t, os.Symlink(inRepoFile, inRepoLink))
|
||||
|
||||
// Symlink inside repo → file outside repo (escape)
|
||||
escapeLink := filepath.Join(repoDir, "values", "escape-link.yaml")
|
||||
require.NoError(t, os.Symlink(outsideFile, escapeLink))
|
||||
|
||||
// Two-hop symlink: inside repo → another symlink (still inside) → file inside repo
|
||||
hop1 := filepath.Join(repoDir, "values", "hop1.yaml")
|
||||
require.NoError(t, os.Symlink(inRepoLink, hop1)) // hop1 → inRepoLink → real.yaml
|
||||
|
||||
// Two-hop symlink: inside repo → another symlink (inside repo) → file outside repo
|
||||
hop2 := filepath.Join(repoDir, "values", "hop2.yaml")
|
||||
require.NoError(t, os.Symlink(escapeLink, hop2)) // hop2 → escape-link → secret.yaml
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
matches []string
|
||||
expectErr bool
|
||||
errContains string
|
||||
}{
|
||||
{
|
||||
name: "regular file inside root passes",
|
||||
matches: []string{inRepoFile},
|
||||
},
|
||||
{
|
||||
name: "symlink inside root pointing to file inside root passes",
|
||||
matches: []string{inRepoLink},
|
||||
},
|
||||
{
|
||||
name: "two-hop chain that stays within root passes",
|
||||
matches: []string{hop1},
|
||||
},
|
||||
{
|
||||
name: "symlink pointing directly outside root is rejected",
|
||||
matches: []string{escapeLink},
|
||||
expectErr: true,
|
||||
errContains: "resolved to outside repository root",
|
||||
},
|
||||
{
|
||||
name: "two-hop chain that escapes root is rejected",
|
||||
matches: []string{hop2},
|
||||
expectErr: true,
|
||||
errContains: "resolved to outside repository root",
|
||||
},
|
||||
{
|
||||
name: "multiple matches all inside root pass",
|
||||
matches: []string{inRepoFile, inRepoLink, hop1},
|
||||
},
|
||||
{
|
||||
name: "one bad match in a list fails the whole call",
|
||||
matches: []string{inRepoFile, escapeLink},
|
||||
expectErr: true,
|
||||
errContains: "resolved to outside repository root",
|
||||
},
|
||||
{
|
||||
name: "empty matches list is a no-op",
|
||||
matches: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
err := verifyGlobMatchesWithinRoot(tt.matches, repoDir)
|
||||
if tt.expectErr {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.errContains)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test_getResolvedValueFiles_glob_symlink_escape is an integration-level check
|
||||
// that verifyGlobMatchesWithinRoot is wired into glob expansion correctly: a
|
||||
// symlink inside the repo pointing outside must cause getResolvedValueFiles to
|
||||
// return an error rather than silently including the external file.
|
||||
func Test_getResolvedValueFiles_glob_symlink_escape(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tempDir := t.TempDir()
|
||||
paths := utilio.NewRandomizedTempPaths(tempDir)
|
||||
|
||||
repoDir := filepath.Join(tempDir, "repo")
|
||||
outsideDir := filepath.Join(tempDir, "outside")
|
||||
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(repoDir, "values"), 0o755))
|
||||
require.NoError(t, os.MkdirAll(outsideDir, 0o755))
|
||||
|
||||
require.NoError(t, os.WriteFile(filepath.Join(repoDir, "values", "base.yaml"), []byte{}, 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(outsideDir, "secret.yaml"), []byte("password: hunter2"), 0o644))
|
||||
require.NoError(t, os.Symlink(filepath.Join(outsideDir, "secret.yaml"), filepath.Join(repoDir, "values", "escape.yaml")))
|
||||
|
||||
_, err := getResolvedValueFiles(repoDir, repoDir, &v1alpha1.Env{}, []string{}, []string{"values/*.yaml"}, map[string]*v1alpha1.RefTarget{}, paths, false)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "resolved to outside repository root")
|
||||
}
|
||||
|
||||
func Test_isGlobPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
path string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
path: "prod/*.yaml",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
path: "prod/?.yaml",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
path: "prod[ab].yaml",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
path: "prod/**/*.yaml",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
path: "prod/values.yaml",
|
||||
},
|
||||
{
|
||||
path: "values.yaml",
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
},
|
||||
{
|
||||
path: "/absolute/path/to/*.yaml",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
path: "/absolute/path/to/values.yaml",
|
||||
},
|
||||
{
|
||||
path: "*",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
path: "?",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
path: "[",
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.path, func(t *testing.T) {
|
||||
assert.Equal(t, tt.expected, isGlobPath(tt.path))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getReferencedSource(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
refTarget := &v1alpha1.RefTarget{
|
||||
Repo: v1alpha1.Repository{
|
||||
Repo: "https://github.com/org/repo1",
|
||||
},
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
rawValueFile string
|
||||
refSources map[string]*v1alpha1.RefTarget
|
||||
expected *v1alpha1.RefTarget
|
||||
}{
|
||||
{
|
||||
name: "ref with file path found in map",
|
||||
rawValueFile: "$ref/values.yaml",
|
||||
refSources: map[string]*v1alpha1.RefTarget{
|
||||
"$ref": refTarget,
|
||||
},
|
||||
expected: refTarget,
|
||||
},
|
||||
{
|
||||
name: "ref with file path not in map",
|
||||
rawValueFile: "$ref/values.yaml",
|
||||
refSources: map[string]*v1alpha1.RefTarget{},
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "bare ref without file path found in map",
|
||||
rawValueFile: "$ref",
|
||||
refSources: map[string]*v1alpha1.RefTarget{
|
||||
"$ref": refTarget,
|
||||
},
|
||||
expected: refTarget,
|
||||
},
|
||||
{
|
||||
name: "empty string returns nil",
|
||||
rawValueFile: "",
|
||||
refSources: map[string]*v1alpha1.RefTarget{
|
||||
"$ref": refTarget,
|
||||
},
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "no $ prefix returns nil",
|
||||
rawValueFile: "values.yaml",
|
||||
refSources: map[string]*v1alpha1.RefTarget{
|
||||
"$ref": refTarget,
|
||||
},
|
||||
expected: nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
result := getReferencedSource(tt.rawValueFile, tt.refSources)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorGetGitDirectories(t *testing.T) {
|
||||
// test not using the cache
|
||||
root := "./testdata/git-files-dirs"
|
||||
|
||||
@@ -210,21 +210,9 @@ func (s *terminalHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if pod.Status.Phase != corev1.PodRunning {
|
||||
http.Error(w, "Pod not running", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var findContainer bool
|
||||
for _, c := range pod.Spec.Containers {
|
||||
if container == c.Name {
|
||||
findContainer = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !findContainer {
|
||||
fieldLog.Warn("terminal container not found")
|
||||
http.Error(w, "Cannot find container", http.StatusBadRequest)
|
||||
if !containerRunning(pod, container) {
|
||||
fieldLog.Warn("terminal container not running")
|
||||
http.Error(w, "container find running", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -273,6 +261,20 @@ func podExists(treeNodes []appv1.ResourceNode, podName, namespace string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func containerRunning(pod *corev1.Pod, containerName string) bool {
|
||||
return containerStatusRunning(pod.Status.ContainerStatuses, containerName) ||
|
||||
containerStatusRunning(pod.Status.InitContainerStatuses, containerName)
|
||||
}
|
||||
|
||||
func containerStatusRunning(statuses []corev1.ContainerStatus, containerName string) bool {
|
||||
for i := range statuses {
|
||||
if statuses[i].Name == containerName {
|
||||
return statuses[i].State.Running != nil
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const EndOfTransmission = "\u0004"
|
||||
|
||||
// PtyHandler is what remotecommand expects from a pty
|
||||
|
||||
@@ -5,9 +5,12 @@ import (
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/argo-cd/gitops-engine/pkg/utils/kube"
|
||||
"github.com/stretchr/testify/assert"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
appv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/v3/util/argo"
|
||||
@@ -79,6 +82,115 @@ func TestPodExists(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainerRunning(t *testing.T) {
|
||||
for _, tcase := range []struct {
|
||||
name string
|
||||
pod *corev1.Pod
|
||||
containerName string
|
||||
expectedResult bool
|
||||
}{
|
||||
{
|
||||
name: "empty container",
|
||||
pod: &corev1.Pod{},
|
||||
containerName: "",
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
name: "container not found",
|
||||
pod: &corev1.Pod{},
|
||||
containerName: "not-found",
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
name: "container running",
|
||||
pod: &corev1.Pod{
|
||||
Status: corev1.PodStatus{
|
||||
ContainerStatuses: []corev1.ContainerStatus{
|
||||
{
|
||||
Name: "test",
|
||||
State: corev1.ContainerState{
|
||||
Running: &corev1.ContainerStateRunning{
|
||||
StartedAt: metav1.NewTime(time.Now()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
containerName: "test",
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
name: "init container running",
|
||||
pod: &corev1.Pod{
|
||||
Status: corev1.PodStatus{
|
||||
ContainerStatuses: []corev1.ContainerStatus{
|
||||
{
|
||||
Name: "test",
|
||||
State: corev1.ContainerState{
|
||||
Running: &corev1.ContainerStateRunning{
|
||||
StartedAt: metav1.NewTime(time.Now()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
InitContainerStatuses: []corev1.ContainerStatus{
|
||||
{
|
||||
Name: "test-init",
|
||||
State: corev1.ContainerState{
|
||||
Running: &corev1.ContainerStateRunning{
|
||||
StartedAt: metav1.NewTime(time.Now()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
containerName: "test-init",
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
name: "container not running",
|
||||
pod: &corev1.Pod{
|
||||
Status: corev1.PodStatus{
|
||||
ContainerStatuses: []corev1.ContainerStatus{
|
||||
{
|
||||
Name: "test",
|
||||
State: corev1.ContainerState{
|
||||
Running: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
containerName: "test",
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
name: "init container not running",
|
||||
pod: &corev1.Pod{
|
||||
Status: corev1.PodStatus{
|
||||
InitContainerStatuses: []corev1.ContainerStatus{
|
||||
{
|
||||
Name: "test-init",
|
||||
State: corev1.ContainerState{
|
||||
Running: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
containerName: "test-init",
|
||||
expectedResult: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tcase.name, func(t *testing.T) {
|
||||
result := containerRunning(tcase.pod, tcase.containerName)
|
||||
assert.Equalf(t, tcase.expectedResult, result, "Expected result %v, but got %v", tcase.expectedResult, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsValidPodName(t *testing.T) {
|
||||
for _, tcase := range []struct {
|
||||
name string
|
||||
|
||||
@@ -156,6 +156,79 @@ func TestHelmIgnoreMissingValueFiles(t *testing.T) {
|
||||
Expect(ErrorRegex("Error: open .*does-not-exist-values.yaml: no such file or directory", ""))
|
||||
}
|
||||
|
||||
// TestHelmGlobValueFiles verifies that a glob pattern in valueFiles expands to all matching
|
||||
// files and that they are applied in lexical order (last file wins in helm merging).
|
||||
// envs/*.yaml expands to envs/a.yaml then envs/b.yaml - b.yaml is last, so foo = "b-value".
|
||||
func TestHelmGlobValueFiles(t *testing.T) {
|
||||
fixture.SkipOnEnv(t, "HELM")
|
||||
ctx := Given(t)
|
||||
ctx.Path("helm-glob-values").
|
||||
When().
|
||||
CreateApp().
|
||||
AppSet("--values", "envs/*.yaml").
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationSucceeded)).
|
||||
Expect(HealthIs(health.HealthStatusHealthy)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeSynced)).
|
||||
And(func(_ *Application) {
|
||||
val := errors.NewHandler(t).FailOnErr(fixture.Run(".", "kubectl", "-n", ctx.DeploymentNamespace(),
|
||||
"get", "cm", "my-map", "-o", "jsonpath={.data.foo}")).(string)
|
||||
assert.Equal(t, "b-value", val)
|
||||
})
|
||||
}
|
||||
|
||||
// TestHelmRecursiveGlobValueFiles verifies that the ** double-star pattern recursively
|
||||
// matches files at any depth. envs/**/*.yaml expands (zero-segments first) to:
|
||||
// envs/a.yaml, envs/b.yaml, envs/nested/c.yaml - c.yaml is last, so foo = "c-value".
|
||||
func TestHelmRecursiveGlobValueFiles(t *testing.T) {
|
||||
fixture.SkipOnEnv(t, "HELM")
|
||||
ctx := Given(t)
|
||||
ctx.Path("helm-glob-values").
|
||||
When().
|
||||
CreateApp().
|
||||
AppSet("--values", "envs/**/*.yaml").
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationSucceeded)).
|
||||
Expect(HealthIs(health.HealthStatusHealthy)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeSynced)).
|
||||
And(func(_ *Application) {
|
||||
val := errors.NewHandler(t).FailOnErr(fixture.Run(".", "kubectl", "-n", ctx.DeploymentNamespace(),
|
||||
"get", "cm", "my-map", "-o", "jsonpath={.data.foo}")).(string)
|
||||
assert.Equal(t, "c-value", val)
|
||||
})
|
||||
}
|
||||
|
||||
// TestHelmGlobValueFilesNoMatch verifies that a glob pattern with no matching files
|
||||
// surfaces as a comparison error on the application.
|
||||
func TestHelmGlobValueFilesNoMatch(t *testing.T) {
|
||||
fixture.SkipOnEnv(t, "HELM")
|
||||
Given(t).
|
||||
Path("helm-glob-values").
|
||||
When().
|
||||
CreateApp().
|
||||
AppSet("--values", "nonexistent/*.yaml").
|
||||
Then().
|
||||
Expect(Condition(ApplicationConditionComparisonError, `values file glob "nonexistent/*.yaml" matched no files`))
|
||||
}
|
||||
|
||||
// TestHelmGlobValueFilesIgnoreMissing verifies that a non-matching glob pattern is
|
||||
// silently skipped when ignoreMissingValueFiles is set, and the app syncs successfully.
|
||||
func TestHelmGlobValueFilesIgnoreMissing(t *testing.T) {
|
||||
fixture.SkipOnEnv(t, "HELM")
|
||||
Given(t).
|
||||
Path("helm-glob-values").
|
||||
When().
|
||||
CreateApp().
|
||||
AppSet("--values", "nonexistent/*.yaml", "--ignore-missing-value-files").
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationSucceeded)).
|
||||
Expect(HealthIs(health.HealthStatusHealthy)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeSynced))
|
||||
}
|
||||
|
||||
func TestHelmValuesMultipleUnset(t *testing.T) {
|
||||
Given(t).
|
||||
Path("helm").
|
||||
|
||||
3
test/e2e/testdata/helm-glob-values/Chart.yaml
vendored
Normal file
3
test/e2e/testdata/helm-glob-values/Chart.yaml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
apiVersion: v2
|
||||
version: 1.0.0
|
||||
name: helm-glob-values
|
||||
1
test/e2e/testdata/helm-glob-values/envs/a.yaml
vendored
Normal file
1
test/e2e/testdata/helm-glob-values/envs/a.yaml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
foo: a-value
|
||||
1
test/e2e/testdata/helm-glob-values/envs/b.yaml
vendored
Normal file
1
test/e2e/testdata/helm-glob-values/envs/b.yaml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
foo: b-value
|
||||
1
test/e2e/testdata/helm-glob-values/envs/nested/c.yaml
vendored
Normal file
1
test/e2e/testdata/helm-glob-values/envs/nested/c.yaml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
foo: c-value
|
||||
6
test/e2e/testdata/helm-glob-values/templates/config-map.yaml
vendored
Normal file
6
test/e2e/testdata/helm-glob-values/templates/config-map.yaml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: my-map
|
||||
data:
|
||||
foo: {{.Values.foo}}
|
||||
1
test/e2e/testdata/helm-glob-values/values.yaml
vendored
Normal file
1
test/e2e/testdata/helm-glob-values/values.yaml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
foo: default
|
||||
0
ui/dist/app/assets/images/resources/.gitkeep
vendored
Normal file
0
ui/dist/app/assets/images/resources/.gitkeep
vendored
Normal file
@@ -5,4 +5,5 @@ import "embed"
|
||||
// Embedded contains embedded UI resources
|
||||
//
|
||||
//go:embed dist/app
|
||||
//go:embed all:dist/app/assets/images/resources
|
||||
var Embedded embed.FS
|
||||
|
||||
@@ -8,6 +8,7 @@ import {services} from '../../../shared/services';
|
||||
import {
|
||||
ApplicationSyncWindowStatusIcon,
|
||||
ComparisonStatusIcon,
|
||||
formatApplicationSetProgressiveSyncStep,
|
||||
getAppDefaultSource,
|
||||
getAppDefaultSyncRevisionExtra,
|
||||
getAppOperationState,
|
||||
@@ -134,7 +135,7 @@ const ProgressiveSyncStatus = ({application}: {application: models.Application})
|
||||
<div className='application-status-panel__item-value' style={{color: getProgressiveSyncStatusColor(appResource.status)}}>
|
||||
{getProgressiveSyncStatusIcon({status: appResource.status})} {appResource.status}
|
||||
</div>
|
||||
{appResource?.step && <div className='application-status-panel__item-value'>Wave: {appResource.step}</div>}
|
||||
{appResource?.step !== undefined && <div className='application-status-panel__item-value'>{formatApplicationSetProgressiveSyncStep(appResource.step)}</div>}
|
||||
{lastTransitionTime && (
|
||||
<div className='application-status-panel__item-name' style={{marginBottom: '0.5em'}}>
|
||||
Last Transition: <br />
|
||||
|
||||
@@ -1858,6 +1858,14 @@ export function getAppUrl(app: appModels.AbstractApplication): string {
|
||||
return `${basePath}/${app.metadata.namespace}/${app.metadata.name}`;
|
||||
}
|
||||
|
||||
/** RollingSync step for display; backend uses -1 when no step matches the app's labels. */
|
||||
export function formatApplicationSetProgressiveSyncStep(step: string | undefined): string {
|
||||
if (step === '-1') {
|
||||
return 'Step: unmatched label';
|
||||
}
|
||||
return `Step: ${step ?? ''}`;
|
||||
}
|
||||
|
||||
export const getProgressiveSyncStatusIcon = ({status, isButton}: {status: string; isButton?: boolean}) => {
|
||||
const getIconProps = () => {
|
||||
switch (status) {
|
||||
|
||||
@@ -910,6 +910,12 @@ func verifyGenerateManifests(
|
||||
// and not whether it actually contains any manifests.
|
||||
_, err = repoClient.GenerateManifest(ctx, &req)
|
||||
if err != nil {
|
||||
// A glob pattern matching no files is a runtime condition, not a spec error —
|
||||
// the files may be added later. Skip adding an InvalidSpecError here and let
|
||||
// the app controller surface it as a ComparisonError during reconciliation.
|
||||
if status.Code(err) == codes.NotFound && strings.Contains(err.Error(), "matched no files") {
|
||||
continue
|
||||
}
|
||||
errMessage := fmt.Sprintf("Unable to generate manifests in %s: %s", source.Path, err)
|
||||
conditions = append(conditions, argoappv1.ApplicationCondition{
|
||||
Type: argoappv1.ApplicationConditionInvalidSpecError,
|
||||
|
||||
Reference in New Issue
Block a user