diff --git a/Gopkg.lock b/Gopkg.lock index b0ceced1e7c9..153c5cc4d908 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -44,25 +44,6 @@ pruneopts = "" revision = "de5bf2ad457846296e2031421a34e2568e304e35" -[[projects]] - branch = "master" - digest = "1:a74730e052a45a3fab1d310fdef2ec17ae3d6af16228421e238320846f2aaec8" - name = "github.com/alecthomas/template" - packages = [ - ".", - "parse", - ] - pruneopts = "" - revision = "a0175ee3bccc567396460bf5acd36800cb10c49c" - -[[projects]] - branch = "master" - digest = "1:8483994d21404c8a1d489f6be756e25bfccd3b45d65821f25695577791a08e68" - name = "github.com/alecthomas/units" - packages = ["."] - pruneopts = "" - revision = "2efee857e7cfd4f3d0138cc3cbb1b4966962b93a" - [[projects]] branch = "master" digest = "1:72347a6143ccb58245c6f8055662ae6cb2d5dd655699f0fc479c25cc610fc582" @@ -523,7 +504,6 @@ packages = [ "expfmt", "internal/bitbucket.org/ww/goautoneg", - "log", "model", ] pruneopts = "" @@ -714,8 +694,6 @@ "cpu", "unix", "windows", - "windows/registry", - "windows/svc/eventlog", ] pruneopts = "" revision = "904bdc257025c7b3f43c19360ad3ab85783fad78" @@ -784,14 +762,6 @@ revision = "b1f26356af11148e710935ed1ac8a7f5702c7612" version = "v1.1.0" -[[projects]] - digest = "1:15d017551627c8bb091bde628215b2861bed128855343fdd570c62d08871f6e1" - name = "gopkg.in/alecthomas/kingpin.v2" - packages = ["."] - pruneopts = "" - revision = "947dcec5ba9c011838740e680966fd7087a71d0d" - version = "v2.2.6" - [[projects]] digest = "1:75fb3fcfc73a8c723efde7777b40e8e8ff9babf30d8c56160d01beffea8a95a6" name = "gopkg.in/inf.v0" @@ -1258,7 +1228,6 @@ "github.com/pkg/errors", "github.com/prometheus/client_golang/prometheus", "github.com/prometheus/client_golang/prometheus/promhttp", - "github.com/prometheus/common/log", "github.com/sirupsen/logrus", "github.com/spf13/cobra", "github.com/stretchr/testify/assert", @@ -1272,6 +1241,7 @@ "gopkg.in/jcmturner/gokrb5.v5/credentials", "gopkg.in/jcmturner/gokrb5.v5/keytab", "gopkg.in/src-d/go-git.v4", + "gopkg.in/src-d/go-git.v4/config", "gopkg.in/src-d/go-git.v4/plumbing/transport", "gopkg.in/src-d/go-git.v4/plumbing/transport/http", "gopkg.in/src-d/go-git.v4/plumbing/transport/ssh", @@ -1291,6 +1261,7 @@ "k8s.io/apimachinery/pkg/util/runtime", "k8s.io/apimachinery/pkg/util/validation", "k8s.io/apimachinery/pkg/util/wait", + "k8s.io/apimachinery/pkg/version", "k8s.io/apimachinery/pkg/watch", "k8s.io/client-go/discovery", "k8s.io/client-go/discovery/fake", diff --git a/api/openapi-spec/swagger.json b/api/openapi-spec/swagger.json index c5f55f7426aa..0563075e8669 100644 --- a/api/openapi-spec/swagger.json +++ b/api/openapi-spec/swagger.json @@ -541,6 +541,14 @@ } } }, + "io.argoproj.workflow.v1alpha1.PodGC": { + "description": "PodGC describes how to delete completed pods as they complete", + "properties": { + "strategy": { + "type": "string" + } + } + }, "io.argoproj.workflow.v1alpha1.RawArtifact": { "description": "RawArtifact allows raw string content to be placed as an artifact in a container", "required": [ @@ -1265,6 +1273,10 @@ "type": "integer", "format": "int64" }, + "podGC": { + "description": "PodGC describes the strategy to use when to deleting completed pods", + "$ref": "#/definitions/io.argoproj.workflow.v1alpha1.PodGC" + }, "podPriority": { "description": "Priority to apply to workflow pods.", "type": "integer", diff --git a/examples/pod-gc-strategy.yaml b/examples/pod-gc-strategy.yaml new file mode 100644 index 000000000000..de20cbd2668f --- /dev/null +++ b/examples/pod-gc-strategy.yaml @@ -0,0 +1,35 @@ +# pod gc provides the ability to delete pods automatically without deleting the workflow +apiVersion: argoproj.io/v1alpha1 +kind: Workflow +metadata: + generateName: pod-gc-strategy- +spec: + entrypoint: pod-gc-strategy + + podGC: + # pod gc strategy must be one of the following + # * OnPodCompletion - delete pods immediately when pod is completed (including errors/failures) + # * OnPodSuccess - delete pods immediately when pod is successful + # * OnWorkflowCompletion - delete pods when workflow is completed + # * OnWorkflowSuccess - delete pods when workflow is successful + strategy: OnPodSuccess + + templates: + - name: pod-gc-strategy + steps: + - - name: fail + template: fail + - name: succeed + template: succeed + + - name: fail + container: + image: alpine:3.7 + command: [sh, -c] + args: ["exit 1"] + + - name: succeed + container: + image: alpine:3.7 + command: [sh, -c] + args: ["exit 0"] diff --git a/pkg/apis/workflow/v1alpha1/openapi_generated.go b/pkg/apis/workflow/v1alpha1/openapi_generated.go index 6a3b17882bd1..5a0de628b3c4 100644 --- a/pkg/apis/workflow/v1alpha1/openapi_generated.go +++ b/pkg/apis/workflow/v1alpha1/openapi_generated.go @@ -34,6 +34,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1.NoneStrategy": schema_pkg_apis_workflow_v1alpha1_NoneStrategy(ref), "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1.Outputs": schema_pkg_apis_workflow_v1alpha1_Outputs(ref), "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1.Parameter": schema_pkg_apis_workflow_v1alpha1_Parameter(ref), + "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1.PodGC": schema_pkg_apis_workflow_v1alpha1_PodGC(ref), "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1.RawArtifact": schema_pkg_apis_workflow_v1alpha1_RawArtifact(ref), "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1.ResourceTemplate": schema_pkg_apis_workflow_v1alpha1_ResourceTemplate(ref), "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1.RetryStrategy": schema_pkg_apis_workflow_v1alpha1_RetryStrategy(ref), @@ -1041,6 +1042,25 @@ func schema_pkg_apis_workflow_v1alpha1_Parameter(ref common.ReferenceCallback) c } } +func schema_pkg_apis_workflow_v1alpha1_PodGC(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "PodGC describes how to delete completed pods as they complete", + Properties: map[string]spec.Schema{ + "strategy": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + Dependencies: []string{}, + } +} + func schema_pkg_apis_workflow_v1alpha1_RawArtifact(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -2396,6 +2416,12 @@ func schema_pkg_apis_workflow_v1alpha1_WorkflowSpec(ref common.ReferenceCallback Format: "", }, }, + "podGC": { + SchemaProps: spec.SchemaProps{ + Description: "PodGC describes the strategy to use when to deleting completed pods", + Ref: ref("github.com/argoproj/argo/pkg/apis/workflow/v1alpha1.PodGC"), + }, + }, "podPriorityClassName": { SchemaProps: spec.SchemaProps{ Description: "PriorityClassName to apply to workflow pods.", @@ -2434,7 +2460,7 @@ func schema_pkg_apis_workflow_v1alpha1_WorkflowSpec(ref common.ReferenceCallback }, }, Dependencies: []string{ - "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1.Arguments", "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1.ArtifactRepositoryRef", "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1.Template", "k8s.io/api/core/v1.Affinity", "k8s.io/api/core/v1.HostAlias", "k8s.io/api/core/v1.LocalObjectReference", "k8s.io/api/core/v1.PersistentVolumeClaim", "k8s.io/api/core/v1.PodDNSConfig", "k8s.io/api/core/v1.PodSecurityContext", "k8s.io/api/core/v1.Toleration", "k8s.io/api/core/v1.Volume"}, + "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1.Arguments", "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1.ArtifactRepositoryRef", "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1.PodGC", "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1.Template", "k8s.io/api/core/v1.Affinity", "k8s.io/api/core/v1.HostAlias", "k8s.io/api/core/v1.LocalObjectReference", "k8s.io/api/core/v1.PersistentVolumeClaim", "k8s.io/api/core/v1.PodDNSConfig", "k8s.io/api/core/v1.PodSecurityContext", "k8s.io/api/core/v1.Toleration", "k8s.io/api/core/v1.Volume"}, } } diff --git a/pkg/apis/workflow/v1alpha1/workflow_types.go b/pkg/apis/workflow/v1alpha1/workflow_types.go index 8c0980fd5faf..e92b1fffbcf4 100644 --- a/pkg/apis/workflow/v1alpha1/workflow_types.go +++ b/pkg/apis/workflow/v1alpha1/workflow_types.go @@ -52,6 +52,17 @@ const ( NodeTypeSuspend NodeType = "Suspend" ) +// PodGCStrategy is the strategy when to delete completed pods for GC. +type PodGCStrategy string + +// PodGCStrategy +const ( + PodGCOnPodCompletion PodGCStrategy = "OnPodCompletion" + PodGCOnPodSuccess PodGCStrategy = "OnPodSuccess" + PodGCOnWorkflowCompletion PodGCStrategy = "OnWorkflowCompletion" + PodGCOnWorkflowSuccess PodGCStrategy = "OnWorkflowSuccess" +) + // TemplateGetter is an interface to get templates. type TemplateGetter interface { GetNamespace() string @@ -178,6 +189,9 @@ type WorkflowSpec struct { // +optional SchedulerName string `json:"schedulerName,omitempty"` + // PodGC describes the strategy to use when to deleting completed pods + PodGC *PodGC `json:"podGC,omitempty"` + // PriorityClassName to apply to workflow pods. PodPriorityClassName string `json:"podPriorityClassName,omitempty"` @@ -395,6 +409,11 @@ type Artifact struct { Optional bool `json:"optional,omitempty"` } +// PodGC describes how to delete completed pods as they complete +type PodGC struct { + Strategy PodGCStrategy `json:"strategy,omitempty"` +} + // ArchiveStrategy describes how to archive files/directory when saving artifacts type ArchiveStrategy struct { Tar *TarStrategy `json:"tar,omitempty"` @@ -716,6 +735,11 @@ func (ws *WorkflowStatus) Completed() bool { return isCompletedPhase(ws.Phase) } +// Successful return whether or not the workflow has succeeded +func (ws *WorkflowStatus) Successful() bool { + return ws.Phase == NodeSucceeded +} + // Remove returns whether or not the node has completed execution func (n NodeStatus) Completed() bool { return isCompletedPhase(n.Phase) || n.IsDaemoned() && n.Phase != NodePending diff --git a/pkg/apis/workflow/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/workflow/v1alpha1/zz_generated.deepcopy.go index 17762ffd2863..81a01da08f82 100644 --- a/pkg/apis/workflow/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/workflow/v1alpha1/zz_generated.deepcopy.go @@ -617,6 +617,22 @@ func (in *Parameter) DeepCopy() *Parameter { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PodGC) DeepCopyInto(out *PodGC) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PodGC. +func (in *PodGC) DeepCopy() *PodGC { + if in == nil { + return nil + } + out := new(PodGC) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RawArtifact) DeepCopyInto(out *RawArtifact) { *out = *in @@ -1129,6 +1145,11 @@ func (in *WorkflowSpec) DeepCopyInto(out *WorkflowSpec) { *out = new(int32) **out = **in } + if in.PodGC != nil { + in, out := &in.PodGC, &out.PodGC + *out = new(PodGC) + **out = **in + } if in.PodPriority != nil { in, out := &in.PodPriority, &out.PodPriority *out = new(int32) diff --git a/workflow/common/util.go b/workflow/common/util.go index 1be0d6cb7bc0..dd008e959829 100644 --- a/workflow/common/util.go +++ b/workflow/common/util.go @@ -19,6 +19,7 @@ import ( "github.com/valyala/fasttemplate" apiv1 "k8s.io/api/core/v1" apierr "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" @@ -444,6 +445,21 @@ func addPodMetadata(c kubernetes.Interface, field, podName, namespace, key, valu return err } +const deleteRetries = 3 + +// DeletePod deletes a pod. Ignores NotFound error +func DeletePod(c kubernetes.Interface, podName, namespace string) error { + var err error + for attempt := 0; attempt < deleteRetries; attempt++ { + err = c.CoreV1().Pods(namespace).Delete(podName, &metav1.DeleteOptions{}) + if err == nil || apierr.IsNotFound(err) { + break + } + time.Sleep(100 * time.Millisecond) + } + return err +} + // IsPodTemplate returns whether the template corresponds to a pod func IsPodTemplate(tmpl *wfv1.Template) bool { if tmpl.Container != nil || tmpl.Script != nil || tmpl.Resource != nil { diff --git a/workflow/controller/controller.go b/workflow/controller/controller.go index e34a0bf878da..2f4fca6195db 100644 --- a/workflow/controller/controller.go +++ b/workflow/controller/controller.go @@ -6,6 +6,7 @@ import ( "strings" "time" + wfv1 "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1" log "github.com/sirupsen/logrus" apiv1 "k8s.io/api/core/v1" apierr "k8s.io/apimachinery/pkg/api/errors" @@ -58,6 +59,7 @@ type WorkflowController struct { wfQueue workqueue.RateLimitingInterface podQueue workqueue.RateLimitingInterface completedPods chan string + gcPods chan string // pods to be deleted depend on GC strategy throttler Throttler wfDBctx sqldb.DBRepository } @@ -89,6 +91,7 @@ func NewWorkflowController( wfQueue: workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()), podQueue: workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()), completedPods: make(chan string, 512), + gcPods: make(chan string, 512), } wfc.throttler = NewThrottler(0, wfc.wfQueue) return &wfc @@ -148,6 +151,7 @@ func (wfc *WorkflowController) Run(ctx context.Context, wfWorkers, podWorkers in go wfc.wfInformer.Run(ctx.Done()) go wfc.podInformer.Run(ctx.Done()) go wfc.podLabeler(ctx.Done()) + go wfc.podGarbageCollector(ctx.Done()) // Wait for all involved caches to be synced, before processing items from the queue is started for _, informer := range []cache.SharedIndexInformer{wfc.wfInformer, wfc.podInformer} { @@ -192,6 +196,30 @@ func (wfc *WorkflowController) podLabeler(stopCh <-chan struct{}) { } } +// podGarbageCollector will delete all pods on the controllers gcPods channel as completed +func (wfc *WorkflowController) podGarbageCollector(stopCh <-chan struct{}) { + for { + select { + case <-stopCh: + return + case pod := <-wfc.gcPods: + parts := strings.Split(pod, "/") + if len(parts) != 2 { + log.Warnf("Unexpected item on gcPods channel: %s", pod) + continue + } + namespace := parts[0] + podName := parts[1] + err := common.DeletePod(wfc.kubeclientset, podName, namespace) + if err != nil { + log.Errorf("Failed to delete pod %s/%s for gc: %+v", namespace, podName, err) + } else { + log.Infof("Delete pod %s/%s for gc successfully", namespace, podName) + } + } + } +} + func (wfc *WorkflowController) runWorker() { for wfc.processNextItem() { } @@ -270,6 +298,24 @@ func (wfc *WorkflowController) processNextItem() bool { woc.operate() if woc.wf.Status.Completed() { wfc.throttler.Remove(key) + // Send all completed pods to gcPods channel to delete it later depend on the PodGCStrategy. + var doPodGC bool + if woc.wf.Spec.PodGC != nil { + switch woc.wf.Spec.PodGC.Strategy { + case wfv1.PodGCOnWorkflowCompletion: + doPodGC = true + case wfv1.PodGCOnWorkflowSuccess: + if woc.wf.Status.Successful() { + doPodGC = true + } + } + } + if doPodGC { + for podName := range woc.completedPods { + pod := fmt.Sprintf("%s/%s", woc.wf.ObjectMeta.Namespace, podName) + woc.controller.gcPods <- pod + } + } } // TODO: operate should return error if it was unable to operate properly diff --git a/workflow/controller/operator.go b/workflow/controller/operator.go index fee6db288ae3..2e51d5225904 100644 --- a/workflow/controller/operator.go +++ b/workflow/controller/operator.go @@ -62,6 +62,8 @@ type wfOperationCtx struct { artifactRepository *config.ArtifactRepository // map of pods which need to be labeled with completed=true completedPods map[string]bool + // map of pods which is identified as succeeded=true + succeededPods map[string]bool // deadline is the dealine time in which this operation should relinquish // its hold on the workflow so that an operation does not run for too long // and starve other workqueue items. It also enables workflow progress to @@ -110,6 +112,7 @@ func newWorkflowOperationCtx(wf *wfv1.Workflow, wfc *WorkflowController) *wfOper volumes: wf.Spec.DeepCopy().Volumes, artifactRepository: &wfc.Config.ArtifactRepository, completedPods: make(map[string]bool), + succeededPods: make(map[string]bool), deadline: time.Now().UTC().Add(maxOperationTime), tmplCtx: templateresolution.NewContext(wfc.wfclientset, wf.Namespace, wf), } @@ -395,8 +398,26 @@ func (woc *wfOperationCtx) persistUpdates() { // It is important that we *never* label pods as completed until we successfully updated the workflow // Failing to do so means we can have inconsistent state. - for podName := range woc.completedPods { - woc.controller.completedPods <- fmt.Sprintf("%s/%s", woc.wf.ObjectMeta.Namespace, podName) + // TODO: The completedPods will be labeled multiple times. I think it would be improved in the future. + // Send succeeded pods or completed pods to gcPods channel to delete it later depend on the PodGCStrategy. + // Notice we do not need to label the pod if we will delete it later for GC. Otherwise, that may even result in + // errors if we label a pod that was deleted already. + if woc.wf.Spec.PodGC != nil { + switch woc.wf.Spec.PodGC.Strategy { + case wfv1.PodGCOnPodSuccess: + for podName := range woc.succeededPods { + woc.controller.gcPods <- fmt.Sprintf("%s/%s", woc.wf.ObjectMeta.Namespace, podName) + } + case wfv1.PodGCOnPodCompletion: + for podName := range woc.completedPods { + woc.controller.gcPods <- fmt.Sprintf("%s/%s", woc.wf.ObjectMeta.Namespace, podName) + } + } + } else { + // label pods which will not be deleted + for podName := range woc.completedPods { + woc.controller.completedPods <- fmt.Sprintf("%s/%s", woc.wf.ObjectMeta.Namespace, podName) + } } } @@ -552,6 +573,9 @@ func (woc *wfOperationCtx) podReconciliation() error { } woc.completedPods[pod.ObjectMeta.Name] = true } + if node.Successful() { + woc.succeededPods[pod.ObjectMeta.Name] = true + } } } diff --git a/workflow/validate/validate.go b/workflow/validate/validate.go index 7929909d9e48..b58a1c0abfd7 100644 --- a/workflow/validate/validate.go +++ b/workflow/validate/validate.go @@ -124,16 +124,25 @@ func ValidateWorkflow(wfClientset wfclientset.Interface, namespace string, wf *w } _, err = ctx.validateTemplateHolder(&wfv1.Template{Template: wf.Spec.Entrypoint}, tmplCtx, &wf.Spec.Arguments, map[string]interface{}{}) if err != nil { - return errors.Errorf(errors.CodeBadRequest, "spec.entrypoint %s", err.Error()) + return err } if wf.Spec.OnExit != "" { // now when validating onExit, {{workflow.status}} is now available as a global ctx.globalParams[common.GlobalVarWorkflowStatus] = placeholderValue _, err = ctx.validateTemplateHolder(&wfv1.Template{Template: wf.Spec.OnExit}, tmplCtx, &wf.Spec.Arguments, map[string]interface{}{}) if err != nil { - return errors.Errorf(errors.CodeBadRequest, "spec.onExit %s", err.Error()) + return err } } + + if wf.Spec.PodGC != nil { + switch wf.Spec.PodGC.Strategy { + case wfv1.PodGCOnPodCompletion, wfv1.PodGCOnPodSuccess, wfv1.PodGCOnWorkflowCompletion, wfv1.PodGCOnWorkflowSuccess: + default: + return errors.Errorf(errors.CodeBadRequest, "podGC.strategy unknown strategy '%s'", wf.Spec.PodGC.Strategy) + } + } + // Check if all templates can be resolved. for _, template := range wf.Spec.Templates { _, err := ctx.validateTemplateHolder(&wfv1.Template{Template: template.Name}, tmplCtx, &FakeArguments{}, map[string]interface{}{}) @@ -443,7 +452,7 @@ func validateLeaf(scope map[string]interface{}, tmpl *wfv1.Template) error { case "get", "create", "apply", "delete", "replace", "patch": // OK default: - return errors.Errorf(errors.CodeBadRequest, "templates.%s.resource.action must be either get, create, apply, delete or replace", tmpl.Name) + return errors.Errorf(errors.CodeBadRequest, "templates.%s.resource.action must be one of: get, create, apply, delete, replace, patch", tmpl.Name) } // Try to unmarshal the given manifest. obj := unstructured.Unstructured{} diff --git a/workflow/validate/validate_test.go b/workflow/validate/validate_test.go index 9a44cc033e58..92673071a04d 100644 --- a/workflow/validate/validate_test.go +++ b/workflow/validate/validate_test.go @@ -1651,6 +1651,7 @@ spec: templates: - name: whalesay resource: + action: apply manifest: | invalid-yaml-line kind: ConfigMap @@ -1670,19 +1671,49 @@ spec: resource: action: foo manifest: | - apiVersion: v1 - kind: ConfigMap - metadata: - name: whalesay-cm + apiVersion: v1 + kind: ConfigMap + metadata: + name: whalesay-cm ` // TestInvalidResourceWorkflow verifies an error against a workflow of an invalid resource. func TestInvalidResourceWorkflow(t *testing.T) { wf := unmarshalWf(invalidResourceWorkflow) err := ValidateWorkflow(wfClientset, metav1.NamespaceDefault, wf, ValidateOpts{}) - assert.Error(t, err, "templates.whalesay.resource.manifest must be a valid yaml") + assert.EqualError(t, err, "templates.whalesay.resource.manifest must be a valid yaml") wf = unmarshalWf(invalidActionResourceWorkflow) err = ValidateWorkflow(wfClientset, metav1.NamespaceDefault, wf, ValidateOpts{}) - assert.Error(t, err, "templates.whalesay.resource.action must be either get, create, apply, delete or replace") + assert.EqualError(t, err, "templates.whalesay.resource.action must be one of: get, create, apply, delete, replace, patch") +} + +var invalidPodGC = ` +apiVersion: argoproj.io/v1alpha1 +kind: Workflow +metadata: + generateName: pod-gc-strategy-unknown- +spec: + podGC: + strategy: Foo + entrypoint: whalesay + templates: + - name: whalesay + container: + image: docker/whalesay:latest + command: [cowsay] + args: ["hello world"] +` + +// TestUnknownPodGCStrategy verifies pod gc strategy is correct. +func TestUnknownPodGCStrategy(t *testing.T) { + wf := unmarshalWf(invalidPodGC) + err := ValidateWorkflow(wfClientset, metav1.NamespaceDefault, wf, ValidateOpts{}) + assert.EqualError(t, err, "podGC.strategy unknown strategy 'Foo'") + + for _, strat := range []wfv1.PodGCStrategy{wfv1.PodGCOnPodCompletion, wfv1.PodGCOnPodSuccess, wfv1.PodGCOnWorkflowCompletion, wfv1.PodGCOnWorkflowSuccess} { + wf.Spec.PodGC.Strategy = strat + err = ValidateWorkflow(wfClientset, metav1.NamespaceDefault, wf, ValidateOpts{}) + assert.NoError(t, err) + } }