mirror of
https://github.com/argoproj/argo-cd.git
synced 2026-02-20 01:28:45 +01:00
Add custom resource health through lua
This commit is contained in:
committed by
dthomson25
parent
cefa9d9ba4
commit
00421bb46e
28
Gopkg.lock
generated
28
Gopkg.lock
generated
@@ -778,6 +778,19 @@
|
||||
pruneopts = ""
|
||||
revision = "ecda9a501e8220fae3b4b600c3db4b0ba22cfc68"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:525776d99293affd2c61dfb573007ff9f22863068c20c220ef3f58620758c341"
|
||||
name = "github.com/yuin/gopher-lua"
|
||||
packages = [
|
||||
".",
|
||||
"ast",
|
||||
"parse",
|
||||
"pm",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "732aa6820ec4fb93d60c4057dd574c33db8ad4e7"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:2ea6df0f542cc95a5e374e9cdd81eaa599ed0d55366eef92d2f6b9efa2795c07"
|
||||
@@ -1084,11 +1097,12 @@
|
||||
version = "v0.1.2"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:81314a486195626940617e43740b4fa073f265b0715c9f54ce2027fee1cb5f61"
|
||||
digest = "1:cedccf16b71e86db87a24f8d4c70b0a855872eb967cb906a66b95de56aefbd0d"
|
||||
name = "gopkg.in/yaml.v2"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "eb3733d160e74a9c7e442f435eb3bea458e1d19f"
|
||||
revision = "51d6538a90f86fe93ac480b35f37b2be17fef232"
|
||||
version = "v2.2.2"
|
||||
|
||||
[[projects]]
|
||||
branch = "release-1.12"
|
||||
@@ -1387,6 +1401,14 @@
|
||||
revision = "17c77c7898218073f14c8d573582e8d2313dc740"
|
||||
version = "v1.12.2"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:9b9f12f4c13ca4a4f4b4554c00ba46cb2910ff4079825d96d520b03c447e6da5"
|
||||
name = "layeh.com/gopher-json"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "97fed8db84274c421dbfffbb28ec859901556b97"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
@@ -1439,6 +1461,7 @@
|
||||
"github.com/vmihailenco/msgpack",
|
||||
"github.com/yudai/gojsondiff",
|
||||
"github.com/yudai/gojsondiff/formatter",
|
||||
"github.com/yuin/gopher-lua",
|
||||
"golang.org/x/crypto/bcrypt",
|
||||
"golang.org/x/crypto/ssh",
|
||||
"golang.org/x/crypto/ssh/terminal",
|
||||
@@ -1509,6 +1532,7 @@
|
||||
"k8s.io/kubernetes/pkg/apis/core",
|
||||
"k8s.io/kubernetes/pkg/kubectl/scheme",
|
||||
"k8s.io/kubernetes/pkg/util/node",
|
||||
"layeh.com/gopher-json",
|
||||
]
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
||||
@@ -1005,8 +1005,8 @@ func (ctrl *ApplicationController) watchSettings(ctx context.Context) {
|
||||
select {
|
||||
case newSettings := <-updateCh:
|
||||
newAppLabelKey := newSettings.GetAppInstanceLabelKey()
|
||||
*ctrl.settings = *newSettings
|
||||
if prevAppLabelKey != newAppLabelKey {
|
||||
ctrl.settings.AppInstanceLabelKey = newAppLabelKey
|
||||
log.Infof("label key changed: %s -> %s", prevAppLabelKey, newAppLabelKey)
|
||||
ctrl.stateCache.Invalidate()
|
||||
prevAppLabelKey = newAppLabelKey
|
||||
|
||||
@@ -270,7 +270,7 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, revision st
|
||||
syncStatus.Revision = manifestInfo.Revision
|
||||
}
|
||||
|
||||
healthStatus, err := health.SetApplicationHealth(resourceSummaries, GetLiveObjs(managedResources))
|
||||
healthStatus, err := health.SetApplicationHealth(resourceSummaries, GetLiveObjs(managedResources), m.settings.ResourceOverrides)
|
||||
if err != nil {
|
||||
conditions = append(conditions, appv1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error()})
|
||||
}
|
||||
|
||||
51
resource_customizations/argoproj.io/Rollout/health.lua
Normal file
51
resource_customizations/argoproj.io/Rollout/health.lua
Normal file
@@ -0,0 +1,51 @@
|
||||
hs = {}
|
||||
if obj.status ~= nil then
|
||||
if obj.status.conditions ~= nil then
|
||||
for i, condition in ipairs(obj.status.conditions) do
|
||||
if condition.type == "InvalidSpec" then
|
||||
hs.status = "Degraded"
|
||||
hs.message = condition.message
|
||||
return hs
|
||||
end
|
||||
end
|
||||
end
|
||||
if obj.status.currentPodHash ~= nil then
|
||||
if obj.spec.replicas ~= nil and obj.status.updatedReplicas < obj.spec.replicas then
|
||||
hs.status = "Progressing"
|
||||
hs.message = "Waiting for roll out to finish: More replicas need to be updated"
|
||||
return hs
|
||||
end
|
||||
local verifyingPreview = false
|
||||
if obj.status.verifyingPreview ~= nil then
|
||||
verifyingPreview = obj.status.verifyingPreview
|
||||
end
|
||||
if verifyingPreview and obj.status.previewSelector ~= nil and obj.status.previewSelector == obj.status.currentPodHash then
|
||||
hs.status = "Healthy"
|
||||
hs.message = "The preview Service is serving traffic to the current pod spec"
|
||||
return hs
|
||||
end
|
||||
|
||||
if obj.status.replicas > obj.status.updatedReplicas then
|
||||
hs.status = "Progressing"
|
||||
hs.message = "Waiting for roll out to finish: old replicas are pending termination"
|
||||
return hs
|
||||
end
|
||||
if obj.status.availableReplicas < obj.status.updatedReplicas then
|
||||
hs.status = "Progressing"
|
||||
hs.message = "Waiting for roll out to finish: updated replicas are still becoming available"
|
||||
return hs
|
||||
end
|
||||
|
||||
if obj.status.activeSelector ~= nil and obj.status.activeSelector == obj.status.currentPodHash then
|
||||
hs.status = "Healthy"
|
||||
hs.message = "The active Service is serving traffic to the current pod spec"
|
||||
return hs
|
||||
end
|
||||
hs.status = "Progressing"
|
||||
hs.message = "The current pod spec is not receiving traffic from the active service"
|
||||
return hs
|
||||
end
|
||||
end
|
||||
hs.status = "Unknown"
|
||||
hs.message = "Rollout should not reach here. Please file a bug at https://github.com/argoproj/argo-cd/issues/new"
|
||||
return hs
|
||||
25
resource_customizations/argoproj.io/Rollout/health_test.yaml
Normal file
25
resource_customizations/argoproj.io/Rollout/health_test.yaml
Normal file
@@ -0,0 +1,25 @@
|
||||
tests:
|
||||
- healthStatus:
|
||||
status: Healthy
|
||||
message: The active Service is serving traffic to the current pod spec
|
||||
inputPath: testdata/healthy_servingActiveService.yaml
|
||||
- healthStatus:
|
||||
status: Healthy
|
||||
message: The preview Service is serving traffic to the current pod spec
|
||||
inputPath: testdata/healthy_servingPreviewService.yaml
|
||||
- healthStatus:
|
||||
status: Progressing
|
||||
message: "Waiting for roll out to finish: More replicas need to be updated"
|
||||
inputPath: testdata/progressing_addingMoreReplicas.yaml
|
||||
- healthStatus:
|
||||
status: Progressing
|
||||
message: "Waiting for roll out to finish: old replicas are pending termination"
|
||||
inputPath: testdata/progressing_killingOldReplicas.yaml
|
||||
- healthStatus:
|
||||
status: Progressing
|
||||
message: "Waiting for roll out to finish: updated replicas are still becoming available"
|
||||
inputPath: testdata/progressing_waitingUntilAvailable.yaml
|
||||
- healthStatus:
|
||||
status: Degraded
|
||||
message: Rollout has missing field '.Spec.Strategy.Type'
|
||||
inputPath: testdata/degraded_invalidSpec.yaml
|
||||
12
resource_customizations/argoproj.io/Rollout/testdata/degraded_invalidSpec.yaml
vendored
Normal file
12
resource_customizations/argoproj.io/Rollout/testdata/degraded_invalidSpec.yaml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Rollout
|
||||
metadata:
|
||||
name: invalidSpec
|
||||
status:
|
||||
conditions:
|
||||
- type: AnotherValidCondition
|
||||
status: true
|
||||
- type: InvalidSpec
|
||||
status: true
|
||||
message: Rollout has missing field '.Spec.Strategy.Type'
|
||||
reason: MissingStrategy
|
||||
57
resource_customizations/argoproj.io/Rollout/testdata/healthy_servingActiveService.yaml
vendored
Normal file
57
resource_customizations/argoproj.io/Rollout/testdata/healthy_servingActiveService.yaml
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Rollout
|
||||
metadata:
|
||||
annotations:
|
||||
kubectl.kubernetes.io/last-applied-configuration: |
|
||||
{"apiVersion":"argoproj.io/v1alpha1","kind":"Rollout","metadata":{"annotations":{},"labels":{"app.kubernetes.io/instance":"guestbook-default","ksonnet.io/component":"guestbook-ui"},"name":"ks-guestbook-ui","namespace":"default"},"spec":{"minReadySeconds":30,"replicas":1,"selector":{"matchLabels":{"app":"ks-guestbook-ui"}},"strategy":{"blueGreen":{"activeService":"ks-guestbook-ui-active","previewService":"ks-guestbook-ui-preview"},"type":"BlueGreenUpdate"},"template":{"metadata":{"labels":{"app":"ks-guestbook-ui"}},"spec":{"containers":[{"image":"gcr.io/heptio-images/ks-guestbook-demo:0.2","name":"ks-guestbook-ui","ports":[{"containerPort":80}]}]}}}}
|
||||
rollout.argoproj.io/revision: "1"
|
||||
clusterName: ""
|
||||
creationTimestamp: 2019-01-22T16:52:54Z
|
||||
generation: 1
|
||||
labels:
|
||||
app.kubernetes.io/instance: guestbook-default
|
||||
ksonnet.io/component: guestbook-ui
|
||||
name: ks-guestbook-ui
|
||||
namespace: default
|
||||
resourceVersion: "153353"
|
||||
selfLink: /apis/argoproj.io/v1alpha1/namespaces/default/rollouts/ks-guestbook-ui
|
||||
uid: 29802403-1e66-11e9-a6a4-025000000001
|
||||
spec:
|
||||
minReadySeconds: 30
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: ks-guestbook-ui
|
||||
strategy:
|
||||
blueGreen:
|
||||
activeService: ks-guestbook-ui-active
|
||||
previewService: ks-guestbook-ui-preview
|
||||
type: BlueGreenUpdate
|
||||
template:
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
app: ks-guestbook-ui
|
||||
spec:
|
||||
containers:
|
||||
- image: gcr.io/heptio-images/ks-guestbook-demo:0.2
|
||||
name: ks-guestbook-ui
|
||||
ports:
|
||||
- containerPort: 80
|
||||
resources: {}
|
||||
status:
|
||||
activeSelector: dc689d967
|
||||
availableReplicas: 1
|
||||
conditions:
|
||||
- lastTransitionTime: 2019-01-24T09:51:02Z
|
||||
lastUpdateTime: 2019-01-24T09:51:02Z
|
||||
message: Rollout is serving traffic from the active service.
|
||||
reason: Available
|
||||
status: "True"
|
||||
type: Available
|
||||
currentPodHash: dc689d967
|
||||
observedGeneration: 77646c9d4c
|
||||
previewSelector: ""
|
||||
readyReplicas: 1
|
||||
replicas: 1
|
||||
updatedReplicas: 1
|
||||
55
resource_customizations/argoproj.io/Rollout/testdata/healthy_servingPreviewService.yaml
vendored
Normal file
55
resource_customizations/argoproj.io/Rollout/testdata/healthy_servingPreviewService.yaml
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Rollout
|
||||
metadata:
|
||||
annotations:
|
||||
rollout.argoproj.io/revision: "7"
|
||||
clusterName: ""
|
||||
creationTimestamp: 2019-01-22T16:52:54Z
|
||||
generation: 1
|
||||
labels:
|
||||
app.kubernetes.io/instance: guestbook-default
|
||||
name: ks-guestbook-ui
|
||||
namespace: default
|
||||
resourceVersion: "164113"
|
||||
selfLink: /apis/argoproj.io/v1alpha1/namespaces/default/rollouts/ks-guestbook-ui
|
||||
uid: 29802403-1e66-11e9-a6a4-025000000001
|
||||
spec:
|
||||
minReadySeconds: 30
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: ks-guestbook-ui
|
||||
strategy:
|
||||
blueGreen:
|
||||
activeService: ks-guestbook-ui-active
|
||||
previewService: ks-guestbook-ui-preview
|
||||
type: BlueGreenUpdate
|
||||
template:
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
app: ks-guestbook-ui
|
||||
spec:
|
||||
containers:
|
||||
- image: gcr.io/heptio-images/ks-guestbook-demo:0.1
|
||||
name: ks-guestbook-ui
|
||||
ports:
|
||||
- containerPort: 83
|
||||
resources: {}
|
||||
status:
|
||||
activeSelector: 85f9884f5d
|
||||
availableReplicas: 6
|
||||
conditions:
|
||||
- lastTransitionTime: 2019-01-25T07:44:26Z
|
||||
lastUpdateTime: 2019-01-25T07:44:26Z
|
||||
message: Rollout is serving traffic from the active service.
|
||||
reason: Available
|
||||
status: "True"
|
||||
type: Available
|
||||
currentPodHash: 697fb9575c
|
||||
observedGeneration: 767f98959f
|
||||
previewSelector: 697fb9575c
|
||||
readyReplicas: 6
|
||||
replicas: 6
|
||||
updatedReplicas: 3
|
||||
verifyingPreview: true
|
||||
54
resource_customizations/argoproj.io/Rollout/testdata/progressing_addingMoreReplicas.yaml
vendored
Normal file
54
resource_customizations/argoproj.io/Rollout/testdata/progressing_addingMoreReplicas.yaml
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Rollout
|
||||
metadata:
|
||||
annotations:
|
||||
rollout.argoproj.io/revision: "7"
|
||||
clusterName: ""
|
||||
creationTimestamp: 2019-01-22T16:52:54Z
|
||||
generation: 1
|
||||
labels:
|
||||
app.kubernetes.io/instance: guestbook-default
|
||||
name: ks-guestbook-ui
|
||||
namespace: default
|
||||
resourceVersion: "164023"
|
||||
selfLink: /apis/argoproj.io/v1alpha1/namespaces/default/rollouts/ks-guestbook-ui
|
||||
uid: 29802403-1e66-11e9-a6a4-025000000001
|
||||
spec:
|
||||
minReadySeconds: 30
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: ks-guestbook-ui
|
||||
strategy:
|
||||
blueGreen:
|
||||
activeService: ks-guestbook-ui-active
|
||||
previewService: ks-guestbook-ui-preview
|
||||
type: BlueGreenUpdate
|
||||
template:
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
app: ks-guestbook-ui
|
||||
spec:
|
||||
containers:
|
||||
- image: gcr.io/heptio-images/ks-guestbook-demo:0.1
|
||||
name: ks-guestbook-ui
|
||||
ports:
|
||||
- containerPort: 83
|
||||
resources: {}
|
||||
status:
|
||||
activeSelector: 85f9884f5d
|
||||
availableReplicas: 3
|
||||
conditions:
|
||||
- lastTransitionTime: 2019-01-25T07:44:26Z
|
||||
lastUpdateTime: 2019-01-25T07:44:26Z
|
||||
message: Rollout is serving traffic from the active service.
|
||||
reason: Available
|
||||
status: "True"
|
||||
type: Available
|
||||
currentPodHash: 697fb9575c
|
||||
observedGeneration: 767f98959f
|
||||
previewSelector: ""
|
||||
readyReplicas: 3
|
||||
replicas: 3
|
||||
updatedReplicas: 0
|
||||
57
resource_customizations/argoproj.io/Rollout/testdata/progressing_killingOldReplicas.yaml
vendored
Normal file
57
resource_customizations/argoproj.io/Rollout/testdata/progressing_killingOldReplicas.yaml
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Rollout
|
||||
metadata:
|
||||
annotations:
|
||||
kubectl.kubernetes.io/last-applied-configuration: |
|
||||
{"apiVersion":"argoproj.io/v1alpha1","kind":"Rollout","metadata":{"annotations":{},"labels":{"app.kubernetes.io/instance":"guestbook-default","ksonnet.io/component":"guestbook-ui"},"name":"ks-guestbook-ui","namespace":"default"},"spec":{"minReadySeconds":30,"replicas":3,"selector":{"matchLabels":{"app":"ks-guestbook-ui"}},"strategy":{"blueGreen":{"activeService":"ks-guestbook-ui-active","previewService":"ks-guestbook-ui-preview"},"type":"BlueGreenUpdate"},"template":{"metadata":{"labels":{"app":"ks-guestbook-ui"}},"spec":{"containers":[{"image":"gcr.io/heptio-images/ks-guestbook-demo:0.1","name":"ks-guestbook-ui","ports":[{"containerPort":83}]}]}}}}
|
||||
rollout.argoproj.io/revision: "7"
|
||||
clusterName: ""
|
||||
creationTimestamp: 2019-01-22T16:52:54Z
|
||||
generation: 1
|
||||
labels:
|
||||
app.kubernetes.io/instance: guestbook-default
|
||||
ksonnet.io/component: guestbook-ui
|
||||
name: ks-guestbook-ui
|
||||
namespace: default
|
||||
resourceVersion: "164141"
|
||||
selfLink: /apis/argoproj.io/v1alpha1/namespaces/default/rollouts/ks-guestbook-ui
|
||||
uid: 29802403-1e66-11e9-a6a4-025000000001
|
||||
spec:
|
||||
minReadySeconds: 30
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: ks-guestbook-ui
|
||||
strategy:
|
||||
blueGreen:
|
||||
activeService: ks-guestbook-ui-active
|
||||
previewService: ks-guestbook-ui-preview
|
||||
type: BlueGreenUpdate
|
||||
template:
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
app: ks-guestbook-ui
|
||||
spec:
|
||||
containers:
|
||||
- image: gcr.io/heptio-images/ks-guestbook-demo:0.1
|
||||
name: ks-guestbook-ui
|
||||
ports:
|
||||
- containerPort: 83
|
||||
resources: {}
|
||||
status:
|
||||
activeSelector: 697fb9575c
|
||||
availableReplicas: 6
|
||||
conditions:
|
||||
- lastTransitionTime: 2019-01-25T07:44:26Z
|
||||
lastUpdateTime: 2019-01-25T07:44:26Z
|
||||
message: Rollout is serving traffic from the active service.
|
||||
reason: Available
|
||||
status: "True"
|
||||
type: Available
|
||||
currentPodHash: 697fb9575c
|
||||
observedGeneration: 767f98959f
|
||||
previewSelector: ""
|
||||
readyReplicas: 6
|
||||
replicas: 6
|
||||
updatedReplicas: 3
|
||||
57
resource_customizations/argoproj.io/Rollout/testdata/progressing_waitingUntilAvailable.yaml
vendored
Normal file
57
resource_customizations/argoproj.io/Rollout/testdata/progressing_waitingUntilAvailable.yaml
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Rollout
|
||||
metadata:
|
||||
annotations:
|
||||
kubectl.kubernetes.io/last-applied-configuration: |
|
||||
{"apiVersion":"argoproj.io/v1alpha1","kind":"Rollout","metadata":{"annotations":{},"labels":{"app.kubernetes.io/instance":"guestbook-default","ksonnet.io/component":"guestbook-ui"},"name":"ks-guestbook-ui","namespace":"default"},"spec":{"minReadySeconds":30,"replicas":3,"selector":{"matchLabels":{"app":"ks-guestbook-ui"}},"strategy":{"blueGreen":{"activeService":"ks-guestbook-ui-active","previewService":"ks-guestbook-ui-preview"},"type":"BlueGreenUpdate"},"template":{"metadata":{"labels":{"app":"ks-guestbook-ui"}},"spec":{"containers":[{"image":"gcr.io/heptio-images/ks-guestbook-demo:0.1","name":"ks-guestbook-ui","ports":[{"containerPort":83}]}]}}}}
|
||||
rollout.argoproj.io/revision: "1"
|
||||
clusterName: ""
|
||||
creationTimestamp: 2019-01-25T16:19:09Z
|
||||
generation: 1
|
||||
labels:
|
||||
app.kubernetes.io/instance: guestbook-default
|
||||
ksonnet.io/component: guestbook-ui
|
||||
name: ks-guestbook-ui
|
||||
namespace: default
|
||||
resourceVersion: "164590"
|
||||
selfLink: /apis/argoproj.io/v1alpha1/namespaces/default/rollouts/ks-guestbook-ui
|
||||
uid: f1b99cb0-20bc-11e9-a811-025000000001
|
||||
spec:
|
||||
minReadySeconds: 30
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: ks-guestbook-ui
|
||||
strategy:
|
||||
blueGreen:
|
||||
activeService: ks-guestbook-ui-active
|
||||
previewService: ks-guestbook-ui-preview
|
||||
type: BlueGreenUpdate
|
||||
template:
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
app: ks-guestbook-ui
|
||||
spec:
|
||||
containers:
|
||||
- image: gcr.io/heptio-images/ks-guestbook-demo:0.1
|
||||
name: ks-guestbook-ui
|
||||
ports:
|
||||
- containerPort: 83
|
||||
resources: {}
|
||||
status:
|
||||
activeSelector: 697fb9575c
|
||||
availableReplicas: 0
|
||||
conditions:
|
||||
- lastTransitionTime: 2019-01-25T16:19:09Z
|
||||
lastUpdateTime: 2019-01-25T16:19:09Z
|
||||
message: Rollout is not serving traffic from the active service.
|
||||
reason: Available
|
||||
status: "False"
|
||||
type: Available
|
||||
currentPodHash: 697fb9575c
|
||||
observedGeneration: 767f98959f
|
||||
previewSelector: ""
|
||||
readyReplicas: 3
|
||||
replicas: 3
|
||||
updatedReplicas: 3
|
||||
66
resource_customizations/health_test.go
Normal file
66
resource_customizations/health_test.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package resource_customizations
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/lua"
|
||||
)
|
||||
|
||||
type TestStructure struct {
|
||||
Tests []IndividualTest `yaml:"tests"`
|
||||
}
|
||||
|
||||
type IndividualTest struct {
|
||||
InputPath string `yaml:"inputPath"`
|
||||
HealthStatus appv1.HealthStatus `yaml:"healthStatus"`
|
||||
}
|
||||
|
||||
func getObj(path string) *unstructured.Unstructured {
|
||||
yamlBytes, err := ioutil.ReadFile(path)
|
||||
errors.CheckError(err)
|
||||
obj := make(map[string]interface{})
|
||||
err = yaml.Unmarshal(yamlBytes, &obj)
|
||||
errors.CheckError(err)
|
||||
return &unstructured.Unstructured{Object: obj}
|
||||
}
|
||||
|
||||
func TestLuaHealthScript(t *testing.T) {
|
||||
err := filepath.Walk(".", func(path string, f os.FileInfo, err error) error {
|
||||
if !strings.Contains(path, "health.lua") {
|
||||
return nil
|
||||
}
|
||||
errors.CheckError(err)
|
||||
dir := filepath.Dir(path)
|
||||
yamlBytes, err := ioutil.ReadFile(dir + "/health_test.yaml")
|
||||
errors.CheckError(err)
|
||||
var resourceTest TestStructure
|
||||
err = yaml.Unmarshal(yamlBytes, &resourceTest)
|
||||
errors.CheckError(err)
|
||||
for i := range resourceTest.Tests {
|
||||
test := resourceTest.Tests[i]
|
||||
t.Run(test.InputPath, func(t *testing.T) {
|
||||
vm := lua.VM{
|
||||
UseOpenLibs: true,
|
||||
}
|
||||
obj := getObj(filepath.Join(dir, test.InputPath))
|
||||
script, err := vm.GetHealthScript(obj)
|
||||
errors.CheckError(err)
|
||||
result, err := vm.ExecuteHealthLua(obj, script)
|
||||
errors.CheckError(err)
|
||||
assert.Equal(t, &test.HealthStatus, result)
|
||||
})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
@@ -3,8 +3,8 @@ package health
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/api/apps/v1"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
v1 "k8s.io/api/apps/v1"
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
coreV1 "k8s.io/api/core/v1"
|
||||
extv1beta1 "k8s.io/api/extensions/v1beta1"
|
||||
@@ -16,10 +16,12 @@ import (
|
||||
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
hookutil "github.com/argoproj/argo-cd/util/hook"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/util/lua"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
)
|
||||
|
||||
// SetApplicationHealth updates the health statuses of all resources performed in the comparison
|
||||
func SetApplicationHealth(resStatuses []appv1.ResourceStatus, liveObjs []*unstructured.Unstructured) (*appv1.HealthStatus, error) {
|
||||
func SetApplicationHealth(resStatuses []appv1.ResourceStatus, liveObjs []*unstructured.Unstructured, resourceOverrides map[string]settings.ResourceOverride) (*appv1.HealthStatus, error) {
|
||||
var savedErr error
|
||||
appHealth := appv1.HealthStatus{Status: appv1.HealthStatusHealthy}
|
||||
for i, liveObj := range liveObjs {
|
||||
@@ -28,7 +30,7 @@ func SetApplicationHealth(resStatuses []appv1.ResourceStatus, liveObjs []*unstru
|
||||
if liveObj == nil {
|
||||
resHealth = &appv1.HealthStatus{Status: appv1.HealthStatusMissing}
|
||||
} else {
|
||||
resHealth, err = GetResourceHealth(liveObj)
|
||||
resHealth, err = GetResourceHealth(liveObj, resourceOverrides)
|
||||
if err != nil && savedErr == nil {
|
||||
savedErr = err
|
||||
}
|
||||
@@ -44,10 +46,22 @@ func SetApplicationHealth(resStatuses []appv1.ResourceStatus, liveObjs []*unstru
|
||||
}
|
||||
|
||||
// GetResourceHealth returns the health of a k8s resource
|
||||
func GetResourceHealth(obj *unstructured.Unstructured) (*appv1.HealthStatus, error) {
|
||||
func GetResourceHealth(obj *unstructured.Unstructured, resourceOverrides map[string]settings.ResourceOverride) (*appv1.HealthStatus, error) {
|
||||
var err error
|
||||
var health *appv1.HealthStatus
|
||||
|
||||
health, err = getResourceHealthFromLuaScript(obj, resourceOverrides)
|
||||
if err != nil {
|
||||
health = &appv1.HealthStatus{
|
||||
Status: appv1.HealthStatusUnknown,
|
||||
Message: err.Error(),
|
||||
}
|
||||
return health, err
|
||||
}
|
||||
if health != nil {
|
||||
return health, nil
|
||||
}
|
||||
|
||||
gvk := obj.GroupVersionKind()
|
||||
switch gvk.Group {
|
||||
case "apps", "extensions":
|
||||
@@ -113,6 +127,24 @@ func IsWorse(current, new appv1.HealthStatusCode) bool {
|
||||
return newIndex > currentIndex
|
||||
}
|
||||
|
||||
func getResourceHealthFromLuaScript(obj *unstructured.Unstructured, resourceOverrides map[string]settings.ResourceOverride) (*appv1.HealthStatus, error) {
|
||||
luaVM := lua.VM{
|
||||
ResourceOverrides: resourceOverrides,
|
||||
}
|
||||
script, err := luaVM.GetHealthScript(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if script == "" {
|
||||
return nil, nil
|
||||
}
|
||||
result, err := luaVM.ExecuteHealthLua(obj, script)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func getPVCHealth(obj *unstructured.Unstructured) (*appv1.HealthStatus, error) {
|
||||
pvc := &coreV1.PersistentVolumeClaim{}
|
||||
err := scheme.Scheme.Convert(obj, pvc, nil)
|
||||
|
||||
@@ -18,7 +18,7 @@ func assertAppHealth(t *testing.T, yamlPath string, expectedStatus appv1.HealthS
|
||||
var obj unstructured.Unstructured
|
||||
err = yaml.Unmarshal(yamlBytes, &obj)
|
||||
assert.Nil(t, err)
|
||||
health, err := GetResourceHealth(&obj)
|
||||
health, err := GetResourceHealth(&obj, nil)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, health)
|
||||
assert.Equal(t, expectedStatus, health.Status)
|
||||
@@ -106,13 +106,13 @@ func TestSetApplicationHealth(t *testing.T) {
|
||||
&runningPod,
|
||||
&failedJob,
|
||||
}
|
||||
healthStatus, err := SetApplicationHealth(resources, liveObjs)
|
||||
healthStatus, err := SetApplicationHealth(resources, liveObjs, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, appv1.HealthStatusDegraded, healthStatus.Status)
|
||||
|
||||
// now mark the job as a hook and retry. it should ignore the hook and consider the app healthy
|
||||
failedJob.SetAnnotations(map[string]string{common.AnnotationKeyHook: "PreSync"})
|
||||
healthStatus, err = SetApplicationHealth(resources, liveObjs)
|
||||
healthStatus, err = SetApplicationHealth(resources, liveObjs, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, appv1.HealthStatusHealthy, healthStatus.Status)
|
||||
|
||||
|
||||
178
util/lua/lua.go
Normal file
178
util/lua/lua.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package lua
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/gobuffalo/packr"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/yuin/gopher-lua"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
luajson "layeh.com/gopher-json"
|
||||
|
||||
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
)
|
||||
|
||||
const (
|
||||
incorrectReturnType = "expect table output from Lua script, not %s"
|
||||
invalidHealthStatus = "Lua returned an invalid health status"
|
||||
resourceCustomizationBuiltInPath = "../../resource_customizations"
|
||||
healthScript = "health.lua"
|
||||
)
|
||||
|
||||
var (
|
||||
box packr.Box
|
||||
)
|
||||
|
||||
func init() {
|
||||
box = packr.NewBox(resourceCustomizationBuiltInPath)
|
||||
}
|
||||
|
||||
// VM Defines a struct that implements the luaVM
|
||||
type VM struct {
|
||||
ResourceOverrides map[string]settings.ResourceOverride
|
||||
// UseOpenLibs flag to enable open libraries. Libraries are always disabled while running, but enabled during testing to allow the use of print statements
|
||||
UseOpenLibs bool
|
||||
}
|
||||
|
||||
func (vm VM) runLua(obj *unstructured.Unstructured, script string) (*lua.LState, error) {
|
||||
l := lua.NewState(lua.Options{
|
||||
SkipOpenLibs: !vm.UseOpenLibs,
|
||||
})
|
||||
defer l.Close()
|
||||
// Opens table library to allow access to functions to manulate tables
|
||||
for _, pair := range []struct {
|
||||
n string
|
||||
f lua.LGFunction
|
||||
}{
|
||||
{lua.LoadLibName, lua.OpenPackage},
|
||||
{lua.BaseLibName, lua.OpenBase},
|
||||
{lua.TabLibName, lua.OpenTable},
|
||||
} {
|
||||
if err := l.CallByParam(lua.P{
|
||||
Fn: l.NewFunction(pair.f),
|
||||
NRet: 0,
|
||||
Protect: true,
|
||||
}, lua.LString(pair.n)); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||
defer cancel()
|
||||
l.SetContext(ctx)
|
||||
objectValue := decodeValue(l, obj.Object)
|
||||
l.SetGlobal("obj", objectValue)
|
||||
err := l.DoString(script)
|
||||
return l, err
|
||||
}
|
||||
|
||||
// ExecuteHealthLua runs the lua script to generate the health status of a resource
|
||||
func (vm VM) ExecuteHealthLua(obj *unstructured.Unstructured, script string) (*appv1.HealthStatus, error) {
|
||||
l, err := vm.runLua(obj, script)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
returnValue := l.Get(-1)
|
||||
if returnValue.Type() == lua.LTTable {
|
||||
jsonBytes, err := luajson.Encode(returnValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
healthStatus := &appv1.HealthStatus{}
|
||||
err = json.Unmarshal(jsonBytes, healthStatus)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !isValidHealthStatusCode(healthStatus.Status) {
|
||||
return &appv1.HealthStatus{
|
||||
Status: appv1.HealthStatusUnknown,
|
||||
Message: invalidHealthStatus,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return healthStatus, nil
|
||||
}
|
||||
return nil, fmt.Errorf(incorrectReturnType, returnValue.Type().String())
|
||||
}
|
||||
|
||||
// GetScript attempts to read lua script from config and then filesystem for that resource
|
||||
func (vm VM) GetHealthScript(obj *unstructured.Unstructured) (string, error) {
|
||||
key := getConfigMapKey(obj)
|
||||
if script, ok := vm.ResourceOverrides[key]; ok && script.HealthLua != "" {
|
||||
return script.HealthLua, nil
|
||||
}
|
||||
return vm.getPredefinedLuaScripts(key, healthScript)
|
||||
}
|
||||
|
||||
func getConfigMapKey(obj *unstructured.Unstructured) string {
|
||||
gvk := obj.GroupVersionKind()
|
||||
if gvk.Group == "" {
|
||||
return gvk.Kind
|
||||
}
|
||||
return fmt.Sprintf("%s/%s", gvk.Group, gvk.Kind)
|
||||
|
||||
}
|
||||
|
||||
func (vm VM) getPredefinedLuaScripts(objKey string, scriptType string) (string, error) {
|
||||
data, err := box.MustBytes(filepath.Join(objKey, scriptType))
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
log.Debugf("No Lua Script found for resource key '%s'", objKey)
|
||||
return "", nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func isValidHealthStatusCode(statusCode string) bool {
|
||||
switch statusCode {
|
||||
case appv1.HealthStatusUnknown, appv1.HealthStatusProgressing, appv1.HealthStatusHealthy, appv1.HealthStatusDegraded, appv1.HealthStatusMissing:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Took logic from the link below and added the int, int32, and int64 types since the value would have type int64
|
||||
// while actually running in the controller and it was not reproducible through testing.
|
||||
// https://github.com/layeh/gopher-json/blob/97fed8db84274c421dbfffbb28ec859901556b97/json.go#L154
|
||||
func decodeValue(L *lua.LState, value interface{}) lua.LValue {
|
||||
switch converted := value.(type) {
|
||||
case bool:
|
||||
return lua.LBool(converted)
|
||||
case float64:
|
||||
return lua.LNumber(converted)
|
||||
case string:
|
||||
return lua.LString(converted)
|
||||
case json.Number:
|
||||
return lua.LString(converted)
|
||||
case int:
|
||||
return lua.LNumber(converted)
|
||||
case int32:
|
||||
return lua.LNumber(converted)
|
||||
case int64:
|
||||
return lua.LNumber(converted)
|
||||
case []interface{}:
|
||||
arr := L.CreateTable(len(converted), 0)
|
||||
for _, item := range converted {
|
||||
arr.Append(decodeValue(L, item))
|
||||
}
|
||||
return arr
|
||||
case map[string]interface{}:
|
||||
tbl := L.CreateTable(0, len(converted))
|
||||
for key, item := range converted {
|
||||
tbl.RawSetH(lua.LString(key), decodeValue(L, item))
|
||||
}
|
||||
return tbl
|
||||
case nil:
|
||||
return lua.LNil
|
||||
}
|
||||
|
||||
return lua.LNil
|
||||
}
|
||||
125
util/lua/lua_test.go
Normal file
125
util/lua/lua_test.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package lua
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
"github.com/stretchr/testify/assert"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
)
|
||||
|
||||
const objJSON = `
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Rollout
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/instance: helm-guestbook
|
||||
name: helm-guestbook
|
||||
namespace: default
|
||||
resourceVersion: "123"
|
||||
`
|
||||
const objWithNoScriptJSON = `
|
||||
apiVersion: not-an-endpoint.io/v1alpha1
|
||||
kind: Test
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/instance: helm-guestbook
|
||||
name: helm-guestbook
|
||||
namespace: default
|
||||
resourceVersion: "123"
|
||||
`
|
||||
|
||||
const newHealthStatusFunction = `a = {}
|
||||
a.status = "Healthy"
|
||||
a.message ="NeedsToBeChanged"
|
||||
if obj.metadata.name == "helm-guestbook" then
|
||||
a.message = "testMessage"
|
||||
end
|
||||
return a`
|
||||
|
||||
func StrToUnstructured(jsonStr string) *unstructured.Unstructured {
|
||||
obj := make(map[string]interface{})
|
||||
err := yaml.Unmarshal([]byte(jsonStr), &obj)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &unstructured.Unstructured{Object: obj}
|
||||
}
|
||||
|
||||
func TestExecuteNewHealthStatusFunction(t *testing.T) {
|
||||
testObj := StrToUnstructured(objJSON)
|
||||
vm := VM{}
|
||||
status, err := vm.ExecuteHealthLua(testObj, newHealthStatusFunction)
|
||||
assert.Nil(t, err)
|
||||
expectedHealthStatus := &appv1.HealthStatus{
|
||||
Status: "Healthy",
|
||||
Message: "testMessage",
|
||||
}
|
||||
assert.Equal(t, expectedHealthStatus, status)
|
||||
|
||||
}
|
||||
|
||||
const osLuaScript = `os.getenv("HOME")`
|
||||
|
||||
func TestFailExternalLibCall(t *testing.T) {
|
||||
testObj := StrToUnstructured(objJSON)
|
||||
vm := VM{}
|
||||
_, err := vm.ExecuteHealthLua(testObj, osLuaScript)
|
||||
assert.Error(t, err, "")
|
||||
assert.IsType(t, &lua.ApiError{}, err)
|
||||
}
|
||||
|
||||
const returnInt = `return 1`
|
||||
|
||||
func TestFailLuaReturnNonTable(t *testing.T) {
|
||||
testObj := StrToUnstructured(objJSON)
|
||||
vm := VM{}
|
||||
_, err := vm.ExecuteHealthLua(testObj, returnInt)
|
||||
assert.Equal(t, fmt.Errorf(incorrectReturnType, "number"), err)
|
||||
}
|
||||
|
||||
const invalidHealthStatusStatus = `local healthStatus = {}
|
||||
healthStatus.status = "test"
|
||||
return healthStatus
|
||||
`
|
||||
|
||||
func TestInvalidHealthStatusStatus(t *testing.T) {
|
||||
testObj := StrToUnstructured(objJSON)
|
||||
vm := VM{}
|
||||
status, err := vm.ExecuteHealthLua(testObj, invalidHealthStatusStatus)
|
||||
assert.Nil(t, err)
|
||||
expectedStatus := &appv1.HealthStatus{
|
||||
Status: appv1.HealthStatusUnknown,
|
||||
Message: invalidHealthStatus,
|
||||
}
|
||||
assert.Equal(t, expectedStatus, status)
|
||||
}
|
||||
|
||||
const infiniteLoop = `while true do ; end`
|
||||
|
||||
func TestHandleInfiniteLoop(t *testing.T) {
|
||||
testObj := StrToUnstructured(objJSON)
|
||||
vm := VM{}
|
||||
_, err := vm.ExecuteHealthLua(testObj, infiniteLoop)
|
||||
assert.IsType(t, &lua.ApiError{}, err)
|
||||
}
|
||||
|
||||
func TestGetPredefinedLuaScript(t *testing.T) {
|
||||
testObj := StrToUnstructured(objJSON)
|
||||
vm := VM{}
|
||||
script, err := vm.GetHealthScript(testObj)
|
||||
assert.Nil(t, err)
|
||||
assert.NotEmpty(t, script)
|
||||
}
|
||||
|
||||
func TestGetNonExistentPredefinedLuaScript(t *testing.T) {
|
||||
testObj := StrToUnstructured(objWithNoScriptJSON)
|
||||
vm := VM{}
|
||||
script, err := vm.GetHealthScript(testObj)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "", script)
|
||||
}
|
||||
1
util/lua/testdata/example.lua
vendored
Normal file
1
util/lua/testdata/example.lua
vendored
Normal file
@@ -0,0 +1 @@
|
||||
return 'Hello World'
|
||||
@@ -63,6 +63,13 @@ type ArgoCDSettings struct {
|
||||
HelmRepositories []HelmRepoCredentials
|
||||
// AppInstanceLabelKey is the configured application instance label key used to label apps. May be empty
|
||||
AppInstanceLabelKey string
|
||||
// ResourceOverrides holds the overrides for specific resources. The keys are in the format of `group/kind`
|
||||
// (e.g. argoproj.io/rollout) for the resource that is being overridden
|
||||
ResourceOverrides map[string]ResourceOverride
|
||||
}
|
||||
|
||||
type ResourceOverride struct {
|
||||
HealthLua string `json:"health.lua,omitempty"`
|
||||
}
|
||||
|
||||
type OIDCConfig struct {
|
||||
@@ -118,6 +125,8 @@ const (
|
||||
settingsWebhookBitbucketUUIDKey = "webhook.bitbucket.uuid"
|
||||
// settingsApplicationInstanceLabelKey is the key to configure injected app instance label key
|
||||
settingsApplicationInstanceLabelKey = "application.instanceLabelKey"
|
||||
// resourcesCustomizationsKey is the key to the map of resource overrides
|
||||
resourcesCustomizationsKey = "resource.customizations"
|
||||
)
|
||||
|
||||
// SettingsManager holds config info for a new manager with which to access Kubernetes ConfigMaps.
|
||||
@@ -326,6 +335,16 @@ func updateSettingsFromConfigMap(settings *ArgoCDSettings, argoCDCM *apiv1.Confi
|
||||
settings.HelmRepositories = helmRepositories
|
||||
}
|
||||
}
|
||||
|
||||
if value, ok := argoCDCM.Data[resourcesCustomizationsKey]; ok {
|
||||
resourceOverrides := map[string]ResourceOverride{}
|
||||
err := yaml.Unmarshal([]byte(value), &resourceOverrides)
|
||||
if err != nil {
|
||||
errors = append(errors, err)
|
||||
} else {
|
||||
settings.ResourceOverrides = resourceOverrides
|
||||
}
|
||||
}
|
||||
if len(errors) > 0 {
|
||||
return errors[0]
|
||||
}
|
||||
@@ -437,6 +456,15 @@ func (mgr *SettingsManager) SaveSettings(settings *ArgoCDSettings) error {
|
||||
} else {
|
||||
delete(argoCDCM.Data, settingsApplicationInstanceLabelKey)
|
||||
}
|
||||
|
||||
if len(settings.ResourceOverrides) > 0 {
|
||||
yamlBytes, err := yaml.Marshal(settings.ResourceOverrides)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
argoCDCM.Data[resourcesCustomizationsKey] = string(yamlBytes)
|
||||
}
|
||||
|
||||
if createCM {
|
||||
_, err = mgr.clientset.CoreV1().ConfigMaps(mgr.namespace).Create(argoCDCM)
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user