diff --git a/workflow/controller/dag.go b/workflow/controller/dag.go index 20e5048eba96..d782f3916c64 100644 --- a/workflow/controller/dag.go +++ b/workflow/controller/dag.go @@ -204,7 +204,7 @@ func (d *dagContext) hasMoreRetries(node *wfv1.NodeStatus) bool { func (woc *wfOperationCtx) executeDAG(nodeName string, tmplCtx *templateresolution.Context, templateScope string, tmpl *wfv1.Template, orgTmpl wfv1.TemplateHolder, boundaryID string) (*wfv1.NodeStatus, error) { node := woc.getNodeByName(nodeName) if node == nil { - node = woc.initializeExecutableNode(nodeName, wfv1.NodeTypeSteps, templateScope, tmpl, orgTmpl, boundaryID, wfv1.NodeRunning) + node = woc.initializeExecutableNode(nodeName, wfv1.NodeTypeDAG, templateScope, tmpl, orgTmpl, boundaryID, wfv1.NodeRunning) } defer func() { @@ -307,7 +307,7 @@ func (woc *wfOperationCtx) executeDAGTask(dagCtx *dagContext, taskName string) { if node != nil && node.Completed() { // Run the node's onExit node, if any. Only leaf nodes will have their onExit nodes executed here. Nodes that // have dependencies will have their onExit nodes executed below - hasOnExitNode, onExitNode, err := woc.runOnExitNode(task.Name, task.OnExit, dagCtx.boundaryID) + hasOnExitNode, onExitNode, err := woc.runOnExitNode(task.Name, task.OnExit, dagCtx.boundaryID, dagCtx.tmplCtx) if hasOnExitNode && (onExitNode == nil || !onExitNode.Completed() || err != nil) { // The onExit node is either not complete or has errored out, return. return @@ -325,7 +325,7 @@ func (woc *wfOperationCtx) executeDAGTask(dagCtx *dagContext, taskName string) { depTask := dagCtx.getTask(depName) // Run the node's onExit node, if any. Only nodes that have dependencies will have their onExit nodes // executed here. Leaf nodes will have their onExit nodes executed above - hasOnExitNode, onExitNode, err := woc.runOnExitNode(depTask.Name, depTask.OnExit, dagCtx.boundaryID) + hasOnExitNode, onExitNode, err := woc.runOnExitNode(depTask.Name, depTask.OnExit, dagCtx.boundaryID, dagCtx.tmplCtx) if hasOnExitNode && (onExitNode == nil || !onExitNode.Completed() || err != nil) { // The onExit node is either not complete or has errored out, return. return @@ -383,10 +383,13 @@ func (woc *wfOperationCtx) executeDAGTask(dagCtx *dagContext, taskName string) { } } + // The template scope of this dag. + dagTemplateScope := dagCtx.tmplCtx.GetCurrentTemplateBase().GetTemplateScope() + // First resolve/substitute params/artifacts from our dependencies newTask, err := woc.resolveDependencyReferences(dagCtx, task) if err != nil { - woc.initializeNode(nodeName, wfv1.NodeTypeSkipped, task, dagCtx.boundaryID, wfv1.NodeError, err.Error()) + woc.initializeNode(nodeName, wfv1.NodeTypeSkipped, dagTemplateScope, task, dagCtx.boundaryID, wfv1.NodeError, err.Error()) connectDependencies(nodeName) return } @@ -395,7 +398,7 @@ func (woc *wfOperationCtx) executeDAGTask(dagCtx *dagContext, taskName string) { // expandedTasks will be a single element list of the same task expandedTasks, err := woc.expandTask(*newTask) if err != nil { - woc.initializeNode(nodeName, wfv1.NodeTypeSkipped, task, dagCtx.boundaryID, wfv1.NodeError, err.Error()) + woc.initializeNode(nodeName, wfv1.NodeTypeSkipped, dagTemplateScope, task, dagCtx.boundaryID, wfv1.NodeError, err.Error()) connectDependencies(nodeName) return } @@ -406,7 +409,7 @@ func (woc *wfOperationCtx) executeDAGTask(dagCtx *dagContext, taskName string) { if len(task.WithItems) > 0 || task.WithParam != "" || task.WithSequence != nil { if taskGroupNode == nil { connectDependencies(nodeName) - taskGroupNode = woc.initializeNode(nodeName, wfv1.NodeTypeTaskGroup, task, dagCtx.boundaryID, wfv1.NodeRunning, "") + taskGroupNode = woc.initializeNode(nodeName, wfv1.NodeTypeTaskGroup, dagTemplateScope, task, dagCtx.boundaryID, wfv1.NodeRunning, "") } } @@ -421,12 +424,12 @@ func (woc *wfOperationCtx) executeDAGTask(dagCtx *dagContext, taskName string) { // Check the task's when clause to decide if it should execute proceed, err := shouldExecute(t.When) if err != nil { - woc.initializeNode(taskNodeName, wfv1.NodeTypeSkipped, task, dagCtx.boundaryID, wfv1.NodeError, err.Error()) + woc.initializeNode(taskNodeName, wfv1.NodeTypeSkipped, dagTemplateScope, task, dagCtx.boundaryID, wfv1.NodeError, err.Error()) continue } if !proceed { skipReason := fmt.Sprintf("when '%s' evaluated false", t.When) - woc.initializeNode(taskNodeName, wfv1.NodeTypeSkipped, task, dagCtx.boundaryID, wfv1.NodeSkipped, skipReason) + woc.initializeNode(taskNodeName, wfv1.NodeTypeSkipped, dagTemplateScope, task, dagCtx.boundaryID, wfv1.NodeSkipped, skipReason) continue } } diff --git a/workflow/controller/operator.go b/workflow/controller/operator.go index 6b1192804ebd..ed16dfada913 100644 --- a/workflow/controller/operator.go +++ b/workflow/controller/operator.go @@ -78,10 +78,6 @@ type wfOperationCtx struct { // workflowDeadline is the deadline which the workflow is expected to complete before we // terminate the workflow. workflowDeadline *time.Time - - // tmplCtx is the context of template search. - tmplCtx *templateresolution.Context - // auditLogger is the argo audit logger auditLogger *argo.AuditLogger } @@ -121,7 +117,6 @@ func newWorkflowOperationCtx(wf *wfv1.Workflow, wfc *WorkflowController) *wfOper deadline: time.Now().UTC().Add(maxOperationTime), auditLogger: argo.NewAuditLogger(wf.ObjectMeta.Namespace, wfc.kubeclientset, wf.ObjectMeta.Name), } - woc.tmplCtx = templateresolution.NewContext(wfc.wftmplInformer.Lister().WorkflowTemplates(wf.Namespace), wf, &woc) if woc.wf.Status.Nodes == nil { woc.wf.Status.Nodes = make(map[string]wfv1.NodeStatus) @@ -228,9 +223,12 @@ func (woc *wfOperationCtx) operate() { return } + // Create a starting template context. + tmplCtx := woc.createTemplateContext("") + var workflowStatus wfv1.NodePhase var workflowMessage string - node, err := woc.executeTemplate(woc.wf.ObjectMeta.Name, &wfv1.Template{Template: woc.wf.Spec.Entrypoint}, woc.tmplCtx, woc.wf.Spec.Arguments, "") + node, err := woc.executeTemplate(woc.wf.ObjectMeta.Name, &wfv1.Template{Template: woc.wf.Spec.Entrypoint}, tmplCtx, woc.wf.Spec.Arguments, "") if err != nil { msg := fmt.Sprintf("%s error in entry template execution: %+v", woc.wf.Name, err) // the error are handled in the callee so just log it. @@ -266,7 +264,7 @@ func (woc *wfOperationCtx) operate() { } woc.log.Infof("Running OnExit handler: %s", woc.wf.Spec.OnExit) onExitNodeName := woc.wf.ObjectMeta.Name + ".onExit" - onExitNode, err = woc.executeTemplate(onExitNodeName, &wfv1.Template{Template: woc.wf.Spec.OnExit}, woc.tmplCtx, woc.wf.Spec.Arguments, "") + onExitNode, err = woc.executeTemplate(onExitNodeName, &wfv1.Template{Template: woc.wf.Spec.OnExit}, tmplCtx, woc.wf.Spec.Arguments, "") if err != nil { // the error are handled in the callee so just log it. woc.log.Errorf("%s error in exit template execution: %+v", woc.wf.Name, err) @@ -1241,7 +1239,7 @@ func (woc *wfOperationCtx) executeTemplate(nodeName string, orgTmpl wfv1.Templat newTmplCtx, resolvedTmpl, err := tmplCtx.ResolveTemplate(orgTmpl) if err != nil { - return woc.initializeNodeOrMarkError(node, nodeName, wfv1.NodeTypeSkipped, orgTmpl, boundaryID, err), err + return woc.initializeNodeOrMarkError(node, nodeName, wfv1.NodeTypeSkipped, templateScope, orgTmpl, boundaryID, err), err } localParams := make(map[string]string) @@ -1253,7 +1251,7 @@ func (woc *wfOperationCtx) executeTemplate(nodeName string, orgTmpl wfv1.Templat // Inputs has been processed with arguments already, so pass empty arguments. processedTmpl, err := common.ProcessArgs(resolvedTmpl, &args, woc.globalParams, localParams, false) if err != nil { - return woc.initializeNodeOrMarkError(node, nodeName, wfv1.NodeTypeSkipped, orgTmpl, boundaryID, err), err + return woc.initializeNodeOrMarkError(node, nodeName, wfv1.NodeTypeSkipped, templateScope, orgTmpl, boundaryID, err), err } // Check if we exceeded template or workflow parallelism and immediately return if we did @@ -1303,7 +1301,7 @@ func (woc *wfOperationCtx) executeTemplate(nodeName string, orgTmpl wfv1.Templat if processedTmpl.IsPodType() { processedTmpl, err = common.SubstituteParams(processedTmpl, map[string]string{}, map[string]string{common.LocalVarPodName: woc.wf.NodeID(nodeName)}) if err != nil { - return woc.initializeNodeOrMarkError(node, nodeName, wfv1.NodeTypeSkipped, orgTmpl, boundaryID, err), err + return woc.initializeNodeOrMarkError(node, nodeName, wfv1.NodeTypeSkipped, templateScope, orgTmpl, boundaryID, err), err } } } @@ -1323,7 +1321,7 @@ func (woc *wfOperationCtx) executeTemplate(nodeName string, orgTmpl wfv1.Templat node, err = woc.executeSuspend(nodeName, templateScope, processedTmpl, orgTmpl, boundaryID) default: err = errors.Errorf(errors.CodeBadRequest, "Template '%s' missing specification", processedTmpl.Name) - return woc.initializeNode(nodeName, wfv1.NodeTypeSkipped, orgTmpl, boundaryID, wfv1.NodeError, err.Error()), err + return woc.initializeNode(nodeName, wfv1.NodeTypeSkipped, templateScope, orgTmpl, boundaryID, wfv1.NodeError, err.Error()), err } if err != nil { node = woc.markNodeError(node.Name, err) @@ -1427,15 +1425,13 @@ var stepsOrDagSeparator = regexp.MustCompile(`^(\[\d+\])?\.`) // initializeExecutableNode initializes a node and stores the template. func (woc *wfOperationCtx) initializeExecutableNode(nodeName string, nodeType wfv1.NodeType, templateScope string, executeTmpl *wfv1.Template, orgTmpl wfv1.TemplateHolder, boundaryID string, phase wfv1.NodePhase, messages ...string) *wfv1.NodeStatus { - node := woc.initializeNode(nodeName, nodeType, orgTmpl, boundaryID, phase) + node := woc.initializeNode(nodeName, nodeType, templateScope, orgTmpl, boundaryID, phase) // Set the input values to the node. if executeTmpl.Inputs.HasInputs() { node.Inputs = executeTmpl.Inputs.DeepCopy() } - node.TemplateScope = templateScope - // Update the node woc.wf.Status.Nodes[node.ID] = *node woc.updated = true @@ -1444,14 +1440,14 @@ func (woc *wfOperationCtx) initializeExecutableNode(nodeName string, nodeType wf } // initializeNodeOrMarkError initializes an error node or mark a node if it already exists. -func (woc *wfOperationCtx) initializeNodeOrMarkError(node *wfv1.NodeStatus, nodeName string, nodeType wfv1.NodeType, orgTmpl wfv1.TemplateHolder, boundaryID string, err error) *wfv1.NodeStatus { +func (woc *wfOperationCtx) initializeNodeOrMarkError(node *wfv1.NodeStatus, nodeName string, nodeType wfv1.NodeType, templateScope string, orgTmpl wfv1.TemplateHolder, boundaryID string, err error) *wfv1.NodeStatus { if node != nil { return woc.markNodeError(nodeName, err) } - return woc.initializeNode(nodeName, wfv1.NodeTypeSkipped, orgTmpl, boundaryID, wfv1.NodeError, err.Error()) + return woc.initializeNode(nodeName, wfv1.NodeTypeSkipped, templateScope, orgTmpl, boundaryID, wfv1.NodeError, err.Error()) } -func (woc *wfOperationCtx) initializeNode(nodeName string, nodeType wfv1.NodeType, orgTmpl wfv1.TemplateHolder, boundaryID string, phase wfv1.NodePhase, messages ...string) *wfv1.NodeStatus { +func (woc *wfOperationCtx) initializeNode(nodeName string, nodeType wfv1.NodeType, templateScope string, orgTmpl wfv1.TemplateHolder, boundaryID string, phase wfv1.NodePhase, messages ...string) *wfv1.NodeStatus { woc.log.Debugf("Initializing node %s: template: %s, boundaryID: %s", nodeName, common.GetTemplateHolderString(orgTmpl), boundaryID) nodeID := woc.wf.NodeID(nodeName) @@ -1480,6 +1476,8 @@ func (woc *wfOperationCtx) initializeNode(nodeName string, nodeType wfv1.NodeTyp node.DisplayName = nodeName } + node.TemplateScope = templateScope + if node.Completed() && node.FinishedAt.IsZero() { node.FinishedAt = node.StartedAt } @@ -1552,7 +1550,8 @@ func (woc *wfOperationCtx) checkParallelism(tmpl *wfv1.Template, node *wfv1.Node if !ok { return errors.InternalError("boundaryNode not found") } - _, boundaryTemplate, err := woc.tmplCtx.ResolveTemplate(&boundaryNode) + tmplCtx := woc.createTemplateContext(boundaryNode.TemplateScope) + _, boundaryTemplate, err := tmplCtx.ResolveTemplate(&boundaryNode) if err != nil { return err } @@ -1691,7 +1690,8 @@ func (woc *wfOperationCtx) executeScript(nodeName string, templateScope string, includeScriptOutput := false if boundaryNode, ok := woc.wf.Status.Nodes[boundaryID]; ok { - _, parentTemplate, err := woc.tmplCtx.ResolveTemplate(&boundaryNode) + tmplCtx := woc.createTemplateContext(boundaryNode.TemplateScope) + _, parentTemplate, err := tmplCtx.ResolveTemplate(&boundaryNode) if err != nil { return node, err } @@ -2133,11 +2133,20 @@ func (woc *wfOperationCtx) substituteParamsInVolumes(params map[string]string) e return nil } -func (woc *wfOperationCtx) runOnExitNode(parentName, templateRef, boundaryID string) (bool, *wfv1.NodeStatus, error) { +// createTemplateContext creates a new template context. +func (woc *wfOperationCtx) createTemplateContext(templateScope string) *templateresolution.Context { + ctx := templateresolution.NewContext(woc.controller.wftmplInformer.Lister().WorkflowTemplates(woc.wf.Namespace), woc.wf, woc) + if templateScope != "" { + ctx = ctx.WithLazyWorkflowTemplate(woc.wf.Namespace, templateScope) + } + return ctx +} + +func (woc *wfOperationCtx) runOnExitNode(parentName, templateRef, boundaryID string, tmplCtx *templateresolution.Context) (bool, *wfv1.NodeStatus, error) { if templateRef != "" { woc.log.Infof("Running OnExit handler: %s", templateRef) onExitNodeName := parentName + ".onExit" - onExitNode, err := woc.executeTemplate(onExitNodeName, &wfv1.Template{Template: templateRef}, woc.tmplCtx, woc.wf.Spec.Arguments, boundaryID) + onExitNode, err := woc.executeTemplate(onExitNodeName, &wfv1.Template{Template: templateRef}, tmplCtx, woc.wf.Spec.Arguments, boundaryID) return true, onExitNode, err } return false, nil, nil diff --git a/workflow/controller/operator_template_scope_test.go b/workflow/controller/operator_template_scope_test.go new file mode 100644 index 000000000000..67bcb8b0b578 --- /dev/null +++ b/workflow/controller/operator_template_scope_test.go @@ -0,0 +1,429 @@ +package controller + +import ( + "testing" + + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + wfv1 "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1" +) + +var testTemplateScopeWorkflowYaml = ` +apiVersion: argoproj.io/v1alpha1 +kind: Workflow +metadata: + name: test-template-scope +spec: + entrypoint: entry + templates: + - name: entry + templateRef: + name: test-template-scope-1 + template: steps +` + +var testTemplateScopeWorkflowTemplateYaml1 = ` +apiVersion: argoproj.io/v1alpha1 +kind: WorkflowTemplate +metadata: + name: test-template-scope-1 +spec: + templates: + - name: steps + steps: + - - name: hello + template: hello + - name: other-wftmpl + templateRef: + name: test-template-scope-2 + template: steps + - name: hello + script: + image: python:alpine3.6 + command: [python] + source: | + print("hello world") +` + +var testTemplateScopeWorkflowTemplateYaml2 = ` +apiVersion: argoproj.io/v1alpha1 +kind: WorkflowTemplate +metadata: + name: test-template-scope-2 +spec: + templates: + - name: steps + steps: + - - name: hello + template: hello + - name: hello + script: + image: python:alpine3.6 + command: [python] + source: | + print("hello world") +` + +func TestTemplateScope(t *testing.T) { + controller := newController() + wfcset := controller.wfclientset.ArgoprojV1alpha1().Workflows("") + wfctmplset := controller.wfclientset.ArgoprojV1alpha1().WorkflowTemplates("") + + wf := unmarshalWF(testTemplateScopeWorkflowYaml) + _, err := wfcset.Create(wf) + assert.NoError(t, err) + wftmpl := unmarshalWFTmpl(testTemplateScopeWorkflowTemplateYaml1) + _, err = wfctmplset.Create(wftmpl) + assert.NoError(t, err) + wftmpl = unmarshalWFTmpl(testTemplateScopeWorkflowTemplateYaml2) + _, err = wfctmplset.Create(wftmpl) + assert.NoError(t, err) + + woc := newWorkflowOperationCtx(wf, controller) + woc.operate() + + wf, err = wfcset.Get(wf.Name, metav1.GetOptions{}) + assert.NoError(t, err) + + node := findNodeByName(wf.Status.Nodes, "test-template-scope") + if assert.NotNil(t, node, "Node %s not found", "test-templte-scope") { + assert.Equal(t, wfv1.NodeTypeSteps, node.Type) + assert.Equal(t, "", node.TemplateScope) + } + + node = findNodeByName(wf.Status.Nodes, "test-template-scope[0]") + if assert.NotNil(t, node, "Node %s not found", "test-templte-scope[0]") { + assert.Equal(t, wfv1.NodeTypeStepGroup, node.Type) + assert.Equal(t, "test-template-scope-1", node.TemplateScope) + } + + node = findNodeByName(wf.Status.Nodes, "test-template-scope[0].hello") + if assert.NotNil(t, node, "Node %s not found", "test-templte-scope[0].hello") { + assert.Equal(t, wfv1.NodeTypePod, node.Type) + assert.Equal(t, "test-template-scope-1", node.TemplateScope) + } + + node = findNodeByName(wf.Status.Nodes, "test-template-scope[0].other-wftmpl") + if assert.NotNil(t, node, "Node %s not found", "test-template-scope[0].other-wftmpl") { + assert.Equal(t, wfv1.NodeTypeSteps, node.Type) + assert.Equal(t, "test-template-scope-1", node.TemplateScope) + } + + node = findNodeByName(wf.Status.Nodes, "test-template-scope[0].other-wftmpl[0]") + if assert.NotNil(t, node, "Node %s not found", "test-template-scope[0].other-wftmpl[0]") { + assert.Equal(t, wfv1.NodeTypeStepGroup, node.Type) + assert.Equal(t, "test-template-scope-2", node.TemplateScope) + } + + node = findNodeByName(wf.Status.Nodes, "test-template-scope[0].other-wftmpl[0].hello") + if assert.NotNil(t, node, "Node %s not found", "test-template-scope[0].other-wftmpl[0].hello") { + assert.Equal(t, wfv1.NodeTypePod, node.Type) + assert.Equal(t, "test-template-scope-2", node.TemplateScope) + } +} + +var testTemplateScopeWithParamWorkflowYaml = ` +apiVersion: argoproj.io/v1alpha1 +kind: Workflow +metadata: + name: test-template-scope-with-param +spec: + entrypoint: main + templates: + - name: main + templateRef: + name: test-template-scope-with-param-1 + template: main +` + +var testTemplateScopeWithParamWorkflowTemplateYaml1 = ` +apiVersion: argoproj.io/v1alpha1 +kind: WorkflowTemplate +metadata: + name: test-template-scope-with-param-1 +spec: + templates: + - name: main + steps: + - - name: print-string + template: print-string + arguments: + parameters: + - name: letter + value: '{{item}}' + withParam: '["x", "y", "z"]' + - name: print-string + inputs: + parameters: + - name: letter + container: + image: alpine:3.6 + command: [sh, -c] + args: ["echo {{inputs.parameters.letter}}"] +` + +func TestTemplateScopeWithParam(t *testing.T) { + controller := newController() + wfcset := controller.wfclientset.ArgoprojV1alpha1().Workflows("") + wfctmplset := controller.wfclientset.ArgoprojV1alpha1().WorkflowTemplates("") + + wf := unmarshalWF(testTemplateScopeWithParamWorkflowYaml) + _, err := wfcset.Create(wf) + assert.NoError(t, err) + wftmpl := unmarshalWFTmpl(testTemplateScopeWithParamWorkflowTemplateYaml1) + _, err = wfctmplset.Create(wftmpl) + assert.NoError(t, err) + + woc := newWorkflowOperationCtx(wf, controller) + woc.operate() + + wf, err = wfcset.Get(wf.Name, metav1.GetOptions{}) + assert.NoError(t, err) + + node := findNodeByName(wf.Status.Nodes, "test-template-scope-with-param") + if assert.NotNil(t, node, "Node %s not found", "test-template-scope-with-param") { + assert.Equal(t, wfv1.NodeTypeSteps, node.Type) + assert.Equal(t, "", node.TemplateScope) + } + + node = findNodeByName(wf.Status.Nodes, "test-template-scope-with-param[0]") + if assert.NotNil(t, node, "Node %s not found", "test-template-scope-with-param[0]") { + assert.Equal(t, wfv1.NodeTypeStepGroup, node.Type) + assert.Equal(t, "test-template-scope-with-param-1", node.TemplateScope) + } + + node = findNodeByName(wf.Status.Nodes, "test-template-scope-with-param[0].print-string(0:x)") + if assert.NotNil(t, node, "Node %s not found", "test-template-scope-with-param[0].print-string(0:x)") { + assert.Equal(t, wfv1.NodeTypePod, node.Type) + assert.Equal(t, "test-template-scope-with-param-1", node.TemplateScope) + } + + node = findNodeByName(wf.Status.Nodes, "test-template-scope-with-param[0].print-string(1:y)") + if assert.NotNil(t, node, "Node %s not found", "test-template-scope-with-param[0].print-string(1:y)") { + assert.Equal(t, wfv1.NodeTypePod, node.Type) + assert.Equal(t, "test-template-scope-with-param-1", node.TemplateScope) + } + + node = findNodeByName(wf.Status.Nodes, "test-template-scope-with-param[0].print-string(2:z)") + if assert.NotNil(t, node, "Node %s not found", "test-template-scope-with-param[0].print-string(2:z)") { + assert.Equal(t, wfv1.NodeTypePod, node.Type) + assert.Equal(t, "test-template-scope-with-param-1", node.TemplateScope) + } +} + +var testTemplateScopeNestedStepsWithParamsWorkflowYaml = ` +apiVersion: argoproj.io/v1alpha1 +kind: Workflow +metadata: + name: test-template-scope-nested-steps-with-params +spec: + entrypoint: main + templates: + - name: main + templateRef: + name: test-template-scope-nested-steps-with-params-1 + template: main +` + +var testTemplateScopeNestedStepsWithParamsWorkflowTemplateYaml1 = ` +apiVersion: argoproj.io/v1alpha1 +kind: WorkflowTemplate +metadata: + name: test-template-scope-nested-steps-with-params-1 +spec: + templates: + - name: main + steps: + - - name: main + template: sub + - name: sub + steps: + - - name: print-string + template: print-string + arguments: + parameters: + - name: letter + value: '{{item}}' + withParam: '["x", "y", "z"]' + - name: print-string + inputs: + parameters: + - name: letter + container: + image: alpine:3.6 + command: [sh, -c] + args: ["echo {{inputs.parameters.letter}}"] +` + +func TestTemplateScopeNestedStepsWithParams(t *testing.T) { + controller := newController() + wfcset := controller.wfclientset.ArgoprojV1alpha1().Workflows("") + wfctmplset := controller.wfclientset.ArgoprojV1alpha1().WorkflowTemplates("") + + wf := unmarshalWF(testTemplateScopeNestedStepsWithParamsWorkflowYaml) + _, err := wfcset.Create(wf) + assert.NoError(t, err) + wftmpl := unmarshalWFTmpl(testTemplateScopeNestedStepsWithParamsWorkflowTemplateYaml1) + _, err = wfctmplset.Create(wftmpl) + assert.NoError(t, err) + + woc := newWorkflowOperationCtx(wf, controller) + woc.operate() + + wf, err = wfcset.Get(wf.Name, metav1.GetOptions{}) + assert.NoError(t, err) + + node := findNodeByName(wf.Status.Nodes, "test-template-scope-nested-steps-with-params") + if assert.NotNil(t, node, "Node %s not found", "test-template-scope-with-param") { + assert.Equal(t, wfv1.NodeTypeSteps, node.Type) + assert.Equal(t, "", node.TemplateScope) + } + + node = findNodeByName(wf.Status.Nodes, "test-template-scope-nested-steps-with-params[0]") + if assert.NotNil(t, node, "Node %s not found", "test-template-scope-with-param[0]") { + assert.Equal(t, wfv1.NodeTypeStepGroup, node.Type) + assert.Equal(t, "test-template-scope-nested-steps-with-params-1", node.TemplateScope) + } + + node = findNodeByName(wf.Status.Nodes, "test-template-scope-nested-steps-with-params[0].main") + if assert.NotNil(t, node, "Node %s not found", "test-template-scope-nested-steps-with-params[0].main") { + assert.Equal(t, wfv1.NodeTypeSteps, node.Type) + assert.Equal(t, "test-template-scope-nested-steps-with-params-1", node.TemplateScope) + } + + node = findNodeByName(wf.Status.Nodes, "test-template-scope-nested-steps-with-params[0].main[0]") + if assert.NotNil(t, node, "Node %s not found", "test-template-scope-nested-steps-with-params[0].main[0]") { + assert.Equal(t, wfv1.NodeTypeStepGroup, node.Type) + assert.Equal(t, "test-template-scope-nested-steps-with-params-1", node.TemplateScope) + } + + node = findNodeByName(wf.Status.Nodes, "test-template-scope-nested-steps-with-params[0].main[0].print-string(0:x)") + if assert.NotNil(t, node, "Node %s not found", "test-template-scope-nested-steps-with-params[0].main[0].print-string(0:x)") { + assert.Equal(t, wfv1.NodeTypePod, node.Type) + assert.Equal(t, "test-template-scope-nested-steps-with-params-1", node.TemplateScope) + } + + node = findNodeByName(wf.Status.Nodes, "test-template-scope-nested-steps-with-params[0].main[0].print-string(1:y)") + if assert.NotNil(t, node, "Node %s not found", "test-template-scope-nested-steps-with-params[0].main[0].print-string(1:y)") { + assert.Equal(t, wfv1.NodeTypePod, node.Type) + assert.Equal(t, "test-template-scope-nested-steps-with-params-1", node.TemplateScope) + } + + node = findNodeByName(wf.Status.Nodes, "test-template-scope-nested-steps-with-params[0].main[0].print-string(2:z)") + if assert.NotNil(t, node, "Node %s not found", "test-template-scope-nested-steps-with-params[0].main[0].print-string(2:z)") { + assert.Equal(t, wfv1.NodeTypePod, node.Type) + assert.Equal(t, "test-template-scope-nested-steps-with-params-1", node.TemplateScope) + } +} + +var testTemplateScopeDAGWorkflowYaml = ` +apiVersion: argoproj.io/v1alpha1 +kind: Workflow +metadata: + name: test-template-scope-dag +spec: + entrypoint: main + templates: + - name: main + templateRef: + name: test-template-scope-dag-1 + template: main +` + +var testTemplateScopeDAGWorkflowTemplateYaml1 = ` +apiVersion: argoproj.io/v1alpha1 +kind: WorkflowTemplate +metadata: + name: test-template-scope-dag-1 +spec: + templates: + - name: main + dag: + tasks: + - name: A + template: print-string + arguments: + parameters: + - name: letter + value: 'A' + - name: B + template: print-string + arguments: + parameters: + - name: letter + value: '{{item}}' + withParam: '["x", "y", "z"]' + - name: print-string + inputs: + parameters: + - name: letter + container: + image: alpine:3.6 + command: [sh, -c] + args: ["echo {{inputs.parameters.letter}}"] +` + +func TestTemplateScopeDAG(t *testing.T) { + controller := newController() + wfcset := controller.wfclientset.ArgoprojV1alpha1().Workflows("") + wfctmplset := controller.wfclientset.ArgoprojV1alpha1().WorkflowTemplates("") + + wf := unmarshalWF(testTemplateScopeDAGWorkflowYaml) + _, err := wfcset.Create(wf) + assert.NoError(t, err) + wftmpl := unmarshalWFTmpl(testTemplateScopeDAGWorkflowTemplateYaml1) + _, err = wfctmplset.Create(wftmpl) + assert.NoError(t, err) + + woc := newWorkflowOperationCtx(wf, controller) + woc.operate() + + wf, err = wfcset.Get(wf.Name, metav1.GetOptions{}) + assert.NoError(t, err) + + node := findNodeByName(wf.Status.Nodes, "test-template-scope-dag") + if assert.NotNil(t, node, "Node %s not found", "test-template-scope-dag") { + assert.Equal(t, wfv1.NodeTypeDAG, node.Type) + assert.Equal(t, "", node.TemplateScope) + } + + node = findNodeByName(wf.Status.Nodes, "test-template-scope-dag.A") + if assert.NotNil(t, node, "Node %s not found", "test-template-scope-dag.A") { + assert.Equal(t, wfv1.NodeTypePod, node.Type) + assert.Equal(t, "test-template-scope-dag-1", node.TemplateScope) + } + + node = findNodeByName(wf.Status.Nodes, "test-template-scope-dag.B") + if assert.NotNil(t, node, "Node %s not found", "test-template-scope-dag.B") { + assert.Equal(t, wfv1.NodeTypeTaskGroup, node.Type) + assert.Equal(t, "test-template-scope-dag-1", node.TemplateScope) + } + + node = findNodeByName(wf.Status.Nodes, "test-template-scope-dag.B(0:x)") + if assert.NotNil(t, node, "Node %s not found", "test-template-scope-dag.B(0:x") { + assert.Equal(t, wfv1.NodeTypePod, node.Type) + assert.Equal(t, "test-template-scope-dag-1", node.TemplateScope) + } + + node = findNodeByName(wf.Status.Nodes, "test-template-scope-dag.B(1:y)") + if assert.NotNil(t, node, "Node %s not found", "test-template-scope-dag.B(0:x") { + assert.Equal(t, wfv1.NodeTypePod, node.Type) + assert.Equal(t, "test-template-scope-dag-1", node.TemplateScope) + } + + node = findNodeByName(wf.Status.Nodes, "test-template-scope-dag.B(2:z)") + if assert.NotNil(t, node, "Node %s not found", "test-template-scope-dag.B(0:x") { + assert.Equal(t, wfv1.NodeTypePod, node.Type) + assert.Equal(t, "test-template-scope-dag-1", node.TemplateScope) + } +} + +func findNodeByName(nodes map[string]wfv1.NodeStatus, name string) *wfv1.NodeStatus { + for _, node := range nodes { + if node.Name == name { + return &node + } + } + return nil +} diff --git a/workflow/controller/operator_test.go b/workflow/controller/operator_test.go index ac9299586239..38ca46514861 100644 --- a/workflow/controller/operator_test.go +++ b/workflow/controller/operator_test.go @@ -126,7 +126,7 @@ func TestProcessNodesWithRetries(t *testing.T) { // Add the parent node for retries. nodeName := "test-node" nodeID := woc.wf.NodeID(nodeName) - node := woc.initializeNode(nodeName, wfv1.NodeTypeRetry, &wfv1.Template{}, "", wfv1.NodeRunning) + node := woc.initializeNode(nodeName, wfv1.NodeTypeRetry, "", &wfv1.Template{}, "", wfv1.NodeRunning) retries := wfv1.RetryStrategy{} retryLimit := int32(2) retries.Limit = &retryLimit @@ -142,7 +142,7 @@ func TestProcessNodesWithRetries(t *testing.T) { // Add child nodes. for i := 0; i < 2; i++ { childNode := fmt.Sprintf("child-node-%d", i) - woc.initializeNode(childNode, wfv1.NodeTypePod, &wfv1.Template{}, "", wfv1.NodeRunning) + woc.initializeNode(childNode, wfv1.NodeTypePod, "", &wfv1.Template{}, "", wfv1.NodeRunning) woc.addChildNode(nodeName, childNode) } @@ -174,7 +174,7 @@ func TestProcessNodesWithRetries(t *testing.T) { // Add a third node that has failed. childNode := "child-node-3" - woc.initializeNode(childNode, wfv1.NodeTypePod, &wfv1.Template{}, "", wfv1.NodeFailed) + woc.initializeNode(childNode, wfv1.NodeTypePod, "", &wfv1.Template{}, "", wfv1.NodeFailed) woc.addChildNode(nodeName, childNode) n = woc.getNodeByName(nodeName) n, _, err = woc.processNodeRetries(n, retries) @@ -197,7 +197,7 @@ func TestProcessNodesWithRetriesOnErrors(t *testing.T) { // Add the parent node for retries. nodeName := "test-node" nodeID := woc.wf.NodeID(nodeName) - node := woc.initializeNode(nodeName, wfv1.NodeTypeRetry, &wfv1.Template{}, "", wfv1.NodeRunning) + node := woc.initializeNode(nodeName, wfv1.NodeTypeRetry, "", &wfv1.Template{}, "", wfv1.NodeRunning) retries := wfv1.RetryStrategy{} retryLimit := int32(2) retries.Limit = &retryLimit @@ -214,7 +214,7 @@ func TestProcessNodesWithRetriesOnErrors(t *testing.T) { // Add child nodes. for i := 0; i < 2; i++ { childNode := fmt.Sprintf("child-node-%d", i) - woc.initializeNode(childNode, wfv1.NodeTypePod, &wfv1.Template{}, "", wfv1.NodeRunning) + woc.initializeNode(childNode, wfv1.NodeTypePod, "", &wfv1.Template{}, "", wfv1.NodeRunning) woc.addChildNode(nodeName, childNode) } @@ -246,7 +246,7 @@ func TestProcessNodesWithRetriesOnErrors(t *testing.T) { // Add a third node that has errored. childNode := "child-node-3" - woc.initializeNode(childNode, wfv1.NodeTypePod, &wfv1.Template{}, "", wfv1.NodeError) + woc.initializeNode(childNode, wfv1.NodeTypePod, "", &wfv1.Template{}, "", wfv1.NodeError) woc.addChildNode(nodeName, childNode) n = woc.getNodeByName(nodeName) n, _, err = woc.processNodeRetries(n, retries) @@ -269,7 +269,7 @@ func TestProcessNodesNoRetryWithError(t *testing.T) { // Add the parent node for retries. nodeName := "test-node" nodeID := woc.wf.NodeID(nodeName) - node := woc.initializeNode(nodeName, wfv1.NodeTypeRetry, &wfv1.Template{}, "", wfv1.NodeRunning) + node := woc.initializeNode(nodeName, wfv1.NodeTypeRetry, "", &wfv1.Template{}, "", wfv1.NodeRunning) retries := wfv1.RetryStrategy{} retryLimit := int32(2) retries.Limit = &retryLimit @@ -286,7 +286,7 @@ func TestProcessNodesNoRetryWithError(t *testing.T) { // Add child nodes. for i := 0; i < 2; i++ { childNode := fmt.Sprintf("child-node-%d", i) - woc.initializeNode(childNode, wfv1.NodeTypePod, &wfv1.Template{}, "", wfv1.NodeRunning) + woc.initializeNode(childNode, wfv1.NodeTypePod, "", &wfv1.Template{}, "", wfv1.NodeRunning) woc.addChildNode(nodeName, childNode) } @@ -1990,123 +1990,6 @@ func TestStepsOnExit(t *testing.T) { assert.True(t, onExitNodeIsPresent) } -var testTemplateScopeWorkflowYaml = ` -apiVersion: argoproj.io/v1alpha1 -kind: Workflow -metadata: - name: test-template-scope -spec: - entrypoint: entry - templates: - - name: entry - templateRef: - name: test-template-scope-1 - template: steps -` - -var testTemplateScopeWorkflowTemplateYaml1 = ` -apiVersion: argoproj.io/v1alpha1 -kind: WorkflowTemplate -metadata: - name: test-template-scope-1 -spec: - templates: - - name: steps - steps: - - - name: hello - template: hello - - name: other-wftmpl - templateRef: - name: test-template-scope-2 - template: steps - - name: hello - script: - image: python:alpine3.6 - command: [python] - source: | - print("hello world") -` - -var testTemplateScopeWorkflowTemplateYaml2 = ` -apiVersion: argoproj.io/v1alpha1 -kind: WorkflowTemplate -metadata: - name: test-template-scope-2 -spec: - templates: - - name: steps - steps: - - - name: hello - template: hello - - name: hello - script: - image: python:alpine3.6 - command: [python] - source: | - print("hello world") -` - -func TestTemplateScope(t *testing.T) { - controller := newController() - wfcset := controller.wfclientset.ArgoprojV1alpha1().Workflows("") - wfctmplset := controller.wfclientset.ArgoprojV1alpha1().WorkflowTemplates("") - - wf := unmarshalWF(testTemplateScopeWorkflowYaml) - _, err := wfcset.Create(wf) - assert.NoError(t, err) - wftmpl := unmarshalWFTmpl(testTemplateScopeWorkflowTemplateYaml1) - _, err = wfctmplset.Create(wftmpl) - assert.NoError(t, err) - wftmpl = unmarshalWFTmpl(testTemplateScopeWorkflowTemplateYaml2) - _, err = wfctmplset.Create(wftmpl) - assert.NoError(t, err) - - woc := newWorkflowOperationCtx(wf, controller) - woc.operate() - - wf, err = wfcset.Get(wf.Name, metav1.GetOptions{}) - assert.NoError(t, err) - - node := findNodeByName(wf.Status.Nodes, "test-template-scope") - if assert.NotNil(t, node, "Node %s not found", "test-templte-scope") { - assert.Equal(t, "", node.TemplateScope) - } - - node = findNodeByName(wf.Status.Nodes, "test-template-scope[0]") - if assert.NotNil(t, node, "Node %s not found", "test-templte-scope[0]") { - assert.Equal(t, "", node.TemplateScope) - } - - node = findNodeByName(wf.Status.Nodes, "test-template-scope[0].hello") - if assert.NotNil(t, node, "Node %s not found", "test-templte-scope[0].hello") { - assert.Equal(t, "test-template-scope-1", node.TemplateScope) - } - - node = findNodeByName(wf.Status.Nodes, "test-template-scope[0].other-wftmpl") - if assert.NotNil(t, node, "Node %s not found", "test-template-scope[0].other-wftmpl") { - assert.Equal(t, "test-template-scope-1", node.TemplateScope) - } - - node = findNodeByName(wf.Status.Nodes, "test-template-scope[0].other-wftmpl[0]") - if assert.NotNil(t, node, "Node %s not found", "test-template-scope[0].other-wftmpl[0]") { - assert.Equal(t, "", node.TemplateScope) - } - - node = findNodeByName(wf.Status.Nodes, "test-template-scope[0].other-wftmpl[0].hello") - if assert.NotNil(t, node, "Node %s not found", "test-template-scope[0].other-wftmpl[0].hello") { - assert.Equal(t, "test-template-scope-2", node.TemplateScope) - } -} - -func findNodeByName(nodes map[string]wfv1.NodeStatus, name string) *wfv1.NodeStatus { - for _, node := range nodes { - if node.Name == name { - return &node - } - } - return nil -} - var invalidSpec = ` apiVersion: argoproj.io/v1alpha1 kind: Workflow diff --git a/workflow/controller/steps.go b/workflow/controller/steps.go index f89bd8c75ad5..f6f0aede8bad 100644 --- a/workflow/controller/steps.go +++ b/workflow/controller/steps.go @@ -38,6 +38,9 @@ func (woc *wfOperationCtx) executeSteps(nodeName string, tmplCtx *templateresolu } }() + // The template scope of this step. + stepTemplateScope := tmplCtx.GetCurrentTemplateBase().GetTemplateScope() + stepsCtx := stepsContext{ boundaryID: node.ID, scope: &wfScope{ @@ -51,7 +54,7 @@ func (woc *wfOperationCtx) executeSteps(nodeName string, tmplCtx *templateresolu for i, stepGroup := range tmpl.Steps { sgNodeName := fmt.Sprintf("%s[%d]", nodeName, i) if woc.getNodeByName(sgNodeName) == nil { - _ = woc.initializeNode(sgNodeName, wfv1.NodeTypeStepGroup, tmpl, stepsCtx.boundaryID, wfv1.NodeRunning) + _ = woc.initializeNode(sgNodeName, wfv1.NodeTypeStepGroup, stepTemplateScope, tmpl, stepsCtx.boundaryID, wfv1.NodeRunning) } else { _ = woc.markNodePhase(sgNodeName, wfv1.NodeRunning) } @@ -110,7 +113,7 @@ func (woc *wfOperationCtx) executeSteps(nodeName string, tmplCtx *templateresolu } if len(childNodes) > 0 { // Expanded child nodes should be created from the same template. - _, tmpl, err := woc.tmplCtx.ResolveTemplate(&childNodes[0]) + _, tmpl, err := stepsCtx.tmplCtx.ResolveTemplate(&childNodes[0]) if err != nil { return node, err } @@ -192,6 +195,9 @@ func (woc *wfOperationCtx) executeStepGroup(stepGroup []wfv1.WorkflowStep, sgNod // Maps nodes to their steps nodeSteps := make(map[string]wfv1.WorkflowStep) + // The template scope of this step group. + stepTemplateScope := stepsCtx.tmplCtx.GetCurrentTemplateBase().GetTemplateScope() + // Kick off all parallel steps in the group for _, step := range stepGroup { childNodeName := fmt.Sprintf("%s.%s", sgNodeName, step.Name) @@ -199,7 +205,7 @@ func (woc *wfOperationCtx) executeStepGroup(stepGroup []wfv1.WorkflowStep, sgNod // Check the step's when clause to decide if it should execute proceed, err := shouldExecute(step.When) if err != nil { - woc.initializeNode(childNodeName, wfv1.NodeTypeSkipped, &step, stepsCtx.boundaryID, wfv1.NodeError, err.Error()) + woc.initializeNode(childNodeName, wfv1.NodeTypeSkipped, stepTemplateScope, &step, stepsCtx.boundaryID, wfv1.NodeError, err.Error()) woc.addChildNode(sgNodeName, childNodeName) woc.markNodeError(childNodeName, err) return woc.markNodeError(sgNodeName, err) @@ -208,7 +214,7 @@ func (woc *wfOperationCtx) executeStepGroup(stepGroup []wfv1.WorkflowStep, sgNod if woc.getNodeByName(childNodeName) == nil { skipReason := fmt.Sprintf("when '%s' evaluated false", step.When) woc.log.Infof("Skipping %s: %s", childNodeName, skipReason) - woc.initializeNode(childNodeName, wfv1.NodeTypeSkipped, &step, stepsCtx.boundaryID, wfv1.NodeSkipped, skipReason) + woc.initializeNode(childNodeName, wfv1.NodeTypeSkipped, stepTemplateScope, &step, stepsCtx.boundaryID, wfv1.NodeSkipped, skipReason) woc.addChildNode(sgNodeName, childNodeName) } continue @@ -241,7 +247,7 @@ func (woc *wfOperationCtx) executeStepGroup(stepGroup []wfv1.WorkflowStep, sgNod if !childNode.Completed() { completed = false } else { - hasOnExitNode, onExitNode, err := woc.runOnExitNode(step.Name, step.OnExit, stepsCtx.boundaryID) + hasOnExitNode, onExitNode, err := woc.runOnExitNode(step.Name, step.OnExit, stepsCtx.boundaryID, stepsCtx.tmplCtx) if hasOnExitNode && (onExitNode == nil || !onExitNode.Completed() || err != nil) { // The onExit node is either not complete or has errored out, return. completed = false diff --git a/workflow/controller/workflowpod_test.go b/workflow/controller/workflowpod_test.go index 77855df8b392..eba25c0e26cd 100644 --- a/workflow/controller/workflowpod_test.go +++ b/workflow/controller/workflowpod_test.go @@ -62,7 +62,7 @@ script: func TestScriptTemplateWithVolume(t *testing.T) { tmpl := unmarshalTemplate(scriptTemplateWithInputArtifact) woc := newWoc() - _, err := woc.executeScript(tmpl.Name, woc.tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), tmpl, tmpl, "") + _, err := woc.executeScript(tmpl.Name, "", tmpl, tmpl, "") assert.NoError(t, err) } @@ -131,7 +131,8 @@ func TestScriptTemplateWithoutVolumeOptionalArtifact(t *testing.T) { func TestWFLevelServiceAccount(t *testing.T) { woc := newWoc() woc.wf.Spec.ServiceAccountName = "foo" - _, err := woc.executeContainer(woc.wf.Spec.Entrypoint, woc.tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") + tmplCtx := woc.createTemplateContext("") + _, err := woc.executeContainer(woc.wf.Spec.Entrypoint, tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") assert.NoError(t, err) pods, err := woc.controller.kubeclientset.CoreV1().Pods("").List(metav1.ListOptions{}) assert.NoError(t, err) @@ -146,7 +147,8 @@ func TestTmplServiceAccount(t *testing.T) { woc := newWoc() woc.wf.Spec.ServiceAccountName = "foo" woc.wf.Spec.Templates[0].ServiceAccountName = "tmpl" - _, err := woc.executeContainer(woc.wf.Spec.Entrypoint, woc.tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") + tmplCtx := woc.createTemplateContext("") + _, err := woc.executeContainer(woc.wf.Spec.Entrypoint, tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") assert.NoError(t, err) pods, err := woc.controller.kubeclientset.CoreV1().Pods("").List(metav1.ListOptions{}) assert.NoError(t, err) @@ -164,7 +166,8 @@ func TestWFLevelAutomountServiceAccountToken(t *testing.T) { falseValue := false woc.wf.Spec.AutomountServiceAccountToken = &falseValue woc.wf.Spec.Executor = &wfv1.ExecutorConfig{ServiceAccountName: "foo"} - _, err = woc.executeContainer(woc.wf.Spec.Entrypoint, woc.tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") + tmplCtx := woc.createTemplateContext("") + _, err = woc.executeContainer(woc.wf.Spec.Entrypoint, tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") assert.NoError(t, err) pods, err := woc.controller.kubeclientset.CoreV1().Pods("").List(metav1.ListOptions{}) assert.NoError(t, err) @@ -184,7 +187,8 @@ func TestTmplLevelAutomountServiceAccountToken(t *testing.T) { woc.wf.Spec.AutomountServiceAccountToken = &trueValue woc.wf.Spec.Executor = &wfv1.ExecutorConfig{ServiceAccountName: "foo"} woc.wf.Spec.Templates[0].AutomountServiceAccountToken = &falseValue - _, err = woc.executeContainer(woc.wf.Spec.Entrypoint, woc.tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") + tmplCtx := woc.createTemplateContext("") + _, err = woc.executeContainer(woc.wf.Spec.Entrypoint, tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") assert.NoError(t, err) pods, err := woc.controller.kubeclientset.CoreV1().Pods("").List(metav1.ListOptions{}) assert.NoError(t, err) @@ -210,7 +214,8 @@ func TestWFLevelExecutorServiceAccountName(t *testing.T) { assert.NoError(t, err) woc.wf.Spec.Executor = &wfv1.ExecutorConfig{ServiceAccountName: "foo"} - _, err = woc.executeContainer(woc.wf.Spec.Entrypoint, woc.tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") + tmplCtx := woc.createTemplateContext("") + _, err = woc.executeContainer(woc.wf.Spec.Entrypoint, tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") assert.NoError(t, err) pods, err := woc.controller.kubeclientset.CoreV1().Pods("").List(metav1.ListOptions{}) assert.NoError(t, err) @@ -233,7 +238,8 @@ func TestTmplLevelExecutorServiceAccountName(t *testing.T) { woc.wf.Spec.Executor = &wfv1.ExecutorConfig{ServiceAccountName: "foo"} woc.wf.Spec.Templates[0].Executor = &wfv1.ExecutorConfig{ServiceAccountName: "tmpl"} - _, err = woc.executeContainer(woc.wf.Spec.Entrypoint, woc.tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") + tmplCtx := woc.createTemplateContext("") + _, err = woc.executeContainer(woc.wf.Spec.Entrypoint, tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") assert.NoError(t, err) pods, err := woc.controller.kubeclientset.CoreV1().Pods("").List(metav1.ListOptions{}) assert.NoError(t, err) @@ -254,7 +260,8 @@ func TestImagePullSecrets(t *testing.T) { Name: "secret-name", }, } - _, err := woc.executeContainer(woc.wf.Spec.Entrypoint, woc.tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") + tmplCtx := woc.createTemplateContext("") + _, err := woc.executeContainer(woc.wf.Spec.Entrypoint, tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") assert.NoError(t, err) pods, err := woc.controller.kubeclientset.CoreV1().Pods("").List(metav1.ListOptions{}) assert.NoError(t, err) @@ -286,7 +293,8 @@ func TestAffinity(t *testing.T) { }, }, } - _, err := woc.executeContainer(woc.wf.Spec.Entrypoint, woc.tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") + tmplCtx := woc.createTemplateContext("") + _, err := woc.executeContainer(woc.wf.Spec.Entrypoint, tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") assert.NoError(t, err) pods, err := woc.controller.kubeclientset.CoreV1().Pods("").List(metav1.ListOptions{}) assert.NoError(t, err) @@ -303,7 +311,8 @@ func TestTolerations(t *testing.T) { Operator: "Exists", Effect: "NoSchedule", }} - _, err := woc.executeContainer(woc.wf.Spec.Entrypoint, woc.tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") + tmplCtx := woc.createTemplateContext("") + _, err := woc.executeContainer(woc.wf.Spec.Entrypoint, tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") assert.NoError(t, err) pods, err := woc.controller.kubeclientset.CoreV1().Pods("").List(metav1.ListOptions{}) assert.NoError(t, err) @@ -316,7 +325,8 @@ func TestTolerations(t *testing.T) { // TestMetadata verifies ability to carry forward annotations and labels func TestMetadata(t *testing.T) { woc := newWoc() - _, err := woc.executeContainer(woc.wf.Spec.Entrypoint, woc.tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") + tmplCtx := woc.createTemplateContext("") + _, err := woc.executeContainer(woc.wf.Spec.Entrypoint, tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") assert.NoError(t, err) pods, err := woc.controller.kubeclientset.CoreV1().Pods("").List(metav1.ListOptions{}) assert.NoError(t, err) @@ -467,7 +477,8 @@ func TestVolumeAndVolumeMounts(t *testing.T) { woc.wf.Spec.Templates[0].Container.VolumeMounts = volumeMounts woc.controller.Config.ContainerRuntimeExecutor = common.ContainerRuntimeExecutorDocker - _, err := woc.executeContainer(woc.wf.Spec.Entrypoint, woc.tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") + tmplCtx := woc.createTemplateContext("") + _, err := woc.executeContainer(woc.wf.Spec.Entrypoint, tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") assert.NoError(t, err) pods, err := woc.controller.kubeclientset.CoreV1().Pods("").List(metav1.ListOptions{}) assert.NoError(t, err) @@ -488,7 +499,8 @@ func TestVolumeAndVolumeMounts(t *testing.T) { woc.wf.Spec.Templates[0].Container.VolumeMounts = volumeMounts woc.controller.Config.ContainerRuntimeExecutor = common.ContainerRuntimeExecutorKubelet - _, err := woc.executeContainer(woc.wf.Spec.Entrypoint, woc.tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") + tmplCtx := woc.createTemplateContext("") + _, err := woc.executeContainer(woc.wf.Spec.Entrypoint, tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") assert.NoError(t, err) pods, err := woc.controller.kubeclientset.CoreV1().Pods("").List(metav1.ListOptions{}) assert.NoError(t, err) @@ -508,7 +520,8 @@ func TestVolumeAndVolumeMounts(t *testing.T) { woc.wf.Spec.Templates[0].Container.VolumeMounts = volumeMounts woc.controller.Config.ContainerRuntimeExecutor = common.ContainerRuntimeExecutorK8sAPI - _, err := woc.executeContainer(woc.wf.Spec.Entrypoint, woc.tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") + tmplCtx := woc.createTemplateContext("") + _, err := woc.executeContainer(woc.wf.Spec.Entrypoint, tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") assert.NoError(t, err) pods, err := woc.controller.kubeclientset.CoreV1().Pods("").List(metav1.ListOptions{}) assert.NoError(t, err) @@ -553,7 +566,8 @@ func TestVolumesPodSubstitution(t *testing.T) { woc.wf.Spec.Templates[0].Inputs.Parameters = inputParameters woc.controller.Config.ContainerRuntimeExecutor = common.ContainerRuntimeExecutorDocker - _, err := woc.executeContainer(woc.wf.Spec.Entrypoint, woc.tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") + tmplCtx := woc.createTemplateContext("") + _, err := woc.executeContainer(woc.wf.Spec.Entrypoint, tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") assert.NoError(t, err) pods, err := woc.controller.kubeclientset.CoreV1().Pods("").List(metav1.ListOptions{}) assert.NoError(t, err) @@ -589,7 +603,8 @@ func TestOutOfCluster(t *testing.T) { SecretKey: "bar", } - _, err := woc.executeContainer(woc.wf.Spec.Entrypoint, woc.tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") + tmplCtx := woc.createTemplateContext("") + _, err := woc.executeContainer(woc.wf.Spec.Entrypoint, tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") assert.NoError(t, err) pods, err := woc.controller.kubeclientset.CoreV1().Pods("").List(metav1.ListOptions{}) assert.NoError(t, err) @@ -612,7 +627,8 @@ func TestOutOfCluster(t *testing.T) { VolumeName: "kube-config-secret", } - _, err := woc.executeContainer(woc.wf.Spec.Entrypoint, woc.tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") + tmplCtx := woc.createTemplateContext("") + _, err := woc.executeContainer(woc.wf.Spec.Entrypoint, tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") assert.NoError(t, err) pods, err := woc.controller.kubeclientset.CoreV1().Pods("").List(metav1.ListOptions{}) assert.NoError(t, err) @@ -633,7 +649,8 @@ func TestPriority(t *testing.T) { woc := newWoc() woc.wf.Spec.Templates[0].PriorityClassName = "foo" woc.wf.Spec.Templates[0].Priority = &priority - _, err := woc.executeContainer(woc.wf.Spec.Entrypoint, woc.tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") + tmplCtx := woc.createTemplateContext("") + _, err := woc.executeContainer(woc.wf.Spec.Entrypoint, tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") assert.NoError(t, err) pods, err := woc.controller.kubeclientset.CoreV1().Pods("").List(metav1.ListOptions{}) assert.NoError(t, err) @@ -647,7 +664,8 @@ func TestPriority(t *testing.T) { func TestSchedulerName(t *testing.T) { woc := newWoc() woc.wf.Spec.Templates[0].SchedulerName = "foo" - _, err := woc.executeContainer(woc.wf.Spec.Entrypoint, woc.tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") + tmplCtx := woc.createTemplateContext("") + _, err := woc.executeContainer(woc.wf.Spec.Entrypoint, tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") assert.NoError(t, err) pods, err := woc.controller.kubeclientset.CoreV1().Pods("").List(metav1.ListOptions{}) assert.NoError(t, err) @@ -699,7 +717,8 @@ func TestInitContainers(t *testing.T) { }, } - _, err := woc.executeContainer(woc.wf.Spec.Entrypoint, woc.tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") + tmplCtx := woc.createTemplateContext("") + _, err := woc.executeContainer(woc.wf.Spec.Entrypoint, tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") assert.NoError(t, err) pods, err := woc.controller.kubeclientset.CoreV1().Pods("").List(metav1.ListOptions{}) assert.NoError(t, err) @@ -758,7 +777,8 @@ func TestSidecars(t *testing.T) { }, } - _, err := woc.executeContainer(woc.wf.Spec.Entrypoint, woc.tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") + tmplCtx := woc.createTemplateContext("") + _, err := woc.executeContainer(woc.wf.Spec.Entrypoint, tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") assert.NoError(t, err) pods, err := woc.controller.kubeclientset.CoreV1().Pods("").List(metav1.ListOptions{}) assert.NoError(t, err) @@ -810,7 +830,8 @@ func TestTemplateLocalVolumes(t *testing.T) { woc.wf.Spec.Templates[0].Container.VolumeMounts = volumeMounts woc.wf.Spec.Templates[0].Volumes = localVolumes - _, err := woc.executeContainer(woc.wf.Spec.Entrypoint, woc.tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") + tmplCtx := woc.createTemplateContext("") + _, err := woc.executeContainer(woc.wf.Spec.Entrypoint, tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") assert.NoError(t, err) pods, err := woc.controller.kubeclientset.CoreV1().Pods("").List(metav1.ListOptions{}) assert.NoError(t, err) @@ -831,7 +852,8 @@ func TestWFLevelHostAliases(t *testing.T) { {IP: "127.0.0.1"}, {IP: "127.0.0.1"}, } - _, err := woc.executeContainer(woc.wf.Spec.Entrypoint, woc.tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") + tmplCtx := woc.createTemplateContext("") + _, err := woc.executeContainer(woc.wf.Spec.Entrypoint, tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") assert.NoError(t, err) pods, err := woc.controller.kubeclientset.CoreV1().Pods("").List(metav1.ListOptions{}) assert.NoError(t, err) @@ -848,7 +870,8 @@ func TestTmplLevelHostAliases(t *testing.T) { {IP: "127.0.0.1"}, {IP: "127.0.0.1"}, } - _, err := woc.executeContainer(woc.wf.Spec.Entrypoint, woc.tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") + tmplCtx := woc.createTemplateContext("") + _, err := woc.executeContainer(woc.wf.Spec.Entrypoint, tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") assert.NoError(t, err) pods, err := woc.controller.kubeclientset.CoreV1().Pods("").List(metav1.ListOptions{}) assert.NoError(t, err) @@ -865,7 +888,8 @@ func TestWFLevelSecurityContext(t *testing.T) { woc.wf.Spec.SecurityContext = &apiv1.PodSecurityContext{ RunAsUser: &runAsUser, } - _, err := woc.executeContainer(woc.wf.Spec.Entrypoint, woc.tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") + tmplCtx := woc.createTemplateContext("") + _, err := woc.executeContainer(woc.wf.Spec.Entrypoint, tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") assert.NoError(t, err) pods, err := woc.controller.kubeclientset.CoreV1().Pods("").List(metav1.ListOptions{}) assert.NoError(t, err) @@ -882,7 +906,8 @@ func TestTmplLevelSecurityContext(t *testing.T) { woc.wf.Spec.Templates[0].SecurityContext = &apiv1.PodSecurityContext{ RunAsUser: &runAsUser, } - _, err := woc.executeContainer(woc.wf.Spec.Entrypoint, woc.tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") + tmplCtx := woc.createTemplateContext("") + _, err := woc.executeContainer(woc.wf.Spec.Entrypoint, tmplCtx.GetCurrentTemplateBase().GetTemplateScope(), &woc.wf.Spec.Templates[0], &woc.wf.Spec.Templates[0], "") assert.NoError(t, err) pods, err := woc.controller.kubeclientset.CoreV1().Pods("").List(metav1.ListOptions{}) assert.NoError(t, err) @@ -901,7 +926,7 @@ spec: entrypoint: whalesay templates: - name: whalesay - podSpecPatch: '{"containers":[{"name":"main", "resources":{"limits":{"cpu": "800m"}}}]}' + podSpecPatch: '{"containers":[{"name":"main", "resources":{"limits":{"cpu": "800m"}}}]}' container: image: docker/whalesay:latest command: [cowsay] diff --git a/workflow/templateresolution/context.go b/workflow/templateresolution/context.go index 56962779be8d..bf50b2e66838 100644 --- a/workflow/templateresolution/context.go +++ b/workflow/templateresolution/context.go @@ -173,10 +173,7 @@ func (ctx *Context) resolveTemplateImpl(tmplHolder wfv1.TemplateHolder, depth in } // Update the template base of the context. - newTmplCtx, err := ctx.WithTemplateHolder(tmplHolder) - if err != nil { - return nil, nil, err - } + newTmplCtx := ctx.WithTemplateHolder(tmplHolder) // Return a concrete template without digging into it. if tmpl.GetType() != wfv1.TemplateTypeUnknown { @@ -199,12 +196,12 @@ func (ctx *Context) resolveTemplateImpl(tmplHolder wfv1.TemplateHolder, depth in } // WithTemplateHolder creates new context with a template base of a given template holder. -func (ctx *Context) WithTemplateHolder(tmplHolder wfv1.TemplateHolder) (*Context, error) { +func (ctx *Context) WithTemplateHolder(tmplHolder wfv1.TemplateHolder) *Context { tmplRef := tmplHolder.GetTemplateRef() if tmplRef != nil { return ctx.WithLazyWorkflowTemplate(ctx.tmplBase.GetNamespace(), tmplRef.Name) } else { - return ctx.WithTemplateBase(ctx.tmplBase), nil + return ctx.WithTemplateBase(ctx.tmplBase) } } @@ -214,6 +211,6 @@ func (ctx *Context) WithTemplateBase(tmplBase wfv1.TemplateGetter) *Context { } // WithLazyWorkflowTemplate creates new context with the wfv1.WorkflowTemplate of the given name with lazy loading. -func (ctx *Context) WithLazyWorkflowTemplate(namespace, name string) (*Context, error) { - return NewContext(ctx.wftmplGetter, NewLazyWorkflowTemplate(ctx.wftmplGetter, namespace, name), ctx.storage), nil +func (ctx *Context) WithLazyWorkflowTemplate(namespace, name string) *Context { + return NewContext(ctx.wftmplGetter, NewLazyWorkflowTemplate(ctx.wftmplGetter, namespace, name), ctx.storage) } diff --git a/workflow/templateresolution/context_test.go b/workflow/templateresolution/context_test.go index bbc6e0eb26cd..8ef674122eb7 100644 --- a/workflow/templateresolution/context_test.go +++ b/workflow/templateresolution/context_test.go @@ -217,10 +217,7 @@ func TestWithTemplateHolder(t *testing.T) { var tmplGetter wfv1.TemplateGetter // Get the template base of existing template name. tmplHolder := wfv1.Template{Template: "whalesay"} - newCtx, err := ctx.WithTemplateHolder(&tmplHolder) - if !assert.NoError(t, err) { - t.Fatal(err) - } + newCtx := ctx.WithTemplateHolder(&tmplHolder) tmplGetter, ok := newCtx.GetCurrentTemplateBase().(*wfv1.WorkflowTemplate) if !assert.True(t, ok) { t.Fatal("tmplBase is not a WorkflowTemplate") @@ -229,10 +226,7 @@ func TestWithTemplateHolder(t *testing.T) { // Get the template base of unexisting template name. tmplHolder = wfv1.Template{Template: "unknown"} - newCtx, err = ctx.WithTemplateHolder(&tmplHolder) - if !assert.NoError(t, err) { - t.Fatal(err) - } + newCtx = ctx.WithTemplateHolder(&tmplHolder) tmplGetter, ok = newCtx.GetCurrentTemplateBase().(*wfv1.WorkflowTemplate) if !assert.True(t, ok) { t.Fatal("tmplBase is not a WorkflowTemplate") @@ -241,10 +235,7 @@ func TestWithTemplateHolder(t *testing.T) { // Get the template base of existing template reference. tmplHolder = wfv1.Template{TemplateRef: &wfv1.TemplateRef{Name: "some-workflow-template", Template: "whalesay"}} - newCtx, err = ctx.WithTemplateHolder(&tmplHolder) - if !assert.NoError(t, err) { - t.Fatal(err) - } + newCtx = ctx.WithTemplateHolder(&tmplHolder) tmplGetter, ok = newCtx.GetCurrentTemplateBase().(*lazyWorkflowTemplate) if !assert.True(t, ok) { t.Fatal("tmplBase is not a lazy WorkflowTemplate") @@ -253,10 +244,7 @@ func TestWithTemplateHolder(t *testing.T) { // Get the template base of unexisting template reference. tmplHolder = wfv1.Template{TemplateRef: &wfv1.TemplateRef{Name: "unknown-workflow-template", Template: "whalesay"}} - newCtx, err = ctx.WithTemplateHolder(&tmplHolder) - if !assert.NoError(t, err) { - t.Fatal(err) - } + newCtx = ctx.WithTemplateHolder(&tmplHolder) tmplGetter, ok = newCtx.GetCurrentTemplateBase().(*lazyWorkflowTemplate) if !assert.True(t, ok) { t.Fatal("tmplBase is not a lazy WorkflowTemplate") @@ -405,10 +393,7 @@ func TestOnWorkflowTemplate(t *testing.T) { } // Get the template base of existing template name. - newCtx, err := ctx.WithLazyWorkflowTemplate("namespace", "another-workflow-template") - if err != nil { - t.Fatal(err) - } + newCtx := ctx.WithLazyWorkflowTemplate("namespace", "another-workflow-template") tmpl := newCtx.tmplBase.GetTemplateByName("whalesay") assert.NotNil(t, tmpl) }