Skip to content

Commit 849f77e

Browse files
authored
fix: AnalysisRun args could not be resolved from secret (argoproj#1213)
* fix: Fix ValueFrom resolution and validation in AnalysisTemplate
1 parent a2a9f35 commit 849f77e

File tree

9 files changed

+194
-119
lines changed

9 files changed

+194
-119
lines changed

analysis/analysis.go

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,6 @@ func (c *Controller) reconcileAnalysisRun(origRun *v1alpha1.AnalysisRun) *v1alph
4141
log := logutil.WithAnalysisRun(origRun)
4242
run := origRun.DeepCopy()
4343

44-
metrics, err := analysisutil.ResolveMetrics(run.Spec.Metrics, run.Spec.Args)
45-
if err != nil {
46-
message := fmt.Sprintf("unable to resolve metric arguments: %v", err)
47-
log.Warn(message)
48-
run.Status.Phase = v1alpha1.AnalysisPhaseError
49-
run.Status.Message = message
50-
c.recordAnalysisRunCompletionEvent(run)
51-
return run
52-
}
53-
run.Spec.Metrics = metrics
54-
5544
if run.Status.MetricResults == nil {
5645
run.Status.MetricResults = make([]v1alpha1.MetricResult, 0)
5746
err := analysisutil.ValidateMetrics(run.Spec.Metrics)
@@ -67,7 +56,7 @@ func (c *Controller) reconcileAnalysisRun(origRun *v1alpha1.AnalysisRun) *v1alph
6756

6857
tasks := generateMetricTasks(run)
6958
log.Infof("taking %d measurements", len(tasks))
70-
err = c.runMeasurements(run, tasks)
59+
err := c.runMeasurements(run, tasks)
7160
if err != nil {
7261
message := fmt.Sprintf("unable to resolve metric arguments: %v", err)
7362
log.Warn(message)

analysis/analysis_test.go

Lines changed: 38 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1087,42 +1087,6 @@ func TestResolveMetricArgsUnableToSubstitute(t *testing.T) {
10871087
assert.Equal(t, newRun.Status.Message, "unable to resolve metric arguments: failed to resolve {{args.metric-name}}")
10881088
}
10891089

1090-
func TestSecretContentReferenceValueFromError(t *testing.T) {
1091-
f := newFixture(t)
1092-
defer f.Close()
1093-
c, _, _ := f.newController(noResyncPeriodFunc)
1094-
argName := "apikey"
1095-
argVal := "value"
1096-
run := &v1alpha1.AnalysisRun{
1097-
Spec: v1alpha1.AnalysisRunSpec{
1098-
Args: []v1alpha1.Argument{{
1099-
Name: argName,
1100-
Value: &argVal,
1101-
ValueFrom: &v1alpha1.ValueFrom{
1102-
SecretKeyRef: &v1alpha1.SecretKeyRef{
1103-
Name: "web-metric-secret",
1104-
Key: "apikey",
1105-
},
1106-
}},
1107-
},
1108-
Metrics: []v1alpha1.Metric{{
1109-
Name: "rate",
1110-
Provider: v1alpha1.MetricProvider{
1111-
Web: &v1alpha1.WebMetric{
1112-
Headers: []v1alpha1.WebMetricHeader{{
1113-
Key: "apikey",
1114-
Value: "{{args.apikey}}",
1115-
}},
1116-
},
1117-
},
1118-
}},
1119-
},
1120-
}
1121-
newRun := c.reconcileAnalysisRun(run)
1122-
assert.Equal(t, v1alpha1.AnalysisPhaseError, newRun.Status.Phase)
1123-
assert.Equal(t, fmt.Sprintf("unable to resolve metric arguments: arg '%v' has both Value and ValueFrom fields", argName), newRun.Status.Message)
1124-
}
1125-
11261090
// TestSecretContentReferenceSuccess verifies that secret arguments are properly resolved
11271091
func TestSecretContentReferenceSuccess(t *testing.T) {
11281092
f := newFixture(t)
@@ -1353,6 +1317,44 @@ func TestKeyNotInSecret(t *testing.T) {
13531317
assert.Equal(t, "key 'key-name' does not exist in secret 'secret-name'", err.Error())
13541318
}
13551319

1320+
func TestSecretResolution(t *testing.T) {
1321+
f := newFixture(t)
1322+
secretName, secretKey, secretData := "web-metric-secret", "apikey", "12345"
1323+
secret := &corev1.Secret{
1324+
ObjectMeta: metav1.ObjectMeta{
1325+
Name: secretName,
1326+
Namespace: metav1.NamespaceDefault,
1327+
},
1328+
Data: map[string][]byte{
1329+
secretKey: []byte(secretData),
1330+
},
1331+
}
1332+
defer f.Close()
1333+
c, _, _ := f.newController(noResyncPeriodFunc)
1334+
f.kubeclient.CoreV1().Secrets(metav1.NamespaceDefault).Create(context.TODO(), secret, metav1.CreateOptions{})
1335+
1336+
args := []v1alpha1.Argument{{
1337+
Name: "secret",
1338+
ValueFrom: &v1alpha1.ValueFrom{
1339+
SecretKeyRef: &v1alpha1.SecretKeyRef{
1340+
Name: secretName,
1341+
Key: secretKey,
1342+
},
1343+
},
1344+
}}
1345+
tasks := []metricTask{{
1346+
metric: v1alpha1.Metric{
1347+
Name: "metric-name",
1348+
SuccessCondition: "{{args.secret}}",
1349+
},
1350+
incompleteMeasurement: nil,
1351+
}}
1352+
metricTaskList, secretList, _ := c.resolveArgs(tasks, args, metav1.NamespaceDefault)
1353+
1354+
assert.Equal(t, secretData, metricTaskList[0].metric.SuccessCondition)
1355+
assert.Contains(t, secretList, secretData)
1356+
}
1357+
13561358
// TestAssessMetricFailureInconclusiveOrError verifies that assessMetricFailureInconclusiveOrError returns the correct phases and messages
13571359
// for Failed, Inconclusive, and Error metrics respectively
13581360
func TestAssessMetricFailureInconclusiveOrError(t *testing.T) {

examples/rollout-secret.yaml

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# This example demonstrates a Rollout which starts and finishes analysis at a specific canary step.
2-
# The AnalysisTemplate references an Secret object, which contains an API, token and passes it to a Web metric provider.
2+
# The AnalysisTemplate references an Secret object, which contains the URL, and passes it to a Web metric provider.
33
#
44
# Prerequisites: None
55

@@ -20,7 +20,7 @@ spec:
2020
spec:
2121
containers:
2222
- name: rollouts-demo
23-
image: argoproj/rollouts-demo:green
23+
image: argoproj/rollouts-demo:blue
2424
imagePullPolicy: Always
2525
ports:
2626
- containerPort: 8080
@@ -36,31 +36,27 @@ spec:
3636
apiVersion: v1
3737
kind: Secret
3838
metadata:
39-
name: token-secret
39+
name: example-secret
4040
type: Opaque
4141
data:
42-
# This API Token is fake. Its value decoded is "test".
43-
apiToken: dGVzdAo=
42+
secretUrl: aHR0cHM6Ly9naXN0LmdpdGh1YnVzZXJjb250ZW50LmNvbS9raGhpcmFuaS8yYWIxMTIzMjQwMjUxOGQ1Mjc3YWYwMzBkZDg5MTZkNy9yYXcvZDI3MmY1NTFmMmQxODA2YTAzOTc0ZGJhZWYxMWRmZDU1MTAyZmVlYS9leGFtcGxlLmpzb24=
4443
---
4544
kind: AnalysisTemplate
4645
apiVersion: argoproj.io/v1alpha1
4746
metadata:
4847
name: analysis-secret
4948
spec:
5049
args:
51-
- name: api-token
50+
- name: secret-url
5251
valueFrom:
5352
secretKeyRef:
54-
name: token-secret
55-
key: apiToken
53+
name: example-secret
54+
key: secretUrl
5655
metrics:
5756
- name: webmetric
5857
successCondition: result == 'It worked!'
5958
provider:
6059
web:
6160
# placeholders are resolved when an AnalysisRun is created
62-
url: "https://gist.githubusercontent.com/khhirani/2ab11232402518d5277af030dd8916d7/raw/d272f551f2d1806a03974dbaef11dfd55102feea/example.json"
63-
headers:
64-
- key: Test
65-
value: "{{ args.api-token }}"
61+
url: "{{args.secret-url}}"
6662
jsonPath: "{$.message}"

pkg/apis/rollouts/validation/validation_references.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ func ValidateAnalysisTemplateWithType(rollout *v1alpha1.Rollout, template *v1alp
139139

140140
if templateType != BackgroundAnalysis {
141141
setArgValuePlaceHolder(templateSpec.Args)
142-
resolvedMetrics, err := analysisutil.ResolveMetrics(templateSpec.Metrics, templateSpec.Args)
142+
resolvedMetrics, err := validateAnalysisMetrics(templateSpec.Metrics, templateSpec.Args)
143143
if err != nil {
144144
msg := fmt.Sprintf("AnalysisTemplate %s: %v", templateName, err)
145145
allErrs = append(allErrs, field.Invalid(fldPath, templateName, msg))
@@ -293,3 +293,25 @@ func GetAnalysisTemplateWithTypeFieldPath(templateType AnalysisTemplateType, can
293293
}
294294
return fldPath
295295
}
296+
297+
// validateAnalysisMetrics validates the metrics of an Analysis object
298+
func validateAnalysisMetrics(metrics []v1alpha1.Metric, args []v1alpha1.Argument) ([]v1alpha1.Metric, error) {
299+
for i, arg := range args {
300+
if arg.ValueFrom != nil {
301+
if arg.Value != nil {
302+
return nil, fmt.Errorf("arg '%s' has both Value and ValueFrom fields", arg.Name)
303+
}
304+
argVal := "dummy-value"
305+
args[i].Value = &argVal
306+
}
307+
}
308+
309+
for i, metric := range metrics {
310+
resolvedMetric, err := analysisutil.ResolveMetricArgs(metric, args)
311+
if err != nil {
312+
return nil, err
313+
}
314+
metrics[i] = *resolvedMetric
315+
}
316+
return metrics, nil
317+
}

pkg/apis/rollouts/validation/validation_references_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,3 +497,49 @@ func toUnstructured(t *testing.T, manifest string) *k8sunstructured.Unstructured
497497
}
498498
return obj
499499
}
500+
501+
func TestValidateAnalysisMetrics(t *testing.T) {
502+
count, failureLimit := "5", "1"
503+
args := []v1alpha1.Argument{
504+
{
505+
Name: "count",
506+
Value: &count,
507+
},
508+
{
509+
Name: "failure-limit",
510+
Value: &failureLimit,
511+
},
512+
{
513+
Name: "secret",
514+
ValueFrom: &v1alpha1.ValueFrom{
515+
SecretKeyRef: &v1alpha1.SecretKeyRef{
516+
Name: "web-metric-secret",
517+
Key: "apikey",
518+
},
519+
},
520+
},
521+
}
522+
523+
countVal := intstr.FromString("{{args.count}}")
524+
failureLimitVal := intstr.FromString("{{args.failure-limit}}")
525+
metrics := []v1alpha1.Metric{{
526+
Name: "metric-name",
527+
Count: &countVal,
528+
FailureLimit: &failureLimitVal,
529+
}}
530+
531+
t.Run("Success", func(t *testing.T) {
532+
resolvedMetrics, err := validateAnalysisMetrics(metrics, args)
533+
assert.Nil(t, err)
534+
assert.Equal(t, count, resolvedMetrics[0].Count.String())
535+
assert.Equal(t, failureLimit, resolvedMetrics[0].FailureLimit.String())
536+
})
537+
538+
t.Run("Error: arg has both Value and ValueFrom", func(t *testing.T) {
539+
args[2].Value = pointer.StringPtr("secret-value")
540+
_, err := validateAnalysisMetrics(metrics, args)
541+
assert.NotNil(t, err)
542+
assert.Equal(t, "arg 'secret' has both Value and ValueFrom fields", err.Error())
543+
544+
})
545+
}

test/e2e/analysis_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,3 +598,24 @@ spec:
598598
Then().
599599
ExpectAnalysisRunCount(1)
600600
}
601+
602+
func (s *AnalysisSuite) TestAnalysisWithSecret() {
603+
(s.Given().
604+
RolloutObjects("@functional/rollout-secret.yaml").
605+
When().
606+
ApplyManifests().
607+
WaitForRolloutStatus("Healthy").
608+
Then().
609+
ExpectAnalysisRunCount(0).
610+
When().
611+
UpdateSpec().
612+
WaitForRolloutStatus("Paused").
613+
Then().
614+
ExpectAnalysisRunCount(1).
615+
When().
616+
WaitForInlineAnalysisRunPhase("Successful").
617+
PromoteRollout().
618+
WaitForRolloutStatus("Healthy").
619+
Then().
620+
ExpectStableRevision("2"))
621+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
apiVersion: argoproj.io/v1alpha1
2+
kind: Rollout
3+
metadata:
4+
name: rollout-secret
5+
spec:
6+
replicas: 1
7+
revisionHistoryLimit: 2
8+
selector:
9+
matchLabels:
10+
app: rollout-secret
11+
template:
12+
metadata:
13+
labels:
14+
app: rollout-secret
15+
spec:
16+
containers:
17+
- name: rollouts-demo
18+
image: argoproj/rollouts-demo:blue
19+
imagePullPolicy: Always
20+
ports:
21+
- containerPort: 8080
22+
strategy:
23+
canary:
24+
steps:
25+
- setWeight: 25
26+
- analysis:
27+
templates:
28+
- templateName: analysis-secret
29+
- pause: {}
30+
---
31+
apiVersion: v1
32+
kind: Secret
33+
metadata:
34+
name: example-secret
35+
type: Opaque
36+
data:
37+
secretUrl: aHR0cHM6Ly9naXN0LmdpdGh1YnVzZXJjb250ZW50LmNvbS9raGhpcmFuaS8yYWIxMTIzMjQwMjUxOGQ1Mjc3YWYwMzBkZDg5MTZkNy9yYXcvZDI3MmY1NTFmMmQxODA2YTAzOTc0ZGJhZWYxMWRmZDU1MTAyZmVlYS9leGFtcGxlLmpzb24=
38+
---
39+
kind: AnalysisTemplate
40+
apiVersion: argoproj.io/v1alpha1
41+
metadata:
42+
name: analysis-secret
43+
spec:
44+
args:
45+
- name: secret-url
46+
valueFrom:
47+
secretKeyRef:
48+
name: example-secret
49+
key: secretUrl
50+
metrics:
51+
- name: webmetric
52+
successCondition: result == 'It worked!'
53+
provider:
54+
web:
55+
url: "{{args.secret-url}}"
56+
jsonPath: "{$.message}"

utils/analysis/factory.go

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ func StepLabels(index int32, podHash, instanceID string) map[string]string {
9696
return labels
9797
}
9898

99-
// resolveMetricArgs resolves args for single metric in AnalysisRun
99+
// ResolveMetricArgs resolves args for single metric in AnalysisRun
100100
// Returns resolved metric
101101
// Uses ResolveQuotedArgs to handle escaped quotes
102102
func ResolveMetricArgs(metric v1alpha1.Metric, args []v1alpha1.Argument) (*v1alpha1.Metric, error) {
@@ -117,27 +117,6 @@ func ResolveMetricArgs(metric v1alpha1.Metric, args []v1alpha1.Argument) (*v1alp
117117
return &newMetric, nil
118118
}
119119

120-
func ResolveMetrics(metrics []v1alpha1.Metric, args []v1alpha1.Argument) ([]v1alpha1.Metric, error) {
121-
for i, arg := range args {
122-
if arg.ValueFrom != nil {
123-
if arg.Value != nil {
124-
return nil, fmt.Errorf("arg '%s' has both Value and ValueFrom fields", arg.Name)
125-
}
126-
argVal := "dummy-value"
127-
args[i].Value = &argVal
128-
}
129-
}
130-
131-
for i, metric := range metrics {
132-
resolvedMetric, err := ResolveMetricArgs(metric, args)
133-
if err != nil {
134-
return nil, err
135-
}
136-
metrics[i] = *resolvedMetric
137-
}
138-
return metrics, nil
139-
}
140-
141120
// ValidateMetrics validates an analysis template spec
142121
func ValidateMetrics(metrics []v1alpha1.Metric) error {
143122
if len(metrics) == 0 {

0 commit comments

Comments
 (0)