diff --git a/.github/workflows/ci-build.yaml b/.github/workflows/ci-build.yaml index 5687969a93d8..688bbe3f84c7 100644 --- a/.github/workflows/ci-build.yaml +++ b/.github/workflows/ci-build.yaml @@ -196,33 +196,46 @@ jobs: include: - test: test-executor profile: minimal + useApi: false - test: test-corefunctional profile: minimal + useApi: false - test: test-functional profile: minimal + useApi: false - test: test-api profile: mysql + useApi: true - test: test-cli profile: mysql + useApi: true - test: test-cron profile: minimal + useApi: false - test: test-examples profile: minimal + useApi: false - test: test-plugins profile: plugins + useApi: false - test: test-java-sdk profile: minimal + useApi: true - test: test-python-sdk profile: minimal + useApi: true - test: test-executor install_k3s_version: v1.28.11+k3s1 profile: minimal + useApi: false - test: test-corefunctional install_k3s_version: v1.28.11+k3s1 profile: minimal + useApi: false - test: test-functional install_k3s_version: v1.28.11+k3s1 profile: minimal + useApi: false steps: - name: Install socat (needed by Kubernetes) run: sudo apt-get -y install socat @@ -281,17 +294,17 @@ jobs: run: make controller kit STATIC_FILES=false - name: Build CLI run: make cli STATIC_FILES=false - if: ${{matrix.test == 'test-api' || matrix.test == 'test-cli' || matrix.test == 'test-java-sdk' || matrix.test == 'test-python-sdk'}} + if: ${{matrix.useApi}} - name: Start controller/API run: | make start PROFILE=${{matrix.profile}} \ AUTH_MODE=client STATIC_FILES=false \ LOG_LEVEL=info \ - API=${{matrix.test == 'test-api' || matrix.test == 'test-cli' || matrix.test == 'test-java-sdk' || matrix.test == 'test-python-sdk'}} \ + API=${{matrix.useApi}} \ UI=false \ POD_STATUS_CAPTURE_FINALIZER=true > /tmp/argo.log 2>&1 & - name: Wait for controller to be up - run: make wait API=${{matrix.test == 'test-api' || matrix.test == 'test-cli' || matrix.test == 'test-java-sdk' || matrix.test == 'test-python-sdk'}} + run: make wait API=${{matrix.useApi}} timeout-minutes: 5 - name: Run tests ${{matrix.test}} run: make ${{matrix.test}} E2E_SUITE_TIMEOUT=20m STATIC_FILES=false diff --git a/.spelling b/.spelling index f9c44d655bcf..8ac37516c84f 100644 --- a/.spelling +++ b/.spelling @@ -185,6 +185,7 @@ memoizing metadata minikube mutex +mutexes namespace namespaces natively @@ -205,6 +206,7 @@ sandboxed shortcodes stateful stderr +symlinks temporality triaged un-reconciled diff --git a/Dockerfile.windows b/Dockerfile.windows index e5711efb73b4..bf7efeef0d58 100644 --- a/Dockerfile.windows +++ b/Dockerfile.windows @@ -50,7 +50,7 @@ ARG GIT_TREE_STATE WORKDIR C:/Users/ContainerAdministrator/go/src/github.com/argoproj/argo-workflows COPY . . # run in git bash for all the shell commands in Makefile to work -RUN bash -c 'make dist/argoexec GIT_COMMIT=${GIT_COMMIT} GIT_TAG=${GIT_TAG} GIT_TREE_STATE=${GIT_TREE_STATE}' +RUN bash -c 'make dist/argoexec GIT_COMMIT=${GIT_COMMIT} GIT_TAG=${GIT_TAG} GIT_TREE_STATE=${GIT_TREE_STATE} HACK_PKG_FILES_AS_PKGS=true' #################################################################################################### # argoexec diff --git a/Makefile b/Makefile index f9ae511426d3..e2ac5268bbeb 100644 --- a/Makefile +++ b/Makefile @@ -107,9 +107,19 @@ ifndef $(GOPATH) endif # -- file lists -ARGOEXEC_PKG_FILES := $(shell go list -f '{{ join .Deps "\n" }}' ./cmd/argoexec/ | grep 'argoproj/argo-workflows/v3/' | xargs go list -f '{{ range $$file := .GoFiles }}{{ print $$.ImportPath "/" $$file "\n" }}{{ end }}' | cut -c 39-) -CLI_PKG_FILES := $(shell go list -f '{{ join .Deps "\n" }}' ./cmd/argo/ | grep 'argoproj/argo-workflows/v3/' | xargs go list -f '{{ range $$file := .GoFiles }}{{ print $$.ImportPath "/" $$file "\n" }}{{ end }}' | cut -c 39-) -CONTROLLER_PKG_FILES := $(shell go list -f '{{ join .Deps "\n" }}' ./cmd/workflow-controller/ | grep 'argoproj/argo-workflows/v3/' | xargs go list -f '{{ range $$file := .GoFiles }}{{ print $$.ImportPath "/" $$file "\n" }}{{ end }}' | cut -c 39-) +HACK_PKG_FILES_AS_PKGS ?= false +ifeq ($(HACK_PKG_FILES_AS_PKGS),false) + ARGOEXEC_PKG_FILES := $(shell go list -f '{{ join .Deps "\n" }}' ./cmd/argoexec/ | grep 'argoproj/argo-workflows/v3/' | xargs go list -f '{{ range $$file := .GoFiles }}{{ print $$.ImportPath "/" $$file "\n" }}{{ end }}' | cut -c 39-) + CLI_PKG_FILES := $(shell go list -f '{{ join .Deps "\n" }}' ./cmd/argo/ | grep 'argoproj/argo-workflows/v3/' | xargs go list -f '{{ range $$file := .GoFiles }}{{ print $$.ImportPath "/" $$file "\n" }}{{ end }}' | cut -c 39-) + CONTROLLER_PKG_FILES := $(shell go list -f '{{ join .Deps "\n" }}' ./cmd/workflow-controller/ | grep 'argoproj/argo-workflows/v3/' | xargs go list -f '{{ range $$file := .GoFiles }}{{ print $$.ImportPath "/" $$file "\n" }}{{ end }}' | cut -c 39-) +else +# Building argoexec on windows cannot rebuild the openapi, we need to fall back to the old +# behaviour where we fake dependencies and therefore don't rebuild + ARGOEXEC_PKG_FILES := $(shell echo cmd/argoexec && go list -f '{{ join .Deps "\n" }}' ./cmd/argoexec/ | grep 'argoproj/argo-workflows/v3/' | cut -c 39-) + CLI_PKG_FILES := $(shell echo cmd/argo && go list -f '{{ join .Deps "\n" }}' ./cmd/argo/ | grep 'argoproj/argo-workflows/v3/' | cut -c 39-) + CONTROLLER_PKG_FILES := $(shell echo cmd/workflow-controller && go list -f '{{ join .Deps "\n" }}' ./cmd/workflow-controller/ | grep 'argoproj/argo-workflows/v3/' | cut -c 39-) +endif + TYPES := $(shell find pkg/apis/workflow/v1alpha1 -type f -name '*.go' -not -name openapi_generated.go -not -name '*generated*' -not -name '*test.go') CRDS := $(shell find manifests/base/crds -type f -name 'argoproj.io_*.yaml') SWAGGER_FILES := pkg/apiclient/_.primary.swagger.json \ diff --git a/api/jsonschema/schema.json b/api/jsonschema/schema.json index 54b359072bea..a3e4fbe234a1 100644 --- a/api/jsonschema/schema.json +++ b/api/jsonschema/schema.json @@ -6229,8 +6229,8 @@ "description": "PodGC describes how to delete completed pods as they complete", "properties": { "deleteDelayDuration": { - "$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.Duration", - "description": "DeleteDelayDuration specifies the duration before pods in the GC queue get deleted." + "description": "DeleteDelayDuration specifies the duration before pods in the GC queue get deleted.", + "type": "string" }, "labelSelector": { "$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.LabelSelector", @@ -10847,15 +10847,6 @@ }, "type": "object" }, - "io.k8s.apimachinery.pkg.apis.meta.v1.Duration": { - "description": "Duration is a wrapper around time.Duration which supports correct\nmarshaling to YAML and JSON. In particular, it marshals into strings, which\ncan be used as map keys in json.", - "properties": { - "duration": { - "type": "string" - } - }, - "type": "object" - }, "io.k8s.apimachinery.pkg.apis.meta.v1.FieldsV1": { "description": "FieldsV1 stores a set of fields in a data structure like a Trie, in JSON format.\n\nEach key is either a '.' representing the field itself, and will always map to an empty set, or a string representing a sub-field or item. The string will follow one of these four formats: 'f:\u003cname\u003e', where \u003cname\u003e is the name of a field in a struct, or key in a map 'v:\u003cvalue\u003e', where \u003cvalue\u003e is the exact json formatted value of a list item 'i:\u003cindex\u003e', where \u003cindex\u003e is position of a item in a list 'k:\u003ckeys\u003e', where \u003ckeys\u003e is a map of a list item's key fields to their unique values If a key maps to an empty Fields value, the field that key represents is part of the set.\n\nThe exact format is defined in sigs.k8s.io/structured-merge-diff", "type": "object" diff --git a/api/openapi-spec/swagger.json b/api/openapi-spec/swagger.json index e34099acc667..d2b65f525926 100644 --- a/api/openapi-spec/swagger.json +++ b/api/openapi-spec/swagger.json @@ -10171,7 +10171,7 @@ "properties": { "deleteDelayDuration": { "description": "DeleteDelayDuration specifies the duration before pods in the GC queue get deleted.", - "$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.Duration" + "type": "string" }, "labelSelector": { "description": "LabelSelector is the label selector to check if the pods match the labels before being added to the pod GC queue.", @@ -14760,15 +14760,6 @@ } } }, - "io.k8s.apimachinery.pkg.apis.meta.v1.Duration": { - "description": "Duration is a wrapper around time.Duration which supports correct\nmarshaling to YAML and JSON. In particular, it marshals into strings, which\ncan be used as map keys in json.", - "type": "object", - "properties": { - "duration": { - "type": "string" - } - } - }, "io.k8s.apimachinery.pkg.apis.meta.v1.FieldsV1": { "description": "FieldsV1 stores a set of fields in a data structure like a Trie, in JSON format.\n\nEach key is either a '.' representing the field itself, and will always map to an empty set, or a string representing a sub-field or item. The string will follow one of these four formats: 'f:\u003cname\u003e', where \u003cname\u003e is the name of a field in a struct, or key in a map 'v:\u003cvalue\u003e', where \u003cvalue\u003e is the exact json formatted value of a list item 'i:\u003cindex\u003e', where \u003cindex\u003e is position of a item in a list 'k:\u003ckeys\u003e', where \u003ckeys\u003e is a map of a list item's key fields to their unique values If a key maps to an empty Fields value, the field that key represents is part of the set.\n\nThe exact format is defined in sigs.k8s.io/structured-merge-diff", "type": "object" diff --git a/docs/environment-variables.md b/docs/environment-variables.md index 32064c4cccf0..66f0145724e5 100644 --- a/docs/environment-variables.md +++ b/docs/environment-variables.md @@ -29,7 +29,6 @@ This document outlines environment variables that can be used to customize behav | `CRON_SYNC_PERIOD` | `time.Duration` | `10s` | How often to sync cron workflows. | | `DEFAULT_REQUEUE_TIME` | `time.Duration` | `10s` | The re-queue time for the rate limiter of the workflow queue. | | `DISABLE_MAX_RECURSION` | `bool` | `false` | Set to true to disable the recursion preventer, which will stop a workflow running which has called into a child template 100 times | -| `POD_ABSENT_TIMEOUT` | `time.Duration` | `2m` | The time used to determine if pod absence should imply node failure | | `EXPRESSION_TEMPLATES` | `bool` | `true` | Escape hatch to disable expression templates. | | `EVENT_AGGREGATION_WITH_ANNOTATIONS` | `bool` | `false` | Whether event annotations will be used when aggregating events. | | `GZIP_IMPLEMENTATION` | `string` | `PGZip` | The implementation of compression/decompression. Currently only "`PGZip`" and "`GZip`" are supported. | @@ -46,6 +45,7 @@ This document outlines environment variables that can be used to customize behav | `OPERATION_DURATION_METRIC_BUCKET_COUNT` | `int` | `6` | The number of buckets to collect the metric for the operation duration. | | `POD_NAMES` | `string` | `v2` | Whether to have pod names contain the template name (v2) or be the node id (v1) - should be set the same for Argo Server. | | `RECENTLY_STARTED_POD_DURATION` | `time.Duration` | `10s` | The duration of a pod before the pod is considered to be recently started. | +| `RECENTLY_DELETED_POD_DURATION` | `time.Duration` | `10s` | The duration of a pod before the pod is considered to be recently deleted. | | `RETRY_BACKOFF_DURATION` | `time.Duration` | `10ms` | The retry back-off duration when retrying API calls. | | `RETRY_BACKOFF_FACTOR` | `float` | `2.0` | The retry back-off factor when retrying API calls. | | `RETRY_BACKOFF_STEPS` | `int` | `5` | The retry back-off steps when retrying API calls. | diff --git a/docs/fields.md b/docs/fields.md index 620316c7f1cf..b6bce771b4b5 100644 --- a/docs/fields.md +++ b/docs/fields.md @@ -1564,7 +1564,7 @@ PodGC describes how to delete completed pods as they complete ### Fields | Field Name | Field Type | Description | |:----------:|:----------:|---------------| -|`deleteDelayDuration`|[`Duration`](#duration)|DeleteDelayDuration specifies the duration before pods in the GC queue get deleted.| +|`deleteDelayDuration`|`string`|DeleteDelayDuration specifies the duration before pods in the GC queue get deleted.| |`labelSelector`|[`LabelSelector`](#labelselector)|LabelSelector is the label selector to check if the pods match the labels before being added to the pod GC queue.| |`strategy`|`string`|Strategy is the strategy to use. One of "OnPodCompletion", "OnPodSuccess", "OnWorkflowCompletion", "OnWorkflowSuccess". If unset, does not delete Pods| @@ -5147,23 +5147,6 @@ ObjectReference contains enough information to let you inspect or modify the ref |`resourceVersion`|`string`|Specific resourceVersion to which this reference is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency| |`uid`|`string`|UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids| -## Duration - -Duration is a wrapper around time.Duration which supports correct marshaling to YAML and JSON. In particular, it marshals into strings, which can be used as map keys in json. - -
-Examples with this field (click to open) - -- [`pod-gc-strategy-with-label-selector.yaml`](https://github.com/argoproj/argo-workflows/blob/main/examples/pod-gc-strategy-with-label-selector.yaml) - -- [`pod-gc-strategy.yaml`](https://github.com/argoproj/argo-workflows/blob/main/examples/pod-gc-strategy.yaml) -
- -### Fields -| Field Name | Field Type | Description | -|:----------:|:----------:|---------------| -|`duration`|`string`|_No description available_| - ## LabelSelector A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects. diff --git a/docs/metrics.md b/docs/metrics.md index d136d2c06f89..984dd2c3bb57 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -336,10 +336,11 @@ The rate of this shows how busy that area of the controller is. Queues: -- `workflow_queue`: the queue of Workflow updates from the cluster - `cron_wf_queue`: the queue of CronWorkflow updates from the cluster -- `workflow_ttl_queue`: workflows which are queued for deletion due to age - `pod_cleanup_queue`: pods which are queued for deletion +- `workflow_queue`: the queue of Workflow updates from the cluster +- `workflow_ttl_queue`: workflows which are queued for deletion due to age +- `workflow_archive_queue`: workflows which are queued for archiving This and associated metrics are all directly sourced from the [client-go workqueue metrics](https://godocs.io/k8s.io/client-go/util/workqueue) diff --git a/docs/parallelism.md b/docs/parallelism.md new file mode 100644 index 000000000000..bae14e3c45fe --- /dev/null +++ b/docs/parallelism.md @@ -0,0 +1,43 @@ +# Limiting parallelism + +You can restrict the number of parallel workflow executions. + +## Controller-level + +You can limit the total number of parallel workflow executions in the [workflow controller ConfigMap](workflow-controller-configmap.yaml): + +```yaml +data: + parallelism: "10" +``` + +You can also limit the total number of parallel workflow executions in a single namespace: + +```yaml +data: + namespaceParallelism: "4" +``` + +!!! Note + Workflows that are executing but restricted from running more nodes due to other mechanisms will still count toward parallelism limits. + +### Priority + +You can set a `priority` on workflows: + +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: Workflow +metadata: + generateName: priority- +spec: + priority: 3 + # ... +``` + +Workflows that have not started due to Controller-level parallelism will be queued: workflows with higher priority numbers will start before lower priority ones. +The default is `priority: 0`. + +## Synchronization + +You can also use [mutexes, semaphores, and parallelism](synchronization.md) to control the parallel execution of workflows and templates. diff --git a/docs/running-locally.md b/docs/running-locally.md index ee876b7b2078..4d269818e9cd 100644 --- a/docs/running-locally.md +++ b/docs/running-locally.md @@ -108,6 +108,10 @@ You can use the `TARGET_PLATFORM` environment variable to compile images for spe make argoexec-image TARGET_PLATFORM=linux/arm64,linux/amd64 ``` +!!! Note "Error `expected 'package', found signal_darwin`" + You may see this error if symlinks are not configured for your `git` installation. + Run `git config core.symlinks true` to correct this. + To also start the API on : ```bash diff --git a/docs/synchronization.md b/docs/synchronization.md index 74776d5c800e..480952cb0221 100644 --- a/docs/synchronization.md +++ b/docs/synchronization.md @@ -2,15 +2,15 @@ > v2.10 and after -## Introduction +You can limit the parallel execution of workflows or templates: -Synchronization enables users to limit the parallel execution of certain workflows or -templates within a workflow without having to restrict others. +- You can use mutexes to restrict workflows or templates to only having a single concurrent execution. +- You can use semaphores to restrict workflows or templates to a configured number of parallel executions. +- You can use parallelism to restrict concurrent tasks or steps within a single workflow. -Users can create multiple synchronization configurations in the `ConfigMap` that can be referred to -from a workflow or template within a workflow. Alternatively, users can -configure a mutex to prevent concurrent execution of templates or -workflows using the same mutex. +The term "locks" on this page means mutexes and semaphores. + +You can create multiple semaphore configurations in a `ConfigMap` that can be referred to from a workflow or template. For example: @@ -24,11 +24,15 @@ data: template: "2" # Two instances of template can run at a given time in particular namespace ``` -### Workflow-level Synchronization +!!! Warning + Each synchronization block may only refer to either a semaphore or a mutex. + If you specify both only the semaphore will be locked. + +## Workflow-level Synchronization -Workflow-level synchronization limits parallel execution of the workflow if workflows have the same synchronization reference. -In this example, Workflow refers to `workflow` synchronization key which is configured as limit 1, -so only one workflow instance will be executed at given time even multiple workflows created. +You can limit parallel execution of workflows by using the same synchronization reference. + +In this example the synchronization key `workflow` is configured as limit `"1"`, so only one workflow instance will execute at a time even if multiple workflows are created. Using a semaphore configured by a `ConfigMap`: @@ -52,7 +56,7 @@ spec: args: ["hello world"] ``` -Using a mutex: +Using a mutex is equivalent to a limit `"1"` semaphore: ```yaml apiVersion: argoproj.io/v1alpha1 @@ -72,11 +76,12 @@ spec: args: ["hello world"] ``` -### Template-level Synchronization +## Template-level Synchronization + +You can limit parallel execution of templates by using the same synchronization reference. -Template-level synchronization limits parallel execution of the template across workflows, if templates have the same synchronization reference. -In this example, `acquire-lock` template has synchronization reference of `template` key which is configured as limit 2, -so two instances of templates will be executed at a given time: even multiple steps/tasks within workflow or different workflows referring to the same template. +In this example the synchronization key `template` is configured as limit `"2"`, so a maximum of two instances of the `acquire-lock` template will execute at a time. +This applies even when multiple steps or tasks within a workflow or different workflows refer to the same template. Using a semaphore configured by a `ConfigMap`: @@ -110,7 +115,7 @@ spec: args: ["sleep 10; echo acquired lock"] ``` -Using a mutex: +Using a mutex will limit to a single concurrent execution of the template: ```yaml apiVersion: argoproj.io/v1alpha1 @@ -147,8 +152,29 @@ Examples: 1. [Step level semaphore](https://github.com/argoproj/argo-workflows/blob/main/examples/synchronization-tmpl-level.yaml) 1. [Step level mutex](https://github.com/argoproj/argo-workflows/blob/main/examples/synchronization-mutex-tmpl-level.yaml) -### Other Parallelism support +## Queuing + +When a workflow cannot acquire a lock it will be placed into a ordered queue. + +You can set a [`priority`](parallelism.md#priority) on workflows. +The queue is first ordered by priority: a higher priority number is placed before a lower priority number. +The queue is then ordered by `creationTimestamp`: older workflows are placed before newer workflows. + +Workflows can only acquire a lock if they are at the front of the queue for that lock. + +## Workflow-level parallelism + +You can use `parallelism` within a workflow or template to restrict the total concurrent executions of steps or tasks. +(Note that this only restricts concurrent executions within the same workflow.) + +Examples: + +1. [`parallelism-limit.yaml`](https://github.com/argoproj/argo-workflows/blob/main/examples/parallelism-limit.yaml) restricts the parallelism of a [loop](walk-through/loops.md) +1. [`parallelism-nested.yaml`](https://github.com/argoproj/argo-workflows/blob/main/examples/parallelism-nested.yaml) restricts the parallelism of a nested loop +1. [`parallelism-nested-dag.yaml`](https://github.com/argoproj/argo-workflows/blob/main/examples/parallelism-nested-dag.yaml) restricts the number of dag tasks that can be run at any one time +1. [`parallelism-nested-workflow.yaml`](https://github.com/argoproj/argo-workflows/blob/main/examples/parallelism-nested-workflow.yaml) shows how parallelism is inherited by children +1. [`parallelism-template-limit.yaml`](https://github.com/argoproj/argo-workflows/blob/main/examples/parallelism-template-limit.yaml) shows how parallelism of looped templates is also restricted + +## Other Parallelism support -In addition to this synchronization, the workflow controller supports a parallelism setting that applies to all workflows -in the system (it is not granular to a class of workflows, or tasks withing them). Furthermore, there is a parallelism setting -at the workflow and template level, but this only restricts total concurrent executions of tasks within the same workflow. +You can also [restrict parallelism at the Controller-level](parallelism.md). diff --git a/go.mod b/go.mod index bc6ab871a244..88f023089b8c 100644 --- a/go.mod +++ b/go.mod @@ -190,7 +190,7 @@ require ( github.com/dimchansky/utfbom v1.1.1 // indirect github.com/docker/cli v24.0.7+incompatible // indirect github.com/docker/distribution v2.8.2+incompatible // indirect - github.com/docker/docker v26.1.5+incompatible // indirect + github.com/docker/docker v27.1.1+incompatible // indirect github.com/docker/docker-credential-helpers v0.7.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/emicklei/go-restful/v3 v3.10.0 // indirect diff --git a/go.sum b/go.sum index e2dcd42cf863..811b33266eff 100644 --- a/go.sum +++ b/go.sum @@ -221,8 +221,8 @@ github.com/docker/cli v24.0.7+incompatible h1:wa/nIwYFW7BVTGa7SWPVyyXU9lgORqUb1x github.com/docker/cli v24.0.7+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v26.1.5+incompatible h1:NEAxTwEjxV6VbBMBoGG3zPqbiJosIApZjxlbrG9q3/g= -github.com/docker/docker v26.1.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY= +github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.6.4/go.mod h1:ofX3UI0Gz1TteYBjtgs07O36Pyasyp66D2uKT7H8W1c= github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A= github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0= diff --git a/mkdocs.yml b/mkdocs.yml index 3403c489f3d3..52e1dbb74f3b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -252,6 +252,7 @@ nav: - workflow-restrictions.md - sidecar-injection.md - service-account-secrets.md + - parallelism.md - Argo Server: - argo-server.md - argo-server-auth-mode.md diff --git a/persist/sqldb/offload_node_status_repo.go b/persist/sqldb/offload_node_status_repo.go index 64fe982e9f2b..0f8964059caf 100644 --- a/persist/sqldb/offload_node_status_repo.go +++ b/persist/sqldb/offload_node_status_repo.go @@ -95,27 +95,8 @@ func (wdc *nodeOffloadRepo) Save(uid, namespace string, nodes wfv1.Nodes) (strin } logCtx.WithField("err", err).Info("Ignoring duplicate key error") } - - logCtx.Debug("Nodes offloaded, cleaning up old offloads") - - // This might fail, which kind of fine (maybe a bug). - // It might not delete all records, which is also fine, as we always key on resource version. - // We also want to keep enough around so that we can service watches. - rs, err := wdc.session.SQL(). - DeleteFrom(wdc.tableName). - Where(db.Cond{"clustername": wdc.clusterName}). - And(db.Cond{"uid": uid}). - And(db.Cond{"version <>": version}). - And(wdc.oldOffload()). - Exec() - if err != nil { - return "", err - } - rowsAffected, err := rs.RowsAffected() - if err != nil { - return "", err - } - logCtx.WithField("rowsAffected", rowsAffected).Debug("Deleted offloaded nodes") + // Don't need to clean up the old records here, we have a scheduled cleanup mechanism. + // If we clean them up here, when we update, if there is an update conflict, we will not be able to go back. return version, nil } diff --git a/pkg/apis/workflow/v1alpha1/artifact_repository_types_test.go b/pkg/apis/workflow/v1alpha1/artifact_repository_types_test.go index e76029080741..374321e69c74 100644 --- a/pkg/apis/workflow/v1alpha1/artifact_repository_types_test.go +++ b/pkg/apis/workflow/v1alpha1/artifact_repository_types_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "k8s.io/utils/pointer" ) @@ -23,49 +24,43 @@ func TestArtifactRepository(t *testing.T) { r := &ArtifactRepository{Artifactory: &ArtifactoryArtifactRepository{RepoURL: "http://my-repo"}} assert.IsType(t, &ArtifactoryArtifactRepository{}, r.Get()) l := r.ToArtifactLocation() - if assert.NotNil(t, l.Artifactory) { - assert.Equal(t, "http://my-repo/{{workflow.name}}/{{pod.name}}", l.Artifactory.URL) - } + require.NotNil(t, l.Artifactory) + assert.Equal(t, "http://my-repo/{{workflow.name}}/{{pod.name}}", l.Artifactory.URL) }) t.Run("Azure", func(t *testing.T) { r := &ArtifactRepository{Azure: &AzureArtifactRepository{}} assert.IsType(t, &AzureArtifactRepository{}, r.Get()) l := r.ToArtifactLocation() - if assert.NotNil(t, l.Azure) { - assert.Equal(t, "{{workflow.name}}/{{pod.name}}", l.Azure.Blob) - } + require.NotNil(t, l.Azure) + assert.Equal(t, "{{workflow.name}}/{{pod.name}}", l.Azure.Blob) }) t.Run("GCS", func(t *testing.T) { r := &ArtifactRepository{GCS: &GCSArtifactRepository{}} assert.IsType(t, &GCSArtifactRepository{}, r.Get()) l := r.ToArtifactLocation() - if assert.NotNil(t, l.GCS) { - assert.Equal(t, "{{workflow.name}}/{{pod.name}}", l.GCS.Key) - } + require.NotNil(t, l.GCS) + assert.Equal(t, "{{workflow.name}}/{{pod.name}}", l.GCS.Key) }) t.Run("HDFS", func(t *testing.T) { r := &ArtifactRepository{HDFS: &HDFSArtifactRepository{}} assert.IsType(t, &HDFSArtifactRepository{}, r.Get()) l := r.ToArtifactLocation() - if assert.NotNil(t, l.HDFS) { - assert.Equal(t, "{{workflow.name}}/{{pod.name}}", l.HDFS.Path) - } + require.NotNil(t, l.HDFS) + assert.Equal(t, "{{workflow.name}}/{{pod.name}}", l.HDFS.Path) }) t.Run("OSS", func(t *testing.T) { r := &ArtifactRepository{OSS: &OSSArtifactRepository{}} assert.IsType(t, &OSSArtifactRepository{}, r.Get()) l := r.ToArtifactLocation() - if assert.NotNil(t, l.OSS) { - assert.Equal(t, "{{workflow.name}}/{{pod.name}}", l.OSS.Key) - } + require.NotNil(t, l.OSS) + assert.Equal(t, "{{workflow.name}}/{{pod.name}}", l.OSS.Key) }) t.Run("S3", func(t *testing.T) { r := &ArtifactRepository{S3: &S3ArtifactRepository{KeyPrefix: "my-key-prefix"}} assert.IsType(t, &S3ArtifactRepository{}, r.Get()) l := r.ToArtifactLocation() - if assert.NotNil(t, l.S3) { - assert.Equal(t, "my-key-prefix/{{workflow.name}}/{{pod.name}}", l.S3.Key) - } + require.NotNil(t, l.S3) + assert.Equal(t, "my-key-prefix/{{workflow.name}}/{{pod.name}}", l.S3.Key) }) } diff --git a/pkg/apis/workflow/v1alpha1/data_types_test.go b/pkg/apis/workflow/v1alpha1/data_types_test.go index 6f59edca6131..d02e46a86bb6 100644 --- a/pkg/apis/workflow/v1alpha1/data_types_test.go +++ b/pkg/apis/workflow/v1alpha1/data_types_test.go @@ -4,12 +4,12 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestGetArtifactIfNeeded(t *testing.T) { data := &DataSource{ArtifactPaths: &ArtifactPaths{Artifact{Name: "foo"}}} art, needed := data.GetArtifactIfNeeded() - if assert.True(t, needed) { - assert.Equal(t, "foo", art.Name) - } + require.True(t, needed) + assert.Equal(t, "foo", art.Name) } diff --git a/pkg/apis/workflow/v1alpha1/generated.pb.go b/pkg/apis/workflow/v1alpha1/generated.pb.go index e068bc96bf88..3ce4fb3ad3c0 100644 --- a/pkg/apis/workflow/v1alpha1/generated.pb.go +++ b/pkg/apis/workflow/v1alpha1/generated.pb.go @@ -4448,699 +4448,697 @@ func init() { } var fileDescriptor_724696e352c3df5f = []byte{ - // 11059 bytes of a gzipped FileDescriptorProto + // 11028 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe4, 0x7d, 0x6b, 0x70, 0x24, 0xc7, - 0x79, 0x18, 0x67, 0x81, 0x05, 0xb0, 0xdf, 0x02, 0x38, 0x5c, 0xdf, 0x6b, 0x09, 0x92, 0x07, 0x7a, - 0x28, 0x32, 0xa4, 0x4d, 0xe1, 0xcc, 0xa3, 0x94, 0x30, 0x52, 0x22, 0x09, 0x8f, 0x03, 0xee, 0x88, - 0xc3, 0x01, 0xec, 0xc5, 0xf1, 0x4c, 0x8a, 0x96, 0x34, 0xd8, 0x6d, 0xec, 0x0e, 0xb1, 0x3b, 0xb3, - 0x9c, 0x99, 0xc5, 0x1d, 0xf8, 0x90, 0x14, 0x5a, 0xcf, 0x58, 0xb6, 0x62, 0x59, 0x92, 0x25, 0x25, - 0xa9, 0x52, 0x14, 0x29, 0x61, 0xc9, 0xae, 0xb8, 0xec, 0x5f, 0x29, 0xbb, 0xf2, 0x27, 0x95, 0x72, - 0x29, 0xe5, 0x54, 0x45, 0xae, 0x28, 0x25, 0xfd, 0xb0, 0xc1, 0xe8, 0x92, 0xe8, 0x47, 0x12, 0x55, - 0x25, 0xaa, 0xd8, 0xb1, 0x2f, 0x8f, 0x4a, 0xf5, 0x73, 0xba, 0x67, 0x67, 0x71, 0x8b, 0xbb, 0x06, - 0x4e, 0x65, 0xff, 0x02, 0xf6, 0xeb, 0xee, 0xef, 0xeb, 0xee, 0xe9, 0xfe, 0xfa, 0x7b, 0xf5, 0xd7, - 0xb0, 0xde, 0xf0, 0x93, 0x66, 0x77, 0x73, 0xb6, 0x16, 0xb6, 0xcf, 0x79, 0x51, 0x23, 0xec, 0x44, - 0xe1, 0xcb, 0xec, 0x9f, 0x77, 0x5e, 0x0f, 0xa3, 0xed, 0xad, 0x56, 0x78, 0x3d, 0x3e, 0xb7, 0xf3, - 0xf4, 0xb9, 0xce, 0x76, 0xe3, 0x9c, 0xd7, 0xf1, 0xe3, 0x73, 0x12, 0x7a, 0x6e, 0xe7, 0x29, 0xaf, - 0xd5, 0x69, 0x7a, 0x4f, 0x9d, 0x6b, 0x90, 0x80, 0x44, 0x5e, 0x42, 0xea, 0xb3, 0x9d, 0x28, 0x4c, - 0x42, 0xf4, 0x81, 0x14, 0xe3, 0xac, 0xc4, 0xc8, 0xfe, 0xf9, 0xb0, 0xc2, 0x38, 0xbb, 0xf3, 0xf4, - 0x6c, 0x67, 0xbb, 0x31, 0x4b, 0x31, 0xce, 0x4a, 0xe8, 0xac, 0xc4, 0x38, 0xfd, 0x4e, 0xad, 0x4f, - 0x8d, 0xb0, 0x11, 0x9e, 0x63, 0x88, 0x37, 0xbb, 0x5b, 0xec, 0x17, 0xfb, 0xc1, 0xfe, 0xe3, 0x04, - 0xa7, 0xdd, 0xed, 0x67, 0xe2, 0x59, 0x3f, 0xa4, 0xfd, 0x3b, 0x57, 0x0b, 0x23, 0x72, 0x6e, 0xa7, - 0xa7, 0x53, 0xd3, 0xef, 0xd0, 0xea, 0x74, 0xc2, 0x96, 0x5f, 0xdb, 0xcd, 0xab, 0xf5, 0xae, 0xb4, - 0x56, 0xdb, 0xab, 0x35, 0xfd, 0x80, 0x44, 0xbb, 0xe9, 0xd0, 0xdb, 0x24, 0xf1, 0xf2, 0x5a, 0x9d, - 0xeb, 0xd7, 0x2a, 0xea, 0x06, 0x89, 0xdf, 0x26, 0x3d, 0x0d, 0xfe, 0xfa, 0xed, 0x1a, 0xc4, 0xb5, - 0x26, 0x69, 0x7b, 0x3d, 0xed, 0x9e, 0xee, 0xd7, 0xae, 0x9b, 0xf8, 0xad, 0x73, 0x7e, 0x90, 0xc4, - 0x49, 0x94, 0x6d, 0xe4, 0x5e, 0x80, 0x91, 0xb9, 0x76, 0xd8, 0x0d, 0x12, 0xf4, 0x5e, 0x28, 0xee, - 0x78, 0xad, 0x2e, 0xa9, 0x38, 0x0f, 0x3b, 0x8f, 0x97, 0xe6, 0x1f, 0xfd, 0xce, 0xde, 0xcc, 0x7d, - 0x37, 0xf7, 0x66, 0x8a, 0xcf, 0x53, 0xe0, 0xad, 0xbd, 0x99, 0x93, 0x24, 0xa8, 0x85, 0x75, 0x3f, - 0x68, 0x9c, 0x7b, 0x39, 0x0e, 0x83, 0xd9, 0x2b, 0xdd, 0xf6, 0x26, 0x89, 0x30, 0x6f, 0xe3, 0xfe, - 0xbb, 0x02, 0x1c, 0x9b, 0x8b, 0x6a, 0x4d, 0x7f, 0x87, 0x54, 0x13, 0x8a, 0xbf, 0xb1, 0x8b, 0x9a, - 0x30, 0x94, 0x78, 0x11, 0x43, 0x57, 0x3e, 0xbf, 0x3a, 0x7b, 0xb7, 0xdf, 0x7d, 0x76, 0xc3, 0x8b, - 0x24, 0xee, 0xf9, 0xd1, 0x9b, 0x7b, 0x33, 0x43, 0x1b, 0x5e, 0x84, 0x29, 0x09, 0xd4, 0x82, 0xe1, - 0x20, 0x0c, 0x48, 0xa5, 0xc0, 0x48, 0x5d, 0xb9, 0x7b, 0x52, 0x57, 0xc2, 0x40, 0x8d, 0x63, 0x7e, - 0xec, 0xe6, 0xde, 0xcc, 0x30, 0x85, 0x60, 0x46, 0x85, 0x8e, 0xeb, 0x55, 0xbf, 0x53, 0x19, 0xb2, - 0x35, 0xae, 0x17, 0xfd, 0x8e, 0x39, 0xae, 0x17, 0xfd, 0x0e, 0xa6, 0x24, 0xdc, 0xcf, 0x16, 0xa0, - 0x34, 0x17, 0x35, 0xba, 0x6d, 0x12, 0x24, 0x31, 0xfa, 0x18, 0x40, 0xc7, 0x8b, 0xbc, 0x36, 0x49, - 0x48, 0x14, 0x57, 0x9c, 0x87, 0x87, 0x1e, 0x2f, 0x9f, 0x5f, 0xb9, 0x7b, 0xf2, 0xeb, 0x12, 0xe7, - 0x3c, 0x12, 0x9f, 0x1c, 0x14, 0x28, 0xc6, 0x1a, 0x49, 0xf4, 0x1a, 0x94, 0xbc, 0x28, 0xf1, 0xb7, - 0xbc, 0x5a, 0x12, 0x57, 0x0a, 0x8c, 0xfe, 0xb3, 0x77, 0x4f, 0x7f, 0x4e, 0xa0, 0x9c, 0x3f, 0x2e, - 0xc8, 0x97, 0x24, 0x24, 0xc6, 0x29, 0x3d, 0xf7, 0xf7, 0x86, 0xa1, 0x3c, 0x17, 0x25, 0xcb, 0x0b, - 0xd5, 0xc4, 0x4b, 0xba, 0x31, 0xfa, 0x43, 0x07, 0x4e, 0xc4, 0x7c, 0xda, 0x7c, 0x12, 0xaf, 0x47, - 0x61, 0x8d, 0xc4, 0x31, 0xa9, 0x8b, 0x79, 0xd9, 0xb2, 0xd2, 0x2f, 0x49, 0x6c, 0xb6, 0xda, 0x4b, - 0xe8, 0x42, 0x90, 0x44, 0xbb, 0xf3, 0x4f, 0x89, 0x3e, 0x9f, 0xc8, 0xa9, 0xf1, 0xe6, 0xdb, 0x33, - 0x48, 0x0e, 0x85, 0x62, 0xe2, 0x9f, 0x18, 0xe7, 0xf5, 0x1a, 0x7d, 0xd5, 0x81, 0xf1, 0x4e, 0x58, - 0x8f, 0x31, 0xa9, 0x85, 0xdd, 0x0e, 0xa9, 0x8b, 0xe9, 0xfd, 0xb0, 0xdd, 0x61, 0xac, 0x6b, 0x14, - 0x78, 0xff, 0x4f, 0x8a, 0xfe, 0x8f, 0xeb, 0x45, 0xd8, 0xe8, 0x0a, 0x7a, 0x06, 0xc6, 0x83, 0x30, - 0xa9, 0x76, 0x48, 0xcd, 0xdf, 0xf2, 0x49, 0x9d, 0x2d, 0xfc, 0xb1, 0xb4, 0xe5, 0x15, 0xad, 0x0c, - 0x1b, 0x35, 0xa7, 0x97, 0xa0, 0xd2, 0x6f, 0xe6, 0xd0, 0x14, 0x0c, 0x6d, 0x93, 0x5d, 0xce, 0x6c, - 0x30, 0xfd, 0x17, 0x9d, 0x94, 0x0c, 0x88, 0x6e, 0xe3, 0x31, 0xc1, 0x59, 0xde, 0x53, 0x78, 0xc6, - 0x99, 0x7e, 0x3f, 0x1c, 0xef, 0xe9, 0xfa, 0x41, 0x10, 0xb8, 0xdf, 0x1d, 0x81, 0x31, 0xf9, 0x29, - 0xd0, 0xc3, 0x30, 0x1c, 0x78, 0x6d, 0xc9, 0xe7, 0xc6, 0xc5, 0x38, 0x86, 0xaf, 0x78, 0x6d, 0xba, - 0xc3, 0xbd, 0x36, 0xa1, 0x35, 0x3a, 0x5e, 0xd2, 0x64, 0x78, 0xb4, 0x1a, 0xeb, 0x5e, 0xd2, 0xc4, - 0xac, 0x04, 0x3d, 0x08, 0xc3, 0xed, 0xb0, 0x4e, 0xd8, 0x5c, 0x14, 0x39, 0x87, 0x58, 0x0d, 0xeb, - 0x04, 0x33, 0x28, 0x6d, 0xbf, 0x15, 0x85, 0xed, 0xca, 0xb0, 0xd9, 0x7e, 0x29, 0x0a, 0xdb, 0x98, - 0x95, 0xa0, 0xaf, 0x38, 0x30, 0x25, 0xd7, 0xf6, 0xe5, 0xb0, 0xe6, 0x25, 0x7e, 0x18, 0x54, 0x8a, - 0x8c, 0xa3, 0x60, 0x7b, 0x5b, 0x4a, 0x62, 0x9e, 0xaf, 0x88, 0x2e, 0x4c, 0x65, 0x4b, 0x70, 0x4f, - 0x2f, 0xd0, 0x79, 0x80, 0x46, 0x2b, 0xdc, 0xf4, 0x5a, 0x74, 0x42, 0x2a, 0x23, 0x6c, 0x08, 0x8a, - 0x33, 0x2c, 0xab, 0x12, 0xac, 0xd5, 0x42, 0x37, 0x60, 0xd4, 0xe3, 0xdc, 0xbf, 0x32, 0xca, 0x06, - 0xf1, 0x9c, 0x8d, 0x41, 0x18, 0xc7, 0xc9, 0x7c, 0xf9, 0xe6, 0xde, 0xcc, 0xa8, 0x00, 0x62, 0x49, - 0x0e, 0x3d, 0x09, 0x63, 0x61, 0x87, 0xf6, 0xdb, 0x6b, 0x55, 0xc6, 0xd8, 0xc2, 0x9c, 0x12, 0x7d, - 0x1d, 0x5b, 0x13, 0x70, 0xac, 0x6a, 0xa0, 0x27, 0x60, 0x34, 0xee, 0x6e, 0xd2, 0xef, 0x58, 0x29, - 0xb1, 0x81, 0x1d, 0x13, 0x95, 0x47, 0xab, 0x1c, 0x8c, 0x65, 0x39, 0x7a, 0x37, 0x94, 0x23, 0x52, - 0xeb, 0x46, 0x31, 0xa1, 0x1f, 0xb6, 0x02, 0x0c, 0xf7, 0x09, 0x51, 0xbd, 0x8c, 0xd3, 0x22, 0xac, - 0xd7, 0x43, 0xef, 0x83, 0x49, 0xfa, 0x81, 0x2f, 0xdc, 0xe8, 0x44, 0x24, 0x8e, 0xe9, 0x57, 0x2d, - 0x33, 0x42, 0xa7, 0x45, 0xcb, 0xc9, 0x25, 0xa3, 0x14, 0x67, 0x6a, 0xa3, 0xd7, 0x01, 0x3c, 0xc5, - 0x33, 0x2a, 0xe3, 0x6c, 0x32, 0x2f, 0xdb, 0x5b, 0x11, 0xcb, 0x0b, 0xf3, 0x93, 0xf4, 0x3b, 0xa6, - 0xbf, 0xb1, 0x46, 0x8f, 0xce, 0x4f, 0x9d, 0xb4, 0x48, 0x42, 0xea, 0x95, 0x09, 0x36, 0x60, 0x35, - 0x3f, 0x8b, 0x1c, 0x8c, 0x65, 0xb9, 0xfb, 0xf7, 0x0b, 0xa0, 0x61, 0x41, 0xf3, 0x30, 0x26, 0xf8, - 0x9a, 0xd8, 0x92, 0xf3, 0x8f, 0xc9, 0xef, 0x20, 0xbf, 0xe0, 0xad, 0xbd, 0x5c, 0x7e, 0xa8, 0xda, - 0xa1, 0x37, 0xa0, 0xdc, 0x09, 0xeb, 0xab, 0x24, 0xf1, 0xea, 0x5e, 0xe2, 0x89, 0xd3, 0xdc, 0xc2, - 0x09, 0x23, 0x31, 0xce, 0x1f, 0xa3, 0x9f, 0x6e, 0x3d, 0x25, 0x81, 0x75, 0x7a, 0xe8, 0x59, 0x40, - 0x31, 0x89, 0x76, 0xfc, 0x1a, 0x99, 0xab, 0xd5, 0xa8, 0x48, 0xc4, 0x36, 0xc0, 0x10, 0x1b, 0xcc, - 0xb4, 0x18, 0x0c, 0xaa, 0xf6, 0xd4, 0xc0, 0x39, 0xad, 0xdc, 0xef, 0x15, 0x60, 0x52, 0x1b, 0x6b, - 0x87, 0xd4, 0xd0, 0x5b, 0x0e, 0x1c, 0x53, 0xc7, 0xd9, 0xfc, 0xee, 0x15, 0xba, 0xaa, 0xf8, 0x61, - 0x45, 0x6c, 0x7e, 0x5f, 0x4a, 0x4b, 0xfd, 0x14, 0x74, 0x38, 0xaf, 0x3f, 0x23, 0xc6, 0x70, 0x2c, - 0x53, 0x8a, 0xb3, 0xdd, 0x9a, 0xfe, 0xb2, 0x03, 0x27, 0xf3, 0x50, 0xe4, 0xf0, 0xdc, 0xa6, 0xce, - 0x73, 0xad, 0x32, 0x2f, 0x4a, 0x95, 0x0e, 0x46, 0xe7, 0xe3, 0xff, 0xaf, 0x00, 0x53, 0xfa, 0x12, - 0x62, 0x92, 0xc0, 0xbf, 0x74, 0xe0, 0x94, 0x1c, 0x01, 0x26, 0x71, 0xb7, 0x95, 0x99, 0xde, 0xb6, - 0xd5, 0xe9, 0xe5, 0x27, 0xe9, 0x5c, 0x1e, 0x3d, 0x3e, 0xcd, 0x0f, 0x89, 0x69, 0x3e, 0x95, 0x5b, - 0x07, 0xe7, 0x77, 0x75, 0xfa, 0x9b, 0x0e, 0x4c, 0xf7, 0x47, 0x9a, 0x33, 0xf1, 0x1d, 0x73, 0xe2, - 0x5f, 0xb4, 0x37, 0x48, 0x4e, 0x9e, 0x4d, 0x3f, 0x1b, 0xac, 0xfe, 0x01, 0x7e, 0x6b, 0x0c, 0x7a, - 0xce, 0x10, 0xf4, 0x14, 0x94, 0x05, 0x3b, 0xbe, 0x1c, 0x36, 0x62, 0xd6, 0xc9, 0x31, 0xbe, 0xd7, - 0xe6, 0x52, 0x30, 0xd6, 0xeb, 0xa0, 0x3a, 0x14, 0xe2, 0xa7, 0x45, 0xd7, 0x2d, 0xb0, 0xb7, 0xea, - 0xd3, 0x4a, 0x8a, 0x1c, 0xb9, 0xb9, 0x37, 0x53, 0xa8, 0x3e, 0x8d, 0x0b, 0xf1, 0xd3, 0x54, 0x52, - 0x6f, 0xf8, 0x89, 0x3d, 0x49, 0x7d, 0xd9, 0x4f, 0x14, 0x1d, 0x26, 0xa9, 0x2f, 0xfb, 0x09, 0xa6, - 0x24, 0xa8, 0x06, 0xd2, 0x4c, 0x92, 0x0e, 0x3b, 0xf1, 0xad, 0x68, 0x20, 0x17, 0x37, 0x36, 0xd6, - 0x15, 0x2d, 0x26, 0x5f, 0x50, 0x08, 0x66, 0x54, 0xd0, 0x67, 0x1c, 0x3a, 0xe3, 0xbc, 0x30, 0x8c, - 0x76, 0x85, 0xe0, 0x70, 0xd5, 0xde, 0x12, 0x08, 0xa3, 0x5d, 0x45, 0x5c, 0x7c, 0x48, 0x55, 0x80, - 0x75, 0xd2, 0x6c, 0xe0, 0xf5, 0xad, 0x98, 0xc9, 0x09, 0x76, 0x06, 0xbe, 0xb8, 0x54, 0xcd, 0x0c, - 0x7c, 0x71, 0xa9, 0x8a, 0x19, 0x15, 0xfa, 0x41, 0x23, 0xef, 0xba, 0x90, 0x31, 0x2c, 0x7c, 0x50, - 0xec, 0x5d, 0x37, 0x3f, 0x28, 0xf6, 0xae, 0x63, 0x4a, 0x82, 0x52, 0x0a, 0xe3, 0x98, 0x89, 0x14, - 0x56, 0x28, 0xad, 0x55, 0xab, 0x26, 0xa5, 0xb5, 0x6a, 0x15, 0x53, 0x12, 0x6c, 0x91, 0xd6, 0x62, - 0x26, 0x8f, 0xd8, 0x59, 0xa4, 0x0b, 0x19, 0x4a, 0xcb, 0x0b, 0x55, 0x4c, 0x49, 0x50, 0x96, 0xe1, - 0xbd, 0xda, 0x8d, 0xb8, 0x30, 0x53, 0x3e, 0xbf, 0x66, 0x61, 0xbd, 0x50, 0x74, 0x8a, 0x5a, 0xe9, - 0xe6, 0xde, 0x4c, 0x91, 0x81, 0x30, 0x27, 0xe4, 0xfe, 0xc1, 0x50, 0xca, 0x2e, 0x24, 0x3f, 0x47, + 0x79, 0x18, 0x67, 0x81, 0xc5, 0xe3, 0x5b, 0x00, 0x87, 0xeb, 0x7b, 0x2d, 0x71, 0xe4, 0x81, 0x1e, + 0x8a, 0x0c, 0x69, 0x51, 0x38, 0xf3, 0x28, 0x25, 0x8c, 0x94, 0x48, 0xc2, 0xe3, 0x80, 0x3b, 0xe2, + 0x70, 0x00, 0x7b, 0x71, 0x3c, 0x93, 0xa2, 0x25, 0x0d, 0x76, 0x1b, 0xbb, 0x43, 0xec, 0xce, 0x2c, + 0x67, 0x66, 0x71, 0x07, 0x3e, 0x24, 0x85, 0x7a, 0xc7, 0xb2, 0x15, 0xcb, 0x92, 0x2c, 0x29, 0x49, + 0x95, 0xa2, 0x48, 0x09, 0x4b, 0x76, 0x25, 0x65, 0xff, 0x4a, 0xd9, 0x95, 0x3f, 0xa9, 0x94, 0x4b, + 0x29, 0xa7, 0x2a, 0x72, 0x45, 0x29, 0xe9, 0x87, 0x0d, 0x46, 0x97, 0x44, 0x3f, 0x92, 0xa8, 0x2a, + 0x51, 0xc5, 0x8e, 0x7d, 0x79, 0x94, 0xab, 0x9f, 0xd3, 0x3d, 0x3b, 0x8b, 0x5b, 0xe0, 0x1a, 0x38, + 0x96, 0xfd, 0x0b, 0xd8, 0xaf, 0xbb, 0xbf, 0xaf, 0xbb, 0xa7, 0xfb, 0xeb, 0xef, 0xd5, 0x5f, 0xc3, + 0x5a, 0xdd, 0x4f, 0x1a, 0x9d, 0x8d, 0x99, 0x6a, 0xd8, 0x3a, 0xef, 0x45, 0xf5, 0xb0, 0x1d, 0x85, + 0x2f, 0xb1, 0x7f, 0xde, 0x75, 0x23, 0x8c, 0xb6, 0x36, 0x9b, 0xe1, 0x8d, 0xf8, 0xfc, 0xf6, 0x53, + 0xe7, 0xdb, 0x5b, 0xf5, 0xf3, 0x5e, 0xdb, 0x8f, 0xcf, 0x4b, 0xe8, 0xf9, 0xed, 0x27, 0xbd, 0x66, + 0xbb, 0xe1, 0x3d, 0x79, 0xbe, 0x4e, 0x02, 0x12, 0x79, 0x09, 0xa9, 0xcd, 0xb4, 0xa3, 0x30, 0x09, + 0xd1, 0x07, 0x53, 0x8c, 0x33, 0x12, 0x23, 0xfb, 0xe7, 0x23, 0x0a, 0xe3, 0xcc, 0xf6, 0x53, 0x33, + 0xed, 0xad, 0xfa, 0x0c, 0xc5, 0x38, 0x23, 0xa1, 0x33, 0x12, 0xe3, 0xd4, 0xbb, 0xb4, 0x3e, 0xd5, + 0xc3, 0x7a, 0x78, 0x9e, 0x21, 0xde, 0xe8, 0x6c, 0xb2, 0x5f, 0xec, 0x07, 0xfb, 0x8f, 0x13, 0x9c, + 0x72, 0xb7, 0x9e, 0x8e, 0x67, 0xfc, 0x90, 0xf6, 0xef, 0x7c, 0x35, 0x8c, 0xc8, 0xf9, 0xed, 0xae, + 0x4e, 0x4d, 0xbd, 0x43, 0xab, 0xd3, 0x0e, 0x9b, 0x7e, 0x75, 0x27, 0xaf, 0xd6, 0xbb, 0xd3, 0x5a, + 0x2d, 0xaf, 0xda, 0xf0, 0x03, 0x12, 0xed, 0xa4, 0x43, 0x6f, 0x91, 0xc4, 0xcb, 0x6b, 0x75, 0xbe, + 0x57, 0xab, 0xa8, 0x13, 0x24, 0x7e, 0x8b, 0x74, 0x35, 0xf8, 0xeb, 0x77, 0x6a, 0x10, 0x57, 0x1b, + 0xa4, 0xe5, 0x75, 0xb5, 0x7b, 0xaa, 0x57, 0xbb, 0x4e, 0xe2, 0x37, 0xcf, 0xfb, 0x41, 0x12, 0x27, + 0x51, 0xb6, 0x91, 0x7b, 0x11, 0x86, 0x66, 0x5b, 0x61, 0x27, 0x48, 0xd0, 0xfb, 0xa0, 0xb8, 0xed, + 0x35, 0x3b, 0xa4, 0xec, 0x3c, 0xe4, 0x3c, 0x36, 0x3a, 0xf7, 0xc8, 0xf7, 0x76, 0xa7, 0xef, 0xbb, + 0xb5, 0x3b, 0x5d, 0x7c, 0x8e, 0x02, 0x6f, 0xef, 0x4e, 0x9f, 0x24, 0x41, 0x35, 0xac, 0xf9, 0x41, + 0xfd, 0xfc, 0x4b, 0x71, 0x18, 0xcc, 0x5c, 0xed, 0xb4, 0x36, 0x48, 0x84, 0x79, 0x1b, 0xf7, 0xdf, + 0x17, 0xe0, 0xd8, 0x6c, 0x54, 0x6d, 0xf8, 0xdb, 0xa4, 0x92, 0x50, 0xfc, 0xf5, 0x1d, 0xd4, 0x80, + 0x81, 0xc4, 0x8b, 0x18, 0xba, 0xd2, 0x85, 0x95, 0x99, 0xbb, 0xfd, 0xee, 0x33, 0xeb, 0x5e, 0x24, + 0x71, 0xcf, 0x0d, 0xdf, 0xda, 0x9d, 0x1e, 0x58, 0xf7, 0x22, 0x4c, 0x49, 0xa0, 0x26, 0x0c, 0x06, + 0x61, 0x40, 0xca, 0x05, 0x46, 0xea, 0xea, 0xdd, 0x93, 0xba, 0x1a, 0x06, 0x6a, 0x1c, 0x73, 0x23, + 0xb7, 0x76, 0xa7, 0x07, 0x29, 0x04, 0x33, 0x2a, 0x74, 0x5c, 0xaf, 0xf8, 0xed, 0xf2, 0x80, 0xad, + 0x71, 0xbd, 0xe0, 0xb7, 0xcd, 0x71, 0xbd, 0xe0, 0xb7, 0x31, 0x25, 0xe1, 0x7e, 0xbe, 0x00, 0xa3, + 0xb3, 0x51, 0xbd, 0xd3, 0x22, 0x41, 0x12, 0xa3, 0x8f, 0x03, 0xb4, 0xbd, 0xc8, 0x6b, 0x91, 0x84, + 0x44, 0x71, 0xd9, 0x79, 0x68, 0xe0, 0xb1, 0xd2, 0x85, 0xe5, 0xbb, 0x27, 0xbf, 0x26, 0x71, 0xce, + 0x21, 0xf1, 0xc9, 0x41, 0x81, 0x62, 0xac, 0x91, 0x44, 0xaf, 0xc2, 0xa8, 0x17, 0x25, 0xfe, 0xa6, + 0x57, 0x4d, 0xe2, 0x72, 0x81, 0xd1, 0x7f, 0xe6, 0xee, 0xe9, 0xcf, 0x0a, 0x94, 0x73, 0xc7, 0x05, + 0xf9, 0x51, 0x09, 0x89, 0x71, 0x4a, 0xcf, 0xfd, 0xdd, 0x41, 0x28, 0xcd, 0x46, 0xc9, 0xd2, 0x7c, + 0x25, 0xf1, 0x92, 0x4e, 0x8c, 0xfe, 0xc0, 0x81, 0x13, 0x31, 0x9f, 0x36, 0x9f, 0xc4, 0x6b, 0x51, + 0x58, 0x25, 0x71, 0x4c, 0x6a, 0x62, 0x5e, 0x36, 0xad, 0xf4, 0x4b, 0x12, 0x9b, 0xa9, 0x74, 0x13, + 0xba, 0x18, 0x24, 0xd1, 0xce, 0xdc, 0x93, 0xa2, 0xcf, 0x27, 0x72, 0x6a, 0xbc, 0xf1, 0xd6, 0x34, + 0x92, 0x43, 0xa1, 0x98, 0xf8, 0x27, 0xc6, 0x79, 0xbd, 0x46, 0x5f, 0x77, 0x60, 0xac, 0x1d, 0xd6, + 0x62, 0x4c, 0xaa, 0x61, 0xa7, 0x4d, 0x6a, 0x62, 0x7a, 0x3f, 0x62, 0x77, 0x18, 0x6b, 0x1a, 0x05, + 0xde, 0xff, 0x93, 0xa2, 0xff, 0x63, 0x7a, 0x11, 0x36, 0xba, 0x82, 0x9e, 0x86, 0xb1, 0x20, 0x4c, + 0x2a, 0x6d, 0x52, 0xf5, 0x37, 0x7d, 0x52, 0x63, 0x0b, 0x7f, 0x24, 0x6d, 0x79, 0x55, 0x2b, 0xc3, + 0x46, 0xcd, 0xa9, 0x45, 0x28, 0xf7, 0x9a, 0x39, 0x34, 0x09, 0x03, 0x5b, 0x64, 0x87, 0x33, 0x1b, + 0x4c, 0xff, 0x45, 0x27, 0x25, 0x03, 0xa2, 0xdb, 0x78, 0x44, 0x70, 0x96, 0xf7, 0x16, 0x9e, 0x76, + 0xa6, 0x3e, 0x00, 0xc7, 0xbb, 0xba, 0xbe, 0x1f, 0x04, 0xee, 0xf7, 0x87, 0x60, 0x44, 0x7e, 0x0a, + 0xf4, 0x10, 0x0c, 0x06, 0x5e, 0x4b, 0xf2, 0xb9, 0x31, 0x31, 0x8e, 0xc1, 0xab, 0x5e, 0x8b, 0xee, + 0x70, 0xaf, 0x45, 0x68, 0x8d, 0xb6, 0x97, 0x34, 0x18, 0x1e, 0xad, 0xc6, 0x9a, 0x97, 0x34, 0x30, + 0x2b, 0x41, 0x0f, 0xc0, 0x60, 0x2b, 0xac, 0x11, 0x36, 0x17, 0x45, 0xce, 0x21, 0x56, 0xc2, 0x1a, + 0xc1, 0x0c, 0x4a, 0xdb, 0x6f, 0x46, 0x61, 0xab, 0x3c, 0x68, 0xb6, 0x5f, 0x8c, 0xc2, 0x16, 0x66, + 0x25, 0xe8, 0x6b, 0x0e, 0x4c, 0xca, 0xb5, 0x7d, 0x25, 0xac, 0x7a, 0x89, 0x1f, 0x06, 0xe5, 0x22, + 0xe3, 0x28, 0xd8, 0xde, 0x96, 0x92, 0x98, 0xe7, 0xca, 0xa2, 0x0b, 0x93, 0xd9, 0x12, 0xdc, 0xd5, + 0x0b, 0x74, 0x01, 0xa0, 0xde, 0x0c, 0x37, 0xbc, 0x26, 0x9d, 0x90, 0xf2, 0x10, 0x1b, 0x82, 0xe2, + 0x0c, 0x4b, 0xaa, 0x04, 0x6b, 0xb5, 0xd0, 0x4d, 0x18, 0xf6, 0x38, 0xf7, 0x2f, 0x0f, 0xb3, 0x41, + 0x3c, 0x6b, 0x63, 0x10, 0xc6, 0x71, 0x32, 0x57, 0xba, 0xb5, 0x3b, 0x3d, 0x2c, 0x80, 0x58, 0x92, + 0x43, 0x4f, 0xc0, 0x48, 0xd8, 0xa6, 0xfd, 0xf6, 0x9a, 0xe5, 0x11, 0xb6, 0x30, 0x27, 0x45, 0x5f, + 0x47, 0x56, 0x05, 0x1c, 0xab, 0x1a, 0xe8, 0x71, 0x18, 0x8e, 0x3b, 0x1b, 0xf4, 0x3b, 0x96, 0x47, + 0xd9, 0xc0, 0x8e, 0x89, 0xca, 0xc3, 0x15, 0x0e, 0xc6, 0xb2, 0x1c, 0xbd, 0x07, 0x4a, 0x11, 0xa9, + 0x76, 0xa2, 0x98, 0xd0, 0x0f, 0x5b, 0x06, 0x86, 0xfb, 0x84, 0xa8, 0x5e, 0xc2, 0x69, 0x11, 0xd6, + 0xeb, 0xa1, 0xf7, 0xc3, 0x04, 0xfd, 0xc0, 0x17, 0x6f, 0xb6, 0x23, 0x12, 0xc7, 0xf4, 0xab, 0x96, + 0x18, 0xa1, 0xd3, 0xa2, 0xe5, 0xc4, 0xa2, 0x51, 0x8a, 0x33, 0xb5, 0xd1, 0x6b, 0x00, 0x9e, 0xe2, + 0x19, 0xe5, 0x31, 0x36, 0x99, 0x57, 0xec, 0xad, 0x88, 0xa5, 0xf9, 0xb9, 0x09, 0xfa, 0x1d, 0xd3, + 0xdf, 0x58, 0xa3, 0x47, 0xe7, 0xa7, 0x46, 0x9a, 0x24, 0x21, 0xb5, 0xf2, 0x38, 0x1b, 0xb0, 0x9a, + 0x9f, 0x05, 0x0e, 0xc6, 0xb2, 0xdc, 0xfd, 0xfb, 0x05, 0xd0, 0xb0, 0xa0, 0x39, 0x18, 0x11, 0x7c, + 0x4d, 0x6c, 0xc9, 0xb9, 0x47, 0xe5, 0x77, 0x90, 0x5f, 0xf0, 0xf6, 0x6e, 0x2e, 0x3f, 0x54, 0xed, + 0xd0, 0xeb, 0x50, 0x6a, 0x87, 0xb5, 0x15, 0x92, 0x78, 0x35, 0x2f, 0xf1, 0xc4, 0x69, 0x6e, 0xe1, + 0x84, 0x91, 0x18, 0xe7, 0x8e, 0xd1, 0x4f, 0xb7, 0x96, 0x92, 0xc0, 0x3a, 0x3d, 0xf4, 0x0c, 0xa0, + 0x98, 0x44, 0xdb, 0x7e, 0x95, 0xcc, 0x56, 0xab, 0x54, 0x24, 0x62, 0x1b, 0x60, 0x80, 0x0d, 0x66, + 0x4a, 0x0c, 0x06, 0x55, 0xba, 0x6a, 0xe0, 0x9c, 0x56, 0xee, 0x0f, 0x0a, 0x30, 0xa1, 0x8d, 0xb5, + 0x4d, 0xaa, 0xe8, 0x4d, 0x07, 0x8e, 0xa9, 0xe3, 0x6c, 0x6e, 0xe7, 0x2a, 0x5d, 0x55, 0xfc, 0xb0, + 0x22, 0x36, 0xbf, 0x2f, 0xa5, 0xa5, 0x7e, 0x0a, 0x3a, 0x9c, 0xd7, 0x9f, 0x11, 0x63, 0x38, 0x96, + 0x29, 0xc5, 0xd9, 0x6e, 0x4d, 0x7d, 0xd5, 0x81, 0x93, 0x79, 0x28, 0x72, 0x78, 0x6e, 0x43, 0xe7, + 0xb9, 0x56, 0x99, 0x17, 0xa5, 0x4a, 0x07, 0xa3, 0xf3, 0xf1, 0xff, 0x5f, 0x80, 0x49, 0x7d, 0x09, + 0x31, 0x49, 0xe0, 0x5f, 0x39, 0x70, 0x4a, 0x8e, 0x00, 0x93, 0xb8, 0xd3, 0xcc, 0x4c, 0x6f, 0xcb, + 0xea, 0xf4, 0xf2, 0x93, 0x74, 0x36, 0x8f, 0x1e, 0x9f, 0xe6, 0x07, 0xc5, 0x34, 0x9f, 0xca, 0xad, + 0x83, 0xf3, 0xbb, 0x3a, 0xf5, 0x6d, 0x07, 0xa6, 0x7a, 0x23, 0xcd, 0x99, 0xf8, 0xb6, 0x39, 0xf1, + 0x2f, 0xd8, 0x1b, 0x24, 0x27, 0xcf, 0xa6, 0x9f, 0x0d, 0x56, 0xff, 0x00, 0xbf, 0x35, 0x02, 0x5d, + 0x67, 0x08, 0x7a, 0x12, 0x4a, 0x82, 0x1d, 0x5f, 0x09, 0xeb, 0x31, 0xeb, 0xe4, 0x08, 0xdf, 0x6b, + 0xb3, 0x29, 0x18, 0xeb, 0x75, 0x50, 0x0d, 0x0a, 0xf1, 0x53, 0xa2, 0xeb, 0x16, 0xd8, 0x5b, 0xe5, + 0x29, 0x25, 0x45, 0x0e, 0xdd, 0xda, 0x9d, 0x2e, 0x54, 0x9e, 0xc2, 0x85, 0xf8, 0x29, 0x2a, 0xa9, + 0xd7, 0xfd, 0xc4, 0x9e, 0xa4, 0xbe, 0xe4, 0x27, 0x8a, 0x0e, 0x93, 0xd4, 0x97, 0xfc, 0x04, 0x53, + 0x12, 0x54, 0x03, 0x69, 0x24, 0x49, 0x9b, 0x9d, 0xf8, 0x56, 0x34, 0x90, 0x4b, 0xeb, 0xeb, 0x6b, + 0x8a, 0x16, 0x93, 0x2f, 0x28, 0x04, 0x33, 0x2a, 0xe8, 0x73, 0x0e, 0x9d, 0x71, 0x5e, 0x18, 0x46, + 0x3b, 0x42, 0x70, 0xb8, 0x66, 0x6f, 0x09, 0x84, 0xd1, 0x8e, 0x22, 0x2e, 0x3e, 0xa4, 0x2a, 0xc0, + 0x3a, 0x69, 0x36, 0xf0, 0xda, 0x66, 0xcc, 0xe4, 0x04, 0x3b, 0x03, 0x5f, 0x58, 0xac, 0x64, 0x06, + 0xbe, 0xb0, 0x58, 0xc1, 0x8c, 0x0a, 0xfd, 0xa0, 0x91, 0x77, 0x43, 0xc8, 0x18, 0x16, 0x3e, 0x28, + 0xf6, 0x6e, 0x98, 0x1f, 0x14, 0x7b, 0x37, 0x30, 0x25, 0x41, 0x29, 0x85, 0x71, 0xcc, 0x44, 0x0a, + 0x2b, 0x94, 0x56, 0x2b, 0x15, 0x93, 0xd2, 0x6a, 0xa5, 0x82, 0x29, 0x09, 0xb6, 0x48, 0xab, 0x31, + 0x93, 0x47, 0xec, 0x2c, 0xd2, 0xf9, 0x0c, 0xa5, 0xa5, 0xf9, 0x0a, 0xa6, 0x24, 0x28, 0xcb, 0xf0, + 0x5e, 0xe9, 0x44, 0x5c, 0x98, 0x29, 0x5d, 0x58, 0xb5, 0xb0, 0x5e, 0x28, 0x3a, 0x45, 0x6d, 0xf4, + 0xd6, 0xee, 0x74, 0x91, 0x81, 0x30, 0x27, 0xe4, 0xfe, 0xfe, 0x40, 0xca, 0x2e, 0x24, 0x3f, 0x47, 0xbf, 0xc6, 0x0e, 0x42, 0xc1, 0x0b, 0x84, 0xe8, 0xeb, 0x1c, 0x9a, 0xe8, 0x7b, 0x82, 0x9f, 0x78, - 0x06, 0x39, 0x9c, 0xa5, 0x8f, 0xbe, 0xe0, 0xf4, 0xea, 0xb6, 0x9e, 0xfd, 0xb3, 0x2c, 0x3d, 0x98, - 0xf9, 0x59, 0xb1, 0xaf, 0xca, 0x3b, 0xfd, 0x19, 0x27, 0x15, 0x22, 0xe2, 0x7e, 0xe7, 0xc0, 0x47, - 0xcc, 0x73, 0xc0, 0xa2, 0x42, 0xae, 0xf3, 0xfd, 0xcf, 0x3a, 0x30, 0x21, 0xe1, 0x54, 0x3c, 0x8e, - 0xd1, 0x0d, 0x18, 0x93, 0x3d, 0x15, 0x5f, 0xcf, 0xa6, 0x2d, 0x40, 0x09, 0xf1, 0xaa, 0x33, 0x8a, - 0x9a, 0xfb, 0xd6, 0x08, 0xa0, 0xf4, 0xac, 0xea, 0x84, 0xb1, 0xcf, 0x38, 0xd1, 0x1d, 0x9c, 0x42, - 0x81, 0x76, 0x0a, 0x3d, 0x6f, 0xf3, 0x14, 0x4a, 0xbb, 0x65, 0x9c, 0x47, 0x5f, 0xc8, 0xf0, 0x6d, - 0x7e, 0x30, 0x7d, 0xf8, 0x50, 0xf8, 0xb6, 0xd6, 0x85, 0xfd, 0x39, 0xf8, 0x8e, 0xe0, 0xe0, 0xfc, - 0xe8, 0xfa, 0x05, 0xbb, 0x1c, 0x5c, 0xeb, 0x45, 0x96, 0x97, 0x47, 0x9c, 0xc3, 0xf2, 0xb3, 0xeb, - 0x9a, 0x55, 0x0e, 0xab, 0x51, 0x35, 0x79, 0x6d, 0xc4, 0x79, 0xed, 0x88, 0x2d, 0x9a, 0x1a, 0xaf, - 0xcd, 0xd2, 0x54, 0x5c, 0xf7, 0x55, 0xc9, 0x75, 0xf9, 0xa9, 0xf5, 0x82, 0x65, 0xae, 0xab, 0xd1, - 0xed, 0xe5, 0xbf, 0xaf, 0xc0, 0xa9, 0xde, 0x7a, 0x98, 0x6c, 0xa1, 0x73, 0x50, 0xaa, 0x85, 0xc1, - 0x96, 0xdf, 0x58, 0xf5, 0x3a, 0x42, 0x5f, 0x53, 0xbc, 0x68, 0x41, 0x16, 0xe0, 0xb4, 0x0e, 0x7a, - 0x88, 0x33, 0x1e, 0x6e, 0x11, 0x29, 0x8b, 0xaa, 0x43, 0x2b, 0x64, 0x97, 0x71, 0xa1, 0xf7, 0x8c, - 0x7d, 0xe5, 0xeb, 0x33, 0xf7, 0x7d, 0xfc, 0x8f, 0x1f, 0xbe, 0xcf, 0xfd, 0xa3, 0x21, 0x78, 0x20, - 0x97, 0xa6, 0x90, 0xd6, 0x7f, 0xcb, 0x90, 0xd6, 0xb5, 0x72, 0xc1, 0x45, 0xae, 0xd9, 0x14, 0x64, - 0x35, 0xf4, 0x79, 0x72, 0xb9, 0x56, 0x8c, 0xf3, 0x3b, 0x45, 0x27, 0x2a, 0xf0, 0xda, 0x24, 0xee, - 0x78, 0x35, 0x22, 0x46, 0xaf, 0x26, 0xea, 0x8a, 0x2c, 0xc0, 0x69, 0x1d, 0xae, 0x42, 0x6f, 0x79, - 0xdd, 0x56, 0x22, 0x0c, 0x65, 0x9a, 0x0a, 0xcd, 0xc0, 0x58, 0x96, 0xa3, 0x7f, 0xe0, 0x00, 0xea, - 0xa5, 0x2a, 0x36, 0xe2, 0xc6, 0x61, 0xcc, 0xc3, 0xfc, 0xe9, 0x9b, 0x9a, 0x12, 0xae, 0x8d, 0x34, - 0xa7, 0x1f, 0xda, 0x37, 0xfd, 0x68, 0x7a, 0x0e, 0x71, 0xe5, 0x60, 0x00, 0x1b, 0x1a, 0x33, 0xb5, - 0xd4, 0x6a, 0x24, 0x8e, 0xb9, 0x39, 0x4e, 0x37, 0xb5, 0x30, 0x30, 0x96, 0xe5, 0x68, 0x06, 0x8a, - 0x24, 0x8a, 0xc2, 0x48, 0xe8, 0xda, 0x6c, 0x19, 0x5f, 0xa0, 0x00, 0xcc, 0xe1, 0xee, 0x8f, 0x0a, - 0x50, 0xe9, 0xa7, 0x9d, 0xa0, 0xdf, 0xd5, 0xf4, 0x6a, 0xa1, 0x39, 0x09, 0xc5, 0x2f, 0x3c, 0x3c, - 0x9d, 0x28, 0xab, 0x00, 0xf6, 0xd1, 0xb0, 0x45, 0x29, 0xce, 0x76, 0x70, 0xfa, 0x8b, 0x9a, 0x86, - 0xad, 0xa3, 0xc8, 0x39, 0xe0, 0xb7, 0xcc, 0x03, 0x7e, 0xdd, 0xf6, 0xa0, 0xf4, 0x63, 0xfe, 0x4f, - 0x8a, 0x70, 0x42, 0x96, 0x56, 0x09, 0x3d, 0x2a, 0x9f, 0xeb, 0x92, 0x68, 0x17, 0x7d, 0xdf, 0x81, - 0x93, 0x5e, 0xd6, 0x74, 0xe3, 0x93, 0x43, 0x98, 0x68, 0x8d, 0xea, 0xec, 0x5c, 0x0e, 0x45, 0x3e, - 0xd1, 0xe7, 0xc5, 0x44, 0x9f, 0xcc, 0xab, 0xd2, 0xc7, 0xee, 0x9e, 0x3b, 0x00, 0xf4, 0x0c, 0x8c, - 0x4b, 0x38, 0x33, 0xf7, 0xf0, 0x2d, 0xae, 0x8c, 0xdb, 0x73, 0x5a, 0x19, 0x36, 0x6a, 0xd2, 0x96, - 0x09, 0x69, 0x77, 0x5a, 0x5e, 0x42, 0x34, 0x43, 0x91, 0x6a, 0xb9, 0xa1, 0x95, 0x61, 0xa3, 0x26, - 0x7a, 0x0c, 0x46, 0x82, 0xb0, 0x4e, 0x2e, 0xd5, 0x85, 0x81, 0x78, 0x52, 0xb4, 0x19, 0xb9, 0xc2, - 0xa0, 0x58, 0x94, 0xa2, 0x47, 0x53, 0x6b, 0x5c, 0x91, 0x6d, 0xa1, 0x72, 0x9e, 0x25, 0x0e, 0xfd, - 0x23, 0x07, 0x4a, 0xb4, 0xc5, 0xc6, 0x6e, 0x87, 0xd0, 0xb3, 0x8d, 0x7e, 0x91, 0xfa, 0xe1, 0x7c, - 0x91, 0x2b, 0x92, 0x8c, 0x69, 0xea, 0x28, 0x29, 0xf8, 0x9b, 0x6f, 0xcf, 0x8c, 0xc9, 0x1f, 0x38, - 0xed, 0xd5, 0xf4, 0x32, 0xdc, 0xdf, 0xf7, 0x6b, 0x1e, 0xc8, 0x15, 0xf0, 0xb7, 0x60, 0xd2, 0xec, - 0xc4, 0x81, 0xfc, 0x00, 0xff, 0x5c, 0xdb, 0x76, 0x7c, 0x5c, 0x82, 0x9f, 0xdd, 0x33, 0x69, 0x56, - 0x2d, 0x86, 0x45, 0xb1, 0xf4, 0xcc, 0xc5, 0xb0, 0x28, 0x16, 0xc3, 0xa2, 0xfb, 0x87, 0x4e, 0xba, - 0x35, 0x35, 0x31, 0x8f, 0x1e, 0xcc, 0xdd, 0xa8, 0x25, 0x18, 0xb1, 0x3a, 0x98, 0xaf, 0xe2, 0xcb, - 0x98, 0xc2, 0xd1, 0x17, 0x35, 0xee, 0x48, 0x9b, 0x75, 0x85, 0x5b, 0xc3, 0x92, 0x89, 0xde, 0x40, - 0xdc, 0xcb, 0xff, 0x44, 0x01, 0xce, 0x76, 0xc1, 0xfd, 0x42, 0x01, 0x1e, 0xda, 0x57, 0x68, 0xcd, - 0xed, 0xb8, 0x73, 0xcf, 0x3b, 0x4e, 0x8f, 0xb5, 0x88, 0x74, 0xc2, 0xab, 0xf8, 0xb2, 0xf8, 0x5e, - 0xea, 0x58, 0xc3, 0x1c, 0x8c, 0x65, 0x39, 0x15, 0x1d, 0xb6, 0xc9, 0xee, 0x52, 0x18, 0xb5, 0xbd, - 0x44, 0x70, 0x07, 0x25, 0x3a, 0xac, 0xc8, 0x02, 0x9c, 0xd6, 0x71, 0xbf, 0xef, 0x40, 0xb6, 0x03, - 0xc8, 0x83, 0xc9, 0x6e, 0x4c, 0x22, 0x7a, 0xa4, 0x56, 0x49, 0x2d, 0x22, 0x72, 0x79, 0x3e, 0x3a, - 0xcb, 0xbd, 0xfd, 0x74, 0x84, 0xb3, 0xb5, 0x30, 0x22, 0xb3, 0x3b, 0x4f, 0xcd, 0xf2, 0x1a, 0x2b, - 0x64, 0xb7, 0x4a, 0x5a, 0x84, 0xe2, 0x98, 0x47, 0x37, 0xf7, 0x66, 0x26, 0xaf, 0x1a, 0x08, 0x70, - 0x06, 0x21, 0x25, 0xd1, 0xf1, 0xe2, 0xf8, 0x7a, 0x18, 0xd5, 0x05, 0x89, 0xc2, 0x81, 0x49, 0xac, - 0x1b, 0x08, 0x70, 0x06, 0xa1, 0xfb, 0x3d, 0xaa, 0x3e, 0xea, 0x52, 0x2b, 0xfa, 0x3a, 0x95, 0x7d, - 0x28, 0x64, 0xbe, 0x15, 0x6e, 0x2e, 0x84, 0x41, 0xe2, 0xf9, 0x01, 0x91, 0xc1, 0x02, 0x1b, 0x96, - 0x64, 0x64, 0x03, 0x77, 0x6a, 0xc3, 0xef, 0x2d, 0xc3, 0x39, 0x7d, 0xa1, 0x32, 0xce, 0x66, 0x2b, - 0xdc, 0xcc, 0x7a, 0x01, 0x69, 0x25, 0xcc, 0x4a, 0xdc, 0x9f, 0x38, 0x70, 0xa6, 0x8f, 0x30, 0x8e, - 0xbe, 0xec, 0xc0, 0xc4, 0xe6, 0x4f, 0xc5, 0xd8, 0xcc, 0x6e, 0xa0, 0xf7, 0xc1, 0x24, 0x05, 0xd0, - 0x93, 0x48, 0xac, 0xcd, 0x82, 0xe9, 0xa1, 0x9a, 0x37, 0x4a, 0x71, 0xa6, 0xb6, 0xfb, 0xeb, 0x05, - 0xc8, 0xa1, 0x82, 0x9e, 0x84, 0x31, 0x12, 0xd4, 0x3b, 0xa1, 0x1f, 0x24, 0x82, 0x19, 0x29, 0xae, - 0x77, 0x41, 0xc0, 0xb1, 0xaa, 0x21, 0xf4, 0x0f, 0x31, 0x31, 0x85, 0x1e, 0xfd, 0x43, 0xf4, 0x3c, - 0xad, 0x83, 0x1a, 0x30, 0xe5, 0x71, 0xff, 0x0a, 0x5b, 0x7b, 0x6c, 0x99, 0x0e, 0x1d, 0x64, 0x99, - 0x9e, 0x64, 0xee, 0xcf, 0x0c, 0x0a, 0xdc, 0x83, 0x14, 0xbd, 0x1b, 0xca, 0xdd, 0x98, 0x54, 0x17, - 0x57, 0x16, 0x22, 0x52, 0xe7, 0x5a, 0xb1, 0xe6, 0xf7, 0xbb, 0x9a, 0x16, 0x61, 0xbd, 0x9e, 0xfb, - 0xaf, 0x1c, 0x18, 0x9d, 0xf7, 0x6a, 0xdb, 0xe1, 0xd6, 0x16, 0x9d, 0x8a, 0x7a, 0x37, 0x4a, 0x0d, - 0x5b, 0xda, 0x54, 0x2c, 0x0a, 0x38, 0x56, 0x35, 0xd0, 0x06, 0x8c, 0xf0, 0x0d, 0x2f, 0xb6, 0xdd, - 0xcf, 0x6b, 0xe3, 0x51, 0x71, 0x3c, 0x6c, 0x39, 0x74, 0x13, 0xbf, 0x35, 0xcb, 0xe3, 0x78, 0x66, - 0x2f, 0x05, 0xc9, 0x5a, 0x54, 0x4d, 0x22, 0x3f, 0x68, 0xcc, 0x03, 0x3d, 0x2e, 0x96, 0x18, 0x0e, - 0x2c, 0x70, 0xd1, 0x61, 0xb4, 0xbd, 0x1b, 0x92, 0x9c, 0x60, 0x3f, 0x6a, 0x18, 0xab, 0x69, 0x11, - 0xd6, 0xeb, 0xb9, 0x7f, 0xe4, 0x40, 0x69, 0xde, 0x8b, 0xfd, 0xda, 0x5f, 0x22, 0xe6, 0xf3, 0x21, - 0x28, 0x2e, 0x78, 0xb5, 0x26, 0x41, 0x57, 0xb3, 0x4a, 0x6f, 0xf9, 0xfc, 0xe3, 0x79, 0x64, 0x94, - 0x02, 0xac, 0x53, 0x9a, 0xe8, 0xa7, 0x1a, 0xbb, 0x6f, 0x3b, 0x30, 0xb9, 0xd0, 0xf2, 0x49, 0x90, - 0x2c, 0x90, 0x28, 0x61, 0x13, 0xd7, 0x80, 0xa9, 0x9a, 0x82, 0xdc, 0xc9, 0xd4, 0xb1, 0xd5, 0xba, - 0x90, 0x41, 0x81, 0x7b, 0x90, 0xa2, 0x3a, 0x1c, 0xe3, 0xb0, 0x74, 0x57, 0x1c, 0x68, 0xfe, 0x98, - 0x75, 0x74, 0xc1, 0xc4, 0x80, 0xb3, 0x28, 0xdd, 0x1f, 0x3b, 0x70, 0x66, 0xa1, 0xd5, 0x8d, 0x13, - 0x12, 0x5d, 0x13, 0xdc, 0x48, 0x8a, 0xb7, 0xe8, 0x23, 0x30, 0xd6, 0x96, 0x1e, 0x5b, 0xe7, 0x36, - 0x0b, 0x98, 0xf1, 0x33, 0x5a, 0x9b, 0x76, 0x66, 0x6d, 0xf3, 0x65, 0x52, 0x4b, 0x56, 0x49, 0xe2, - 0xa5, 0xe1, 0x05, 0x29, 0x0c, 0x2b, 0xac, 0xa8, 0x03, 0xc3, 0x71, 0x87, 0xd4, 0xec, 0x45, 0x77, - 0xc9, 0x31, 0x54, 0x3b, 0xa4, 0x96, 0xf2, 0x75, 0xe6, 0x6b, 0x64, 0x94, 0xdc, 0xff, 0xed, 0xc0, - 0x03, 0x7d, 0xc6, 0x7b, 0xd9, 0x8f, 0x13, 0xf4, 0x52, 0xcf, 0x98, 0x67, 0x07, 0x1b, 0x33, 0x6d, - 0xcd, 0x46, 0xac, 0x18, 0x82, 0x84, 0x68, 0xe3, 0xfd, 0x28, 0x14, 0xfd, 0x84, 0xb4, 0xa5, 0x19, - 0xda, 0x82, 0xc1, 0xa8, 0xcf, 0x58, 0xe6, 0x27, 0x64, 0x8c, 0xdf, 0x25, 0x4a, 0x0f, 0x73, 0xb2, - 0xee, 0x36, 0x8c, 0x2c, 0x84, 0xad, 0x6e, 0x3b, 0x18, 0x2c, 0x52, 0x26, 0xd9, 0xed, 0x90, 0xec, - 0x19, 0xc9, 0xc4, 0x7f, 0x56, 0x22, 0x0d, 0x47, 0x43, 0xf9, 0x86, 0x23, 0xf7, 0x5f, 0x3b, 0x40, - 0x77, 0x55, 0xdd, 0x17, 0x9e, 0x44, 0x8e, 0x8e, 0x13, 0x7c, 0x48, 0x47, 0x77, 0x6b, 0x6f, 0x66, - 0x42, 0x55, 0xd4, 0xf0, 0x7f, 0x08, 0x46, 0x62, 0xa6, 0x92, 0x8b, 0x3e, 0x2c, 0x49, 0xf9, 0x99, - 0x2b, 0xea, 0xb7, 0xf6, 0x66, 0x06, 0x0a, 0xdb, 0x9c, 0x55, 0xb8, 0x85, 0xd3, 0x53, 0x60, 0xa5, - 0x02, 0x5f, 0x9b, 0xc4, 0xb1, 0xd7, 0x90, 0x1a, 0x9e, 0x12, 0xf8, 0x56, 0x39, 0x18, 0xcb, 0x72, - 0xf7, 0x4b, 0x0e, 0x4c, 0xa8, 0xc3, 0x8b, 0x8a, 0xef, 0xe8, 0x8a, 0x7e, 0xcc, 0xf1, 0x95, 0xf2, - 0x50, 0x1f, 0x8e, 0x23, 0x0e, 0xf2, 0xfd, 0x4f, 0xc1, 0x77, 0xc1, 0x78, 0x9d, 0x74, 0x48, 0x50, - 0x27, 0x41, 0x8d, 0xaa, 0xdf, 0x74, 0x85, 0x94, 0xe6, 0xa7, 0xa8, 0xbe, 0xb9, 0xa8, 0xc1, 0xb1, - 0x51, 0xcb, 0xfd, 0x86, 0x03, 0xf7, 0x2b, 0x74, 0x55, 0x92, 0x60, 0x92, 0x44, 0xbb, 0x2a, 0x4c, - 0xf3, 0x60, 0xa7, 0xd5, 0x35, 0x2a, 0xff, 0x26, 0x11, 0x27, 0x7e, 0x67, 0xc7, 0x55, 0x99, 0x4b, - 0xcb, 0x0c, 0x09, 0x96, 0xd8, 0xdc, 0x5f, 0x1d, 0x82, 0x93, 0x7a, 0x27, 0x15, 0x83, 0xf9, 0x25, - 0x07, 0x40, 0xcd, 0x00, 0x3d, 0x90, 0x87, 0xec, 0xf8, 0xae, 0x8c, 0x2f, 0x95, 0xb2, 0x20, 0x05, - 0x8e, 0xb1, 0x46, 0x16, 0xbd, 0x00, 0xe3, 0x3b, 0x74, 0x53, 0x90, 0x55, 0x2a, 0x2e, 0xc4, 0x95, - 0x21, 0xd6, 0x8d, 0x99, 0xbc, 0x8f, 0xf9, 0x7c, 0x5a, 0x2f, 0x35, 0x07, 0x68, 0xc0, 0x18, 0x1b, - 0xa8, 0xa8, 0xa6, 0x33, 0x11, 0xe9, 0x9f, 0x44, 0xd8, 0xc4, 0x3f, 0x68, 0x71, 0x8c, 0xd9, 0xaf, - 0x3e, 0x7f, 0xfc, 0xe6, 0xde, 0xcc, 0x84, 0x01, 0xc2, 0x66, 0x27, 0xdc, 0x17, 0x80, 0xcd, 0x85, - 0x1f, 0x74, 0xc9, 0x5a, 0x80, 0x1e, 0x91, 0x36, 0x3a, 0xee, 0x57, 0x51, 0x9c, 0x43, 0xb7, 0xd3, - 0x51, 0x5d, 0x76, 0xcb, 0xf3, 0x5b, 0x2c, 0x7c, 0x91, 0xd6, 0x52, 0xba, 0xec, 0x12, 0x83, 0x62, - 0x51, 0xea, 0xce, 0xc2, 0xe8, 0x02, 0x1d, 0x3b, 0x89, 0x28, 0x5e, 0x3d, 0xea, 0x78, 0xc2, 0x88, - 0x3a, 0x96, 0xd1, 0xc5, 0x1b, 0x70, 0x6a, 0x21, 0x22, 0x5e, 0x42, 0xaa, 0x4f, 0xcf, 0x77, 0x6b, - 0xdb, 0x24, 0xe1, 0xa1, 0x5d, 0x31, 0x7a, 0x2f, 0x4c, 0x84, 0xec, 0xc8, 0xb8, 0x1c, 0xd6, 0xb6, - 0xfd, 0xa0, 0x21, 0x4c, 0xae, 0xa7, 0x04, 0x96, 0x89, 0x35, 0xbd, 0x10, 0x9b, 0x75, 0xdd, 0xff, - 0x54, 0x80, 0xf1, 0x85, 0x28, 0x0c, 0x24, 0x5b, 0x3c, 0x82, 0xa3, 0x2c, 0x31, 0x8e, 0x32, 0x0b, - 0xee, 0x4e, 0xbd, 0xff, 0xfd, 0x8e, 0x33, 0xf4, 0xba, 0x62, 0x91, 0x43, 0xb6, 0x54, 0x10, 0x83, - 0x2e, 0xc3, 0x9d, 0x7e, 0x6c, 0x93, 0x81, 0xba, 0xff, 0xd9, 0x81, 0x29, 0xbd, 0xfa, 0x11, 0x9c, - 0xa0, 0xb1, 0x79, 0x82, 0x5e, 0xb1, 0x3b, 0xde, 0x3e, 0xc7, 0xe6, 0xbf, 0x18, 0x35, 0xc7, 0xc9, - 0x7c, 0xdd, 0x5f, 0x71, 0x60, 0xfc, 0xba, 0x06, 0x10, 0x83, 0xb5, 0x2d, 0xc4, 0xbc, 0x43, 0xb2, - 0x19, 0x1d, 0x7a, 0x2b, 0xf3, 0x1b, 0x1b, 0x3d, 0xa1, 0x7c, 0x3f, 0xae, 0x35, 0x49, 0xbd, 0xdb, - 0x92, 0xc7, 0xb7, 0x9a, 0xd2, 0xaa, 0x80, 0x63, 0x55, 0x03, 0xbd, 0x04, 0xc7, 0x6b, 0x61, 0x50, - 0xeb, 0x46, 0x11, 0x09, 0x6a, 0xbb, 0xeb, 0xec, 0x8e, 0x84, 0x38, 0x10, 0x67, 0x45, 0xb3, 0xe3, - 0x0b, 0xd9, 0x0a, 0xb7, 0xf2, 0x80, 0xb8, 0x17, 0x11, 0x77, 0x16, 0xc4, 0xf4, 0xc8, 0x12, 0x0a, - 0x97, 0xe6, 0x2c, 0x60, 0x60, 0x2c, 0xcb, 0xd1, 0x55, 0x38, 0x13, 0x27, 0x5e, 0x94, 0xf8, 0x41, - 0x63, 0x91, 0x78, 0xf5, 0x96, 0x1f, 0x50, 0x55, 0x22, 0x0c, 0xea, 0xdc, 0x95, 0x38, 0x34, 0xff, - 0xc0, 0xcd, 0xbd, 0x99, 0x33, 0xd5, 0xfc, 0x2a, 0xb8, 0x5f, 0x5b, 0xf4, 0x21, 0x98, 0x16, 0xee, - 0x88, 0xad, 0x6e, 0xeb, 0xd9, 0x70, 0x33, 0xbe, 0xe8, 0xc7, 0x54, 0x8f, 0xbf, 0xec, 0xb7, 0xfd, - 0x84, 0x39, 0x0c, 0x8b, 0xf3, 0x67, 0x6f, 0xee, 0xcd, 0x4c, 0x57, 0xfb, 0xd6, 0xc2, 0xfb, 0x60, - 0x40, 0x18, 0x4e, 0x73, 0xe6, 0xd7, 0x83, 0x7b, 0x94, 0xe1, 0x9e, 0xbe, 0xb9, 0x37, 0x73, 0x7a, - 0x29, 0xb7, 0x06, 0xee, 0xd3, 0x92, 0x7e, 0xc1, 0xc4, 0x6f, 0x93, 0x57, 0xc3, 0x80, 0xb0, 0x40, - 0x15, 0xed, 0x0b, 0x6e, 0x08, 0x38, 0x56, 0x35, 0xd0, 0xcb, 0xe9, 0x4a, 0xa4, 0xdb, 0x45, 0x04, - 0x9c, 0x1c, 0x9c, 0xc3, 0x31, 0xd5, 0xe4, 0x9a, 0x86, 0x89, 0x45, 0x52, 0x1a, 0xb8, 0xd1, 0x27, - 0x1c, 0x18, 0x8f, 0x93, 0x50, 0xdd, 0x6b, 0x10, 0x11, 0x27, 0x16, 0x96, 0x7d, 0x55, 0xc3, 0xca, - 0x05, 0x1f, 0x1d, 0x82, 0x0d, 0xaa, 0xe8, 0xe7, 0xa0, 0x24, 0x17, 0x70, 0x5c, 0x29, 0x33, 0x59, - 0x89, 0xa9, 0x71, 0x72, 0x7d, 0xc7, 0x38, 0x2d, 0x77, 0x7f, 0x34, 0x04, 0xa8, 0x97, 0xad, 0xa1, - 0x15, 0x18, 0xf1, 0x6a, 0x89, 0xbf, 0x23, 0xa3, 0x09, 0x1f, 0xc9, 0x3b, 0xf2, 0xf9, 0xf4, 0x60, - 0xb2, 0x45, 0xe8, 0xaa, 0x26, 0x29, 0x2f, 0x9c, 0x63, 0x4d, 0xb1, 0x40, 0x81, 0x42, 0x38, 0xde, - 0xf2, 0xe2, 0x44, 0xd2, 0xaf, 0xd3, 0xcf, 0x24, 0x0e, 0x83, 0x9f, 0x1d, 0xec, 0x43, 0xd0, 0x16, - 0xf3, 0xa7, 0xe8, 0x6e, 0xbb, 0x9c, 0x45, 0x84, 0x7b, 0x71, 0xa3, 0x8f, 0x31, 0xd9, 0x89, 0x0b, - 0xb6, 0x52, 0x68, 0x59, 0xb1, 0x22, 0x57, 0x70, 0x9c, 0x86, 0xdc, 0x24, 0xc8, 0x60, 0x8d, 0x24, - 0x3a, 0x07, 0x25, 0xb6, 0x2b, 0x48, 0x9d, 0xf0, 0xbd, 0x3d, 0x94, 0x8a, 0xb8, 0x55, 0x59, 0x80, - 0xd3, 0x3a, 0x9a, 0x0c, 0xc1, 0xb7, 0x73, 0x1f, 0x19, 0x02, 0x3d, 0x03, 0xc5, 0x4e, 0xd3, 0x8b, - 0x65, 0x84, 0xba, 0x2b, 0x79, 0xf2, 0x3a, 0x05, 0x32, 0xc6, 0xa3, 0x7d, 0x4b, 0x06, 0xc4, 0xbc, - 0x81, 0xfb, 0x6f, 0x00, 0x46, 0x17, 0xe7, 0x96, 0x37, 0xbc, 0x78, 0x7b, 0x00, 0x0d, 0x87, 0x6e, - 0x32, 0x21, 0x8a, 0x66, 0xd9, 0xa4, 0x14, 0x51, 0xb1, 0xaa, 0x81, 0x02, 0x18, 0xf1, 0x03, 0xca, - 0x57, 0x2a, 0x93, 0xb6, 0xbc, 0x08, 0x4a, 0x5b, 0x63, 0x66, 0x9e, 0x4b, 0x0c, 0x3b, 0x16, 0x54, - 0xd0, 0xeb, 0x50, 0xf2, 0xe4, 0x05, 0x21, 0x71, 0xba, 0xaf, 0xd8, 0x30, 0x8f, 0x0b, 0x94, 0x7a, - 0x80, 0x92, 0x00, 0xe1, 0x94, 0x20, 0xfa, 0xb8, 0x03, 0x65, 0x39, 0x74, 0x4c, 0xb6, 0x84, 0xe7, - 0x7a, 0xd5, 0xde, 0x98, 0x31, 0xd9, 0xe2, 0xd1, 0x2b, 0x1a, 0x00, 0xeb, 0x24, 0x7b, 0x34, 0xa2, - 0xe2, 0x20, 0x1a, 0x11, 0xba, 0x0e, 0xa5, 0xeb, 0x7e, 0xd2, 0x64, 0xe7, 0xb7, 0xf0, 0x98, 0x2d, - 0xdd, 0x7d, 0xaf, 0x29, 0xba, 0x74, 0xc6, 0xae, 0x49, 0x02, 0x38, 0xa5, 0x45, 0xb7, 0x03, 0xfd, - 0xc1, 0x2e, 0x58, 0x31, 0xce, 0x5f, 0x32, 0x1b, 0xb0, 0x02, 0x9c, 0xd6, 0xa1, 0x53, 0x3c, 0x4e, - 0x7f, 0x55, 0xc9, 0x2b, 0x5d, 0xca, 0x5a, 0x44, 0x44, 0xa2, 0x85, 0x75, 0x25, 0x31, 0xf2, 0xc9, - 0xba, 0xa6, 0xd1, 0xc0, 0x06, 0x45, 0xba, 0x47, 0xae, 0x37, 0x49, 0x20, 0x6e, 0x4c, 0xa8, 0x3d, - 0x72, 0xad, 0x49, 0x02, 0xcc, 0x4a, 0xd0, 0xeb, 0x5c, 0x43, 0xe3, 0xaa, 0x82, 0xe0, 0xf5, 0x97, - 0xed, 0x68, 0x2f, 0x1c, 0x27, 0xbf, 0xb4, 0x90, 0xfe, 0xc6, 0x1a, 0x3d, 0xca, 0x31, 0xc2, 0xe0, - 0xc2, 0x0d, 0x3f, 0x11, 0x57, 0x2d, 0x14, 0xc7, 0x58, 0x63, 0x50, 0x2c, 0x4a, 0x79, 0x64, 0x06, - 0x5d, 0x04, 0x31, 0xbb, 0x57, 0x51, 0xd2, 0x23, 0x33, 0x18, 0x18, 0xcb, 0x72, 0xf4, 0x0f, 0x1d, - 0x28, 0x36, 0xc3, 0x70, 0x3b, 0xae, 0x4c, 0xb0, 0xc5, 0x61, 0x41, 0x62, 0x16, 0x1c, 0x67, 0xf6, - 0x22, 0x45, 0x6b, 0x5e, 0x1e, 0x2b, 0x32, 0xd8, 0xad, 0xbd, 0x99, 0xc9, 0xcb, 0xfe, 0x16, 0xa9, - 0xed, 0xd6, 0x5a, 0x84, 0x41, 0xde, 0x7c, 0x5b, 0x83, 0x5c, 0xd8, 0x21, 0x41, 0x82, 0x79, 0xaf, - 0xa6, 0x3f, 0xeb, 0x00, 0xa4, 0x88, 0x72, 0x5c, 0xa0, 0xc4, 0x0c, 0x1a, 0xb0, 0xa0, 0x2e, 0x1b, - 0x5d, 0xd3, 0x7d, 0xaa, 0xff, 0xd6, 0x81, 0x32, 0x1d, 0x9c, 0x64, 0x81, 0x8f, 0xc1, 0x48, 0xe2, - 0x45, 0x0d, 0x22, 0xdd, 0x00, 0xea, 0x73, 0x6c, 0x30, 0x28, 0x16, 0xa5, 0x28, 0x80, 0x62, 0xe2, - 0xc5, 0xdb, 0x52, 0x48, 0xbf, 0x64, 0x6d, 0x8a, 0x53, 0xf9, 0x9c, 0xfe, 0x8a, 0x31, 0x27, 0x83, - 0x1e, 0x87, 0x31, 0x7a, 0x74, 0x2c, 0x79, 0xb1, 0x8c, 0xcc, 0x19, 0xa7, 0x4c, 0x7c, 0x49, 0xc0, - 0xb0, 0x2a, 0x75, 0x7f, 0xbd, 0x00, 0xc3, 0x8b, 0x5c, 0x5d, 0x1b, 0x89, 0xc3, 0x6e, 0x54, 0x23, - 0x42, 0x6c, 0xb7, 0xb0, 0xa6, 0x29, 0xde, 0x2a, 0xc3, 0xa9, 0x29, 0x4c, 0xec, 0x37, 0x16, 0xb4, - 0xd0, 0x17, 0x1d, 0x98, 0x4c, 0x22, 0x2f, 0x88, 0xb7, 0x98, 0xc3, 0xc5, 0x0f, 0x03, 0x31, 0x45, - 0x16, 0x56, 0xe1, 0x86, 0x81, 0xb7, 0x9a, 0x90, 0x4e, 0xea, 0xf7, 0x31, 0xcb, 0x70, 0xa6, 0x0f, - 0xee, 0x6f, 0x38, 0x00, 0x69, 0xef, 0xd1, 0x67, 0x1c, 0x98, 0xf0, 0xf4, 0x88, 0x50, 0x31, 0x47, - 0x6b, 0xf6, 0xbc, 0xb3, 0x0c, 0x2d, 0xb7, 0x54, 0x18, 0x20, 0x6c, 0x12, 0x76, 0xdf, 0x0d, 0x45, - 0xb6, 0x3b, 0x98, 0x4a, 0x23, 0x2c, 0xdb, 0x59, 0x53, 0x96, 0xb4, 0x78, 0x63, 0x55, 0xc3, 0x7d, - 0x09, 0x26, 0x2f, 0xdc, 0x20, 0xb5, 0x6e, 0x12, 0x46, 0xdc, 0xae, 0xdf, 0xe7, 0x06, 0x90, 0x73, - 0x47, 0x37, 0x80, 0xbe, 0xed, 0x40, 0x59, 0x0b, 0x0f, 0xa4, 0x27, 0x75, 0x63, 0xa1, 0xca, 0xcd, - 0x17, 0x62, 0xaa, 0x56, 0xac, 0x04, 0x20, 0x72, 0x94, 0xe9, 0x31, 0xa2, 0x40, 0x38, 0x25, 0x78, - 0x9b, 0xf0, 0x3d, 0xf7, 0x0f, 0x1c, 0x38, 0x95, 0x1b, 0xcb, 0x78, 0x8f, 0xbb, 0x6d, 0xb8, 0xd0, - 0x0b, 0x03, 0xb8, 0xd0, 0x7f, 0xc7, 0x81, 0x14, 0x13, 0x65, 0x45, 0x9b, 0x69, 0xcf, 0x35, 0x56, - 0x24, 0x28, 0x89, 0x52, 0xf4, 0x3a, 0x9c, 0x31, 0xbf, 0xe0, 0x1d, 0x7a, 0x53, 0xb8, 0xea, 0x99, - 0x8f, 0x09, 0xf7, 0x23, 0xe1, 0x7e, 0xd5, 0x81, 0xe2, 0xb2, 0xd7, 0x6d, 0x90, 0x81, 0x8c, 0x61, - 0x94, 0x8f, 0x45, 0xc4, 0x6b, 0x25, 0x52, 0x75, 0x10, 0x7c, 0x0c, 0x0b, 0x18, 0x56, 0xa5, 0x68, - 0x0e, 0x4a, 0x61, 0x87, 0x18, 0x1e, 0xc0, 0x47, 0xe4, 0xec, 0xad, 0xc9, 0x02, 0x7a, 0xec, 0x30, - 0xea, 0x0a, 0x82, 0xd3, 0x56, 0xee, 0xf7, 0x8b, 0x50, 0xd6, 0x6e, 0xbd, 0x50, 0x59, 0x20, 0x22, - 0x9d, 0x30, 0x2b, 0x2f, 0xd3, 0x05, 0x83, 0x59, 0x09, 0xdd, 0x83, 0x11, 0xd9, 0xf1, 0x63, 0xce, - 0xb6, 0x8c, 0x3d, 0x88, 0x05, 0x1c, 0xab, 0x1a, 0x68, 0x06, 0x8a, 0x75, 0xd2, 0x49, 0x9a, 0xac, - 0x7b, 0xc3, 0x3c, 0xf4, 0x6f, 0x91, 0x02, 0x30, 0x87, 0xd3, 0x0a, 0x5b, 0x24, 0xa9, 0x35, 0x99, - 0xdd, 0x57, 0xc4, 0x06, 0x2e, 0x51, 0x00, 0xe6, 0xf0, 0x1c, 0x1f, 0x65, 0xf1, 0xf0, 0x7d, 0x94, - 0x23, 0x96, 0x7d, 0x94, 0xa8, 0x03, 0x27, 0xe2, 0xb8, 0xb9, 0x1e, 0xf9, 0x3b, 0x5e, 0x42, 0xd2, - 0xd5, 0x37, 0x7a, 0x10, 0x3a, 0x67, 0xd8, 0x3d, 0xf4, 0xea, 0xc5, 0x2c, 0x16, 0x9c, 0x87, 0x1a, - 0x55, 0xe1, 0x94, 0x1f, 0xc4, 0xa4, 0xd6, 0x8d, 0xc8, 0xa5, 0x46, 0x10, 0x46, 0xe4, 0x62, 0x18, - 0x53, 0x74, 0xe2, 0x16, 0xad, 0x8a, 0x96, 0xbd, 0x94, 0x57, 0x09, 0xe7, 0xb7, 0x45, 0xcb, 0x70, - 0xbc, 0xee, 0xc7, 0xde, 0x66, 0x8b, 0x54, 0xbb, 0x9b, 0xed, 0x90, 0x2b, 0xde, 0x25, 0x86, 0xf0, - 0x7e, 0x69, 0x25, 0x5a, 0xcc, 0x56, 0xc0, 0xbd, 0x6d, 0xd0, 0x33, 0x30, 0x1e, 0xfb, 0x41, 0xa3, - 0x45, 0xe6, 0x23, 0x2f, 0xa8, 0x35, 0xc5, 0xf5, 0x5b, 0x65, 0x4d, 0xaf, 0x6a, 0x65, 0xd8, 0xa8, - 0xc9, 0xf6, 0x3c, 0x6f, 0x93, 0x91, 0x06, 0x45, 0x6d, 0x51, 0xea, 0xfe, 0xc0, 0x81, 0x71, 0x3d, - 0x52, 0x9d, 0x4a, 0xda, 0xd0, 0x5c, 0x5c, 0xaa, 0xf2, 0xb3, 0xc0, 0xde, 0x89, 0x7f, 0x51, 0xe1, - 0x4c, 0x95, 0xe5, 0x14, 0x86, 0x35, 0x9a, 0x03, 0xdc, 0x3b, 0x7f, 0x04, 0x8a, 0x5b, 0x21, 0x15, - 0x48, 0x86, 0x4c, 0x33, 0xfc, 0x12, 0x05, 0x62, 0x5e, 0xe6, 0xfe, 0x4f, 0x07, 0x4e, 0xe7, 0x07, - 0xe1, 0xff, 0x34, 0x0c, 0xf2, 0x3c, 0x00, 0x1d, 0x8a, 0xc1, 0xd4, 0xb5, 0xcc, 0x13, 0xb2, 0x04, - 0x6b, 0xb5, 0x06, 0x1b, 0xf6, 0x9f, 0x51, 0xa1, 0x38, 0xa5, 0xf3, 0x39, 0x07, 0x26, 0x28, 0xd9, - 0x95, 0x68, 0xd3, 0x18, 0xed, 0x9a, 0x9d, 0xd1, 0x2a, 0xb4, 0xa9, 0xb7, 0xc1, 0x00, 0x63, 0x93, - 0x38, 0xfa, 0x39, 0x28, 0x79, 0xf5, 0x7a, 0x44, 0xe2, 0x58, 0xf9, 0xed, 0x98, 0x2d, 0x6a, 0x4e, - 0x02, 0x71, 0x5a, 0x4e, 0x99, 0x68, 0xb3, 0xbe, 0x15, 0x53, 0xbe, 0x24, 0x18, 0xb7, 0x62, 0xa2, - 0x94, 0x08, 0x85, 0x63, 0x55, 0xc3, 0xfd, 0x95, 0x61, 0x30, 0x69, 0xa3, 0x3a, 0x1c, 0xdb, 0x8e, - 0x36, 0x17, 0x58, 0xd8, 0xc3, 0x9d, 0x84, 0x1f, 0xb0, 0xb0, 0x80, 0x15, 0x13, 0x03, 0xce, 0xa2, - 0x14, 0x54, 0x56, 0xc8, 0x6e, 0xe2, 0x6d, 0xde, 0x71, 0xf0, 0xc1, 0x8a, 0x89, 0x01, 0x67, 0x51, - 0xa2, 0x77, 0x43, 0x79, 0x3b, 0xda, 0x94, 0x2c, 0x3a, 0x1b, 0xc9, 0xb2, 0x92, 0x16, 0x61, 0xbd, - 0x1e, 0x9d, 0xc2, 0xed, 0x68, 0x93, 0x9e, 0x8a, 0x32, 0x0f, 0x83, 0x9a, 0xc2, 0x15, 0x01, 0xc7, - 0xaa, 0x06, 0xea, 0x00, 0xda, 0x96, 0xb3, 0xa7, 0x82, 0x3c, 0xc4, 0x49, 0x32, 0x78, 0x8c, 0x08, - 0x8b, 0xae, 0x5f, 0xe9, 0xc1, 0x83, 0x73, 0x70, 0xa3, 0x17, 0xe0, 0xcc, 0x76, 0xb4, 0x29, 0x84, - 0x85, 0xf5, 0xc8, 0x0f, 0x6a, 0x7e, 0xc7, 0xc8, 0xb9, 0x30, 0x23, 0xba, 0x7b, 0x66, 0x25, 0xbf, - 0x1a, 0xee, 0xd7, 0xde, 0xfd, 0xdd, 0x61, 0x60, 0xb7, 0x45, 0x29, 0x2f, 0x6c, 0x93, 0xa4, 0x19, - 0xd6, 0xb3, 0xf2, 0xcf, 0x2a, 0x83, 0x62, 0x51, 0x2a, 0x63, 0x48, 0x0b, 0x7d, 0x62, 0x48, 0xaf, - 0xc3, 0x68, 0x93, 0x78, 0x75, 0x12, 0x49, 0x0b, 0xe2, 0x65, 0x3b, 0xf7, 0x5b, 0x2f, 0x32, 0xa4, - 0xa9, 0x1a, 0xce, 0x7f, 0xc7, 0x58, 0x52, 0x43, 0xef, 0x81, 0x49, 0x2a, 0xc8, 0x84, 0xdd, 0x44, - 0x9a, 0xf8, 0xb9, 0x05, 0x91, 0x9d, 0xa8, 0x1b, 0x46, 0x09, 0xce, 0xd4, 0x44, 0x8b, 0x30, 0x25, - 0xcc, 0xf1, 0xca, 0x32, 0x29, 0x26, 0x56, 0x25, 0xc3, 0xa8, 0x66, 0xca, 0x71, 0x4f, 0x0b, 0x16, - 0x03, 0x18, 0xd6, 0xb9, 0x47, 0x56, 0x8f, 0x01, 0x0c, 0xeb, 0xbb, 0x98, 0x95, 0xa0, 0x57, 0x61, - 0x8c, 0xfe, 0x5d, 0x8a, 0xc2, 0xb6, 0xb0, 0xcd, 0xac, 0xdb, 0x99, 0x1d, 0x4a, 0x43, 0x68, 0x8a, - 0x4c, 0xc0, 0x9b, 0x17, 0x54, 0xb0, 0xa2, 0x47, 0xf5, 0x15, 0x79, 0x0e, 0x57, 0xb7, 0xfd, 0xce, - 0xf3, 0x24, 0xf2, 0xb7, 0x76, 0x99, 0xd0, 0x30, 0x96, 0xea, 0x2b, 0x97, 0x7a, 0x6a, 0xe0, 0x9c, - 0x56, 0xee, 0xe7, 0x0a, 0x30, 0xae, 0x5f, 0x3a, 0xbe, 0x5d, 0x60, 0x71, 0x9c, 0x2e, 0x0a, 0xae, - 0x9d, 0x5e, 0xb4, 0x30, 0xec, 0xdb, 0x2d, 0x88, 0x26, 0x0c, 0x7b, 0x5d, 0x21, 0x2d, 0x5a, 0x31, - 0x82, 0xb1, 0x11, 0x77, 0x93, 0x26, 0xbf, 0x9d, 0xc6, 0x42, 0x7e, 0x19, 0x05, 0xf7, 0x93, 0x43, - 0x30, 0x26, 0x0b, 0xd1, 0x27, 0x1c, 0x80, 0x34, 0xf4, 0x4a, 0xb0, 0xd2, 0x75, 0x1b, 0x71, 0x39, - 0x7a, 0xd4, 0x98, 0x66, 0x4b, 0x57, 0x70, 0xac, 0xd1, 0x45, 0x09, 0x8c, 0x84, 0xb4, 0x73, 0xe7, - 0xed, 0x5d, 0x9c, 0x5f, 0xa3, 0x84, 0xcf, 0x33, 0xea, 0xa9, 0xd9, 0x8c, 0xc1, 0xb0, 0xa0, 0x45, - 0x35, 0xc0, 0x4d, 0x19, 0x11, 0x68, 0xcf, 0xc4, 0xac, 0x82, 0x0c, 0x53, 0x85, 0x4e, 0x81, 0x70, - 0x4a, 0xd0, 0x7d, 0x0a, 0x26, 0xcd, 0xcd, 0x40, 0x35, 0x82, 0xcd, 0xdd, 0x84, 0x70, 0x7b, 0xc3, - 0x38, 0xd7, 0x08, 0xe6, 0x29, 0x00, 0x73, 0xb8, 0xfb, 0x3d, 0x2a, 0x07, 0x28, 0xf6, 0x32, 0x80, - 0x89, 0xff, 0x11, 0xdd, 0x58, 0xd6, 0x4f, 0xed, 0xfa, 0x18, 0x94, 0xd8, 0x3f, 0x6c, 0xa3, 0x0f, - 0xd9, 0xf2, 0xdf, 0xa7, 0xfd, 0x14, 0x5b, 0x9d, 0xc9, 0x04, 0xcf, 0x4b, 0x42, 0x38, 0xa5, 0xe9, - 0x86, 0x30, 0x95, 0xad, 0x8d, 0x3e, 0x08, 0xe3, 0xb1, 0x3c, 0x56, 0xd3, 0x2b, 0x74, 0x03, 0x1e, - 0xbf, 0xdc, 0x7b, 0xa6, 0x35, 0xc7, 0x06, 0x32, 0x77, 0x0d, 0x46, 0xac, 0x4e, 0xa1, 0xfb, 0x2d, - 0x07, 0x4a, 0xcc, 0x81, 0xd9, 0x88, 0xbc, 0x76, 0xda, 0x64, 0x68, 0x9f, 0x59, 0x8f, 0x61, 0x94, - 0xeb, 0xe8, 0x32, 0xf0, 0xc7, 0x02, 0x97, 0xe1, 0xf9, 0xee, 0x52, 0x2e, 0xc3, 0x8d, 0x01, 0x31, - 0x96, 0x94, 0xdc, 0x4f, 0x15, 0x60, 0xe4, 0x52, 0xd0, 0xe9, 0xfe, 0x95, 0xcf, 0xb9, 0xb6, 0x0a, - 0xc3, 0x97, 0x12, 0xd2, 0x36, 0x53, 0x03, 0x8e, 0xcf, 0x3f, 0xaa, 0xa7, 0x05, 0xac, 0x98, 0x69, - 0x01, 0xb1, 0x77, 0x5d, 0xc6, 0xc5, 0x09, 0x1b, 0x71, 0x7a, 0x8d, 0xf0, 0x49, 0x28, 0x5d, 0xf6, - 0x36, 0x49, 0x6b, 0x85, 0xec, 0xb2, 0x4b, 0x7f, 0x3c, 0x46, 0xc3, 0x49, 0x15, 0x7b, 0x23, 0x9e, - 0x62, 0x11, 0x26, 0x59, 0x6d, 0xb5, 0x19, 0xa8, 0xe6, 0x40, 0xd2, 0xbc, 0x4a, 0x8e, 0xa9, 0x39, - 0x68, 0x39, 0x95, 0xb4, 0x5a, 0xee, 0x2c, 0x94, 0x53, 0x2c, 0x03, 0x50, 0xfd, 0x49, 0x01, 0x26, - 0x0c, 0x53, 0xb7, 0xe1, 0x00, 0x74, 0x6e, 0xeb, 0x00, 0x34, 0x1c, 0x72, 0x85, 0x7b, 0xed, 0x90, - 0x1b, 0x3a, 0x7a, 0x87, 0x9c, 0xf9, 0x91, 0x86, 0x07, 0xfa, 0x48, 0x2d, 0x18, 0xbe, 0xec, 0x07, - 0xdb, 0x83, 0xf1, 0x99, 0xb8, 0x16, 0x76, 0x7a, 0xf8, 0x4c, 0x95, 0x02, 0x31, 0x2f, 0x93, 0x92, - 0xcb, 0x50, 0xbe, 0xe4, 0xe2, 0x7e, 0xc2, 0x81, 0xf1, 0x55, 0x2f, 0xf0, 0xb7, 0x48, 0x9c, 0xb0, - 0x75, 0x95, 0x1c, 0xea, 0xe5, 0xaf, 0xf1, 0x3e, 0x69, 0x0c, 0xde, 0x74, 0xe0, 0xf8, 0x2a, 0x69, - 0x87, 0xfe, 0xab, 0x5e, 0x1a, 0x76, 0x4a, 0xfb, 0xde, 0xf4, 0x13, 0x11, 0x65, 0xa7, 0xfa, 0x7e, - 0xd1, 0x4f, 0x30, 0x85, 0xdf, 0xc6, 0x8e, 0xcb, 0xae, 0x55, 0x50, 0x05, 0x4d, 0xbb, 0x90, 0x98, - 0x06, 0x94, 0xca, 0x02, 0x9c, 0xd6, 0x71, 0x7f, 0xcf, 0x81, 0x51, 0xde, 0x09, 0x15, 0xa9, 0xeb, - 0xf4, 0xc1, 0xdd, 0x84, 0x22, 0x6b, 0x27, 0x56, 0xf5, 0xb2, 0x05, 0xf1, 0x87, 0xa2, 0xe3, 0x7b, - 0x90, 0xfd, 0x8b, 0x39, 0x01, 0xa6, 0xb6, 0x78, 0x37, 0xe6, 0x54, 0xc4, 0x6d, 0xaa, 0xb6, 0x30, - 0x28, 0x16, 0xa5, 0xee, 0xd7, 0x86, 0x60, 0x4c, 0x65, 0xef, 0x62, 0xb9, 0x15, 0x82, 0x20, 0x4c, - 0x3c, 0x1e, 0xeb, 0xc0, 0x79, 0xf5, 0x07, 0xed, 0x65, 0x0f, 0x9b, 0x9d, 0x4b, 0xb1, 0x73, 0xff, - 0x9d, 0x52, 0x42, 0xb5, 0x12, 0xac, 0x77, 0x02, 0x7d, 0x14, 0x46, 0x5a, 0x94, 0xfb, 0x48, 0xd6, - 0xfd, 0xbc, 0xc5, 0xee, 0x30, 0xb6, 0x26, 0x7a, 0xa2, 0x66, 0x88, 0x03, 0xb1, 0xa0, 0x3a, 0xfd, - 0x3e, 0x98, 0xca, 0xf6, 0xfa, 0x76, 0xf7, 0x25, 0x4b, 0xfa, 0x6d, 0xcb, 0xbf, 0x29, 0xb8, 0xe7, - 0xc1, 0x9b, 0xba, 0xcf, 0x41, 0x79, 0x95, 0x24, 0x91, 0x5f, 0x63, 0x08, 0x6e, 0xb7, 0xb8, 0x06, - 0x92, 0x1f, 0x3e, 0xcd, 0x16, 0x2b, 0xc5, 0x19, 0xa3, 0xd7, 0x01, 0x3a, 0x51, 0x48, 0xf5, 0x57, - 0xd2, 0x95, 0x1f, 0xdb, 0x82, 0x3c, 0xbc, 0xae, 0x70, 0x72, 0x97, 0x73, 0xfa, 0x1b, 0x6b, 0xf4, - 0xdc, 0x17, 0xa1, 0xb8, 0xda, 0x4d, 0xc8, 0x8d, 0x01, 0x38, 0xd6, 0x41, 0x13, 0x08, 0xb8, 0x1f, - 0x84, 0x71, 0x86, 0xfb, 0x62, 0xd8, 0xa2, 0xc7, 0x2a, 0x9d, 0x9a, 0x36, 0xfd, 0x9d, 0x75, 0x0a, - 0xb0, 0x4a, 0x98, 0x97, 0xd1, 0x2d, 0xd3, 0x0c, 0x5b, 0x75, 0x75, 0x99, 0x4a, 0x2d, 0x88, 0x8b, - 0x0c, 0x8a, 0x45, 0xa9, 0xfb, 0x4b, 0x05, 0x28, 0xb3, 0x86, 0x82, 0xdd, 0xec, 0xc2, 0x68, 0x93, - 0xd3, 0x11, 0x73, 0x68, 0x21, 0x44, 0x4b, 0xef, 0xbd, 0xa6, 0xcb, 0x71, 0x00, 0x96, 0xf4, 0x28, - 0xe9, 0xeb, 0x9e, 0x9f, 0x50, 0xd2, 0x85, 0xc3, 0x25, 0x7d, 0x8d, 0x93, 0xc1, 0x92, 0x9e, 0xfb, - 0x8b, 0xc0, 0x2e, 0x29, 0x2f, 0xb5, 0xbc, 0x06, 0x9f, 0xb9, 0x70, 0x9b, 0xd4, 0x05, 0xcf, 0xd5, - 0x66, 0x8e, 0x42, 0xb1, 0x28, 0xe5, 0x17, 0x3f, 0x93, 0xc8, 0x57, 0xc1, 0xcd, 0xda, 0xc5, 0x4f, - 0x06, 0x96, 0xa1, 0xec, 0x75, 0xf7, 0x4b, 0x05, 0x00, 0x96, 0xeb, 0x8d, 0xdf, 0x2d, 0xfe, 0x79, - 0x19, 0xa9, 0x64, 0x3a, 0x12, 0x55, 0xa4, 0x12, 0xbb, 0x3d, 0xad, 0x47, 0x28, 0xe9, 0x77, 0x0e, - 0x0a, 0xfb, 0xdf, 0x39, 0x40, 0x1d, 0x18, 0x0d, 0xbb, 0x09, 0x95, 0x55, 0xc5, 0x61, 0x6f, 0xc1, - 0x8f, 0xbe, 0xc6, 0x11, 0xf2, 0x40, 0x7d, 0xf1, 0x03, 0x4b, 0x32, 0xe8, 0x19, 0x18, 0xeb, 0x44, - 0x61, 0x83, 0x9e, 0xdd, 0xe2, 0x78, 0x7f, 0x50, 0xca, 0x43, 0xeb, 0x02, 0x7e, 0x4b, 0xfb, 0x1f, - 0xab, 0xda, 0xee, 0x1f, 0x4f, 0xf1, 0x79, 0x11, 0x6b, 0x6f, 0x1a, 0x0a, 0xbe, 0xb4, 0x4c, 0x81, - 0x40, 0x51, 0xb8, 0xb4, 0x88, 0x0b, 0x7e, 0x5d, 0xed, 0xab, 0x42, 0xdf, 0x7d, 0xf5, 0x6e, 0x28, - 0xd7, 0xfd, 0xb8, 0xd3, 0xf2, 0x76, 0xaf, 0xe4, 0x98, 0x05, 0x17, 0xd3, 0x22, 0xac, 0xd7, 0x43, - 0x4f, 0x8a, 0x1b, 0x26, 0xc3, 0x86, 0x29, 0x48, 0xde, 0x30, 0x49, 0xef, 0xae, 0xf3, 0xcb, 0x25, - 0xd9, 0x3b, 0xfe, 0xc5, 0x81, 0xef, 0xf8, 0x67, 0x25, 0xb1, 0x91, 0xa3, 0x97, 0xc4, 0xde, 0x0b, - 0x13, 0xf2, 0x27, 0x13, 0x8f, 0x2a, 0x27, 0x59, 0xef, 0x95, 0xb9, 0x7a, 0x43, 0x2f, 0xc4, 0x66, - 0xdd, 0x74, 0xd1, 0x8e, 0x0e, 0xba, 0x68, 0xcf, 0x03, 0x6c, 0x86, 0xdd, 0xa0, 0xee, 0x45, 0xbb, - 0x97, 0x16, 0x45, 0x3c, 0xaa, 0x12, 0xfc, 0xe6, 0x55, 0x09, 0xd6, 0x6a, 0xe9, 0x0b, 0xbd, 0x74, - 0x9b, 0x85, 0xfe, 0x41, 0x28, 0xb1, 0xd8, 0x5d, 0x52, 0x9f, 0x4b, 0x44, 0x88, 0xd1, 0x41, 0x42, - 0x26, 0xd3, 0xa0, 0x43, 0x89, 0x04, 0xa7, 0xf8, 0xd0, 0x87, 0x00, 0xb6, 0xfc, 0xc0, 0x8f, 0x9b, - 0x0c, 0x7b, 0xf9, 0xc0, 0xd8, 0xd5, 0x38, 0x97, 0x14, 0x16, 0xac, 0x61, 0x44, 0x2f, 0xc1, 0x71, - 0x12, 0x27, 0x7e, 0xdb, 0x4b, 0x48, 0x5d, 0xdd, 0xc9, 0xac, 0x30, 0x5b, 0xa6, 0x8a, 0x9e, 0xbe, - 0x90, 0xad, 0x70, 0x2b, 0x0f, 0x88, 0x7b, 0x11, 0x19, 0x3b, 0x72, 0xfa, 0x20, 0x3b, 0x12, 0xfd, - 0xb9, 0x03, 0xc7, 0x23, 0xc2, 0xe3, 0x4e, 0x62, 0xd5, 0xb1, 0x53, 0x8c, 0x1d, 0xd7, 0x6c, 0xa4, - 0x51, 0x57, 0xf9, 0x52, 0x70, 0x96, 0x0a, 0x17, 0x5c, 0x88, 0x1c, 0x7d, 0x4f, 0xf9, 0xad, 0x3c, - 0xe0, 0x9b, 0x6f, 0xcf, 0xcc, 0xf4, 0xa6, 0xf3, 0x57, 0xc8, 0xe9, 0xce, 0xfb, 0xbb, 0x6f, 0xcf, - 0x4c, 0xc9, 0xdf, 0xe9, 0xa4, 0xf5, 0x0c, 0x92, 0x1e, 0xab, 0x9d, 0xb0, 0x7e, 0x69, 0x5d, 0xc4, - 0x82, 0xa9, 0x63, 0x75, 0x9d, 0x02, 0x31, 0x2f, 0x43, 0x8f, 0xc3, 0x58, 0xdd, 0x23, 0xed, 0x30, - 0x50, 0x09, 0x71, 0x99, 0x34, 0xbf, 0x28, 0x60, 0x58, 0x95, 0x52, 0x1d, 0x22, 0x10, 0x47, 0x4a, - 0xe5, 0x01, 0x5b, 0x3a, 0x84, 0x3c, 0xa4, 0x38, 0x55, 0xf9, 0x0b, 0x2b, 0x4a, 0xa8, 0x05, 0x23, - 0x3e, 0x33, 0x54, 0x88, 0x70, 0x53, 0x0b, 0xd6, 0x11, 0x6e, 0xf8, 0x90, 0xc1, 0xa6, 0x8c, 0xf5, - 0x0b, 0x1a, 0xfa, 0x59, 0x73, 0xec, 0x68, 0xce, 0x9a, 0xc7, 0x61, 0xac, 0xd6, 0xf4, 0x5b, 0xf5, - 0x88, 0x04, 0x95, 0x29, 0xa6, 0xb1, 0xb3, 0x99, 0x58, 0x10, 0x30, 0xac, 0x4a, 0xd1, 0xdf, 0x80, - 0x89, 0xb0, 0x9b, 0x30, 0xd6, 0x42, 0xe7, 0x29, 0xae, 0x1c, 0x67, 0xd5, 0x59, 0xf0, 0xd0, 0x9a, - 0x5e, 0x80, 0xcd, 0x7a, 0x94, 0xc5, 0x37, 0xc3, 0x98, 0xa5, 0xf6, 0x61, 0x2c, 0xfe, 0xb4, 0xc9, - 0xe2, 0x2f, 0x6a, 0x65, 0xd8, 0xa8, 0x89, 0xbe, 0xe2, 0xc0, 0xf1, 0x76, 0x56, 0x81, 0xab, 0x9c, - 0x61, 0x33, 0x53, 0xb5, 0x21, 0xe8, 0x67, 0x50, 0xf3, 0xb0, 0xef, 0x1e, 0x30, 0xee, 0xed, 0x04, - 0x4b, 0xb2, 0x15, 0xef, 0x06, 0xb5, 0x66, 0x14, 0x06, 0x66, 0xf7, 0xee, 0xb7, 0x75, 0xb5, 0x8c, - 0xed, 0xed, 0x3c, 0x12, 0xf3, 0xf7, 0xdf, 0xdc, 0x9b, 0x39, 0x95, 0x5b, 0x84, 0xf3, 0x3b, 0x35, - 0xbd, 0x08, 0xa7, 0xf3, 0xf9, 0xc3, 0xed, 0x34, 0x8e, 0x21, 0x5d, 0xe3, 0x58, 0x82, 0xfb, 0xfb, - 0x76, 0x8a, 0x9e, 0x34, 0x52, 0xda, 0x74, 0xcc, 0x93, 0xa6, 0x47, 0x3a, 0x9c, 0x84, 0x71, 0xfd, - 0xfd, 0x07, 0xf7, 0xff, 0x0e, 0x01, 0xa4, 0x76, 0x72, 0xe4, 0xc1, 0x24, 0xb7, 0xc9, 0x5f, 0x5a, - 0xbc, 0xe3, 0x4b, 0xf1, 0x0b, 0x06, 0x02, 0x9c, 0x41, 0x88, 0xda, 0x80, 0x38, 0x84, 0xff, 0xbe, - 0x13, 0xdf, 0x2a, 0x73, 0x45, 0x2e, 0xf4, 0x20, 0xc1, 0x39, 0x88, 0xe9, 0x88, 0x92, 0x70, 0x9b, - 0x04, 0x57, 0xf1, 0xe5, 0x3b, 0xc9, 0xac, 0xc0, 0xbd, 0x71, 0x06, 0x02, 0x9c, 0x41, 0x88, 0x5c, - 0x18, 0x61, 0xb6, 0x19, 0x19, 0xa0, 0xcd, 0xd8, 0x0b, 0x93, 0x34, 0x62, 0x2c, 0x4a, 0xd0, 0x97, - 0x1c, 0x98, 0x94, 0x09, 0x22, 0x98, 0x35, 0x54, 0x86, 0x66, 0x5f, 0xb5, 0xe5, 0xe7, 0xb8, 0xa0, - 0x63, 0x4f, 0x03, 0x1f, 0x0d, 0x70, 0x8c, 0x33, 0x9d, 0x70, 0x5f, 0x80, 0x13, 0x39, 0xcd, 0xad, - 0x68, 0xb4, 0xdf, 0x76, 0xa0, 0xac, 0xe5, 0x2d, 0x44, 0xaf, 0x43, 0x29, 0xac, 0x5a, 0x8f, 0xb6, - 0x5b, 0xab, 0xf6, 0x44, 0xdb, 0x29, 0x10, 0x4e, 0x09, 0x0e, 0x12, 0x24, 0x98, 0x9b, 0x64, 0xf1, - 0x1e, 0x77, 0xfb, 0xc0, 0x41, 0x82, 0xbf, 0x52, 0x84, 0x14, 0xd3, 0x01, 0x13, 0x97, 0xa4, 0x21, - 0x85, 0x85, 0x7d, 0x43, 0x0a, 0xeb, 0x70, 0xcc, 0x63, 0xbe, 0xe4, 0x3b, 0x4c, 0x57, 0xc2, 0xd3, - 0xd6, 0x9a, 0x18, 0x70, 0x16, 0x25, 0xa5, 0x12, 0xa7, 0x4d, 0x19, 0x95, 0xe1, 0x03, 0x53, 0xa9, - 0x9a, 0x18, 0x70, 0x16, 0x25, 0x7a, 0x09, 0x2a, 0x35, 0x76, 0xfd, 0x96, 0x8f, 0xf1, 0xd2, 0xd6, - 0x95, 0x30, 0x59, 0x8f, 0x48, 0x4c, 0x82, 0x44, 0x24, 0x26, 0x7b, 0x58, 0xcc, 0x42, 0x65, 0xa1, - 0x4f, 0x3d, 0xdc, 0x17, 0x03, 0x55, 0x53, 0x98, 0x33, 0xda, 0x4f, 0x76, 0x19, 0x13, 0x11, 0x5e, - 0x7a, 0xa5, 0xa6, 0x54, 0xf5, 0x42, 0x6c, 0xd6, 0x45, 0xbf, 0xec, 0xc0, 0x44, 0x4b, 0x9a, 0xeb, - 0x71, 0xb7, 0x25, 0xb3, 0x6c, 0x62, 0x2b, 0xcb, 0xef, 0xb2, 0x8e, 0x99, 0xcb, 0x12, 0x06, 0x08, - 0x9b, 0xb4, 0xb3, 0xb9, 0x63, 0xc6, 0x06, 0xcc, 0x1d, 0xf3, 0x3d, 0x07, 0xa6, 0xb2, 0xd4, 0xd0, - 0x36, 0x3c, 0xd4, 0xf6, 0xa2, 0xed, 0x4b, 0xc1, 0x56, 0xc4, 0x2e, 0x62, 0x24, 0x7c, 0x31, 0xcc, - 0x6d, 0x25, 0x24, 0x5a, 0xf4, 0x76, 0xb9, 0xfb, 0xb3, 0xa8, 0x9e, 0x69, 0x7a, 0x68, 0x75, 0xbf, - 0xca, 0x78, 0x7f, 0x5c, 0xa8, 0x0a, 0xa7, 0x68, 0x05, 0x96, 0x5a, 0xce, 0x0f, 0x83, 0x94, 0x48, - 0x81, 0x11, 0x51, 0xc1, 0x80, 0xab, 0x79, 0x95, 0x70, 0x7e, 0x5b, 0xf7, 0x02, 0x8c, 0xf0, 0x7b, - 0x71, 0x77, 0xe5, 0x3f, 0x72, 0xff, 0x7d, 0x01, 0xa4, 0x60, 0xf8, 0x57, 0xdb, 0x1d, 0x47, 0x0f, + 0x06, 0x39, 0x9c, 0xa5, 0x8f, 0xbe, 0xe4, 0x74, 0xeb, 0xb6, 0x9e, 0xfd, 0xb3, 0x2c, 0x3d, 0x98, + 0xf9, 0x59, 0xb1, 0xa7, 0xca, 0x3b, 0xf5, 0x39, 0x27, 0x15, 0x22, 0xe2, 0x5e, 0xe7, 0xc0, 0x47, + 0xcd, 0x73, 0xc0, 0xa2, 0x42, 0xae, 0xf3, 0xfd, 0xcf, 0x3b, 0x30, 0x2e, 0xe1, 0x54, 0x3c, 0x8e, + 0xd1, 0x4d, 0x18, 0x91, 0x3d, 0x15, 0x5f, 0xcf, 0xa6, 0x2d, 0x40, 0x09, 0xf1, 0xaa, 0x33, 0x8a, + 0x9a, 0xfb, 0xe6, 0x10, 0xa0, 0xf4, 0xac, 0x6a, 0x87, 0xb1, 0xcf, 0x38, 0xd1, 0x01, 0x4e, 0xa1, + 0x40, 0x3b, 0x85, 0x9e, 0xb3, 0x79, 0x0a, 0xa5, 0xdd, 0x32, 0xce, 0xa3, 0x2f, 0x65, 0xf8, 0x36, + 0x3f, 0x98, 0x3e, 0x72, 0x28, 0x7c, 0x5b, 0xeb, 0xc2, 0xde, 0x1c, 0x7c, 0x5b, 0x70, 0x70, 0x7e, + 0x74, 0xfd, 0xa2, 0x5d, 0x0e, 0xae, 0xf5, 0x22, 0xcb, 0xcb, 0x23, 0xce, 0x61, 0xf9, 0xd9, 0x75, + 0xdd, 0x2a, 0x87, 0xd5, 0xa8, 0x9a, 0xbc, 0x36, 0xe2, 0xbc, 0x76, 0xc8, 0x16, 0x4d, 0x8d, 0xd7, + 0x66, 0x69, 0x2a, 0xae, 0xfb, 0x8a, 0xe4, 0xba, 0xfc, 0xd4, 0x7a, 0xde, 0x32, 0xd7, 0xd5, 0xe8, + 0x76, 0xf3, 0xdf, 0x97, 0xe1, 0x54, 0x77, 0x3d, 0x4c, 0x36, 0xd1, 0x79, 0x18, 0xad, 0x86, 0xc1, + 0xa6, 0x5f, 0x5f, 0xf1, 0xda, 0x42, 0x5f, 0x53, 0xbc, 0x68, 0x5e, 0x16, 0xe0, 0xb4, 0x0e, 0x7a, + 0x90, 0x33, 0x1e, 0x6e, 0x11, 0x29, 0x89, 0xaa, 0x03, 0xcb, 0x64, 0x87, 0x71, 0xa1, 0xf7, 0x8e, + 0x7c, 0xed, 0x9b, 0xd3, 0xf7, 0x7d, 0xe2, 0x8f, 0x1e, 0xba, 0xcf, 0xfd, 0xc3, 0x01, 0x38, 0x9b, + 0x4b, 0x53, 0x48, 0xeb, 0xbf, 0x65, 0x48, 0xeb, 0x5a, 0xb9, 0xe0, 0x22, 0xd7, 0x6d, 0x0a, 0xb2, + 0x1a, 0xfa, 0x3c, 0xb9, 0x5c, 0x2b, 0xc6, 0xf9, 0x9d, 0xa2, 0x13, 0x15, 0x78, 0x2d, 0x12, 0xb7, + 0xbd, 0x2a, 0x11, 0xa3, 0x57, 0x13, 0x75, 0x55, 0x16, 0xe0, 0xb4, 0x0e, 0x57, 0xa1, 0x37, 0xbd, + 0x4e, 0x33, 0x11, 0x86, 0x32, 0x4d, 0x85, 0x66, 0x60, 0x2c, 0xcb, 0xd1, 0x3f, 0x70, 0x00, 0x75, + 0x53, 0x15, 0x1b, 0x71, 0xfd, 0x30, 0xe6, 0x61, 0xee, 0xf4, 0x2d, 0x4d, 0x09, 0xd7, 0x46, 0x9a, + 0xd3, 0x0f, 0xed, 0x9b, 0x7e, 0x2c, 0x3d, 0x87, 0xb8, 0x72, 0xd0, 0x87, 0x0d, 0x8d, 0x99, 0x5a, + 0xaa, 0x55, 0x12, 0xc7, 0xdc, 0x1c, 0xa7, 0x9b, 0x5a, 0x18, 0x18, 0xcb, 0x72, 0x34, 0x0d, 0x45, + 0x12, 0x45, 0x61, 0x24, 0x74, 0x6d, 0xb6, 0x8c, 0x2f, 0x52, 0x00, 0xe6, 0x70, 0xf7, 0x27, 0x05, + 0x28, 0xf7, 0xd2, 0x4e, 0xd0, 0xef, 0x68, 0x7a, 0xb5, 0xd0, 0x9c, 0x84, 0xe2, 0x17, 0x1e, 0x9e, + 0x4e, 0x94, 0x55, 0x00, 0x7b, 0x68, 0xd8, 0xa2, 0x14, 0x67, 0x3b, 0x38, 0xf5, 0x65, 0x4d, 0xc3, + 0xd6, 0x51, 0xe4, 0x1c, 0xf0, 0x9b, 0xe6, 0x01, 0xbf, 0x66, 0x7b, 0x50, 0xfa, 0x31, 0xff, 0xc7, + 0x45, 0x38, 0x21, 0x4b, 0x2b, 0x84, 0x1e, 0x95, 0xcf, 0x76, 0x48, 0xb4, 0x83, 0x7e, 0xe8, 0xc0, + 0x49, 0x2f, 0x6b, 0xba, 0xf1, 0xc9, 0x21, 0x4c, 0xb4, 0x46, 0x75, 0x66, 0x36, 0x87, 0x22, 0x9f, + 0xe8, 0x0b, 0x62, 0xa2, 0x4f, 0xe6, 0x55, 0xe9, 0x61, 0x77, 0xcf, 0x1d, 0x00, 0x7a, 0x1a, 0xc6, + 0x24, 0x9c, 0x99, 0x7b, 0xf8, 0x16, 0x57, 0xc6, 0xed, 0x59, 0xad, 0x0c, 0x1b, 0x35, 0x69, 0xcb, + 0x84, 0xb4, 0xda, 0x4d, 0x2f, 0x21, 0x9a, 0xa1, 0x48, 0xb5, 0x5c, 0xd7, 0xca, 0xb0, 0x51, 0x13, + 0x3d, 0x0a, 0x43, 0x41, 0x58, 0x23, 0x97, 0x6b, 0xc2, 0x40, 0x3c, 0x21, 0xda, 0x0c, 0x5d, 0x65, + 0x50, 0x2c, 0x4a, 0xd1, 0x23, 0xa9, 0x35, 0xae, 0xc8, 0xb6, 0x50, 0x29, 0xcf, 0x12, 0x87, 0xfe, + 0x91, 0x03, 0xa3, 0xb4, 0xc5, 0xfa, 0x4e, 0x9b, 0xd0, 0xb3, 0x8d, 0x7e, 0x91, 0xda, 0xe1, 0x7c, + 0x91, 0xab, 0x92, 0x8c, 0x69, 0xea, 0x18, 0x55, 0xf0, 0x37, 0xde, 0x9a, 0x1e, 0x91, 0x3f, 0x70, + 0xda, 0xab, 0xa9, 0x25, 0xb8, 0xbf, 0xe7, 0xd7, 0xdc, 0x97, 0x2b, 0xe0, 0x6f, 0xc1, 0x84, 0xd9, + 0x89, 0x7d, 0xf9, 0x01, 0xfe, 0x85, 0xb6, 0xed, 0xf8, 0xb8, 0x04, 0x3f, 0xbb, 0x67, 0xd2, 0xac, + 0x5a, 0x0c, 0x0b, 0x62, 0xe9, 0x99, 0x8b, 0x61, 0x41, 0x2c, 0x86, 0x05, 0xf7, 0x0f, 0x9c, 0x74, + 0x6b, 0x6a, 0x62, 0x1e, 0x3d, 0x98, 0x3b, 0x51, 0x53, 0x30, 0x62, 0x75, 0x30, 0x5f, 0xc3, 0x57, + 0x30, 0x85, 0xa3, 0x2f, 0x6b, 0xdc, 0x91, 0x36, 0xeb, 0x08, 0xb7, 0x86, 0x25, 0x13, 0xbd, 0x81, + 0xb8, 0x9b, 0xff, 0x89, 0x02, 0x9c, 0xed, 0x82, 0xfb, 0xa5, 0x02, 0x3c, 0xb8, 0xa7, 0xd0, 0x9a, + 0xdb, 0x71, 0xe7, 0x9e, 0x77, 0x9c, 0x1e, 0x6b, 0x11, 0x69, 0x87, 0xd7, 0xf0, 0x15, 0xf1, 0xbd, + 0xd4, 0xb1, 0x86, 0x39, 0x18, 0xcb, 0x72, 0x2a, 0x3a, 0x6c, 0x91, 0x9d, 0xc5, 0x30, 0x6a, 0x79, + 0x89, 0xe0, 0x0e, 0x4a, 0x74, 0x58, 0x96, 0x05, 0x38, 0xad, 0xe3, 0xfe, 0xd0, 0x81, 0x6c, 0x07, + 0x90, 0x07, 0x13, 0x9d, 0x98, 0x44, 0xf4, 0x48, 0xad, 0x90, 0x6a, 0x44, 0xe4, 0xf2, 0x7c, 0x64, + 0x86, 0x7b, 0xfb, 0xe9, 0x08, 0x67, 0xaa, 0x61, 0x44, 0x66, 0xb6, 0x9f, 0x9c, 0xe1, 0x35, 0x96, + 0xc9, 0x4e, 0x85, 0x34, 0x09, 0xc5, 0x31, 0x87, 0x6e, 0xed, 0x4e, 0x4f, 0x5c, 0x33, 0x10, 0xe0, + 0x0c, 0x42, 0x4a, 0xa2, 0xed, 0xc5, 0xf1, 0x8d, 0x30, 0xaa, 0x09, 0x12, 0x85, 0x7d, 0x93, 0x58, + 0x33, 0x10, 0xe0, 0x0c, 0x42, 0xf7, 0x07, 0x54, 0x7d, 0xd4, 0xa5, 0x56, 0xf4, 0x4d, 0x2a, 0xfb, + 0x50, 0xc8, 0x5c, 0x33, 0xdc, 0x98, 0x0f, 0x83, 0xc4, 0xf3, 0x03, 0x22, 0x83, 0x05, 0xd6, 0x2d, + 0xc9, 0xc8, 0x06, 0xee, 0xd4, 0x86, 0xdf, 0x5d, 0x86, 0x73, 0xfa, 0x42, 0x65, 0x9c, 0x8d, 0x66, + 0xb8, 0x91, 0xf5, 0x02, 0xd2, 0x4a, 0x98, 0x95, 0xb8, 0x3f, 0x73, 0xe0, 0x4c, 0x0f, 0x61, 0x1c, + 0x7d, 0xd5, 0x81, 0xf1, 0x8d, 0xb7, 0xc5, 0xd8, 0xcc, 0x6e, 0xa0, 0xf7, 0xc3, 0x04, 0x05, 0xd0, + 0x93, 0x48, 0xac, 0xcd, 0x82, 0xe9, 0xa1, 0x9a, 0x33, 0x4a, 0x71, 0xa6, 0xb6, 0xfb, 0xeb, 0x05, + 0xc8, 0xa1, 0x82, 0x9e, 0x80, 0x11, 0x12, 0xd4, 0xda, 0xa1, 0x1f, 0x24, 0x82, 0x19, 0x29, 0xae, + 0x77, 0x51, 0xc0, 0xb1, 0xaa, 0x21, 0xf4, 0x0f, 0x31, 0x31, 0x85, 0x2e, 0xfd, 0x43, 0xf4, 0x3c, + 0xad, 0x83, 0xea, 0x30, 0xe9, 0x71, 0xff, 0x0a, 0x5b, 0x7b, 0x6c, 0x99, 0x0e, 0xec, 0x67, 0x99, + 0x9e, 0x64, 0xee, 0xcf, 0x0c, 0x0a, 0xdc, 0x85, 0x14, 0xbd, 0x07, 0x4a, 0x9d, 0x98, 0x54, 0x16, + 0x96, 0xe7, 0x23, 0x52, 0xe3, 0x5a, 0xb1, 0xe6, 0xf7, 0xbb, 0x96, 0x16, 0x61, 0xbd, 0x9e, 0xfb, + 0xaf, 0x1d, 0x18, 0x9e, 0xf3, 0xaa, 0x5b, 0xe1, 0xe6, 0x26, 0x9d, 0x8a, 0x5a, 0x27, 0x4a, 0x0d, + 0x5b, 0xda, 0x54, 0x2c, 0x08, 0x38, 0x56, 0x35, 0xd0, 0x3a, 0x0c, 0xf1, 0x0d, 0x2f, 0xb6, 0xdd, + 0x2f, 0x68, 0xe3, 0x51, 0x71, 0x3c, 0x6c, 0x39, 0x74, 0x12, 0xbf, 0x39, 0xc3, 0xe3, 0x78, 0x66, + 0x2e, 0x07, 0xc9, 0x6a, 0x54, 0x49, 0x22, 0x3f, 0xa8, 0xcf, 0x01, 0x3d, 0x2e, 0x16, 0x19, 0x0e, + 0x2c, 0x70, 0xd1, 0x61, 0xb4, 0xbc, 0x9b, 0x92, 0x9c, 0x60, 0x3f, 0x6a, 0x18, 0x2b, 0x69, 0x11, + 0xd6, 0xeb, 0xb9, 0x7f, 0xe8, 0xc0, 0xe8, 0x9c, 0x17, 0xfb, 0xd5, 0xbf, 0x44, 0xcc, 0xe7, 0xc3, + 0x50, 0x9c, 0xf7, 0xaa, 0x0d, 0x82, 0xae, 0x65, 0x95, 0xde, 0xd2, 0x85, 0xc7, 0xf2, 0xc8, 0x28, + 0x05, 0x58, 0xa7, 0x34, 0xde, 0x4b, 0x35, 0x76, 0xdf, 0x72, 0x60, 0x62, 0xbe, 0xe9, 0x93, 0x20, + 0x99, 0x27, 0x51, 0xc2, 0x26, 0xae, 0x0e, 0x93, 0x55, 0x05, 0x39, 0xc8, 0xd4, 0xb1, 0xd5, 0x3a, + 0x9f, 0x41, 0x81, 0xbb, 0x90, 0xa2, 0x1a, 0x1c, 0xe3, 0xb0, 0x74, 0x57, 0xec, 0x6b, 0xfe, 0x98, + 0x75, 0x74, 0xde, 0xc4, 0x80, 0xb3, 0x28, 0xdd, 0x9f, 0x3a, 0x70, 0x66, 0xbe, 0xd9, 0x89, 0x13, + 0x12, 0x5d, 0x17, 0xdc, 0x48, 0x8a, 0xb7, 0xe8, 0xa3, 0x30, 0xd2, 0x92, 0x1e, 0x5b, 0xe7, 0x0e, + 0x0b, 0x98, 0xf1, 0x33, 0x5a, 0x9b, 0x76, 0x66, 0x75, 0xe3, 0x25, 0x52, 0x4d, 0x56, 0x48, 0xe2, + 0xa5, 0xe1, 0x05, 0x29, 0x0c, 0x2b, 0xac, 0xa8, 0x0d, 0x83, 0x71, 0x9b, 0x54, 0xed, 0x45, 0x77, + 0xc9, 0x31, 0x54, 0xda, 0xa4, 0x9a, 0xf2, 0x75, 0xe6, 0x6b, 0x64, 0x94, 0xdc, 0xff, 0xe3, 0xc0, + 0xd9, 0x1e, 0xe3, 0xbd, 0xe2, 0xc7, 0x09, 0x7a, 0xb1, 0x6b, 0xcc, 0x33, 0xfd, 0x8d, 0x99, 0xb6, + 0x66, 0x23, 0x56, 0x0c, 0x41, 0x42, 0xb4, 0xf1, 0x7e, 0x0c, 0x8a, 0x7e, 0x42, 0x5a, 0xd2, 0x0c, + 0x6d, 0xc1, 0x60, 0xd4, 0x63, 0x2c, 0x73, 0xe3, 0x32, 0xc6, 0xef, 0x32, 0xa5, 0x87, 0x39, 0x59, + 0x77, 0x0b, 0x86, 0xe6, 0xc3, 0x66, 0xa7, 0x15, 0xf4, 0x17, 0x29, 0x93, 0xec, 0xb4, 0x49, 0xf6, + 0x8c, 0x64, 0xe2, 0x3f, 0x2b, 0x91, 0x86, 0xa3, 0x81, 0x7c, 0xc3, 0x91, 0xfb, 0x6f, 0x1c, 0xa0, + 0xbb, 0xaa, 0xe6, 0x0b, 0x4f, 0x22, 0x47, 0xc7, 0x09, 0x3e, 0xa8, 0xa3, 0xbb, 0xbd, 0x3b, 0x3d, + 0xae, 0x2a, 0x6a, 0xf8, 0x3f, 0x0c, 0x43, 0x31, 0x53, 0xc9, 0x45, 0x1f, 0x16, 0xa5, 0xfc, 0xcc, + 0x15, 0xf5, 0xdb, 0xbb, 0xd3, 0x7d, 0x85, 0x6d, 0xce, 0x28, 0xdc, 0xc2, 0xe9, 0x29, 0xb0, 0x52, + 0x81, 0xaf, 0x45, 0xe2, 0xd8, 0xab, 0x4b, 0x0d, 0x4f, 0x09, 0x7c, 0x2b, 0x1c, 0x8c, 0x65, 0xb9, + 0xfb, 0x15, 0x07, 0xc6, 0xd5, 0xe1, 0x45, 0xc5, 0x77, 0x74, 0x55, 0x3f, 0xe6, 0xf8, 0x4a, 0x79, + 0xb0, 0x07, 0xc7, 0x11, 0x07, 0xf9, 0xde, 0xa7, 0xe0, 0xbb, 0x61, 0xac, 0x46, 0xda, 0x24, 0xa8, + 0x91, 0xa0, 0x4a, 0xd5, 0x6f, 0xba, 0x42, 0x46, 0xe7, 0x26, 0xa9, 0xbe, 0xb9, 0xa0, 0xc1, 0xb1, + 0x51, 0xcb, 0xfd, 0x96, 0x03, 0xf7, 0x2b, 0x74, 0x15, 0x92, 0x60, 0x92, 0x44, 0x3b, 0x2a, 0x4c, + 0x73, 0x7f, 0xa7, 0xd5, 0x75, 0x2a, 0xff, 0x26, 0x11, 0x27, 0x7e, 0xb0, 0xe3, 0xaa, 0xc4, 0xa5, + 0x65, 0x86, 0x04, 0x4b, 0x6c, 0xee, 0xaf, 0x0e, 0xc0, 0x49, 0xbd, 0x93, 0x8a, 0xc1, 0x7c, 0xd2, + 0x01, 0x50, 0x33, 0x40, 0x0f, 0xe4, 0x01, 0x3b, 0xbe, 0x2b, 0xe3, 0x4b, 0xa5, 0x2c, 0x48, 0x81, + 0x63, 0xac, 0x91, 0x45, 0xcf, 0xc3, 0xd8, 0x36, 0xdd, 0x14, 0x64, 0x85, 0x8a, 0x0b, 0x71, 0x79, + 0x80, 0x75, 0x63, 0x3a, 0xef, 0x63, 0x3e, 0x97, 0xd6, 0x4b, 0xcd, 0x01, 0x1a, 0x30, 0xc6, 0x06, + 0x2a, 0xaa, 0xe9, 0x8c, 0x47, 0xfa, 0x27, 0x11, 0x36, 0xf1, 0x0f, 0x59, 0x1c, 0x63, 0xf6, 0xab, + 0xcf, 0x1d, 0xbf, 0xb5, 0x3b, 0x3d, 0x6e, 0x80, 0xb0, 0xd9, 0x09, 0xf7, 0x79, 0x60, 0x73, 0xe1, + 0x07, 0x1d, 0xb2, 0x1a, 0xa0, 0x87, 0xa5, 0x8d, 0x8e, 0xfb, 0x55, 0x14, 0xe7, 0xd0, 0xed, 0x74, + 0x54, 0x97, 0xdd, 0xf4, 0xfc, 0x26, 0x0b, 0x5f, 0xa4, 0xb5, 0x94, 0x2e, 0xbb, 0xc8, 0xa0, 0x58, + 0x94, 0xba, 0x33, 0x30, 0x3c, 0x4f, 0xc7, 0x4e, 0x22, 0x8a, 0x57, 0x8f, 0x3a, 0x1e, 0x37, 0xa2, + 0x8e, 0x65, 0x74, 0xf1, 0x3a, 0x9c, 0x9a, 0x8f, 0x88, 0x97, 0x90, 0xca, 0x53, 0x73, 0x9d, 0xea, + 0x16, 0x49, 0x78, 0x68, 0x57, 0x8c, 0xde, 0x07, 0xe3, 0x21, 0x3b, 0x32, 0xae, 0x84, 0xd5, 0x2d, + 0x3f, 0xa8, 0x0b, 0x93, 0xeb, 0x29, 0x81, 0x65, 0x7c, 0x55, 0x2f, 0xc4, 0x66, 0x5d, 0xf7, 0x3f, + 0x17, 0x60, 0x6c, 0x3e, 0x0a, 0x03, 0xc9, 0x16, 0x8f, 0xe0, 0x28, 0x4b, 0x8c, 0xa3, 0xcc, 0x82, + 0xbb, 0x53, 0xef, 0x7f, 0xaf, 0xe3, 0x0c, 0xbd, 0xa6, 0x58, 0xe4, 0x80, 0x2d, 0x15, 0xc4, 0xa0, + 0xcb, 0x70, 0xa7, 0x1f, 0xdb, 0x64, 0xa0, 0xee, 0x7f, 0x71, 0x60, 0x52, 0xaf, 0x7e, 0x04, 0x27, + 0x68, 0x6c, 0x9e, 0xa0, 0x57, 0xed, 0x8e, 0xb7, 0xc7, 0xb1, 0xf9, 0x2f, 0x87, 0xcd, 0x71, 0x32, + 0x5f, 0xf7, 0xd7, 0x1c, 0x18, 0xbb, 0xa1, 0x01, 0xc4, 0x60, 0x6d, 0x0b, 0x31, 0xef, 0x90, 0x6c, + 0x46, 0x87, 0xde, 0xce, 0xfc, 0xc6, 0x46, 0x4f, 0x28, 0xdf, 0x8f, 0xab, 0x0d, 0x52, 0xeb, 0x34, + 0xe5, 0xf1, 0xad, 0xa6, 0xb4, 0x22, 0xe0, 0x58, 0xd5, 0x40, 0x2f, 0xc2, 0xf1, 0x6a, 0x18, 0x54, + 0x3b, 0x51, 0x44, 0x82, 0xea, 0xce, 0x1a, 0xbb, 0x23, 0x21, 0x0e, 0xc4, 0x19, 0xd1, 0xec, 0xf8, + 0x7c, 0xb6, 0xc2, 0xed, 0x3c, 0x20, 0xee, 0x46, 0xc4, 0x9d, 0x05, 0x31, 0x3d, 0xb2, 0x84, 0xc2, + 0xa5, 0x39, 0x0b, 0x18, 0x18, 0xcb, 0x72, 0x74, 0x0d, 0xce, 0xc4, 0x89, 0x17, 0x25, 0x7e, 0x50, + 0x5f, 0x20, 0x5e, 0xad, 0xe9, 0x07, 0x54, 0x95, 0x08, 0x83, 0x1a, 0x77, 0x25, 0x0e, 0xcc, 0x9d, + 0xbd, 0xb5, 0x3b, 0x7d, 0xa6, 0x92, 0x5f, 0x05, 0xf7, 0x6a, 0x8b, 0x3e, 0x0c, 0x53, 0xc2, 0x1d, + 0xb1, 0xd9, 0x69, 0x3e, 0x13, 0x6e, 0xc4, 0x97, 0xfc, 0x98, 0xea, 0xf1, 0x57, 0xfc, 0x96, 0x9f, + 0x30, 0x87, 0x61, 0x71, 0xee, 0xdc, 0xad, 0xdd, 0xe9, 0xa9, 0x4a, 0xcf, 0x5a, 0x78, 0x0f, 0x0c, + 0x08, 0xc3, 0x69, 0xce, 0xfc, 0xba, 0x70, 0x0f, 0x33, 0xdc, 0x53, 0xb7, 0x76, 0xa7, 0x4f, 0x2f, + 0xe6, 0xd6, 0xc0, 0x3d, 0x5a, 0xd2, 0x2f, 0x98, 0xf8, 0x2d, 0xf2, 0x4a, 0x18, 0x10, 0x16, 0xa8, + 0xa2, 0x7d, 0xc1, 0x75, 0x01, 0xc7, 0xaa, 0x06, 0x7a, 0x29, 0x5d, 0x89, 0x74, 0xbb, 0x88, 0x80, + 0x93, 0xfd, 0x73, 0x38, 0xa6, 0x9a, 0x5c, 0xd7, 0x30, 0xb1, 0x48, 0x4a, 0x03, 0x37, 0xfa, 0x94, + 0x03, 0x63, 0x71, 0x12, 0xaa, 0x7b, 0x0d, 0x22, 0xe2, 0xc4, 0xc2, 0xb2, 0xaf, 0x68, 0x58, 0xb9, + 0xe0, 0xa3, 0x43, 0xb0, 0x41, 0x15, 0xbd, 0x13, 0x46, 0xe5, 0x02, 0x8e, 0xcb, 0x25, 0x26, 0x2b, + 0x31, 0x35, 0x4e, 0xae, 0xef, 0x18, 0xa7, 0xe5, 0xee, 0x4f, 0x06, 0x00, 0x75, 0xb3, 0x35, 0xb4, + 0x0c, 0x43, 0x5e, 0x35, 0xf1, 0xb7, 0x65, 0x34, 0xe1, 0xc3, 0x79, 0x47, 0x3e, 0x9f, 0x1e, 0x4c, + 0x36, 0x09, 0x5d, 0xd5, 0x24, 0xe5, 0x85, 0xb3, 0xac, 0x29, 0x16, 0x28, 0x50, 0x08, 0xc7, 0x9b, + 0x5e, 0x9c, 0x48, 0xfa, 0x35, 0xfa, 0x99, 0xc4, 0x61, 0xf0, 0xf3, 0xfd, 0x7d, 0x08, 0xda, 0x62, + 0xee, 0x14, 0xdd, 0x6d, 0x57, 0xb2, 0x88, 0x70, 0x37, 0x6e, 0xf4, 0x71, 0x26, 0x3b, 0x71, 0xc1, + 0x56, 0x0a, 0x2d, 0xcb, 0x56, 0xe4, 0x0a, 0x8e, 0xd3, 0x90, 0x9b, 0x04, 0x19, 0xac, 0x91, 0x44, + 0xe7, 0x61, 0x94, 0xed, 0x0a, 0x52, 0x23, 0x7c, 0x6f, 0x0f, 0xa4, 0x22, 0x6e, 0x45, 0x16, 0xe0, + 0xb4, 0x8e, 0x26, 0x43, 0xf0, 0xed, 0xdc, 0x43, 0x86, 0x40, 0x4f, 0x43, 0xb1, 0xdd, 0xf0, 0x62, + 0x19, 0xa1, 0xee, 0x4a, 0x9e, 0xbc, 0x46, 0x81, 0x8c, 0xf1, 0x68, 0xdf, 0x92, 0x01, 0x31, 0x6f, + 0xe0, 0xfe, 0x5b, 0x80, 0xe1, 0x85, 0xd9, 0xa5, 0x75, 0x2f, 0xde, 0xea, 0x43, 0xc3, 0xa1, 0x9b, + 0x4c, 0x88, 0xa2, 0x59, 0x36, 0x29, 0x45, 0x54, 0xac, 0x6a, 0xa0, 0x00, 0x86, 0xfc, 0x80, 0xf2, + 0x95, 0xf2, 0x84, 0x2d, 0x2f, 0x82, 0xd2, 0xd6, 0x98, 0x99, 0xe7, 0x32, 0xc3, 0x8e, 0x05, 0x15, + 0xf4, 0x1a, 0x8c, 0x7a, 0xf2, 0x82, 0x90, 0x38, 0xdd, 0x97, 0x6d, 0x98, 0xc7, 0x05, 0x4a, 0x3d, + 0x40, 0x49, 0x80, 0x70, 0x4a, 0x10, 0x7d, 0xc2, 0x81, 0x92, 0x1c, 0x3a, 0x26, 0x9b, 0xc2, 0x73, + 0xbd, 0x62, 0x6f, 0xcc, 0x98, 0x6c, 0xf2, 0xe8, 0x15, 0x0d, 0x80, 0x75, 0x92, 0x5d, 0x1a, 0x51, + 0xb1, 0x1f, 0x8d, 0x08, 0xdd, 0x80, 0xd1, 0x1b, 0x7e, 0xd2, 0x60, 0xe7, 0xb7, 0xf0, 0x98, 0x2d, + 0xde, 0x7d, 0xaf, 0x29, 0xba, 0x74, 0xc6, 0xae, 0x4b, 0x02, 0x38, 0xa5, 0x45, 0xb7, 0x03, 0xfd, + 0xc1, 0x2e, 0x58, 0x31, 0xce, 0x3f, 0x6a, 0x36, 0x60, 0x05, 0x38, 0xad, 0x43, 0xa7, 0x78, 0x8c, + 0xfe, 0xaa, 0x90, 0x97, 0x3b, 0x94, 0xb5, 0x88, 0x88, 0x44, 0x0b, 0xeb, 0x4a, 0x62, 0xe4, 0x93, + 0x75, 0x5d, 0xa3, 0x81, 0x0d, 0x8a, 0x74, 0x8f, 0xdc, 0x68, 0x90, 0x40, 0xdc, 0x98, 0x50, 0x7b, + 0xe4, 0x7a, 0x83, 0x04, 0x98, 0x95, 0xa0, 0xd7, 0xb8, 0x86, 0xc6, 0x55, 0x05, 0xc1, 0xeb, 0xaf, + 0xd8, 0xd1, 0x5e, 0x38, 0x4e, 0x7e, 0x69, 0x21, 0xfd, 0x8d, 0x35, 0x7a, 0x94, 0x63, 0x84, 0xc1, + 0xc5, 0x9b, 0x7e, 0x22, 0xae, 0x5a, 0x28, 0x8e, 0xb1, 0xca, 0xa0, 0x58, 0x94, 0xf2, 0xc8, 0x0c, + 0xba, 0x08, 0x62, 0x76, 0xaf, 0x62, 0x54, 0x8f, 0xcc, 0x60, 0x60, 0x2c, 0xcb, 0xd1, 0x3f, 0x74, + 0xa0, 0xd8, 0x08, 0xc3, 0xad, 0xb8, 0x3c, 0xce, 0x16, 0x87, 0x05, 0x89, 0x59, 0x70, 0x9c, 0x99, + 0x4b, 0x14, 0xad, 0x79, 0x79, 0xac, 0xc8, 0x60, 0xb7, 0x77, 0xa7, 0x27, 0xae, 0xf8, 0x9b, 0xa4, + 0xba, 0x53, 0x6d, 0x12, 0x06, 0x79, 0xe3, 0x2d, 0x0d, 0x72, 0x71, 0x9b, 0x04, 0x09, 0xe6, 0xbd, + 0x9a, 0xfa, 0xbc, 0x03, 0x90, 0x22, 0xca, 0x71, 0x81, 0x12, 0x33, 0x68, 0xc0, 0x82, 0xba, 0x6c, + 0x74, 0x4d, 0xf7, 0xa9, 0xfe, 0x3b, 0x07, 0x4a, 0x74, 0x70, 0x92, 0x05, 0x3e, 0x0a, 0x43, 0x89, + 0x17, 0xd5, 0x89, 0x74, 0x03, 0xa8, 0xcf, 0xb1, 0xce, 0xa0, 0x58, 0x94, 0xa2, 0x00, 0x8a, 0x89, + 0x17, 0x6f, 0x49, 0x21, 0xfd, 0xb2, 0xb5, 0x29, 0x4e, 0xe5, 0x73, 0xfa, 0x2b, 0xc6, 0x9c, 0x0c, + 0x7a, 0x0c, 0x46, 0xe8, 0xd1, 0xb1, 0xe8, 0xc5, 0x32, 0x32, 0x67, 0x8c, 0x32, 0xf1, 0x45, 0x01, + 0xc3, 0xaa, 0xd4, 0xfd, 0xf5, 0x02, 0x0c, 0x2e, 0x70, 0x75, 0x6d, 0x28, 0x0e, 0x3b, 0x51, 0x95, + 0x08, 0xb1, 0xdd, 0xc2, 0x9a, 0xa6, 0x78, 0x2b, 0x0c, 0xa7, 0xa6, 0x30, 0xb1, 0xdf, 0x58, 0xd0, + 0x42, 0x5f, 0x76, 0x60, 0x22, 0x89, 0xbc, 0x20, 0xde, 0x64, 0x0e, 0x17, 0x3f, 0x0c, 0xc4, 0x14, + 0x59, 0x58, 0x85, 0xeb, 0x06, 0xde, 0x4a, 0x42, 0xda, 0xa9, 0xdf, 0xc7, 0x2c, 0xc3, 0x99, 0x3e, + 0xb8, 0xbf, 0xe1, 0x00, 0xa4, 0xbd, 0x47, 0x9f, 0x73, 0x60, 0xdc, 0xd3, 0x23, 0x42, 0xc5, 0x1c, + 0xad, 0xda, 0xf3, 0xce, 0x32, 0xb4, 0xdc, 0x52, 0x61, 0x80, 0xb0, 0x49, 0xd8, 0x7d, 0x0f, 0x14, + 0xd9, 0xee, 0x60, 0x2a, 0x8d, 0xb0, 0x6c, 0x67, 0x4d, 0x59, 0xd2, 0xe2, 0x8d, 0x55, 0x0d, 0xf7, + 0x45, 0x98, 0xb8, 0x78, 0x93, 0x54, 0x3b, 0x49, 0x18, 0x71, 0xbb, 0x7e, 0x8f, 0x1b, 0x40, 0xce, + 0x81, 0x6e, 0x00, 0x7d, 0xd7, 0x81, 0x92, 0x16, 0x1e, 0x48, 0x4f, 0xea, 0xfa, 0x7c, 0x85, 0x9b, + 0x2f, 0xc4, 0x54, 0x2d, 0x5b, 0x09, 0x40, 0xe4, 0x28, 0xd3, 0x63, 0x44, 0x81, 0x70, 0x4a, 0xf0, + 0x0e, 0xe1, 0x7b, 0xee, 0xef, 0x3b, 0x70, 0x2a, 0x37, 0x96, 0xf1, 0x1e, 0x77, 0xdb, 0x70, 0xa1, + 0x17, 0xfa, 0x70, 0xa1, 0xff, 0xb6, 0x03, 0x29, 0x26, 0xca, 0x8a, 0x36, 0xd2, 0x9e, 0x6b, 0xac, + 0x48, 0x50, 0x12, 0xa5, 0xe8, 0x35, 0x38, 0x63, 0x7e, 0xc1, 0x03, 0x7a, 0x53, 0xb8, 0xea, 0x99, + 0x8f, 0x09, 0xf7, 0x22, 0xe1, 0x7e, 0xdd, 0x81, 0xe2, 0x92, 0xd7, 0xa9, 0x93, 0xbe, 0x8c, 0x61, + 0x94, 0x8f, 0x45, 0xc4, 0x6b, 0x26, 0x52, 0x75, 0x10, 0x7c, 0x0c, 0x0b, 0x18, 0x56, 0xa5, 0x68, + 0x16, 0x46, 0xc3, 0x36, 0x31, 0x3c, 0x80, 0x0f, 0xcb, 0xd9, 0x5b, 0x95, 0x05, 0xf4, 0xd8, 0x61, + 0xd4, 0x15, 0x04, 0xa7, 0xad, 0xdc, 0x1f, 0x16, 0xa1, 0xa4, 0xdd, 0x7a, 0xa1, 0xb2, 0x40, 0x44, + 0xda, 0x61, 0x56, 0x5e, 0xa6, 0x0b, 0x06, 0xb3, 0x12, 0xba, 0x07, 0x23, 0xb2, 0xed, 0xc7, 0x9c, + 0x6d, 0x19, 0x7b, 0x10, 0x0b, 0x38, 0x56, 0x35, 0xd0, 0x34, 0x14, 0x6b, 0xa4, 0x9d, 0x34, 0x58, + 0xf7, 0x06, 0x79, 0xe8, 0xdf, 0x02, 0x05, 0x60, 0x0e, 0xa7, 0x15, 0x36, 0x49, 0x52, 0x6d, 0x30, + 0xbb, 0xaf, 0x88, 0x0d, 0x5c, 0xa4, 0x00, 0xcc, 0xe1, 0x39, 0x3e, 0xca, 0xe2, 0xe1, 0xfb, 0x28, + 0x87, 0x2c, 0xfb, 0x28, 0x51, 0x1b, 0x4e, 0xc4, 0x71, 0x63, 0x2d, 0xf2, 0xb7, 0xbd, 0x84, 0xa4, + 0xab, 0x6f, 0x78, 0x3f, 0x74, 0xce, 0xb0, 0x7b, 0xe8, 0x95, 0x4b, 0x59, 0x2c, 0x38, 0x0f, 0x35, + 0xaa, 0xc0, 0x29, 0x3f, 0x88, 0x49, 0xb5, 0x13, 0x91, 0xcb, 0xf5, 0x20, 0x8c, 0xc8, 0xa5, 0x30, + 0xa6, 0xe8, 0xc4, 0x2d, 0x5a, 0x15, 0x2d, 0x7b, 0x39, 0xaf, 0x12, 0xce, 0x6f, 0x8b, 0x96, 0xe0, + 0x78, 0xcd, 0x8f, 0xbd, 0x8d, 0x26, 0xa9, 0x74, 0x36, 0x5a, 0x21, 0x57, 0xbc, 0x47, 0x19, 0xc2, + 0xfb, 0xa5, 0x95, 0x68, 0x21, 0x5b, 0x01, 0x77, 0xb7, 0x41, 0x4f, 0xc3, 0x58, 0xec, 0x07, 0xf5, + 0x26, 0x99, 0x8b, 0xbc, 0xa0, 0xda, 0x10, 0xd7, 0x6f, 0x95, 0x35, 0xbd, 0xa2, 0x95, 0x61, 0xa3, + 0x26, 0xdb, 0xf3, 0xbc, 0x4d, 0x46, 0x1a, 0x14, 0xb5, 0x45, 0xa9, 0xfb, 0x23, 0x07, 0xc6, 0xf4, + 0x48, 0x75, 0x2a, 0x69, 0x43, 0x63, 0x61, 0xb1, 0xc2, 0xcf, 0x02, 0x7b, 0x27, 0xfe, 0x25, 0x85, + 0x33, 0x55, 0x96, 0x53, 0x18, 0xd6, 0x68, 0xf6, 0x71, 0xef, 0xfc, 0x61, 0x28, 0x6e, 0x86, 0x54, + 0x20, 0x19, 0x30, 0xcd, 0xf0, 0x8b, 0x14, 0x88, 0x79, 0x99, 0xfb, 0xbf, 0x1c, 0x38, 0x9d, 0x1f, + 0x84, 0xff, 0x76, 0x18, 0xe4, 0x05, 0x00, 0x3a, 0x14, 0x83, 0xa9, 0x6b, 0x99, 0x27, 0x64, 0x09, + 0xd6, 0x6a, 0xf5, 0x37, 0xec, 0x3f, 0xa5, 0x42, 0x71, 0x4a, 0xe7, 0x0b, 0x0e, 0x8c, 0x53, 0xb2, + 0xcb, 0xd1, 0x86, 0x31, 0xda, 0x55, 0x3b, 0xa3, 0x55, 0x68, 0x53, 0x6f, 0x83, 0x01, 0xc6, 0x26, + 0x71, 0xf4, 0x4e, 0x18, 0xf5, 0x6a, 0xb5, 0x88, 0xc4, 0xb1, 0xf2, 0xdb, 0x31, 0x5b, 0xd4, 0xac, + 0x04, 0xe2, 0xb4, 0x9c, 0x32, 0xd1, 0x46, 0x6d, 0x33, 0xa6, 0x7c, 0x49, 0x30, 0x6e, 0xc5, 0x44, + 0x29, 0x11, 0x0a, 0xc7, 0xaa, 0x86, 0xfb, 0x2b, 0x83, 0x60, 0xd2, 0x46, 0x35, 0x38, 0xb6, 0x15, + 0x6d, 0xcc, 0xb3, 0xb0, 0x87, 0x83, 0x84, 0x1f, 0xb0, 0xb0, 0x80, 0x65, 0x13, 0x03, 0xce, 0xa2, + 0x14, 0x54, 0x96, 0xc9, 0x4e, 0xe2, 0x6d, 0x1c, 0x38, 0xf8, 0x60, 0xd9, 0xc4, 0x80, 0xb3, 0x28, + 0xd1, 0x7b, 0xa0, 0xb4, 0x15, 0x6d, 0x48, 0x16, 0x9d, 0x8d, 0x64, 0x59, 0x4e, 0x8b, 0xb0, 0x5e, + 0x8f, 0x4e, 0xe1, 0x56, 0xb4, 0x41, 0x4f, 0x45, 0x99, 0x87, 0x41, 0x4d, 0xe1, 0xb2, 0x80, 0x63, + 0x55, 0x03, 0xb5, 0x01, 0x6d, 0xc9, 0xd9, 0x53, 0x41, 0x1e, 0xe2, 0x24, 0xe9, 0x3f, 0x46, 0x84, + 0x45, 0xd7, 0x2f, 0x77, 0xe1, 0xc1, 0x39, 0xb8, 0xd1, 0xf3, 0x70, 0x66, 0x2b, 0xda, 0x10, 0xc2, + 0xc2, 0x5a, 0xe4, 0x07, 0x55, 0xbf, 0x6d, 0xe4, 0x5c, 0x98, 0x16, 0xdd, 0x3d, 0xb3, 0x9c, 0x5f, + 0x0d, 0xf7, 0x6a, 0xef, 0xfe, 0xce, 0x20, 0xb0, 0xdb, 0xa2, 0x94, 0x17, 0xb6, 0x48, 0xd2, 0x08, + 0x6b, 0x59, 0xf9, 0x67, 0x85, 0x41, 0xb1, 0x28, 0x95, 0x31, 0xa4, 0x85, 0x1e, 0x31, 0xa4, 0x37, + 0x60, 0xb8, 0x41, 0xbc, 0x1a, 0x89, 0xa4, 0x05, 0xf1, 0x8a, 0x9d, 0xfb, 0xad, 0x97, 0x18, 0xd2, + 0x54, 0x0d, 0xe7, 0xbf, 0x63, 0x2c, 0xa9, 0xa1, 0xf7, 0xc2, 0x04, 0x15, 0x64, 0xc2, 0x4e, 0x22, + 0x4d, 0xfc, 0xdc, 0x82, 0xc8, 0x4e, 0xd4, 0x75, 0xa3, 0x04, 0x67, 0x6a, 0xa2, 0x05, 0x98, 0x14, + 0xe6, 0x78, 0x65, 0x99, 0x14, 0x13, 0xab, 0x92, 0x61, 0x54, 0x32, 0xe5, 0xb8, 0xab, 0x05, 0x8b, + 0x01, 0x0c, 0x6b, 0xdc, 0x23, 0xab, 0xc7, 0x00, 0x86, 0xb5, 0x1d, 0xcc, 0x4a, 0xd0, 0x2b, 0x30, + 0x42, 0xff, 0x2e, 0x46, 0x61, 0x4b, 0xd8, 0x66, 0xd6, 0xec, 0xcc, 0x0e, 0xa5, 0x21, 0x34, 0x45, + 0x26, 0xe0, 0xcd, 0x09, 0x2a, 0x58, 0xd1, 0xa3, 0xfa, 0x8a, 0x3c, 0x87, 0x2b, 0x5b, 0x7e, 0xfb, + 0x39, 0x12, 0xf9, 0x9b, 0x3b, 0x4c, 0x68, 0x18, 0x49, 0xf5, 0x95, 0xcb, 0x5d, 0x35, 0x70, 0x4e, + 0x2b, 0xf7, 0x0b, 0x05, 0x18, 0xd3, 0x2f, 0x1d, 0xdf, 0x29, 0xb0, 0x38, 0x4e, 0x17, 0x05, 0xd7, + 0x4e, 0x2f, 0x59, 0x18, 0xf6, 0x9d, 0x16, 0x44, 0x03, 0x06, 0xbd, 0x8e, 0x90, 0x16, 0xad, 0x18, + 0xc1, 0xd8, 0x88, 0x3b, 0x49, 0x83, 0xdf, 0x4e, 0x63, 0x21, 0xbf, 0x8c, 0x82, 0xfb, 0xe9, 0x01, + 0x18, 0x91, 0x85, 0xe8, 0x53, 0x0e, 0x40, 0x1a, 0x7a, 0x25, 0x58, 0xe9, 0x9a, 0x8d, 0xb8, 0x1c, + 0x3d, 0x6a, 0x4c, 0xb3, 0xa5, 0x2b, 0x38, 0xd6, 0xe8, 0xa2, 0x04, 0x86, 0x42, 0xda, 0xb9, 0x0b, + 0xf6, 0x2e, 0xce, 0xaf, 0x52, 0xc2, 0x17, 0x18, 0xf5, 0xd4, 0x6c, 0xc6, 0x60, 0x58, 0xd0, 0xa2, + 0x1a, 0xe0, 0x86, 0x8c, 0x08, 0xb4, 0x67, 0x62, 0x56, 0x41, 0x86, 0xa9, 0x42, 0xa7, 0x40, 0x38, + 0x25, 0xe8, 0x3e, 0x09, 0x13, 0xe6, 0x66, 0xa0, 0x1a, 0xc1, 0xc6, 0x4e, 0x42, 0xb8, 0xbd, 0x61, + 0x8c, 0x6b, 0x04, 0x73, 0x14, 0x80, 0x39, 0xdc, 0xfd, 0x01, 0x95, 0x03, 0x14, 0x7b, 0xe9, 0xc3, + 0xc4, 0xff, 0xb0, 0x6e, 0x2c, 0xeb, 0xa5, 0x76, 0x7d, 0x1c, 0x46, 0xd9, 0x3f, 0x6c, 0xa3, 0x0f, + 0xd8, 0xf2, 0xdf, 0xa7, 0xfd, 0x14, 0x5b, 0x9d, 0xc9, 0x04, 0xcf, 0x49, 0x42, 0x38, 0xa5, 0xe9, + 0x86, 0x30, 0x99, 0xad, 0x8d, 0x3e, 0x04, 0x63, 0xb1, 0x3c, 0x56, 0xd3, 0x2b, 0x74, 0x7d, 0x1e, + 0xbf, 0xdc, 0x7b, 0xa6, 0x35, 0xc7, 0x06, 0x32, 0x77, 0x15, 0x86, 0xac, 0x4e, 0xa1, 0xfb, 0x1d, + 0x07, 0x46, 0x99, 0x03, 0xb3, 0x1e, 0x79, 0xad, 0xb4, 0xc9, 0xc0, 0x1e, 0xb3, 0x1e, 0xc3, 0x30, + 0xd7, 0xd1, 0x65, 0xe0, 0x8f, 0x05, 0x2e, 0xc3, 0xf3, 0xdd, 0xa5, 0x5c, 0x86, 0x1b, 0x03, 0x62, + 0x2c, 0x29, 0xb9, 0x9f, 0x29, 0xc0, 0xd0, 0xe5, 0xa0, 0xdd, 0xf9, 0x2b, 0x9f, 0x73, 0x6d, 0x05, + 0x06, 0x2f, 0x27, 0xa4, 0x65, 0xa6, 0x06, 0x1c, 0x9b, 0x7b, 0x44, 0x4f, 0x0b, 0x58, 0x36, 0xd3, + 0x02, 0x62, 0xef, 0x86, 0x8c, 0x8b, 0x13, 0x36, 0xe2, 0xf4, 0x1a, 0xe1, 0x13, 0x30, 0x7a, 0xc5, + 0xdb, 0x20, 0xcd, 0x65, 0xb2, 0xc3, 0x2e, 0xfd, 0xf1, 0x18, 0x0d, 0x27, 0x55, 0xec, 0x8d, 0x78, + 0x8a, 0x05, 0x98, 0x60, 0xb5, 0xd5, 0x66, 0xa0, 0x9a, 0x03, 0x49, 0xf3, 0x2a, 0x39, 0xa6, 0xe6, + 0xa0, 0xe5, 0x54, 0xd2, 0x6a, 0xb9, 0x33, 0x50, 0x4a, 0xb1, 0xf4, 0x41, 0xf5, 0x67, 0x05, 0x18, + 0x37, 0x4c, 0xdd, 0x86, 0x03, 0xd0, 0xb9, 0xa3, 0x03, 0xd0, 0x70, 0xc8, 0x15, 0xee, 0xb5, 0x43, + 0x6e, 0xe0, 0xe8, 0x1d, 0x72, 0xe6, 0x47, 0x1a, 0xec, 0xeb, 0x23, 0x35, 0x61, 0xf0, 0x8a, 0x1f, + 0x6c, 0xf5, 0xc7, 0x67, 0xe2, 0x6a, 0xd8, 0xee, 0xe2, 0x33, 0x15, 0x0a, 0xc4, 0xbc, 0x4c, 0x4a, + 0x2e, 0x03, 0xf9, 0x92, 0x8b, 0xfb, 0x29, 0x07, 0xc6, 0x56, 0xbc, 0xc0, 0xdf, 0x24, 0x71, 0xc2, + 0xd6, 0x55, 0x72, 0xa8, 0x97, 0xbf, 0xc6, 0x7a, 0xa4, 0x31, 0x78, 0xc3, 0x81, 0xe3, 0x2b, 0xa4, + 0x15, 0xfa, 0xaf, 0x78, 0x69, 0xd8, 0x29, 0xed, 0x7b, 0xc3, 0x4f, 0x44, 0x94, 0x9d, 0xea, 0xfb, + 0x25, 0x3f, 0xc1, 0x14, 0x7e, 0x07, 0x3b, 0x2e, 0xbb, 0x56, 0x41, 0x15, 0x34, 0xed, 0x42, 0x62, + 0x1a, 0x50, 0x2a, 0x0b, 0x70, 0x5a, 0xc7, 0xfd, 0x5d, 0x07, 0x86, 0x79, 0x27, 0x54, 0xa4, 0xae, + 0xd3, 0x03, 0x77, 0x03, 0x8a, 0xac, 0x9d, 0x58, 0xd5, 0x4b, 0x16, 0xc4, 0x1f, 0x8a, 0x8e, 0xef, + 0x41, 0xf6, 0x2f, 0xe6, 0x04, 0x98, 0xda, 0xe2, 0xdd, 0x9c, 0x55, 0x11, 0xb7, 0xa9, 0xda, 0xc2, + 0xa0, 0x58, 0x94, 0xba, 0xdf, 0x18, 0x80, 0x11, 0x95, 0xbd, 0x8b, 0xe5, 0x56, 0x08, 0x82, 0x30, + 0xf1, 0x78, 0xac, 0x03, 0xe7, 0xd5, 0x1f, 0xb2, 0x97, 0x3d, 0x6c, 0x66, 0x36, 0xc5, 0xce, 0xfd, + 0x77, 0x4a, 0x09, 0xd5, 0x4a, 0xb0, 0xde, 0x09, 0xf4, 0x31, 0x18, 0x6a, 0x52, 0xee, 0x23, 0x59, + 0xf7, 0x73, 0x16, 0xbb, 0xc3, 0xd8, 0x9a, 0xe8, 0x89, 0x9a, 0x21, 0x0e, 0xc4, 0x82, 0xea, 0xd4, + 0xfb, 0x61, 0x32, 0xdb, 0xeb, 0x3b, 0xdd, 0x97, 0x1c, 0xd5, 0x6f, 0x5b, 0xfe, 0x4d, 0xc1, 0x3d, + 0xf7, 0xdf, 0xd4, 0x7d, 0x16, 0x4a, 0x2b, 0x24, 0x89, 0xfc, 0x2a, 0x43, 0x70, 0xa7, 0xc5, 0xd5, + 0x97, 0xfc, 0xf0, 0x59, 0xb6, 0x58, 0x29, 0xce, 0x18, 0xbd, 0x06, 0xd0, 0x8e, 0x42, 0xaa, 0xbf, + 0x92, 0x8e, 0xfc, 0xd8, 0x16, 0xe4, 0xe1, 0x35, 0x85, 0x93, 0xbb, 0x9c, 0xd3, 0xdf, 0x58, 0xa3, + 0xe7, 0xbe, 0x00, 0xc5, 0x95, 0x4e, 0x42, 0x6e, 0xf6, 0xc1, 0xb1, 0xf6, 0x9b, 0x40, 0xc0, 0xfd, + 0x10, 0x8c, 0x31, 0xdc, 0x97, 0xc2, 0x26, 0x3d, 0x56, 0xe9, 0xd4, 0xb4, 0xe8, 0xef, 0xac, 0x53, + 0x80, 0x55, 0xc2, 0xbc, 0x8c, 0x6e, 0x99, 0x46, 0xd8, 0xac, 0xa9, 0xcb, 0x54, 0x6a, 0x41, 0x5c, + 0x62, 0x50, 0x2c, 0x4a, 0xdd, 0x4f, 0x16, 0xa0, 0xc4, 0x1a, 0x0a, 0x76, 0xb3, 0x03, 0xc3, 0x0d, + 0x4e, 0x47, 0xcc, 0xa1, 0x85, 0x10, 0x2d, 0xbd, 0xf7, 0x9a, 0x2e, 0xc7, 0x01, 0x58, 0xd2, 0xa3, + 0xa4, 0x6f, 0x78, 0x7e, 0x42, 0x49, 0x17, 0x0e, 0x97, 0xf4, 0x75, 0x4e, 0x06, 0x4b, 0x7a, 0xee, + 0x2f, 0x01, 0xbb, 0xa4, 0xbc, 0xd8, 0xf4, 0xea, 0x7c, 0xe6, 0xc2, 0x2d, 0x52, 0x13, 0x3c, 0x57, + 0x9b, 0x39, 0x0a, 0xc5, 0xa2, 0x94, 0x5f, 0xfc, 0x4c, 0x22, 0x5f, 0x05, 0x37, 0x6b, 0x17, 0x3f, + 0x19, 0x58, 0x86, 0xb2, 0xd7, 0xdc, 0xaf, 0x14, 0x00, 0x58, 0xae, 0x37, 0x7e, 0xb7, 0xf8, 0x17, + 0x64, 0xa4, 0x92, 0xe9, 0x48, 0x54, 0x91, 0x4a, 0xec, 0xf6, 0xb4, 0x1e, 0xa1, 0xa4, 0xdf, 0x39, + 0x28, 0xec, 0x7d, 0xe7, 0x00, 0xb5, 0x61, 0x38, 0xec, 0x24, 0x54, 0x56, 0x15, 0x87, 0xbd, 0x05, + 0x3f, 0xfa, 0x2a, 0x47, 0xc8, 0x03, 0xf5, 0xc5, 0x0f, 0x2c, 0xc9, 0xa0, 0xa7, 0x61, 0xa4, 0x1d, + 0x85, 0x75, 0x7a, 0x76, 0x8b, 0xe3, 0xfd, 0x01, 0x29, 0x0f, 0xad, 0x09, 0xf8, 0x6d, 0xed, 0x7f, + 0xac, 0x6a, 0xbb, 0x7f, 0x34, 0xc9, 0xe7, 0x45, 0xac, 0xbd, 0x29, 0x28, 0xf8, 0xd2, 0x32, 0x05, + 0x02, 0x45, 0xe1, 0xf2, 0x02, 0x2e, 0xf8, 0x35, 0xb5, 0xaf, 0x0a, 0x3d, 0xf7, 0xd5, 0x7b, 0xa0, + 0x54, 0xf3, 0xe3, 0x76, 0xd3, 0xdb, 0xb9, 0x9a, 0x63, 0x16, 0x5c, 0x48, 0x8b, 0xb0, 0x5e, 0x0f, + 0x3d, 0x21, 0x6e, 0x98, 0x0c, 0x1a, 0xa6, 0x20, 0x79, 0xc3, 0x24, 0xbd, 0xbb, 0xce, 0x2f, 0x97, + 0x64, 0xef, 0xf8, 0x17, 0xfb, 0xbe, 0xe3, 0x9f, 0x95, 0xc4, 0x86, 0x8e, 0x5e, 0x12, 0x7b, 0x1f, + 0x8c, 0xcb, 0x9f, 0x4c, 0x3c, 0x2a, 0x9f, 0x64, 0xbd, 0x57, 0xe6, 0xea, 0x75, 0xbd, 0x10, 0x9b, + 0x75, 0xd3, 0x45, 0x3b, 0xdc, 0xef, 0xa2, 0xbd, 0x00, 0xb0, 0x11, 0x76, 0x82, 0x9a, 0x17, 0xed, + 0x5c, 0x5e, 0x10, 0xf1, 0xa8, 0x4a, 0xf0, 0x9b, 0x53, 0x25, 0x58, 0xab, 0xa5, 0x2f, 0xf4, 0xd1, + 0x3b, 0x2c, 0xf4, 0x0f, 0xc1, 0x28, 0x8b, 0xdd, 0x25, 0xb5, 0xd9, 0x44, 0x84, 0x18, 0xed, 0x27, + 0x64, 0x32, 0x0d, 0x3a, 0x94, 0x48, 0x70, 0x8a, 0x0f, 0x7d, 0x18, 0x60, 0xd3, 0x0f, 0xfc, 0xb8, + 0xc1, 0xb0, 0x97, 0xf6, 0x8d, 0x5d, 0x8d, 0x73, 0x51, 0x61, 0xc1, 0x1a, 0x46, 0xf4, 0x22, 0x1c, + 0x27, 0x71, 0xe2, 0xb7, 0xbc, 0x84, 0xd4, 0xd4, 0x9d, 0xcc, 0x32, 0xb3, 0x65, 0xaa, 0xe8, 0xe9, + 0x8b, 0xd9, 0x0a, 0xb7, 0xf3, 0x80, 0xb8, 0x1b, 0x91, 0xb1, 0x23, 0xa7, 0xf6, 0xb3, 0x23, 0xd1, + 0x9f, 0x39, 0x70, 0x3c, 0x22, 0x3c, 0xee, 0x24, 0x56, 0x1d, 0x3b, 0xc5, 0xd8, 0x71, 0xd5, 0x46, + 0x1a, 0x75, 0x95, 0x2f, 0x05, 0x67, 0xa9, 0x70, 0xc1, 0x85, 0xc8, 0xd1, 0x77, 0x95, 0xdf, 0xce, + 0x03, 0xbe, 0xf1, 0xd6, 0xf4, 0x74, 0x77, 0x3a, 0x7f, 0x85, 0x9c, 0xee, 0xbc, 0xbf, 0xfb, 0xd6, + 0xf4, 0xa4, 0xfc, 0x9d, 0x4e, 0x5a, 0xd7, 0x20, 0xe9, 0xb1, 0xda, 0x0e, 0x6b, 0x97, 0xd7, 0x44, + 0x2c, 0x98, 0x3a, 0x56, 0xd7, 0x28, 0x10, 0xf3, 0x32, 0xf4, 0x18, 0x8c, 0xd4, 0x3c, 0xd2, 0x0a, + 0x03, 0x95, 0x10, 0x97, 0x49, 0xf3, 0x0b, 0x02, 0x86, 0x55, 0x29, 0xd5, 0x21, 0x02, 0x71, 0xa4, + 0x94, 0xcf, 0xda, 0xd2, 0x21, 0xe4, 0x21, 0xc5, 0xa9, 0xca, 0x5f, 0x58, 0x51, 0x42, 0x4d, 0x18, + 0xf2, 0x99, 0xa1, 0x42, 0x84, 0x9b, 0x5a, 0xb0, 0x8e, 0x70, 0xc3, 0x87, 0x0c, 0x36, 0x65, 0xac, + 0x5f, 0xd0, 0xd0, 0xcf, 0x9a, 0x63, 0x47, 0x73, 0xd6, 0x3c, 0x06, 0x23, 0xd5, 0x86, 0xdf, 0xac, + 0x45, 0x24, 0x28, 0x4f, 0x32, 0x8d, 0x9d, 0xcd, 0xc4, 0xbc, 0x80, 0x61, 0x55, 0x8a, 0xfe, 0x06, + 0x8c, 0x87, 0x9d, 0x84, 0xb1, 0x16, 0x3a, 0x4f, 0x71, 0xf9, 0x38, 0xab, 0xce, 0x82, 0x87, 0x56, + 0xf5, 0x02, 0x6c, 0xd6, 0xa3, 0x2c, 0xbe, 0x11, 0xc6, 0x2c, 0xb5, 0x0f, 0x63, 0xf1, 0xa7, 0x4d, + 0x16, 0x7f, 0x49, 0x2b, 0xc3, 0x46, 0x4d, 0xf4, 0x35, 0x07, 0x8e, 0xb7, 0xb2, 0x0a, 0x5c, 0xf9, + 0x0c, 0x9b, 0x99, 0x8a, 0x0d, 0x41, 0x3f, 0x83, 0x9a, 0x87, 0x7d, 0x77, 0x81, 0x71, 0x77, 0x27, + 0x58, 0x92, 0xad, 0x78, 0x27, 0xa8, 0x36, 0xa2, 0x30, 0x30, 0xbb, 0x77, 0xbf, 0xad, 0xab, 0x65, + 0x6c, 0x6f, 0xe7, 0x91, 0x98, 0xbb, 0xff, 0xd6, 0xee, 0xf4, 0xa9, 0xdc, 0x22, 0x9c, 0xdf, 0xa9, + 0xa9, 0x05, 0x38, 0x9d, 0xcf, 0x1f, 0xee, 0xa4, 0x71, 0x0c, 0xe8, 0x1a, 0xc7, 0x22, 0xdc, 0xdf, + 0xb3, 0x53, 0xf4, 0xa4, 0x91, 0xd2, 0xa6, 0x63, 0x9e, 0x34, 0x5d, 0xd2, 0xe1, 0x04, 0x8c, 0xe9, + 0xef, 0x3f, 0xb8, 0xff, 0x6f, 0x00, 0x20, 0xb5, 0x93, 0x23, 0x0f, 0x26, 0xb8, 0x4d, 0xfe, 0xf2, + 0xc2, 0x81, 0x2f, 0xc5, 0xcf, 0x1b, 0x08, 0x70, 0x06, 0x21, 0x6a, 0x01, 0xe2, 0x10, 0xfe, 0xfb, + 0x20, 0xbe, 0x55, 0xe6, 0x8a, 0x9c, 0xef, 0x42, 0x82, 0x73, 0x10, 0xd3, 0x11, 0x25, 0xe1, 0x16, + 0x09, 0xae, 0xe1, 0x2b, 0x07, 0xc9, 0xac, 0xc0, 0xbd, 0x71, 0x06, 0x02, 0x9c, 0x41, 0x88, 0x5c, + 0x18, 0x62, 0xb6, 0x19, 0x19, 0xa0, 0xcd, 0xd8, 0x0b, 0x93, 0x34, 0x62, 0x2c, 0x4a, 0xd0, 0x57, + 0x1c, 0x98, 0x90, 0x09, 0x22, 0x98, 0x35, 0x54, 0x86, 0x66, 0x5f, 0xb3, 0xe5, 0xe7, 0xb8, 0xa8, + 0x63, 0x4f, 0x03, 0x1f, 0x0d, 0x70, 0x8c, 0x33, 0x9d, 0x70, 0x9f, 0x87, 0x13, 0x39, 0xcd, 0xad, + 0x68, 0xb4, 0xdf, 0x75, 0xa0, 0xa4, 0xe5, 0x2d, 0x44, 0xaf, 0xc1, 0x68, 0x58, 0xb1, 0x1e, 0x6d, + 0xb7, 0x5a, 0xe9, 0x8a, 0xb6, 0x53, 0x20, 0x9c, 0x12, 0xec, 0x27, 0x48, 0x30, 0x37, 0xc9, 0xe2, + 0x3d, 0xee, 0xf6, 0xbe, 0x83, 0x04, 0x7f, 0xa5, 0x08, 0x29, 0xa6, 0x7d, 0x26, 0x2e, 0x49, 0x43, + 0x0a, 0x0b, 0x7b, 0x86, 0x14, 0xd6, 0xe0, 0x98, 0xc7, 0x7c, 0xc9, 0x07, 0x4c, 0x57, 0xc2, 0xd3, + 0xd6, 0x9a, 0x18, 0x70, 0x16, 0x25, 0xa5, 0x12, 0xa7, 0x4d, 0x19, 0x95, 0xc1, 0x7d, 0x53, 0xa9, + 0x98, 0x18, 0x70, 0x16, 0x25, 0x7a, 0x11, 0xca, 0x55, 0x76, 0xfd, 0x96, 0x8f, 0xf1, 0xf2, 0xe6, + 0xd5, 0x30, 0x59, 0x8b, 0x48, 0x4c, 0x82, 0x44, 0x24, 0x26, 0x7b, 0x48, 0xcc, 0x42, 0x79, 0xbe, + 0x47, 0x3d, 0xdc, 0x13, 0x03, 0x55, 0x53, 0x98, 0x33, 0xda, 0x4f, 0x76, 0x18, 0x13, 0x11, 0x5e, + 0x7a, 0xa5, 0xa6, 0x54, 0xf4, 0x42, 0x6c, 0xd6, 0x45, 0xbf, 0xec, 0xc0, 0x78, 0x53, 0x9a, 0xeb, + 0x71, 0xa7, 0x29, 0xb3, 0x6c, 0x62, 0x2b, 0xcb, 0xef, 0x8a, 0x8e, 0x99, 0xcb, 0x12, 0x06, 0x08, + 0x9b, 0xb4, 0xb3, 0xb9, 0x63, 0x46, 0xfa, 0xcc, 0x1d, 0xf3, 0x03, 0x07, 0x26, 0xb3, 0xd4, 0xd0, + 0x16, 0x3c, 0xd8, 0xf2, 0xa2, 0xad, 0xcb, 0xc1, 0x66, 0xc4, 0x2e, 0x62, 0x24, 0x7c, 0x31, 0xcc, + 0x6e, 0x26, 0x24, 0x5a, 0xf0, 0x76, 0xb8, 0xfb, 0xb3, 0xa8, 0x9e, 0x69, 0x7a, 0x70, 0x65, 0xaf, + 0xca, 0x78, 0x6f, 0x5c, 0xa8, 0x02, 0xa7, 0x68, 0x05, 0x96, 0x5a, 0xce, 0x0f, 0x83, 0x94, 0x48, + 0x81, 0x11, 0x51, 0xc1, 0x80, 0x2b, 0x79, 0x95, 0x70, 0x7e, 0x5b, 0xf7, 0x22, 0x0c, 0xf1, 0x7b, + 0x71, 0x77, 0xe5, 0x3f, 0x72, 0xff, 0x43, 0x01, 0xa4, 0x60, 0xf8, 0x57, 0xdb, 0x1d, 0x47, 0x0f, 0xd1, 0x88, 0x99, 0x94, 0x84, 0xb5, 0x83, 0x1d, 0xa2, 0x22, 0x89, 0xa3, 0x28, 0xa1, 0x12, 0x33, - 0xb9, 0xe1, 0x27, 0x0b, 0x61, 0x5d, 0xda, 0x38, 0x98, 0xc4, 0x7c, 0x41, 0xc0, 0xb0, 0x2a, 0x75, - 0x3f, 0xe1, 0xc0, 0x04, 0x1d, 0x65, 0xab, 0x45, 0x5a, 0xd5, 0x84, 0x74, 0x62, 0x14, 0x43, 0x31, - 0xa6, 0xff, 0xd8, 0x33, 0x05, 0xa6, 0x77, 0x29, 0x49, 0x47, 0x73, 0xd6, 0x50, 0x22, 0x98, 0xd3, - 0x72, 0xdf, 0x1a, 0x82, 0x92, 0x9a, 0xec, 0x01, 0xec, 0xa9, 0xe7, 0xd3, 0xfc, 0xaa, 0x9c, 0x03, - 0x57, 0xb4, 0xdc, 0xaa, 0xb7, 0xe8, 0xd4, 0x05, 0xbb, 0x3c, 0xd1, 0x44, 0x9a, 0x68, 0xf5, 0x49, - 0xd3, 0xd5, 0x7c, 0x5a, 0x5f, 0x7f, 0x5a, 0x7d, 0xe1, 0x73, 0xbe, 0xa1, 0x7b, 0xfa, 0x87, 0x6d, - 0x9d, 0x66, 0xca, 0x8d, 0xd9, 0xdf, 0xc5, 0x9f, 0x79, 0x7a, 0xa7, 0x38, 0xd0, 0xd3, 0x3b, 0x4f, - 0xc0, 0x30, 0x09, 0xba, 0x6d, 0x26, 0x2a, 0x95, 0x98, 0x8a, 0x30, 0x7c, 0x21, 0xe8, 0xb6, 0xcd, - 0x91, 0xb1, 0x2a, 0xe8, 0x7d, 0x50, 0xae, 0x93, 0xb8, 0x16, 0xf9, 0x2c, 0x7b, 0x82, 0xb0, 0xec, - 0x3c, 0xc8, 0xcc, 0x65, 0x29, 0xd8, 0x6c, 0xa8, 0x37, 0x70, 0x5f, 0x85, 0x91, 0xf5, 0x56, 0xb7, - 0xe1, 0x07, 0xa8, 0x03, 0x23, 0x3c, 0x97, 0x82, 0x38, 0xed, 0x2d, 0xe8, 0x9d, 0x9c, 0x55, 0x68, - 0x51, 0x28, 0xfc, 0x4a, 0xad, 0xa0, 0xe3, 0xfe, 0x76, 0x01, 0xa8, 0x6a, 0xbe, 0xbc, 0x80, 0xfe, - 0x76, 0xcf, 0x4b, 0x33, 0x3f, 0x93, 0xf3, 0xd2, 0xcc, 0x04, 0xab, 0x9c, 0xf3, 0xc8, 0x4c, 0x0b, - 0x26, 0x98, 0x73, 0x44, 0x9e, 0x81, 0x42, 0xac, 0x7e, 0x7a, 0xc0, 0xf4, 0x03, 0x7a, 0x53, 0x71, - 0x22, 0xe8, 0x20, 0x6c, 0x22, 0x47, 0xbb, 0x70, 0x82, 0xa7, 0xe9, 0x5c, 0x24, 0x2d, 0x6f, 0xd7, - 0x48, 0xc7, 0x35, 0x70, 0xca, 0x03, 0xd9, 0x8a, 0x07, 0x78, 0x2f, 0xf6, 0xa2, 0xc3, 0x79, 0x34, - 0xdc, 0xdf, 0x1f, 0x06, 0xcd, 0x7d, 0x31, 0xc0, 0xce, 0x7a, 0x25, 0xe3, 0xac, 0x5a, 0xb5, 0xe2, - 0xac, 0x92, 0x1e, 0x20, 0xce, 0xad, 0x4c, 0xff, 0x14, 0xed, 0x54, 0x93, 0xb4, 0x3a, 0x62, 0x5f, - 0xaa, 0x4e, 0x5d, 0x24, 0xad, 0x0e, 0x66, 0x25, 0xea, 0xf2, 0xe1, 0x70, 0xdf, 0xcb, 0x87, 0x4d, - 0x28, 0x36, 0xbc, 0x6e, 0x83, 0x88, 0x68, 0x4d, 0x0b, 0x7e, 0x49, 0x76, 0x1d, 0x82, 0xfb, 0x25, - 0xd9, 0xbf, 0x98, 0x13, 0xa0, 0x8c, 0xa1, 0x29, 0xc3, 0x57, 0x84, 0x41, 0xd7, 0x02, 0x63, 0x50, - 0x11, 0x31, 0x9c, 0x31, 0xa8, 0x9f, 0x38, 0x25, 0x86, 0x3a, 0x30, 0x5a, 0xe3, 0x09, 0x53, 0x84, - 0x7c, 0x73, 0xc9, 0xc6, 0xed, 0x4a, 0x86, 0x90, 0x5b, 0x5e, 0xc4, 0x0f, 0x2c, 0xc9, 0xb8, 0xe7, - 0xa0, 0xac, 0x3d, 0x8e, 0x41, 0x3f, 0x83, 0xca, 0xd5, 0xa1, 0x7d, 0x86, 0x45, 0x2f, 0xf1, 0x30, - 0x2b, 0x71, 0xbf, 0x31, 0x0c, 0xca, 0xee, 0xa6, 0xdf, 0x05, 0xf4, 0x6a, 0x5a, 0x66, 0x21, 0xe3, - 0x5e, 0x7c, 0x18, 0x60, 0x51, 0x4a, 0x65, 0xc0, 0x36, 0x89, 0x1a, 0x4a, 0xe7, 0x16, 0xac, 0x5d, - 0xc9, 0x80, 0xab, 0x7a, 0x21, 0x36, 0xeb, 0x52, 0x01, 0xbe, 0x2d, 0xdc, 0xf9, 0xd9, 0x60, 0x69, - 0xe9, 0xe6, 0xc7, 0xaa, 0x06, 0x4b, 0x4d, 0xd0, 0xd6, 0xbc, 0xff, 0x22, 0x68, 0xd3, 0x86, 0xf3, - 0x49, 0xc3, 0xca, 0x83, 0xab, 0x74, 0x08, 0x36, 0xa8, 0xa2, 0x65, 0x38, 0x1e, 0x93, 0x64, 0xed, - 0x7a, 0x40, 0x22, 0x95, 0x36, 0x40, 0xe4, 0xbe, 0x50, 0x37, 0x25, 0xaa, 0xd9, 0x0a, 0xb8, 0xb7, - 0x4d, 0x6e, 0x9c, 0x6b, 0xf1, 0xc0, 0x71, 0xae, 0x8b, 0x30, 0xb5, 0xe5, 0xf9, 0xad, 0x6e, 0x44, - 0xfa, 0x46, 0xcb, 0x2e, 0x65, 0xca, 0x71, 0x4f, 0x0b, 0x76, 0x59, 0xa7, 0xe5, 0x35, 0xe2, 0xca, - 0xa8, 0x76, 0x59, 0x87, 0x02, 0x30, 0x87, 0xbb, 0xbf, 0xe9, 0x00, 0x4f, 0x3a, 0x34, 0xb7, 0xb5, - 0xe5, 0x07, 0x7e, 0xb2, 0x8b, 0xbe, 0xea, 0xc0, 0x54, 0x10, 0xd6, 0xc9, 0x5c, 0x90, 0xf8, 0x12, - 0x68, 0x2f, 0x13, 0x3c, 0xa3, 0x75, 0x25, 0x83, 0x9e, 0x67, 0xb0, 0xc8, 0x42, 0x71, 0x4f, 0x37, - 0xdc, 0x33, 0x70, 0x2a, 0x17, 0x81, 0xfb, 0xbd, 0x21, 0x30, 0x73, 0x27, 0xa1, 0xe7, 0xa0, 0xd8, - 0x62, 0xd9, 0x3c, 0x9c, 0x3b, 0x4c, 0x8a, 0xc5, 0xe6, 0x8a, 0xa7, 0xfb, 0xe0, 0x98, 0xd0, 0x22, - 0x94, 0x59, 0x42, 0x26, 0x91, 0x6b, 0xa5, 0x60, 0xa4, 0x39, 0x28, 0xe3, 0xb4, 0xe8, 0x96, 0xf9, - 0x13, 0xeb, 0xcd, 0xd0, 0x6b, 0x30, 0xba, 0xc9, 0xd3, 0x52, 0xda, 0xf3, 0x0f, 0x8a, 0x3c, 0x97, - 0x4c, 0x8e, 0x92, 0x49, 0x2f, 0x6f, 0xa5, 0xff, 0x62, 0x49, 0x11, 0xed, 0xc2, 0x98, 0x27, 0xbf, - 0xe9, 0xb0, 0xad, 0xcb, 0x17, 0xc6, 0xfa, 0x11, 0xd1, 0x35, 0xf2, 0x1b, 0x2a, 0x72, 0x99, 0x30, - 0xa4, 0xe2, 0x40, 0x61, 0x48, 0xdf, 0x72, 0x00, 0xd2, 0x37, 0x3c, 0xd0, 0x0d, 0x18, 0x8b, 0x9f, - 0x36, 0x8c, 0x1a, 0x36, 0x6e, 0xdd, 0x0b, 0x8c, 0xda, 0xcd, 0x54, 0x01, 0xc1, 0x8a, 0xda, 0xed, - 0x0c, 0x31, 0x3f, 0x71, 0xe0, 0x64, 0xde, 0x5b, 0x23, 0xf7, 0xb0, 0xc7, 0x07, 0xb5, 0xc1, 0x88, - 0x06, 0xeb, 0x11, 0xd9, 0xf2, 0x6f, 0xe4, 0x24, 0x47, 0xe6, 0x05, 0x38, 0xad, 0xe3, 0xbe, 0x39, - 0x0a, 0x8a, 0xf0, 0x21, 0xd9, 0x6c, 0x1e, 0xa3, 0xfa, 0x55, 0x23, 0xbd, 0x2c, 0xa9, 0xea, 0x61, - 0x06, 0xc5, 0xa2, 0x94, 0xea, 0x58, 0x32, 0x80, 0x5e, 0xb0, 0x6c, 0xb6, 0x0a, 0x65, 0xa0, 0x3d, - 0x56, 0xa5, 0x79, 0x56, 0xa0, 0xe2, 0x91, 0x58, 0x81, 0x46, 0xec, 0x5b, 0x81, 0x9e, 0x80, 0xd1, - 0x28, 0x6c, 0x91, 0x39, 0x7c, 0x45, 0x68, 0x0e, 0x69, 0x00, 0x04, 0x07, 0x63, 0x59, 0x7e, 0x87, - 0x76, 0x10, 0xf4, 0x3b, 0xce, 0x3e, 0x86, 0xa6, 0x92, 0xad, 0x33, 0x21, 0x37, 0x93, 0x1c, 0x53, - 0x83, 0xee, 0xc4, 0x7a, 0xf5, 0x35, 0x07, 0x8e, 0x93, 0xa0, 0x16, 0xed, 0x32, 0x3c, 0x02, 0x9b, - 0xf0, 0x4f, 0x5f, 0xb5, 0xb1, 0xf9, 0x2e, 0x64, 0x91, 0x73, 0x37, 0x50, 0x0f, 0x18, 0xf7, 0x76, - 0x03, 0xad, 0xc1, 0x58, 0xcd, 0x13, 0x2b, 0xa2, 0x7c, 0x90, 0x15, 0xc1, 0xbd, 0x6c, 0x73, 0x62, - 0x29, 0x28, 0x24, 0xee, 0x8f, 0x0a, 0x70, 0x22, 0xa7, 0x4b, 0xec, 0xb2, 0x55, 0x9b, 0xae, 0xc8, - 0x4b, 0xf5, 0xec, 0x7e, 0x5c, 0x11, 0x70, 0xac, 0x6a, 0xa0, 0x75, 0x38, 0xb9, 0xdd, 0x8e, 0x53, - 0x2c, 0x0b, 0x61, 0x90, 0x90, 0x1b, 0x72, 0x77, 0x4a, 0xdf, 0xf5, 0xc9, 0x95, 0x9c, 0x3a, 0x38, - 0xb7, 0x25, 0x15, 0x5f, 0x48, 0xe0, 0x6d, 0xb6, 0x48, 0x5a, 0x24, 0xae, 0x0a, 0x2a, 0xf1, 0xe5, - 0x42, 0xa6, 0x1c, 0xf7, 0xb4, 0x40, 0x9f, 0x71, 0xe0, 0x81, 0x98, 0x44, 0x3b, 0x24, 0xaa, 0xfa, - 0x75, 0xb2, 0xd0, 0x8d, 0x93, 0xb0, 0x4d, 0xa2, 0x3b, 0x34, 0xad, 0xce, 0xdc, 0xdc, 0x9b, 0x79, - 0xa0, 0xda, 0x1f, 0x1b, 0xde, 0x8f, 0x94, 0xfb, 0x19, 0x07, 0x26, 0xab, 0x4c, 0xf1, 0x56, 0xb2, - 0xb4, 0xed, 0x5c, 0xa2, 0x8f, 0xa9, 0xe4, 0x16, 0x19, 0xae, 0x68, 0xa6, 0xa3, 0x70, 0x5f, 0x86, - 0xa9, 0x2a, 0x69, 0x7b, 0x9d, 0x26, 0xbb, 0xe7, 0xcb, 0x63, 0xb7, 0xce, 0x41, 0x29, 0x96, 0xb0, - 0xec, 0xf3, 0x41, 0xaa, 0x32, 0x4e, 0xeb, 0xa0, 0x47, 0x79, 0x9c, 0x99, 0xbc, 0x2d, 0x54, 0xe2, - 0x5a, 0x07, 0x0f, 0x4e, 0x8b, 0xb1, 0x2c, 0x73, 0xdf, 0x72, 0x60, 0x3c, 0x6d, 0x4f, 0xb6, 0x50, - 0x03, 0x8e, 0xd5, 0xb4, 0x9b, 0x76, 0xe9, 0x1d, 0x87, 0xc1, 0x2f, 0xe5, 0xf1, 0x14, 0xc7, 0x26, - 0x12, 0x9c, 0xc5, 0x7a, 0xf0, 0x30, 0xbd, 0xcf, 0x17, 0xe0, 0x98, 0xea, 0xaa, 0x70, 0x32, 0xbe, - 0x91, 0x8d, 0xa6, 0xc3, 0x36, 0xd2, 0xf4, 0x98, 0x73, 0xbf, 0x4f, 0x44, 0xdd, 0x1b, 0xd9, 0x88, - 0xba, 0x43, 0x25, 0xdf, 0xe3, 0x37, 0xfd, 0x56, 0x01, 0xc6, 0x54, 0xd2, 0xa0, 0xe7, 0xa0, 0xc8, - 0x54, 0xc9, 0xbb, 0x13, 0x88, 0x99, 0x5a, 0x8a, 0x39, 0x26, 0x8a, 0x92, 0x45, 0xec, 0xdc, 0x71, - 0xe2, 0xd9, 0x12, 0x37, 0x3e, 0x7a, 0x51, 0x82, 0x39, 0x26, 0xb4, 0x02, 0x43, 0x24, 0xa8, 0x0b, - 0xc9, 0xf8, 0xe0, 0x08, 0xd9, 0x43, 0x5f, 0x17, 0x82, 0x3a, 0xa6, 0x58, 0x58, 0xe6, 0x32, 0x2e, - 0x00, 0x65, 0x9e, 0x75, 0x11, 0xd2, 0x8f, 0x28, 0x75, 0xdf, 0x0f, 0x46, 0xce, 0x3a, 0x91, 0x0b, - 0x5f, 0x28, 0x5d, 0xbd, 0x6f, 0x71, 0x09, 0x6d, 0x2b, 0xad, 0xe3, 0xfe, 0xf2, 0x10, 0x8c, 0x54, - 0xbb, 0x9b, 0x54, 0x49, 0xf8, 0xa6, 0x03, 0x27, 0xae, 0x67, 0xd2, 0x3a, 0xa7, 0x9b, 0xe4, 0xaa, - 0x3d, 0x0b, 0xae, 0x1e, 0x76, 0xf6, 0x80, 0x7c, 0xf4, 0x3e, 0xa7, 0x10, 0xe7, 0x75, 0xc7, 0xc8, - 0xac, 0x3a, 0x74, 0x28, 0x99, 0x55, 0x6f, 0x1c, 0xf2, 0xbd, 0x8b, 0x89, 0x7e, 0x77, 0x2e, 0xdc, - 0xdf, 0x2f, 0x02, 0xf0, 0xaf, 0xb1, 0xd6, 0x49, 0x06, 0xb1, 0xb3, 0x3d, 0x03, 0xe3, 0x0d, 0x12, - 0x90, 0x48, 0x06, 0x15, 0x66, 0x9e, 0x1c, 0x5a, 0xd6, 0xca, 0xb0, 0x51, 0x93, 0x29, 0x35, 0x41, - 0x12, 0xed, 0x72, 0xc1, 0x37, 0x7b, 0xb7, 0x42, 0x95, 0x60, 0xad, 0x16, 0x9a, 0x35, 0x5c, 0x26, - 0xdc, 0xfb, 0x3e, 0xb9, 0x8f, 0x87, 0xe3, 0x7d, 0x30, 0x69, 0x26, 0x2a, 0x11, 0xd2, 0x9e, 0xf2, - 0x96, 0x9b, 0xf9, 0x4d, 0x70, 0xa6, 0x36, 0xdd, 0x05, 0xf5, 0x68, 0x17, 0x77, 0x03, 0x21, 0xf6, - 0xa9, 0x5d, 0xb0, 0xc8, 0xa0, 0x58, 0x94, 0xb2, 0x0c, 0x0f, 0xec, 0x00, 0xe4, 0x70, 0x91, 0x25, - 0x22, 0xcd, 0xf0, 0xa0, 0x95, 0x61, 0xa3, 0x26, 0xa5, 0x20, 0xec, 0x94, 0x60, 0xee, 0xb3, 0x8c, - 0x71, 0xb1, 0x03, 0x93, 0xa1, 0x69, 0x5f, 0xe1, 0x32, 0xd0, 0xbb, 0x06, 0x5c, 0x7a, 0x46, 0x5b, - 0x1e, 0xe5, 0x90, 0x31, 0xc7, 0x64, 0xf0, 0x53, 0xb9, 0x57, 0xbf, 0x82, 0x30, 0x6e, 0xc6, 0xa4, - 0xf6, 0xbd, 0x25, 0xb0, 0x0e, 0x27, 0x3b, 0x61, 0x7d, 0x3d, 0xf2, 0xc3, 0xc8, 0x4f, 0x76, 0x17, - 0x5a, 0x5e, 0x1c, 0xb3, 0x85, 0x31, 0x61, 0xca, 0x43, 0xeb, 0x39, 0x75, 0x70, 0x6e, 0x4b, 0xaa, - 0xa1, 0x74, 0x04, 0x90, 0x45, 0x86, 0x15, 0xb9, 0x44, 0x27, 0x2b, 0x62, 0x55, 0xea, 0x9e, 0x80, - 0xe3, 0xd5, 0x6e, 0xa7, 0xd3, 0xf2, 0x49, 0x5d, 0xb9, 0x24, 0xdc, 0xf7, 0xc3, 0x31, 0x91, 0x77, - 0x55, 0x49, 0x1f, 0x07, 0xca, 0x12, 0xee, 0xfe, 0xb9, 0x03, 0xc7, 0x32, 0x71, 0x38, 0xe8, 0xb5, - 0xac, 0xcc, 0x60, 0x27, 0x1f, 0xa8, 0x26, 0x2d, 0x88, 0xe4, 0x9e, 0x79, 0xf2, 0x47, 0x53, 0x06, - 0xd1, 0x5b, 0xbb, 0xbc, 0xc2, 0x42, 0xcd, 0xf9, 0x91, 0xa2, 0x47, 0xe2, 0xbb, 0x9f, 0x2e, 0x40, - 0x7e, 0xf0, 0x13, 0xfa, 0x68, 0xef, 0x04, 0x3c, 0x67, 0x71, 0x02, 0x44, 0xf4, 0x55, 0xff, 0x39, - 0x08, 0xcc, 0x39, 0x58, 0xb5, 0x34, 0x07, 0x82, 0x6e, 0xef, 0x4c, 0xfc, 0x2f, 0x07, 0xca, 0x1b, - 0x1b, 0x97, 0xd5, 0x39, 0x87, 0xe1, 0x74, 0xcc, 0x2f, 0xe7, 0x33, 0x1f, 0xf1, 0x42, 0xd8, 0xee, - 0x70, 0x97, 0xb1, 0x70, 0x65, 0xb3, 0x14, 0xb8, 0xd5, 0xdc, 0x1a, 0xb8, 0x4f, 0x4b, 0x74, 0x09, - 0x4e, 0xe8, 0x25, 0x55, 0xed, 0xc5, 0xc1, 0xa2, 0x48, 0x88, 0xd3, 0x5b, 0x8c, 0xf3, 0xda, 0x64, - 0x51, 0x09, 0x73, 0x27, 0x3b, 0xae, 0x72, 0x50, 0x89, 0x62, 0x9c, 0xd7, 0xc6, 0x5d, 0x83, 0xf2, - 0x86, 0x17, 0xa9, 0x81, 0x7f, 0x00, 0xa6, 0x6a, 0x61, 0x5b, 0x9a, 0x99, 0x2e, 0x93, 0x1d, 0xd2, - 0x12, 0x43, 0xe6, 0xcf, 0x7c, 0x64, 0xca, 0x70, 0x4f, 0x6d, 0xf7, 0xbf, 0x9f, 0x05, 0x75, 0xd9, - 0x70, 0x80, 0x13, 0xa6, 0xa3, 0xc2, 0x42, 0x8b, 0x96, 0xc3, 0x42, 0x15, 0xaf, 0xcd, 0x84, 0x86, - 0x26, 0x69, 0x68, 0xe8, 0x88, 0xed, 0xd0, 0x50, 0x25, 0x71, 0xf6, 0x84, 0x87, 0x7e, 0xd9, 0x81, - 0xf1, 0x20, 0xac, 0x13, 0xe5, 0xcb, 0x1b, 0x65, 0x62, 0xef, 0x4b, 0xf6, 0xa2, 0xec, 0x79, 0x98, - 0xa3, 0x40, 0xcf, 0x43, 0x96, 0xd5, 0x11, 0xa5, 0x17, 0x61, 0xa3, 0x1f, 0x68, 0x49, 0x33, 0x7c, - 0x72, 0xff, 0xc2, 0x83, 0x79, 0xfa, 0xca, 0x6d, 0xad, 0x98, 0x37, 0x34, 0xb9, 0xa9, 0x64, 0xcb, - 0xa0, 0x27, 0x6f, 0x90, 0x69, 0x6e, 0x12, 0x99, 0xc5, 0x39, 0x95, 0xa7, 0x5c, 0x18, 0xe1, 0xb1, - 0xcd, 0x22, 0xf5, 0x12, 0xf3, 0xde, 0xf1, 0xb8, 0x67, 0x2c, 0x4a, 0x50, 0x22, 0xe3, 0x05, 0xca, - 0xb6, 0xde, 0x64, 0x30, 0xe2, 0x11, 0xf2, 0x03, 0x06, 0xd0, 0xb3, 0xba, 0x1e, 0x3c, 0x3e, 0x88, - 0x1e, 0x3c, 0xd1, 0x57, 0x07, 0xfe, 0x9c, 0x03, 0xe3, 0x35, 0xed, 0x8d, 0x84, 0xca, 0xe3, 0xb6, - 0xde, 0x82, 0xce, 0x7b, 0xca, 0x82, 0x3b, 0x85, 0x8c, 0x37, 0x19, 0x0c, 0xea, 0x2c, 0xdf, 0x24, - 0x53, 0xfa, 0xd9, 0xd1, 0x6f, 0x25, 0xc5, 0x84, 0x69, 0x44, 0x90, 0x71, 0x97, 0x14, 0x86, 0x05, - 0x2d, 0xf4, 0x3a, 0x8c, 0xc9, 0xf0, 0x78, 0x11, 0x46, 0x8e, 0x6d, 0x58, 0xe9, 0x4d, 0x57, 0xa0, - 0x4c, 0x52, 0xc7, 0xa1, 0x58, 0x51, 0x44, 0x4d, 0x18, 0xaa, 0x7b, 0x0d, 0x11, 0x50, 0xbe, 0x6a, - 0x27, 0x09, 0xa8, 0xa4, 0xc9, 0xf4, 0xb3, 0xc5, 0xb9, 0x65, 0x4c, 0x49, 0xa0, 0x1b, 0x69, 0x92, - 0xf9, 0x29, 0x6b, 0xa7, 0xaf, 0x29, 0x26, 0x71, 0xb3, 0x46, 0x4f, 0xce, 0xfa, 0xba, 0xf0, 0x9e, - 0xfe, 0x35, 0x46, 0x76, 0xc9, 0x4e, 0x16, 0x51, 0x9e, 0xb2, 0x24, 0xf5, 0xc0, 0x52, 0x2a, 0xcd, - 0x24, 0xe9, 0x54, 0x7e, 0xd6, 0x16, 0x15, 0x96, 0x78, 0x83, 0x3f, 0xdb, 0xbd, 0xb1, 0xb1, 0x8e, - 0x19, 0x76, 0xd4, 0x82, 0x91, 0x0e, 0x0b, 0x02, 0xa9, 0xfc, 0x9c, 0xad, 0xb3, 0x85, 0x07, 0x95, - 0xf0, 0xb5, 0xc9, 0xff, 0xc7, 0x82, 0x06, 0xba, 0x00, 0xa3, 0xfc, 0xad, 0x14, 0x1e, 0xd0, 0x5f, - 0x3e, 0x3f, 0xdd, 0xff, 0xc5, 0x95, 0xf4, 0xa0, 0xe0, 0xbf, 0x63, 0x2c, 0xdb, 0xa2, 0xcf, 0x3b, - 0x30, 0x49, 0x39, 0x6a, 0xfa, 0xb8, 0x4b, 0x05, 0xd9, 0xe2, 0x59, 0x57, 0x63, 0x2a, 0x91, 0x48, - 0x5e, 0xa3, 0xd4, 0xa4, 0x4b, 0x06, 0x39, 0x9c, 0x21, 0x8f, 0xde, 0x80, 0xb1, 0xd8, 0xaf, 0x93, - 0x9a, 0x17, 0xc5, 0x95, 0x13, 0x87, 0xd3, 0x95, 0xd4, 0x5f, 0x23, 0x08, 0x61, 0x45, 0x12, 0xfd, - 0x1a, 0x7b, 0x5d, 0x53, 0xbc, 0x84, 0x5f, 0xe3, 0x62, 0xfd, 0x49, 0x5b, 0x7b, 0x5f, 0x7a, 0xa6, - 0x24, 0x66, 0xe1, 0xc6, 0x30, 0xc9, 0xe1, 0x2c, 0x7d, 0xf4, 0x77, 0x1c, 0x38, 0xc5, 0xf3, 0xe4, - 0x67, 0x1f, 0x76, 0x38, 0x75, 0x87, 0xf6, 0x19, 0x76, 0x13, 0x61, 0x2e, 0x0f, 0x25, 0xce, 0xa7, - 0xc4, 0xb2, 0xda, 0x9a, 0x6f, 0xf1, 0x9c, 0xb6, 0xea, 0xb7, 0x1c, 0xfc, 0xfd, 0x1d, 0xf4, 0x14, - 0x94, 0x3b, 0xe2, 0x38, 0xf4, 0xe3, 0x36, 0xbb, 0x57, 0x32, 0xc4, 0x6f, 0xfc, 0xad, 0xa7, 0x60, - 0xac, 0xd7, 0x31, 0x52, 0x1c, 0x3f, 0xb1, 0x5f, 0x8a, 0x63, 0x74, 0x15, 0xca, 0x49, 0xd8, 0x12, - 0x59, 0x3e, 0xe3, 0x4a, 0x85, 0xad, 0xc0, 0xb3, 0x79, 0x7b, 0x6b, 0x43, 0x55, 0x4b, 0x35, 0xd9, - 0x14, 0x16, 0x63, 0x1d, 0x0f, 0x8b, 0xe5, 0x15, 0xef, 0x0f, 0x44, 0x4c, 0x85, 0xbd, 0x3f, 0x13, - 0xcb, 0xab, 0x17, 0x62, 0xb3, 0x2e, 0x5a, 0x86, 0xe3, 0x9d, 0x1e, 0x1d, 0x98, 0xdf, 0x67, 0x53, - 0x21, 0x11, 0xbd, 0x0a, 0x70, 0x6f, 0x1b, 0x43, 0xfb, 0x7d, 0x60, 0x3f, 0xed, 0xb7, 0x4f, 0xc2, - 0xdf, 0x07, 0xef, 0x24, 0xe1, 0x2f, 0xaa, 0xc3, 0x83, 0x5e, 0x37, 0x09, 0x59, 0x72, 0x19, 0xb3, - 0x09, 0x0f, 0x6b, 0x7e, 0x98, 0x47, 0x4a, 0xdf, 0xdc, 0x9b, 0x79, 0x70, 0x6e, 0x9f, 0x7a, 0x78, - 0x5f, 0x2c, 0xe8, 0x55, 0x18, 0x23, 0x22, 0x69, 0x71, 0xe5, 0x67, 0x6c, 0x09, 0x09, 0x66, 0x1a, - 0x64, 0x19, 0x31, 0xca, 0x61, 0x58, 0xd1, 0x43, 0x1b, 0x50, 0x6e, 0x86, 0x71, 0x32, 0xd7, 0xf2, - 0xbd, 0x98, 0xc4, 0x95, 0x87, 0xd8, 0xa2, 0xc9, 0x95, 0xbd, 0x2e, 0xca, 0x6a, 0xe9, 0x9a, 0xb9, - 0x98, 0xb6, 0xc4, 0x3a, 0x1a, 0x44, 0x98, 0xf7, 0x92, 0xc5, 0x74, 0x4b, 0x47, 0xd0, 0x59, 0x36, - 0xb0, 0xc7, 0xf2, 0x30, 0xaf, 0x87, 0xf5, 0xaa, 0x59, 0x5b, 0xb9, 0x2f, 0x75, 0x20, 0xce, 0xe2, - 0x44, 0xcf, 0xc0, 0x78, 0x27, 0xac, 0x57, 0x3b, 0xa4, 0xb6, 0xee, 0x25, 0xb5, 0x66, 0x65, 0xc6, - 0xb4, 0xba, 0xad, 0x6b, 0x65, 0xd8, 0xa8, 0x89, 0x3a, 0x30, 0xda, 0xe6, 0x59, 0x07, 0x2a, 0x8f, - 0xd8, 0xd2, 0x6d, 0x44, 0x1a, 0x03, 0x2e, 0x2f, 0x88, 0x1f, 0x58, 0x92, 0x41, 0xff, 0xd8, 0x81, - 0x63, 0x99, 0x9b, 0x52, 0x95, 0x77, 0x58, 0x13, 0x59, 0x4c, 0xc4, 0xf3, 0x8f, 0xb1, 0xe9, 0x33, - 0x81, 0xb7, 0x7a, 0x41, 0x38, 0xdb, 0x23, 0x3e, 0x2f, 0x2c, 0x75, 0x48, 0xe5, 0x51, 0x7b, 0xf3, - 0xc2, 0x10, 0xca, 0x79, 0x61, 0x3f, 0xb0, 0x24, 0x83, 0x9e, 0x80, 0x51, 0x91, 0xe5, 0xaf, 0xf2, - 0x98, 0xe9, 0x82, 0x16, 0xc9, 0x00, 0xb1, 0x2c, 0x9f, 0x7e, 0x3f, 0x1c, 0xef, 0x51, 0xdd, 0x0e, - 0x94, 0xbf, 0xe2, 0x37, 0x1c, 0xd0, 0xaf, 0x56, 0x5b, 0x7f, 0x29, 0xe4, 0x19, 0x18, 0xaf, 0xf1, - 0x67, 0x19, 0xf9, 0xe5, 0xec, 0x61, 0xd3, 0xfe, 0xb9, 0xa0, 0x95, 0x61, 0xa3, 0xa6, 0x7b, 0x11, - 0x50, 0x6f, 0x1a, 0xf7, 0x3b, 0x4a, 0x8e, 0xf4, 0x4f, 0x1d, 0x98, 0x30, 0x64, 0x06, 0xeb, 0x4e, - 0xc6, 0x25, 0x40, 0x6d, 0x3f, 0x8a, 0xc2, 0x48, 0x7f, 0xff, 0x4e, 0x24, 0x50, 0x60, 0x57, 0xd4, - 0x56, 0x7b, 0x4a, 0x71, 0x4e, 0x0b, 0xf7, 0xb7, 0x87, 0x21, 0x0d, 0x99, 0x56, 0x79, 0x72, 0x9d, - 0xbe, 0x79, 0x72, 0x9f, 0x84, 0xb1, 0x97, 0xe3, 0x30, 0x58, 0x4f, 0xb3, 0xe9, 0xaa, 0x6f, 0xf1, - 0x6c, 0x75, 0xed, 0x0a, 0xab, 0xa9, 0x6a, 0xb0, 0xda, 0xaf, 0x2c, 0xf9, 0xad, 0xa4, 0x37, 0xdd, - 0xea, 0xb3, 0xcf, 0x71, 0x38, 0x56, 0x35, 0xd8, 0x53, 0x78, 0x3b, 0x44, 0x19, 0xc6, 0xd3, 0xa7, - 0xf0, 0xf8, 0x0b, 0x0d, 0xac, 0x0c, 0x9d, 0x83, 0x92, 0x32, 0xaa, 0x0b, 0x4b, 0xbd, 0x9a, 0x29, - 0x65, 0x79, 0xc7, 0x69, 0x1d, 0x26, 0x10, 0x0a, 0x43, 0xac, 0x30, 0xa1, 0x54, 0x6d, 0xa8, 0x27, - 0x19, 0xd3, 0x2e, 0xe7, 0xed, 0x12, 0x8c, 0x15, 0xc9, 0x3c, 0x47, 0x6b, 0xe9, 0x50, 0x1c, 0xad, - 0x5a, 0xfc, 0x7e, 0x71, 0xd0, 0xf8, 0x7d, 0x73, 0x6d, 0x8f, 0x0d, 0xb4, 0xb6, 0x3f, 0x39, 0x04, - 0xa3, 0xcf, 0x93, 0x88, 0x65, 0x19, 0x7f, 0x02, 0x46, 0x77, 0xf8, 0xbf, 0xd9, 0xcb, 0x9f, 0xa2, - 0x06, 0x96, 0xe5, 0xf4, 0xbb, 0x6d, 0x76, 0xfd, 0x56, 0x7d, 0x31, 0xdd, 0xc5, 0x69, 0x82, 0x42, - 0x59, 0x80, 0xd3, 0x3a, 0xb4, 0x41, 0x83, 0x4a, 0xf6, 0xed, 0xb6, 0xdf, 0xf3, 0xca, 0xfb, 0xb2, - 0x2c, 0xc0, 0x69, 0x1d, 0xf4, 0x18, 0x8c, 0x34, 0xfc, 0x64, 0xc3, 0x6b, 0x64, 0xdd, 0x84, 0xcb, - 0x0c, 0x8a, 0x45, 0x29, 0x73, 0x13, 0xf9, 0xc9, 0x46, 0x44, 0x98, 0x65, 0xb7, 0x27, 0xf7, 0xc4, - 0xb2, 0x56, 0x86, 0x8d, 0x9a, 0xac, 0x4b, 0xa1, 0x18, 0x99, 0x88, 0xe2, 0x4c, 0xbb, 0x24, 0x0b, - 0x70, 0x5a, 0x87, 0xae, 0xff, 0x5a, 0xd8, 0xee, 0xf8, 0x2d, 0x11, 0x5f, 0xac, 0xad, 0xff, 0x05, - 0x01, 0xc7, 0xaa, 0x06, 0xad, 0x4d, 0x59, 0x18, 0x65, 0x3f, 0xd9, 0x67, 0xc7, 0xd6, 0x05, 0x1c, - 0xab, 0x1a, 0xee, 0xf3, 0x30, 0xc1, 0x77, 0xf2, 0x42, 0xcb, 0xf3, 0xdb, 0xcb, 0x0b, 0xe8, 0x42, - 0x4f, 0xfc, 0xfe, 0x13, 0x39, 0xf1, 0xfb, 0xa7, 0x8c, 0x46, 0xbd, 0x71, 0xfc, 0xee, 0x0f, 0x0a, - 0x30, 0x76, 0x84, 0x2f, 0x37, 0x1e, 0xf9, 0x23, 0xc4, 0xe8, 0x46, 0xe6, 0xd5, 0xc6, 0x75, 0x9b, - 0xd7, 0x71, 0xf6, 0x7d, 0xb1, 0xf1, 0xbf, 0x14, 0xe0, 0xb4, 0xac, 0x2a, 0x75, 0xb9, 0xe5, 0x05, - 0xf6, 0x5e, 0xd6, 0xe1, 0x4f, 0x74, 0x64, 0x4c, 0xf4, 0xba, 0x3d, 0x6d, 0x74, 0x79, 0xa1, 0xef, - 0x54, 0xbf, 0x9a, 0x99, 0x6a, 0x6c, 0x95, 0xea, 0xfe, 0x93, 0xfd, 0x17, 0x0e, 0x4c, 0xe7, 0x4f, - 0xf6, 0x11, 0x3c, 0x94, 0xf9, 0x86, 0xf9, 0x50, 0xe6, 0x2f, 0xd8, 0x5b, 0x62, 0xe6, 0x50, 0xfa, - 0x3c, 0x99, 0xf9, 0x67, 0x0e, 0x9c, 0x94, 0x0d, 0xd8, 0xe9, 0x39, 0xef, 0x07, 0x2c, 0x92, 0xe5, - 0xf0, 0x97, 0xd9, 0xeb, 0xc6, 0x32, 0x7b, 0xd1, 0xde, 0xc0, 0xf5, 0x71, 0xf4, 0x7d, 0x60, 0xfc, - 0x4f, 0x1d, 0xa8, 0xe4, 0x35, 0x38, 0x82, 0x4f, 0xfe, 0x9a, 0xf9, 0xc9, 0x9f, 0x3f, 0x9c, 0x91, - 0xf7, 0xff, 0xe0, 0x95, 0x7e, 0x13, 0x85, 0x5a, 0x52, 0xae, 0x72, 0x6c, 0xf9, 0x68, 0x39, 0x89, - 0x7c, 0x01, 0xad, 0x05, 0x23, 0x31, 0x8b, 0xda, 0x10, 0x4b, 0xe0, 0xa2, 0x0d, 0x69, 0x8b, 0xe2, - 0x13, 0x36, 0x76, 0xf6, 0x3f, 0x16, 0x34, 0xdc, 0xdf, 0x2c, 0xc0, 0x19, 0xf5, 0x00, 0x2e, 0xd9, - 0x21, 0xad, 0x74, 0x7f, 0xb0, 0x37, 0x19, 0x3c, 0xf5, 0xd3, 0xde, 0x9b, 0x0c, 0x29, 0x89, 0x74, - 0x2f, 0xa4, 0x30, 0xac, 0xd1, 0x44, 0x55, 0x38, 0xc5, 0xde, 0x50, 0x58, 0xf2, 0x03, 0xaf, 0xe5, - 0xbf, 0x4a, 0x22, 0x4c, 0xda, 0xe1, 0x8e, 0xd7, 0x12, 0x92, 0xba, 0xba, 0xff, 0xbb, 0x94, 0x57, - 0x09, 0xe7, 0xb7, 0xed, 0xd1, 0xb8, 0x87, 0x06, 0xd5, 0xb8, 0xdd, 0x3f, 0x71, 0x60, 0xfc, 0x08, - 0x9f, 0x0b, 0x0e, 0xcd, 0x2d, 0xf1, 0xac, 0xbd, 0x2d, 0xd1, 0x67, 0x1b, 0xec, 0x15, 0xa1, 0xe7, - 0x05, 0x55, 0xf4, 0x29, 0x47, 0xc5, 0xb5, 0xf0, 0xe0, 0xc1, 0x0f, 0xd9, 0xeb, 0xc7, 0x41, 0x92, - 0x46, 0xa2, 0xaf, 0x65, 0x32, 0x69, 0x16, 0x6c, 0xa5, 0x83, 0xea, 0xe9, 0xcd, 0x1d, 0x64, 0xd4, - 0xfc, 0xb2, 0x03, 0xc0, 0xfb, 0x29, 0x12, 0x71, 0xd3, 0xbe, 0x6d, 0x1e, 0xda, 0x4c, 0x51, 0x22, - 0xbc, 0x6b, 0x6a, 0x0b, 0xa5, 0x05, 0x58, 0xeb, 0xc9, 0x5d, 0xa4, 0xca, 0xbc, 0xeb, 0x2c, 0x9d, - 0x9f, 0x77, 0xe0, 0x58, 0xa6, 0xbb, 0x39, 0xed, 0xb7, 0xcc, 0x27, 0x01, 0x2d, 0x48, 0x56, 0x66, - 0x7a, 0x66, 0xdd, 0x78, 0xf2, 0xdf, 0x5c, 0x30, 0x9e, 0x9e, 0x46, 0xaf, 0x41, 0x49, 0x5a, 0x3e, - 0xe4, 0xf2, 0xb6, 0xf9, 0x34, 0xaa, 0x52, 0x6f, 0x24, 0x24, 0xc6, 0x29, 0xbd, 0x4c, 0xd8, 0x5c, - 0x61, 0xa0, 0xb0, 0xb9, 0x7b, 0xfb, 0xb0, 0x6a, 0xbe, 0x5d, 0x7a, 0xf8, 0x50, 0xec, 0xd2, 0x0f, - 0x5a, 0xb7, 0x4b, 0x3f, 0x74, 0xc4, 0x76, 0x69, 0xcd, 0x49, 0x58, 0xbc, 0x0b, 0x27, 0xe1, 0x6b, - 0x70, 0x72, 0x27, 0x55, 0x3a, 0xd5, 0x4a, 0x12, 0x49, 0x88, 0x9e, 0xc8, 0xb5, 0x46, 0x53, 0x05, - 0x3a, 0x4e, 0x48, 0x90, 0x68, 0xea, 0x6a, 0x1a, 0xb1, 0xf7, 0x7c, 0x0e, 0x3a, 0x9c, 0x4b, 0x24, - 0xeb, 0xed, 0x19, 0x1d, 0xc0, 0xdb, 0xf3, 0x96, 0x03, 0xa7, 0xbc, 0x9e, 0x4b, 0x60, 0x98, 0x6c, - 0x89, 0x90, 0x93, 0x6b, 0xf6, 0x44, 0x08, 0x03, 0xbd, 0x70, 0xab, 0xe5, 0x15, 0xe1, 0xfc, 0x0e, - 0xa1, 0x47, 0x53, 0xd7, 0x3b, 0x8f, 0xf3, 0xcc, 0xf7, 0x93, 0x7f, 0x2d, 0x1b, 0xcf, 0x03, 0x6c, - 0xea, 0x3f, 0x62, 0x57, 0xdb, 0xb6, 0x10, 0xd3, 0x53, 0xbe, 0x8b, 0x98, 0x9e, 0x8c, 0xeb, 0x6d, - 0xdc, 0x92, 0xeb, 0x2d, 0x80, 0x29, 0xbf, 0xed, 0x35, 0xc8, 0x7a, 0xb7, 0xd5, 0xe2, 0x97, 0x48, - 0xe4, 0xe3, 0xb5, 0xb9, 0x16, 0xbc, 0xcb, 0x61, 0xcd, 0x6b, 0x65, 0x9f, 0x2d, 0x57, 0x97, 0x65, - 0x2e, 0x65, 0x30, 0xe1, 0x1e, 0xdc, 0x74, 0xc1, 0xb2, 0x6c, 0x78, 0x24, 0xa1, 0xb3, 0xcd, 0x02, - 0x47, 0xc6, 0xf8, 0x82, 0xbd, 0x98, 0x82, 0xb1, 0x5e, 0x07, 0xad, 0x40, 0xa9, 0x1e, 0xc4, 0xe2, - 0x3e, 0xeb, 0x31, 0xc6, 0xcc, 0xde, 0x49, 0x59, 0xe0, 0xe2, 0x95, 0xaa, 0xba, 0xc9, 0xfa, 0x60, - 0x4e, 0x7a, 0x47, 0x55, 0x8e, 0xd3, 0xf6, 0x68, 0x95, 0x21, 0x13, 0x2f, 0x7b, 0xf1, 0x78, 0x8e, - 0x87, 0xfb, 0x38, 0x8c, 0x16, 0xaf, 0xc8, 0xb7, 0xc9, 0x26, 0x04, 0x39, 0xf1, 0x44, 0x57, 0x8a, - 0x41, 0x7b, 0x44, 0xf8, 0xf8, 0xbe, 0x8f, 0x08, 0xb3, 0xbc, 0xae, 0x49, 0x4b, 0xb9, 0x87, 0xcf, - 0x5a, 0xcb, 0xeb, 0x9a, 0x46, 0x4a, 0x8a, 0xbc, 0xae, 0x29, 0x00, 0xeb, 0x24, 0xd1, 0x5a, 0x3f, - 0x37, 0xf9, 0x09, 0xc6, 0x34, 0x0e, 0xee, 0xf4, 0xd6, 0xfd, 0xa5, 0x27, 0xf7, 0xf5, 0x97, 0xf6, - 0xf8, 0x77, 0x4f, 0x1d, 0xc0, 0xbf, 0xdb, 0x64, 0x19, 0x37, 0x97, 0x17, 0x84, 0x4b, 0xdd, 0x82, - 0x7e, 0xc7, 0x72, 0x7c, 0xf0, 0xc8, 0x53, 0xf6, 0x2f, 0xe6, 0x04, 0xfa, 0x06, 0x54, 0x9f, 0xb9, - 0xe3, 0x80, 0x6a, 0xca, 0x9e, 0x53, 0x38, 0x4b, 0xdd, 0x5a, 0x14, 0xec, 0x39, 0x05, 0x63, 0xbd, - 0x4e, 0xd6, 0x5b, 0x7a, 0xff, 0xa1, 0x79, 0x4b, 0xa7, 0x8f, 0xc0, 0x5b, 0xfa, 0xc0, 0xc0, 0xde, - 0xd2, 0x1b, 0x70, 0xa2, 0x13, 0xd6, 0x17, 0xfd, 0x38, 0xea, 0xb2, 0x5b, 0x75, 0xf3, 0xdd, 0x7a, - 0x83, 0x24, 0xcc, 0xdd, 0x5a, 0x3e, 0xff, 0x4e, 0xbd, 0x93, 0x1d, 0xb6, 0x91, 0xe5, 0x1e, 0xcd, - 0x34, 0x60, 0xa6, 0x13, 0x16, 0x75, 0x9b, 0x53, 0x88, 0xf3, 0x48, 0xe8, 0x7e, 0xda, 0x87, 0x8f, - 0xc6, 0x4f, 0xfb, 0x01, 0x18, 0x8b, 0x9b, 0xdd, 0xa4, 0x1e, 0x5e, 0x0f, 0x98, 0x33, 0xbe, 0x34, - 0xff, 0x0e, 0x65, 0xca, 0x16, 0xf0, 0x5b, 0x7b, 0x33, 0x53, 0xf2, 0x7f, 0xcd, 0x8a, 0x2d, 0x20, - 0xe8, 0xeb, 0x7d, 0xee, 0xef, 0xb8, 0x87, 0x79, 0x7f, 0xe7, 0xcc, 0x81, 0xee, 0xee, 0xe4, 0x39, - 0xa3, 0x1f, 0xf9, 0xa9, 0x73, 0x46, 0x7f, 0xd5, 0x81, 0x89, 0x1d, 0xdd, 0x65, 0x20, 0x1c, 0xe6, - 0x16, 0x02, 0x77, 0x0c, 0x4f, 0xc4, 0xbc, 0x4b, 0xf9, 0x9c, 0x01, 0xba, 0x95, 0x05, 0x60, 0xb3, - 0x27, 0x39, 0x41, 0x45, 0x8f, 0xde, 0xab, 0xa0, 0xa2, 0x37, 0x18, 0x1f, 0x93, 0x4a, 0x2e, 0xf3, - 0xa2, 0xdb, 0x8d, 0x29, 0x96, 0x3c, 0x51, 0x85, 0x14, 0xeb, 0xf4, 0xd0, 0xe7, 0x1c, 0x98, 0x92, - 0x7a, 0x99, 0x70, 0xf9, 0xc5, 0x22, 0x2a, 0xd2, 0xa6, 0x3a, 0xc8, 0xc2, 0xea, 0x37, 0x32, 0x74, - 0x70, 0x0f, 0x65, 0xca, 0xd5, 0x55, 0x10, 0x5a, 0x23, 0x66, 0xc1, 0xbf, 0x42, 0x86, 0x99, 0x4b, - 0xc1, 0x58, 0xaf, 0x83, 0xbe, 0xe1, 0x00, 0x7f, 0x83, 0xbf, 0xf2, 0x04, 0x63, 0xe8, 0x2f, 0x58, - 0x96, 0x4d, 0xd3, 0x57, 0xfd, 0xe7, 0x9f, 0x92, 0xb6, 0x23, 0x06, 0xbb, 0xb5, 0x37, 0x33, 0x69, - 0x3c, 0x4c, 0x14, 0xbf, 0xf9, 0xb6, 0x06, 0x11, 0xb6, 0x4d, 0xd6, 0x35, 0xf4, 0x45, 0x07, 0xa6, - 0xae, 0x67, 0x0c, 0x1a, 0x22, 0x2c, 0x14, 0xdb, 0x37, 0x95, 0xf0, 0xe9, 0xce, 0x42, 0x71, 0x4f, - 0x0f, 0xd0, 0x67, 0x4d, 0x43, 0x27, 0x8f, 0x1f, 0xb5, 0x38, 0x81, 0x19, 0xc3, 0x2a, 0xbf, 0xe6, - 0x96, 0x6f, 0xf1, 0xbc, 0xeb, 0xf8, 0x90, 0x69, 0x3a, 0x98, 0xf4, 0x63, 0xe5, 0x34, 0x25, 0xa6, - 0xbd, 0xc5, 0xc2, 0x66, 0x37, 0x3e, 0xbf, 0x6e, 0x6e, 0xf9, 0xe2, 0x69, 0x98, 0x34, 0x7d, 0x7b, - 0xe8, 0x5d, 0xe6, 0xa3, 0x13, 0x67, 0xb3, 0xf9, 0xfb, 0x27, 0x64, 0x7d, 0x23, 0x87, 0xbf, 0x91, - 0x64, 0xbf, 0x70, 0xa8, 0x49, 0xf6, 0x87, 0x8e, 0x26, 0xc9, 0xfe, 0xd4, 0x61, 0x24, 0xd9, 0x3f, - 0x7e, 0xa0, 0x24, 0xfb, 0xda, 0x23, 0x07, 0xc3, 0xb7, 0x79, 0xe4, 0x60, 0x0e, 0x8e, 0xc9, 0xbb, - 0x3f, 0x44, 0xe4, 0x31, 0xe7, 0x6e, 0xff, 0x33, 0xa2, 0xc9, 0xb1, 0x05, 0xb3, 0x18, 0x67, 0xeb, - 0xd3, 0x4d, 0x56, 0x0c, 0x58, 0xcb, 0x11, 0x5b, 0x2f, 0x20, 0x99, 0x4b, 0x8b, 0xa9, 0xcf, 0x82, - 0x45, 0xc9, 0x68, 0xe7, 0x22, 0x83, 0xdd, 0x92, 0xff, 0x60, 0xde, 0x03, 0xf4, 0x12, 0x54, 0xc2, - 0xad, 0xad, 0x56, 0xe8, 0xd5, 0xd3, 0x97, 0x00, 0x64, 0x5c, 0x02, 0xbf, 0xbb, 0xa9, 0x12, 0xc7, - 0xae, 0xf5, 0xa9, 0x87, 0xfb, 0x62, 0x40, 0x6f, 0x51, 0xc1, 0x24, 0x09, 0x23, 0x52, 0x4f, 0x6d, - 0x35, 0x25, 0x36, 0x66, 0x62, 0x7d, 0xcc, 0x55, 0x93, 0x0e, 0x1f, 0xbd, 0xfa, 0x28, 0x99, 0x52, - 0x9c, 0xed, 0x16, 0x8a, 0xe0, 0x74, 0x27, 0xcf, 0x54, 0x14, 0x8b, 0x1b, 0x4b, 0xfb, 0x19, 0xac, - 0xe4, 0xd6, 0x3d, 0x9d, 0x6b, 0x6c, 0x8a, 0x71, 0x1f, 0xcc, 0x7a, 0xb6, 0xfe, 0xb1, 0xa3, 0xc9, - 0xd6, 0xff, 0x31, 0x00, 0x75, 0x49, 0x5d, 0x1a, 0x1f, 0x56, 0xac, 0x5c, 0xa5, 0xe1, 0x38, 0xb5, - 0x07, 0x52, 0x15, 0x19, 0xac, 0x91, 0x44, 0xff, 0x27, 0xf7, 0x39, 0x0b, 0x6e, 0x61, 0x69, 0x58, - 0x5f, 0x13, 0x3f, 0x75, 0x4f, 0x5a, 0xfc, 0x13, 0x07, 0xa6, 0xf9, 0xca, 0xcb, 0x0a, 0xf7, 0x54, - 0xb4, 0x10, 0x77, 0x7b, 0x6c, 0x87, 0xae, 0xb0, 0x28, 0xbe, 0xaa, 0x41, 0x95, 0x39, 0xba, 0xf7, - 0xe9, 0x09, 0xfa, 0x72, 0x8e, 0x4a, 0x71, 0xcc, 0x96, 0xcd, 0x32, 0xff, 0x51, 0x82, 0x13, 0x37, - 0x07, 0xd1, 0x22, 0xfe, 0x59, 0x5f, 0x93, 0x2a, 0x62, 0xdd, 0xfb, 0xc5, 0x43, 0x32, 0xa9, 0xea, - 0x2f, 0x27, 0x1c, 0xc8, 0xb0, 0xfa, 0x79, 0x07, 0xa6, 0xbc, 0x4c, 0xa8, 0x09, 0xb3, 0x03, 0x59, - 0xb1, 0x49, 0xcd, 0x45, 0x69, 0xfc, 0x0a, 0x13, 0xf2, 0xb2, 0x51, 0x2d, 0xb8, 0x87, 0x38, 0xfa, - 0x81, 0x03, 0x0f, 0x24, 0x5e, 0xbc, 0xcd, 0xf3, 0x12, 0xc7, 0xe9, 0x5d, 0x5d, 0xd1, 0xb9, 0x93, - 0x6c, 0x37, 0xbe, 0x62, 0x7d, 0x37, 0x6e, 0xf4, 0xa7, 0xc9, 0xf7, 0xe5, 0x23, 0x62, 0x5f, 0x3e, - 0xb0, 0x4f, 0x4d, 0xbc, 0x5f, 0xd7, 0xa7, 0x3f, 0xe5, 0xf0, 0xf7, 0xab, 0xfa, 0x8a, 0x7c, 0x9b, - 0xa6, 0xc8, 0x77, 0xd9, 0xe6, 0x0b, 0x3a, 0xba, 0xec, 0xf9, 0xab, 0x0e, 0x9c, 0xcc, 0x3b, 0x91, - 0x72, 0xba, 0xf4, 0x11, 0xb3, 0x4b, 0x16, 0xb5, 0x2c, 0xbd, 0x43, 0x56, 0x1e, 0xf0, 0x98, 0xbe, - 0x02, 0x0f, 0xdf, 0xee, 0x2b, 0xde, 0x0e, 0xdf, 0x98, 0x2e, 0x16, 0xff, 0x69, 0x49, 0xf3, 0x42, - 0x26, 0xa4, 0x63, 0x3d, 0x86, 0x3b, 0x80, 0x11, 0x3f, 0x68, 0xf9, 0x01, 0x11, 0xf7, 0x35, 0x6d, - 0xea, 0xb0, 0xe2, 0x01, 0x1e, 0x8a, 0x1d, 0x0b, 0x2a, 0xf7, 0xd8, 0x29, 0x99, 0x7d, 0xd2, 0x6c, - 0xf8, 0xe8, 0x9f, 0x34, 0xbb, 0x0e, 0xa5, 0xeb, 0x7e, 0xd2, 0x64, 0xc1, 0x14, 0xc2, 0xd7, 0x67, - 0xe1, 0x9e, 0x23, 0x45, 0x97, 0x8e, 0xfd, 0x9a, 0x24, 0x80, 0x53, 0x5a, 0xe8, 0x1c, 0x27, 0xcc, - 0x22, 0xb7, 0xb3, 0x21, 0xb5, 0xd7, 0x64, 0x01, 0x4e, 0xeb, 0xd0, 0xc9, 0x1a, 0xa7, 0xbf, 0x64, - 0x42, 0x24, 0x91, 0xb7, 0xd7, 0x46, 0x3e, 0x46, 0x81, 0x91, 0xdf, 0x26, 0xbe, 0xa6, 0xd1, 0xc0, - 0x06, 0x45, 0x95, 0x3a, 0x79, 0xac, 0x6f, 0xea, 0xe4, 0xd7, 0x99, 0xc0, 0x96, 0xf8, 0x41, 0x97, - 0xac, 0x05, 0x22, 0xde, 0xfb, 0xb2, 0x9d, 0xbb, 0xcf, 0x1c, 0x27, 0x57, 0xc1, 0xd3, 0xdf, 0x58, - 0xa3, 0xa7, 0xb9, 0x5c, 0xca, 0xfb, 0xba, 0x5c, 0x52, 0x93, 0xcb, 0xb8, 0x75, 0x93, 0x4b, 0x42, - 0x3a, 0x56, 0x4c, 0x2e, 0x3f, 0x55, 0xe6, 0x80, 0xbf, 0x70, 0x00, 0x29, 0xb9, 0x4b, 0x31, 0xd4, - 0x23, 0x08, 0xaa, 0xfc, 0xb8, 0x03, 0x10, 0xa8, 0x87, 0x2f, 0xed, 0x9e, 0x82, 0x1c, 0x67, 0xda, - 0x81, 0x14, 0x86, 0x35, 0x9a, 0xee, 0xff, 0x70, 0xd2, 0xd8, 0xe5, 0x74, 0xec, 0x47, 0x10, 0x44, - 0xb6, 0x6b, 0x06, 0x91, 0x6d, 0x58, 0x34, 0xdd, 0xab, 0x61, 0xf4, 0x09, 0x27, 0xfb, 0x71, 0x01, - 0x8e, 0xe9, 0x95, 0xab, 0xe4, 0x28, 0x3e, 0xf6, 0x75, 0x23, 0x82, 0xf6, 0xaa, 0xdd, 0xf1, 0x56, - 0x85, 0x07, 0x28, 0x2f, 0x5a, 0xfb, 0x63, 0x99, 0x68, 0xed, 0x6b, 0xf6, 0x49, 0xef, 0x1f, 0xb2, - 0xfd, 0x5f, 0x1d, 0x38, 0x91, 0x69, 0x71, 0x04, 0x0b, 0x6c, 0xc7, 0x5c, 0x60, 0xcf, 0x59, 0x1f, - 0x75, 0x9f, 0xd5, 0xf5, 0xcd, 0x42, 0xcf, 0x68, 0x99, 0x12, 0xf7, 0x49, 0x07, 0x8a, 0x54, 0x5a, - 0x96, 0xf1, 0x5c, 0x1f, 0x39, 0x94, 0x15, 0xc0, 0xe4, 0x7a, 0xc1, 0x9d, 0x55, 0xff, 0x18, 0x0c, - 0x73, 0xea, 0xd3, 0x9f, 0x70, 0x00, 0xd2, 0x4a, 0xf7, 0x4a, 0x04, 0x76, 0xbf, 0x5d, 0x80, 0x53, - 0xb9, 0xcb, 0x08, 0x7d, 0x5a, 0x59, 0xe4, 0x1c, 0xdb, 0xd1, 0x8a, 0x06, 0x21, 0xdd, 0x30, 0x37, - 0x61, 0x18, 0xe6, 0x84, 0x3d, 0xee, 0x5e, 0x29, 0x30, 0x82, 0x4d, 0x6b, 0x93, 0xf5, 0x23, 0x27, - 0x0d, 0x80, 0x55, 0x79, 0x8d, 0xfe, 0x12, 0x5e, 0xe2, 0x71, 0x7f, 0xac, 0xdd, 0x70, 0x90, 0x03, - 0x3d, 0x02, 0x5e, 0x71, 0xdd, 0xe4, 0x15, 0xd8, 0xbe, 0x1f, 0xb9, 0x0f, 0xb3, 0x78, 0x05, 0xf2, - 0x1c, 0xcb, 0x83, 0x25, 0x45, 0x34, 0xae, 0xc3, 0x16, 0x06, 0xbe, 0x0e, 0x3b, 0x01, 0xe5, 0x17, - 0x7d, 0x95, 0x4d, 0x73, 0x7e, 0xf6, 0x3b, 0x3f, 0x3c, 0x7b, 0xdf, 0x77, 0x7f, 0x78, 0xf6, 0xbe, - 0x1f, 0xfc, 0xf0, 0xec, 0x7d, 0x1f, 0xbf, 0x79, 0xd6, 0xf9, 0xce, 0xcd, 0xb3, 0xce, 0x77, 0x6f, - 0x9e, 0x75, 0x7e, 0x70, 0xf3, 0xac, 0xf3, 0x1f, 0x6e, 0x9e, 0x75, 0xfe, 0xde, 0x7f, 0x3c, 0x7b, - 0xdf, 0x8b, 0x63, 0x72, 0x60, 0xff, 0x3f, 0x00, 0x00, 0xff, 0xff, 0x84, 0x02, 0x48, 0xf8, 0xe2, - 0xd6, 0x00, 0x00, + 0xb9, 0xe9, 0x27, 0xf3, 0x61, 0x4d, 0xda, 0x38, 0x98, 0xc4, 0x7c, 0x51, 0xc0, 0xb0, 0x2a, 0x75, + 0x3f, 0xe5, 0xc0, 0x38, 0x1d, 0x65, 0xb3, 0x49, 0x9a, 0x95, 0x84, 0xb4, 0x63, 0x14, 0x43, 0x31, + 0xa6, 0xff, 0xd8, 0x33, 0x05, 0xa6, 0x77, 0x29, 0x49, 0x5b, 0x73, 0xd6, 0x50, 0x22, 0x98, 0xd3, + 0x72, 0xdf, 0x1c, 0x80, 0x51, 0x35, 0xd9, 0x7d, 0xd8, 0x53, 0x2f, 0xa4, 0xf9, 0x55, 0x39, 0x07, + 0x2e, 0x6b, 0xb9, 0x55, 0x6f, 0xd3, 0xa9, 0x0b, 0x76, 0x78, 0xa2, 0x89, 0x34, 0xd1, 0xea, 0x13, + 0xa6, 0xab, 0xf9, 0xb4, 0xbe, 0xfe, 0xb4, 0xfa, 0xc2, 0xe7, 0x7c, 0x53, 0xf7, 0xf4, 0x0f, 0xda, + 0x3a, 0xcd, 0x94, 0x1b, 0xb3, 0xb7, 0x8b, 0x3f, 0xf3, 0xf4, 0x4e, 0xb1, 0xaf, 0xa7, 0x77, 0x1e, + 0x87, 0x41, 0x12, 0x74, 0x5a, 0x4c, 0x54, 0x1a, 0x65, 0x2a, 0xc2, 0xe0, 0xc5, 0xa0, 0xd3, 0x32, + 0x47, 0xc6, 0xaa, 0xa0, 0xf7, 0x43, 0xa9, 0x46, 0xe2, 0x6a, 0xe4, 0xb3, 0xec, 0x09, 0xc2, 0xb2, + 0xf3, 0x00, 0x33, 0x97, 0xa5, 0x60, 0xb3, 0xa1, 0xde, 0xc0, 0x7d, 0x05, 0x86, 0xd6, 0x9a, 0x9d, + 0xba, 0x1f, 0xa0, 0x36, 0x0c, 0xf1, 0x5c, 0x0a, 0xe2, 0xb4, 0xb7, 0xa0, 0x77, 0x72, 0x56, 0xa1, + 0x45, 0xa1, 0xf0, 0x2b, 0xb5, 0x82, 0x8e, 0xfb, 0xc9, 0x02, 0x50, 0xd5, 0x7c, 0x69, 0x1e, 0xfd, + 0xed, 0xae, 0x97, 0x66, 0x7e, 0x2e, 0xe7, 0xa5, 0x99, 0x71, 0x56, 0x39, 0xe7, 0x91, 0x99, 0x26, + 0x8c, 0x33, 0xe7, 0x88, 0x3c, 0x03, 0x85, 0x58, 0xfd, 0x54, 0x9f, 0xe9, 0x07, 0xf4, 0xa6, 0xe2, + 0x44, 0xd0, 0x41, 0xd8, 0x44, 0x8e, 0x56, 0xe0, 0x04, 0x4f, 0xd3, 0xb9, 0x40, 0x9a, 0xde, 0x4e, + 0x26, 0x1d, 0xd7, 0x59, 0xf9, 0x78, 0xd8, 0x42, 0x77, 0x15, 0x9c, 0xd7, 0xce, 0xfd, 0xbd, 0x41, + 0xd0, 0x5c, 0x12, 0x7d, 0xec, 0x96, 0x97, 0x33, 0x0e, 0xa8, 0x15, 0x2b, 0x0e, 0x28, 0xe9, 0xd5, + 0xe1, 0x1c, 0xc8, 0xf4, 0x39, 0xd1, 0x4e, 0x35, 0x48, 0xb3, 0x2d, 0xc6, 0xa8, 0x3a, 0x75, 0x89, + 0x34, 0xdb, 0x98, 0x95, 0xa8, 0x0b, 0x85, 0x83, 0x3d, 0x2f, 0x14, 0x36, 0xa0, 0x58, 0xf7, 0x3a, + 0x75, 0x22, 0x22, 0x30, 0x2d, 0xf8, 0x1a, 0xd9, 0x15, 0x07, 0xee, 0x6b, 0x64, 0xff, 0x62, 0x4e, + 0x80, 0x6e, 0xf6, 0x86, 0x0c, 0x49, 0x11, 0x46, 0x5a, 0x0b, 0x9b, 0x5d, 0x45, 0xb9, 0xf0, 0xcd, + 0xae, 0x7e, 0xe2, 0x94, 0x18, 0x6a, 0xc3, 0x70, 0x95, 0x27, 0x41, 0x11, 0x32, 0xcb, 0x65, 0x1b, + 0x37, 0x26, 0x19, 0x42, 0x6e, 0x4d, 0x11, 0x3f, 0xb0, 0x24, 0xe3, 0x9e, 0x87, 0x92, 0xf6, 0xe0, + 0x05, 0xfd, 0x0c, 0x2a, 0xff, 0x86, 0xf6, 0x19, 0x16, 0xbc, 0xc4, 0xc3, 0xac, 0xc4, 0xfd, 0xd6, + 0x20, 0x28, 0x5b, 0x9a, 0x7e, 0xbf, 0xcf, 0xab, 0x6a, 0xd9, 0x82, 0x8c, 0xbb, 0xee, 0x61, 0x80, + 0x45, 0x29, 0x95, 0xeb, 0x5a, 0x24, 0xaa, 0x2b, 0x3d, 0x5a, 0xb0, 0x6b, 0x25, 0xd7, 0xad, 0xe8, + 0x85, 0xd8, 0xac, 0x4b, 0x85, 0xf2, 0x96, 0x70, 0xd1, 0x67, 0x03, 0xa0, 0xa5, 0xeb, 0x1e, 0xab, + 0x1a, 0x2c, 0xdd, 0x40, 0x4b, 0xf3, 0xe8, 0x8b, 0x40, 0x4c, 0x1b, 0x0e, 0x25, 0x0d, 0x2b, 0x0f, + 0x98, 0xd2, 0x21, 0xd8, 0xa0, 0x8a, 0x96, 0xe0, 0x78, 0x4c, 0x92, 0xd5, 0x1b, 0x01, 0x89, 0x54, + 0x2a, 0x00, 0x91, 0xcf, 0x42, 0xdd, 0x7e, 0xa8, 0x64, 0x2b, 0xe0, 0xee, 0x36, 0xb9, 0xb1, 0xab, + 0xc5, 0x7d, 0xc7, 0xae, 0x2e, 0xc0, 0xe4, 0xa6, 0xe7, 0x37, 0x3b, 0x11, 0xe9, 0x19, 0x01, 0xbb, + 0x98, 0x29, 0xc7, 0x5d, 0x2d, 0xd8, 0x05, 0x9c, 0xa6, 0x57, 0x8f, 0xcb, 0xc3, 0xda, 0x05, 0x1c, + 0x0a, 0xc0, 0x1c, 0xee, 0xfe, 0xa6, 0x03, 0x3c, 0x91, 0xd0, 0xec, 0xe6, 0xa6, 0x1f, 0xf8, 0xc9, + 0x0e, 0xfa, 0xba, 0x03, 0x93, 0x41, 0x58, 0x23, 0xb3, 0x41, 0xe2, 0x4b, 0xa0, 0xbd, 0xec, 0xee, + 0x8c, 0xd6, 0xd5, 0x0c, 0x7a, 0x9e, 0x95, 0x22, 0x0b, 0xc5, 0x5d, 0xdd, 0x70, 0xcf, 0xc0, 0xa9, + 0x5c, 0x04, 0xee, 0x0f, 0x06, 0xc0, 0xcc, 0x87, 0x84, 0x9e, 0x85, 0x62, 0x93, 0x65, 0xe8, 0x70, + 0x0e, 0x98, 0xe8, 0x8a, 0xcd, 0x15, 0x4f, 0xe1, 0xc1, 0x31, 0xa1, 0x05, 0x28, 0xb1, 0x24, 0x4b, + 0x22, 0x7f, 0x4a, 0xc1, 0x48, 0x5d, 0x50, 0xc2, 0x69, 0xd1, 0x6d, 0xf3, 0x27, 0xd6, 0x9b, 0xa1, + 0x57, 0x61, 0x78, 0x83, 0xa7, 0x9a, 0xb4, 0xe7, 0xf3, 0x13, 0xb9, 0x2b, 0x99, 0x6c, 0x24, 0x13, + 0x59, 0xde, 0x4e, 0xff, 0xc5, 0x92, 0x22, 0xda, 0x81, 0x11, 0x4f, 0x7e, 0xd3, 0x41, 0x5b, 0x17, + 0x2a, 0x8c, 0xf5, 0x23, 0x22, 0x66, 0xe4, 0x37, 0x54, 0xe4, 0x32, 0xa1, 0x45, 0xc5, 0xbe, 0x42, + 0x8b, 0xbe, 0xe3, 0x00, 0xa4, 0xef, 0x72, 0xa0, 0x9b, 0x30, 0x12, 0x3f, 0x65, 0x18, 0x2a, 0x6c, + 0xdc, 0xa4, 0x17, 0x18, 0xb5, 0xdb, 0xa6, 0x02, 0x82, 0x15, 0xb5, 0x3b, 0x19, 0x57, 0x7e, 0xe6, + 0xc0, 0xc9, 0xbc, 0xf7, 0x43, 0xee, 0x61, 0x8f, 0xf7, 0x6b, 0x57, 0x11, 0x0d, 0xd6, 0x22, 0xb2, + 0xe9, 0xdf, 0xcc, 0x49, 0x78, 0xcc, 0x0b, 0x70, 0x5a, 0xc7, 0x7d, 0x63, 0x18, 0x14, 0xe1, 0x43, + 0xb2, 0xc3, 0x3c, 0x4a, 0x75, 0xa6, 0x7a, 0x2a, 0x73, 0xa9, 0x7a, 0x98, 0x41, 0xb1, 0x28, 0xa5, + 0x7a, 0x93, 0x0c, 0x8a, 0x17, 0x2c, 0x9b, 0xad, 0x42, 0x19, 0x3c, 0x8f, 0x55, 0x69, 0x9e, 0x65, + 0xa7, 0x78, 0x24, 0x96, 0x9d, 0x21, 0xfb, 0x96, 0x9d, 0xc7, 0x61, 0x38, 0x0a, 0x9b, 0x64, 0x16, + 0x5f, 0x15, 0xda, 0x40, 0x1a, 0xd4, 0xc0, 0xc1, 0x58, 0x96, 0x1f, 0xd0, 0xb6, 0x81, 0x7e, 0xdb, + 0xd9, 0xc3, 0x78, 0x34, 0x6a, 0xeb, 0x4c, 0xc8, 0xcd, 0x0e, 0xc7, 0x54, 0x9b, 0x83, 0x58, 0xa4, + 0xbe, 0xe1, 0xc0, 0x71, 0x12, 0x54, 0xa3, 0x1d, 0x86, 0x47, 0x60, 0x13, 0x3e, 0xe7, 0x6b, 0x36, + 0x36, 0xdf, 0xc5, 0x2c, 0x72, 0xee, 0xda, 0xe9, 0x02, 0xe3, 0xee, 0x6e, 0xa0, 0x55, 0x18, 0xa9, + 0x7a, 0x62, 0x45, 0x94, 0xf6, 0xb3, 0x22, 0xb8, 0xe7, 0x6c, 0x56, 0x2c, 0x05, 0x85, 0xc4, 0xfd, + 0x49, 0x01, 0x4e, 0xe4, 0x74, 0x89, 0x5d, 0xa0, 0x6a, 0xd1, 0x15, 0x79, 0xb9, 0x96, 0xdd, 0x8f, + 0xcb, 0x02, 0x8e, 0x55, 0x0d, 0xb4, 0x06, 0x27, 0xb7, 0x5a, 0x71, 0x8a, 0x65, 0x3e, 0x0c, 0x12, + 0x72, 0x53, 0xee, 0x4e, 0xe9, 0x8f, 0x3e, 0xb9, 0x9c, 0x53, 0x07, 0xe7, 0xb6, 0xa4, 0xe2, 0x0b, + 0x09, 0xbc, 0x8d, 0x26, 0x49, 0x8b, 0xc4, 0xf5, 0x3f, 0x25, 0xbe, 0x5c, 0xcc, 0x94, 0xe3, 0xae, + 0x16, 0xe8, 0x73, 0x0e, 0x9c, 0x8d, 0x49, 0xb4, 0x4d, 0xa2, 0x8a, 0x5f, 0x23, 0xf3, 0x9d, 0x38, + 0x09, 0x5b, 0x24, 0x3a, 0xa0, 0xb9, 0x74, 0xfa, 0xd6, 0xee, 0xf4, 0xd9, 0x4a, 0x6f, 0x6c, 0x78, + 0x2f, 0x52, 0xee, 0xe7, 0x1c, 0x98, 0xa8, 0x30, 0x65, 0x5a, 0xc9, 0xd2, 0xb6, 0xf3, 0x83, 0x3e, + 0xaa, 0x12, 0x56, 0x64, 0xb8, 0xa2, 0x99, 0x62, 0xc2, 0x7d, 0x09, 0x26, 0x2b, 0xa4, 0xe5, 0xb5, + 0x1b, 0xec, 0xee, 0x2e, 0x8f, 0xc7, 0x3a, 0x0f, 0xa3, 0xb1, 0x84, 0x65, 0x9f, 0x04, 0x52, 0x95, + 0x71, 0x5a, 0x07, 0x3d, 0xc2, 0x63, 0xc7, 0xe4, 0x0d, 0xa0, 0x51, 0xae, 0x75, 0xf0, 0x80, 0xb3, + 0x18, 0xcb, 0x32, 0xf7, 0x4d, 0x07, 0xc6, 0xd2, 0xf6, 0x64, 0x13, 0xd5, 0xe1, 0x58, 0x55, 0xbb, + 0x3d, 0x97, 0xde, 0x5b, 0xe8, 0xff, 0xa2, 0x1d, 0x4f, 0x5b, 0x6c, 0x22, 0xc1, 0x59, 0xac, 0xfb, + 0x0f, 0xbd, 0xfb, 0x62, 0x01, 0x8e, 0xa9, 0xae, 0x0a, 0xc7, 0xe1, 0xeb, 0xd9, 0x08, 0x39, 0x6c, + 0x23, 0xf5, 0x8e, 0x39, 0xf7, 0x7b, 0x44, 0xc9, 0xbd, 0x9e, 0x8d, 0x92, 0x3b, 0x54, 0xf2, 0x5d, + 0xbe, 0xd0, 0xef, 0x14, 0x60, 0x44, 0x25, 0x02, 0x7a, 0x16, 0x8a, 0x4c, 0x95, 0xbc, 0x3b, 0x81, + 0x98, 0xa9, 0xa5, 0x98, 0x63, 0xa2, 0x28, 0x59, 0x14, 0xce, 0x81, 0x93, 0xc9, 0x8e, 0x72, 0x83, + 0xa2, 0x17, 0x25, 0x98, 0x63, 0x42, 0xcb, 0x30, 0x40, 0x82, 0x9a, 0x90, 0x8c, 0xf7, 0x8f, 0x90, + 0x3d, 0xde, 0x75, 0x31, 0xa8, 0x61, 0x8a, 0x85, 0x65, 0x23, 0xe3, 0x02, 0x50, 0xe6, 0xa9, 0x16, + 0x21, 0xfd, 0x88, 0x52, 0xf7, 0x03, 0x60, 0xe4, 0xa1, 0x13, 0xf9, 0xed, 0x85, 0xd2, 0xd5, 0xfd, + 0xbe, 0x96, 0xd0, 0xb6, 0xd2, 0x3a, 0xee, 0x2f, 0x0f, 0xc0, 0x50, 0xa5, 0xb3, 0x41, 0x95, 0x84, + 0x6f, 0x3b, 0x70, 0xe2, 0x46, 0x26, 0x55, 0x73, 0xba, 0x49, 0xae, 0xd9, 0xb3, 0xca, 0xea, 0xa1, + 0x64, 0xca, 0x16, 0x95, 0x53, 0x88, 0xf3, 0xba, 0x63, 0x64, 0x4b, 0x1d, 0x38, 0x94, 0x6c, 0xa9, + 0x37, 0x0f, 0xf9, 0x2e, 0xc5, 0x78, 0xaf, 0x7b, 0x14, 0xee, 0xef, 0x15, 0x01, 0xf8, 0xd7, 0x58, + 0x6d, 0x27, 0xfd, 0xd8, 0xd9, 0x9e, 0x86, 0xb1, 0x3a, 0x09, 0x48, 0x24, 0x03, 0x05, 0x33, 0xcf, + 0x08, 0x2d, 0x69, 0x65, 0xd8, 0xa8, 0xc9, 0x94, 0x9a, 0x20, 0x89, 0x76, 0xb8, 0xe0, 0x9b, 0xbd, + 0x2f, 0xa1, 0x4a, 0xb0, 0x56, 0x0b, 0xcd, 0x18, 0x6e, 0x10, 0xee, 0x51, 0x9f, 0xd8, 0xc3, 0x6b, + 0xf1, 0x7e, 0x98, 0x30, 0x93, 0x8f, 0x08, 0x69, 0x4f, 0x79, 0xc0, 0xcd, 0x9c, 0x25, 0x38, 0x53, + 0x9b, 0xee, 0x82, 0x5a, 0xb4, 0x83, 0x3b, 0x81, 0x10, 0xfb, 0xd4, 0x2e, 0x58, 0x60, 0x50, 0x2c, + 0x4a, 0x59, 0xd6, 0x06, 0x76, 0x00, 0x72, 0xb8, 0xc8, 0xfc, 0x90, 0x66, 0x6d, 0xd0, 0xca, 0xb0, + 0x51, 0x93, 0x52, 0x10, 0x76, 0x4a, 0x30, 0xf7, 0x59, 0xc6, 0xb8, 0xd8, 0x86, 0x89, 0xd0, 0xb4, + 0xaf, 0x70, 0x19, 0xe8, 0xdd, 0x7d, 0x2e, 0x3d, 0xa3, 0x2d, 0x8f, 0x5c, 0xc8, 0x98, 0x63, 0x32, + 0xf8, 0xa9, 0xdc, 0xab, 0x5f, 0x2b, 0x18, 0x33, 0xe3, 0x4c, 0x7b, 0x46, 0xfe, 0xaf, 0xc1, 0xc9, + 0x76, 0x58, 0x5b, 0x8b, 0xfc, 0x30, 0xf2, 0x93, 0x9d, 0xf9, 0xa6, 0x17, 0xc7, 0x6c, 0x61, 0x8c, + 0x9b, 0xf2, 0xd0, 0x5a, 0x4e, 0x1d, 0x9c, 0xdb, 0x92, 0x6a, 0x28, 0x6d, 0x01, 0x64, 0xd1, 0x5e, + 0x45, 0x2e, 0xd1, 0xc9, 0x8a, 0x58, 0x95, 0xba, 0x27, 0xe0, 0x78, 0xa5, 0xd3, 0x6e, 0x37, 0x7d, + 0x52, 0x53, 0x6e, 0x06, 0xf7, 0x03, 0x70, 0x4c, 0xe4, 0x52, 0x55, 0xd2, 0xc7, 0xbe, 0x32, 0x7f, + 0xbb, 0x7f, 0xe6, 0xc0, 0xb1, 0x4c, 0x6c, 0x0d, 0x7a, 0x35, 0x2b, 0x33, 0xd8, 0xc9, 0xf1, 0xa9, + 0x49, 0x0b, 0x22, 0x61, 0x67, 0x9e, 0xfc, 0xd1, 0x90, 0x81, 0xf1, 0xd6, 0x2e, 0xa4, 0xb0, 0xf0, + 0x71, 0x7e, 0xa4, 0xe8, 0xd1, 0xf5, 0xee, 0x67, 0x0b, 0x90, 0x1f, 0xd0, 0x84, 0x3e, 0xd6, 0x3d, + 0x01, 0xcf, 0x5a, 0x9c, 0x00, 0x11, 0x51, 0xd5, 0x7b, 0x0e, 0x02, 0x73, 0x0e, 0x56, 0x2c, 0xcd, + 0x81, 0xa0, 0xdb, 0x3d, 0x13, 0xff, 0xdb, 0x81, 0xd2, 0xfa, 0xfa, 0x15, 0x75, 0xce, 0x61, 0x38, + 0x1d, 0xf3, 0x0b, 0xf7, 0xcc, 0xef, 0x3b, 0x1f, 0xb6, 0xda, 0xdc, 0x0d, 0x2c, 0xdc, 0xd3, 0x2c, + 0xad, 0x6d, 0x25, 0xb7, 0x06, 0xee, 0xd1, 0x12, 0x5d, 0x86, 0x13, 0x7a, 0x49, 0x45, 0x7b, 0x45, + 0xb0, 0x28, 0x92, 0xdc, 0x74, 0x17, 0xe3, 0xbc, 0x36, 0x59, 0x54, 0xc2, 0xdc, 0xc9, 0x8e, 0xab, + 0x1c, 0x54, 0xa2, 0x18, 0xe7, 0xb5, 0x71, 0x57, 0xa1, 0xb4, 0xee, 0x45, 0x6a, 0xe0, 0x1f, 0x84, + 0xc9, 0x6a, 0xd8, 0x92, 0x66, 0xa6, 0x2b, 0x64, 0x9b, 0x34, 0xc5, 0x90, 0xf9, 0xd3, 0x1d, 0x99, + 0x32, 0xdc, 0x55, 0xdb, 0xfd, 0x1f, 0xe7, 0x40, 0x5d, 0x20, 0xec, 0xe3, 0x84, 0x69, 0xab, 0x50, + 0xcf, 0xa2, 0xe5, 0x50, 0x4f, 0xc5, 0x6b, 0x33, 0xe1, 0x9e, 0x49, 0x1a, 0xee, 0x39, 0x64, 0x3b, + 0xdc, 0x53, 0x49, 0x9c, 0x5d, 0x21, 0x9f, 0x5f, 0x75, 0x60, 0x2c, 0x08, 0x6b, 0x44, 0xf9, 0xe7, + 0x86, 0x99, 0xd8, 0xfb, 0xa2, 0xbd, 0xc8, 0x79, 0x1e, 0xba, 0x28, 0xd0, 0xf3, 0x30, 0x64, 0x75, + 0x44, 0xe9, 0x45, 0xd8, 0xe8, 0x07, 0x5a, 0xd4, 0x0c, 0x9f, 0xdc, 0xbf, 0xf0, 0x40, 0x9e, 0xbe, + 0x72, 0x47, 0x2b, 0xe6, 0x4d, 0x4d, 0x6e, 0x1a, 0xb5, 0x65, 0xd0, 0x93, 0xb7, 0xc2, 0x34, 0x37, + 0x89, 0xcc, 0xcc, 0x9c, 0xca, 0x53, 0x2e, 0x0c, 0xf1, 0x78, 0x65, 0x91, 0x4e, 0x89, 0x79, 0xef, + 0x78, 0x2c, 0x33, 0x16, 0x25, 0x28, 0x91, 0x31, 0x00, 0x25, 0x5b, 0xef, 0x2c, 0x18, 0x31, 0x06, + 0xf9, 0x41, 0x00, 0xe8, 0x19, 0x5d, 0x0f, 0x1e, 0xeb, 0x47, 0x0f, 0x1e, 0xef, 0xa9, 0x03, 0x7f, + 0xc1, 0x81, 0xb1, 0xaa, 0xf6, 0xee, 0x41, 0xf9, 0x31, 0x5b, 0xef, 0x3b, 0xe7, 0x3d, 0x4f, 0xc1, + 0x9d, 0x42, 0xc6, 0x3b, 0x0b, 0x06, 0x75, 0x96, 0x43, 0x92, 0x29, 0xfd, 0xec, 0xe8, 0xb7, 0x92, + 0x36, 0xc2, 0x34, 0x22, 0xc8, 0x58, 0x4a, 0x0a, 0xc3, 0x82, 0x16, 0x7a, 0x0d, 0x46, 0x64, 0xc8, + 0xbb, 0x08, 0x0d, 0xc7, 0x36, 0xac, 0xf4, 0xa6, 0x2b, 0x50, 0x26, 0x9e, 0xe3, 0x50, 0xac, 0x28, + 0xa2, 0x06, 0x0c, 0xd4, 0xbc, 0xba, 0x08, 0x12, 0x5f, 0xb1, 0x93, 0xd8, 0x53, 0xd2, 0x64, 0xfa, + 0xd9, 0xc2, 0xec, 0x12, 0xa6, 0x24, 0xd0, 0xcd, 0x34, 0x71, 0xfc, 0xa4, 0xb5, 0xd3, 0xd7, 0x14, + 0x93, 0xb8, 0x59, 0xa3, 0x2b, 0x0f, 0x7d, 0x4d, 0x78, 0x4f, 0xff, 0x1a, 0x23, 0xbb, 0x68, 0x27, + 0x33, 0x28, 0x4f, 0x43, 0x92, 0x7a, 0x60, 0x29, 0x95, 0x46, 0x92, 0xb4, 0xcb, 0x3f, 0x6f, 0x8b, + 0x0a, 0x4b, 0xa6, 0xc1, 0x9f, 0xe2, 0x5e, 0x5f, 0x5f, 0xc3, 0x0c, 0x3b, 0x6a, 0xc2, 0x50, 0x9b, + 0x05, 0x76, 0x94, 0xdf, 0x69, 0xeb, 0x6c, 0xe1, 0x81, 0x22, 0x7c, 0x6d, 0xf2, 0xff, 0xb1, 0xa0, + 0x81, 0x2e, 0xc2, 0x30, 0x7f, 0xff, 0x84, 0x07, 0xe9, 0x97, 0x2e, 0x4c, 0xf5, 0x7e, 0x45, 0x25, + 0x3d, 0x28, 0xf8, 0xef, 0x18, 0xcb, 0xb6, 0xe8, 0x8b, 0x0e, 0x4c, 0x50, 0x8e, 0x9a, 0x3e, 0xd8, + 0x52, 0x46, 0xb6, 0x78, 0xd6, 0xb5, 0x98, 0x4a, 0x24, 0x92, 0xd7, 0x28, 0x35, 0xe9, 0xb2, 0x41, + 0x0e, 0x67, 0xc8, 0xa3, 0xd7, 0x61, 0x24, 0xf6, 0x6b, 0xa4, 0xea, 0x45, 0x71, 0xf9, 0xc4, 0xe1, + 0x74, 0x25, 0xf5, 0xd7, 0x08, 0x42, 0x58, 0x91, 0x44, 0xbf, 0xc6, 0x5e, 0xcc, 0x14, 0xaf, 0xdb, + 0x57, 0xb9, 0x58, 0x7f, 0xd2, 0xd6, 0xde, 0x97, 0x9e, 0x29, 0x89, 0x59, 0xb8, 0x31, 0x4c, 0x72, + 0x38, 0x4b, 0x1f, 0xfd, 0x1d, 0x07, 0x4e, 0xf1, 0xdc, 0xf7, 0xd9, 0xc7, 0x1a, 0x4e, 0x1d, 0xd0, + 0x3e, 0xc3, 0x6e, 0x17, 0xcc, 0xe6, 0xa1, 0xc4, 0xf9, 0x94, 0x58, 0xa6, 0x5a, 0xf3, 0x7d, 0x9d, + 0xd3, 0x56, 0xfd, 0x96, 0xfd, 0xbf, 0xa9, 0x83, 0x9e, 0x84, 0x52, 0x5b, 0x1c, 0x87, 0x7e, 0xdc, + 0x62, 0x77, 0x45, 0x06, 0xf8, 0x2d, 0xbe, 0xb5, 0x14, 0x8c, 0xf5, 0x3a, 0x46, 0xda, 0xe2, 0xc7, + 0xf7, 0x4a, 0x5b, 0x8c, 0xae, 0x41, 0x29, 0x09, 0x9b, 0x22, 0x73, 0x67, 0x5c, 0x2e, 0xb3, 0x15, + 0x78, 0x2e, 0x6f, 0x6f, 0xad, 0xab, 0x6a, 0xa9, 0x26, 0x9b, 0xc2, 0x62, 0xac, 0xe3, 0x61, 0xf1, + 0xb9, 0xe2, 0x4d, 0x81, 0x88, 0xa9, 0xb0, 0xf7, 0x67, 0xe2, 0x73, 0xf5, 0x42, 0x6c, 0xd6, 0x45, + 0x4b, 0x70, 0xbc, 0xdd, 0xa5, 0x03, 0xf3, 0x3b, 0x6a, 0x2a, 0x24, 0xa2, 0x5b, 0x01, 0xee, 0x6e, + 0x63, 0x68, 0xbf, 0x67, 0xf7, 0xd2, 0x7e, 0x7b, 0x24, 0xf1, 0x7d, 0xe0, 0x20, 0x49, 0x7c, 0x51, + 0x0d, 0x1e, 0xf0, 0x3a, 0x49, 0xc8, 0x12, 0xc6, 0x98, 0x4d, 0x78, 0xa8, 0xf2, 0x43, 0x3c, 0xfa, + 0xf9, 0xd6, 0xee, 0xf4, 0x03, 0xb3, 0x7b, 0xd4, 0xc3, 0x7b, 0x62, 0x41, 0xaf, 0xc0, 0x08, 0x11, + 0x89, 0x88, 0xcb, 0x3f, 0x67, 0x4b, 0x48, 0x30, 0x53, 0x1b, 0xcb, 0x28, 0x50, 0x0e, 0xc3, 0x8a, + 0x1e, 0x5a, 0x87, 0x52, 0x23, 0x8c, 0x93, 0xd9, 0xa6, 0xef, 0xc5, 0x24, 0x2e, 0x3f, 0xc8, 0x16, + 0x4d, 0xae, 0xec, 0x75, 0x49, 0x56, 0x4b, 0xd7, 0xcc, 0xa5, 0xb4, 0x25, 0xd6, 0xd1, 0x20, 0xc2, + 0xbc, 0x97, 0x2c, 0x4e, 0x5b, 0x3a, 0x82, 0xce, 0xb1, 0x81, 0x3d, 0x9a, 0x87, 0x79, 0x2d, 0xac, + 0x55, 0xcc, 0xda, 0xca, 0x7d, 0xa9, 0x03, 0x71, 0x16, 0x27, 0x7a, 0x1a, 0xc6, 0xda, 0x61, 0xad, + 0xd2, 0x26, 0xd5, 0x35, 0x2f, 0xa9, 0x36, 0xca, 0xd3, 0xa6, 0xd5, 0x6d, 0x4d, 0x2b, 0xc3, 0x46, + 0x4d, 0xd4, 0x86, 0xe1, 0x16, 0xcf, 0x24, 0x50, 0x7e, 0xd8, 0x96, 0x6e, 0x23, 0x52, 0x13, 0x70, + 0x79, 0x41, 0xfc, 0xc0, 0x92, 0x0c, 0xfa, 0xc7, 0x0e, 0x1c, 0xcb, 0xdc, 0x7e, 0x2a, 0xbf, 0xc3, + 0x9a, 0xc8, 0x62, 0x22, 0x9e, 0x7b, 0x94, 0x4d, 0x9f, 0x09, 0xbc, 0xdd, 0x0d, 0xc2, 0xd9, 0x1e, + 0xf1, 0x79, 0x61, 0xe9, 0x40, 0xca, 0x8f, 0xd8, 0x9b, 0x17, 0x86, 0x50, 0xce, 0x0b, 0xfb, 0x81, + 0x25, 0x19, 0xf4, 0x38, 0x0c, 0x8b, 0xcc, 0x7d, 0xe5, 0x47, 0x4d, 0x17, 0xb4, 0x48, 0xf0, 0x87, + 0x65, 0xf9, 0xd4, 0x07, 0xe0, 0x78, 0x97, 0xea, 0xb6, 0xaf, 0x9c, 0x14, 0xbf, 0xe1, 0x80, 0x7e, + 0x5d, 0xda, 0xfa, 0xeb, 0x1f, 0x4f, 0xc3, 0x58, 0x95, 0x3f, 0xb5, 0xc8, 0x2f, 0x5c, 0x0f, 0x9a, + 0xf6, 0xcf, 0x79, 0xad, 0x0c, 0x1b, 0x35, 0xdd, 0x4b, 0x80, 0xba, 0x53, 0xb3, 0x1f, 0x28, 0xe1, + 0xd1, 0x3f, 0x75, 0x60, 0xdc, 0x90, 0x19, 0xac, 0x3b, 0x19, 0x17, 0x01, 0xb5, 0xfc, 0x28, 0x0a, + 0x23, 0xfd, 0x4d, 0x3b, 0x91, 0x14, 0x81, 0x5d, 0x3b, 0x5b, 0xe9, 0x2a, 0xc5, 0x39, 0x2d, 0xdc, + 0x7f, 0x3e, 0x08, 0x69, 0x18, 0xb4, 0xca, 0x7d, 0xeb, 0xf4, 0xcc, 0x7d, 0xfb, 0x04, 0x8c, 0xbc, + 0x14, 0x87, 0xc1, 0x5a, 0x9a, 0x21, 0x57, 0x7d, 0x8b, 0x67, 0x2a, 0xab, 0x57, 0x59, 0x4d, 0x55, + 0x83, 0xd5, 0x7e, 0x79, 0xd1, 0x6f, 0x26, 0xdd, 0x29, 0x54, 0x9f, 0x79, 0x96, 0xc3, 0xb1, 0xaa, + 0xc1, 0x9e, 0xb7, 0xdb, 0x26, 0xca, 0x30, 0x9e, 0x3e, 0x6f, 0xc7, 0x5f, 0x5d, 0x60, 0x65, 0xe8, + 0x3c, 0x8c, 0x2a, 0xa3, 0xba, 0xb0, 0xd4, 0xab, 0x99, 0x52, 0x96, 0x77, 0x9c, 0xd6, 0x61, 0x02, + 0xa1, 0x30, 0xc4, 0x0a, 0x13, 0x4a, 0xc5, 0x86, 0x7a, 0x92, 0x31, 0xed, 0x72, 0xde, 0x2e, 0xc1, + 0x58, 0x91, 0xcc, 0x73, 0xb4, 0x8e, 0x1e, 0x8a, 0xa3, 0x55, 0x8b, 0xc9, 0x2f, 0xf6, 0x1b, 0x93, + 0x6f, 0xae, 0xed, 0x91, 0xbe, 0xd6, 0xf6, 0xa7, 0x07, 0x60, 0xf8, 0x39, 0x12, 0xb1, 0xcc, 0xe1, + 0x8f, 0xc3, 0xf0, 0x36, 0xff, 0x37, 0x7b, 0xa1, 0x53, 0xd4, 0xc0, 0xb2, 0x9c, 0x7e, 0xb7, 0x8d, + 0x8e, 0xdf, 0xac, 0x2d, 0xa4, 0xbb, 0x38, 0x4d, 0x3a, 0x28, 0x0b, 0x70, 0x5a, 0x87, 0x36, 0xa8, + 0x53, 0xc9, 0xbe, 0xd5, 0xf2, 0xbb, 0x5e, 0x6e, 0x5f, 0x92, 0x05, 0x38, 0xad, 0x83, 0x1e, 0x85, + 0xa1, 0xba, 0x9f, 0xac, 0x7b, 0xf5, 0xac, 0x9b, 0x70, 0x89, 0x41, 0xb1, 0x28, 0x65, 0x6e, 0x22, + 0x3f, 0x59, 0x8f, 0x08, 0xb3, 0xec, 0x76, 0xe5, 0x93, 0x58, 0xd2, 0xca, 0xb0, 0x51, 0x93, 0x75, + 0x29, 0x14, 0x23, 0x13, 0x51, 0x9c, 0x69, 0x97, 0x64, 0x01, 0x4e, 0xeb, 0xd0, 0xf5, 0x5f, 0x0d, + 0x5b, 0x6d, 0xbf, 0x29, 0xe2, 0x8b, 0xb5, 0xf5, 0x3f, 0x2f, 0xe0, 0x58, 0xd5, 0xa0, 0xb5, 0x29, + 0x0b, 0xa3, 0xec, 0x27, 0xfb, 0x94, 0xd8, 0x9a, 0x80, 0x63, 0x55, 0xc3, 0x7d, 0x0e, 0xc6, 0xf9, + 0x4e, 0x9e, 0x6f, 0x7a, 0x7e, 0x6b, 0x69, 0x1e, 0x5d, 0xec, 0x8a, 0xc9, 0x7f, 0x3c, 0x27, 0x26, + 0xff, 0x94, 0xd1, 0xa8, 0x3b, 0x36, 0xdf, 0xfd, 0x51, 0x01, 0x46, 0x8e, 0xf0, 0x35, 0xc6, 0x23, + 0x7f, 0x58, 0x18, 0xdd, 0xcc, 0xbc, 0xc4, 0xb8, 0x66, 0xf3, 0x8a, 0xcd, 0x9e, 0xaf, 0x30, 0xfe, + 0xd7, 0x02, 0x9c, 0x96, 0x55, 0xa5, 0x2e, 0xb7, 0x34, 0xcf, 0xde, 0xc0, 0x3a, 0xfc, 0x89, 0x8e, + 0x8c, 0x89, 0x5e, 0xb3, 0xa7, 0x8d, 0x2e, 0xcd, 0xf7, 0x9c, 0xea, 0x57, 0x32, 0x53, 0x8d, 0xad, + 0x52, 0xdd, 0x7b, 0xb2, 0xff, 0xdc, 0x81, 0xa9, 0xfc, 0xc9, 0x3e, 0x82, 0xc7, 0x2f, 0x5f, 0x37, + 0x1f, 0xbf, 0xfc, 0x45, 0x7b, 0x4b, 0xcc, 0x1c, 0x4a, 0x8f, 0x67, 0x30, 0xff, 0xd4, 0x81, 0x93, + 0xb2, 0x01, 0x3b, 0x3d, 0xe7, 0xfc, 0x80, 0x45, 0xb2, 0x1c, 0xfe, 0x32, 0x7b, 0xcd, 0x58, 0x66, + 0x2f, 0xd8, 0x1b, 0xb8, 0x3e, 0x8e, 0x9e, 0x8f, 0x86, 0xff, 0x89, 0x03, 0xe5, 0xbc, 0x06, 0x47, + 0xf0, 0xc9, 0x5f, 0x35, 0x3f, 0xf9, 0x73, 0x87, 0x33, 0xf2, 0xde, 0x1f, 0xbc, 0xdc, 0x6b, 0xa2, + 0x50, 0x53, 0xca, 0x55, 0x8e, 0x2d, 0x1f, 0x2d, 0x27, 0x91, 0x2f, 0xa0, 0x35, 0x61, 0x28, 0x66, + 0x51, 0x1b, 0x62, 0x09, 0x5c, 0xb2, 0x21, 0x6d, 0x51, 0x7c, 0xc2, 0xc6, 0xce, 0xfe, 0xc7, 0x82, + 0x86, 0xfb, 0x9b, 0x05, 0x38, 0xa3, 0x1e, 0xb5, 0x25, 0xdb, 0xa4, 0x99, 0xee, 0x0f, 0xf6, 0xce, + 0x82, 0xa7, 0x7e, 0xda, 0x7b, 0x67, 0x21, 0x25, 0x91, 0xee, 0x85, 0x14, 0x86, 0x35, 0x9a, 0xa8, + 0x02, 0xa7, 0xd8, 0xbb, 0x08, 0x8b, 0x7e, 0xe0, 0x35, 0xfd, 0x57, 0x48, 0x84, 0x49, 0x2b, 0xdc, + 0xf6, 0x9a, 0x42, 0x52, 0x57, 0x77, 0x7a, 0x17, 0xf3, 0x2a, 0xe1, 0xfc, 0xb6, 0x5d, 0x1a, 0xf7, + 0x40, 0xbf, 0x1a, 0xb7, 0xfb, 0xc7, 0x0e, 0x8c, 0x1d, 0xe1, 0x13, 0xc0, 0xa1, 0xb9, 0x25, 0x9e, + 0xb1, 0xb7, 0x25, 0x7a, 0x6c, 0x83, 0xdd, 0x22, 0x74, 0xbd, 0x8a, 0x8a, 0x3e, 0xe3, 0xa8, 0xb8, + 0x16, 0x1e, 0x3c, 0xf8, 0x61, 0x7b, 0xfd, 0xd8, 0x4f, 0x22, 0x48, 0xf4, 0x8d, 0x4c, 0x76, 0xcc, + 0x82, 0xad, 0x14, 0x4f, 0x5d, 0xbd, 0x39, 0x40, 0x96, 0xcc, 0xaf, 0x3a, 0x00, 0xbc, 0x9f, 0x22, + 0xb9, 0x36, 0xed, 0xdb, 0xc6, 0xa1, 0xcd, 0x14, 0x25, 0xc2, 0xbb, 0xa6, 0xb6, 0x50, 0x5a, 0x80, + 0xb5, 0x9e, 0xdc, 0x45, 0xfa, 0xcb, 0xbb, 0xce, 0xbc, 0xf9, 0x45, 0x07, 0x8e, 0x65, 0xba, 0x9b, + 0xd3, 0x7e, 0xd3, 0x7c, 0xe6, 0xcf, 0x82, 0x64, 0x65, 0xa6, 0x5c, 0xd6, 0x8d, 0x27, 0xff, 0xdd, + 0x05, 0xe3, 0x39, 0x69, 0xf4, 0x2a, 0x8c, 0x4a, 0xcb, 0x87, 0x5c, 0xde, 0x36, 0x9f, 0x3b, 0x55, + 0xea, 0x8d, 0x84, 0xc4, 0x38, 0xa5, 0x97, 0x09, 0x9b, 0x2b, 0xf4, 0x15, 0x36, 0x77, 0x6f, 0x1f, + 0x4b, 0xcd, 0xb7, 0x4b, 0x0f, 0x1e, 0x8a, 0x5d, 0xfa, 0x01, 0xeb, 0x76, 0xe9, 0x07, 0x8f, 0xd8, + 0x2e, 0xad, 0x39, 0x09, 0x8b, 0x77, 0xe1, 0x24, 0x7c, 0x15, 0x4e, 0x6e, 0xa7, 0x4a, 0xa7, 0x5a, + 0x49, 0x22, 0xb1, 0xd0, 0xe3, 0xb9, 0xd6, 0x68, 0xaa, 0x40, 0xc7, 0x09, 0x09, 0x12, 0x4d, 0x5d, + 0x4d, 0x23, 0xf6, 0x9e, 0xcb, 0x41, 0x87, 0x73, 0x89, 0x64, 0xbd, 0x3d, 0xc3, 0x7d, 0x78, 0x7b, + 0xde, 0x74, 0xe0, 0x94, 0xd7, 0x75, 0x09, 0x0c, 0x93, 0x4d, 0x11, 0x72, 0x72, 0xdd, 0x9e, 0x08, + 0x61, 0xa0, 0x17, 0x6e, 0xb5, 0xbc, 0x22, 0x9c, 0xdf, 0x21, 0xf4, 0x48, 0xea, 0x7a, 0xe7, 0x71, + 0x9e, 0xf9, 0x7e, 0xf2, 0x6f, 0x64, 0xe3, 0x79, 0x80, 0x4d, 0xfd, 0x47, 0xed, 0x6a, 0xdb, 0x16, + 0x62, 0x7a, 0x4a, 0x77, 0x11, 0xd3, 0x93, 0x71, 0xbd, 0x8d, 0x59, 0x72, 0xbd, 0x05, 0x30, 0xe9, + 0xb7, 0xbc, 0x3a, 0x59, 0xeb, 0x34, 0x9b, 0xfc, 0x12, 0x89, 0x7c, 0x90, 0x36, 0xd7, 0x82, 0x77, + 0x25, 0xac, 0x7a, 0xcd, 0xec, 0x53, 0xe4, 0xea, 0xb2, 0xcc, 0xe5, 0x0c, 0x26, 0xdc, 0x85, 0x9b, + 0x2e, 0x58, 0x96, 0xe1, 0x8e, 0x24, 0x74, 0xb6, 0x59, 0xe0, 0xc8, 0x08, 0x5f, 0xb0, 0x97, 0x52, + 0x30, 0xd6, 0xeb, 0xa0, 0x65, 0x18, 0xad, 0x05, 0xb1, 0xb8, 0xcf, 0x7a, 0x8c, 0x31, 0xb3, 0x77, + 0x51, 0x16, 0xb8, 0x70, 0xb5, 0xa2, 0x6e, 0xb2, 0x3e, 0x90, 0x93, 0xb2, 0x51, 0x95, 0xe3, 0xb4, + 0x3d, 0x5a, 0x61, 0xc8, 0xc4, 0x6b, 0x5d, 0x3c, 0x9e, 0xe3, 0xa1, 0x1e, 0x0e, 0xa3, 0x85, 0xab, + 0xf2, 0xbd, 0xb1, 0x71, 0x41, 0x4e, 0x3c, 0xbb, 0x95, 0x62, 0xd0, 0x1e, 0x06, 0x3e, 0xbe, 0xe7, + 0xc3, 0xc0, 0x2c, 0x57, 0x6b, 0xd2, 0x54, 0xee, 0xe1, 0x73, 0xd6, 0x72, 0xb5, 0xa6, 0x91, 0x92, + 0x22, 0x57, 0x6b, 0x0a, 0xc0, 0x3a, 0x49, 0xb4, 0xda, 0xcb, 0x4d, 0x7e, 0x82, 0x31, 0x8d, 0xfd, + 0x3b, 0xbd, 0x75, 0x7f, 0xe9, 0xc9, 0x3d, 0xfd, 0xa5, 0x5d, 0xfe, 0xdd, 0x53, 0xfb, 0xf0, 0xef, + 0x36, 0x58, 0x16, 0xcd, 0xa5, 0x79, 0xe1, 0x52, 0xb7, 0xa0, 0xdf, 0xb1, 0xbc, 0x1d, 0x3c, 0xf2, + 0x94, 0xfd, 0x8b, 0x39, 0x81, 0x9e, 0x01, 0xd5, 0x67, 0x0e, 0x1c, 0x50, 0x4d, 0xd9, 0x73, 0x0a, + 0x67, 0xe9, 0x58, 0x8b, 0x82, 0x3d, 0xa7, 0x60, 0xac, 0xd7, 0xc9, 0x7a, 0x4b, 0xef, 0x3f, 0x34, + 0x6f, 0xe9, 0xd4, 0x11, 0x78, 0x4b, 0xcf, 0xf6, 0xed, 0x2d, 0xbd, 0x09, 0x27, 0xda, 0x61, 0x6d, + 0xc1, 0x8f, 0xa3, 0x0e, 0xbb, 0x55, 0x37, 0xd7, 0xa9, 0xd5, 0x49, 0xc2, 0xdc, 0xad, 0xa5, 0x0b, + 0xef, 0xd2, 0x3b, 0xd9, 0x66, 0x1b, 0x59, 0xee, 0xd1, 0x4c, 0x03, 0x66, 0x3a, 0x61, 0x51, 0xb7, + 0x39, 0x85, 0x38, 0x8f, 0x84, 0xee, 0xa7, 0x7d, 0xe8, 0x68, 0xfc, 0xb4, 0x1f, 0x84, 0x91, 0xb8, + 0xd1, 0x49, 0x6a, 0xe1, 0x8d, 0x80, 0x39, 0xe3, 0x47, 0xe7, 0xde, 0xa1, 0x4c, 0xd9, 0x02, 0x7e, + 0x7b, 0x77, 0x7a, 0x52, 0xfe, 0xaf, 0x59, 0xb1, 0x05, 0x04, 0x7d, 0xb3, 0xc7, 0xfd, 0x1d, 0xf7, + 0x30, 0xef, 0xef, 0x9c, 0xd9, 0xd7, 0xdd, 0x9d, 0x3c, 0x67, 0xf4, 0xc3, 0x6f, 0x3b, 0x67, 0xf4, + 0xd7, 0x1d, 0x18, 0xdf, 0xd6, 0x5d, 0x06, 0xc2, 0x61, 0x6e, 0x21, 0x70, 0xc7, 0xf0, 0x44, 0xcc, + 0xb9, 0x94, 0xcf, 0x19, 0xa0, 0xdb, 0x59, 0x00, 0x36, 0x7b, 0x92, 0x13, 0x54, 0xf4, 0xc8, 0xbd, + 0x0a, 0x2a, 0x7a, 0x9d, 0xf1, 0x31, 0xa9, 0xe4, 0x32, 0x2f, 0xba, 0xdd, 0x98, 0x62, 0xc9, 0x13, + 0x55, 0x48, 0xb1, 0x4e, 0x0f, 0x7d, 0xc1, 0x81, 0x49, 0xa9, 0x97, 0x09, 0x97, 0x5f, 0x2c, 0xa2, + 0x22, 0x6d, 0xaa, 0x83, 0x2c, 0xac, 0x7e, 0x3d, 0x43, 0x07, 0x77, 0x51, 0xa6, 0x5c, 0x5d, 0x05, + 0xa1, 0xd5, 0x63, 0x16, 0xfc, 0x2b, 0x64, 0x98, 0xd9, 0x14, 0x8c, 0xf5, 0x3a, 0xe8, 0x5b, 0xea, + 0xb5, 0xff, 0xc7, 0x19, 0x43, 0x7f, 0xde, 0xb2, 0x6c, 0x6a, 0xe3, 0xc9, 0x7f, 0xf4, 0x65, 0x07, + 0x26, 0x6f, 0x64, 0x0c, 0x1a, 0x22, 0x2c, 0x14, 0xdb, 0x37, 0x95, 0xf0, 0xe9, 0xce, 0x42, 0x71, + 0x57, 0x0f, 0xd0, 0xe7, 0x4d, 0x43, 0x27, 0x8f, 0x1f, 0xb5, 0x38, 0x81, 0x19, 0xc3, 0x2a, 0xbf, + 0xe6, 0x96, 0x6f, 0xf1, 0xbc, 0xeb, 0xf8, 0x90, 0x29, 0x3a, 0x98, 0xf4, 0x63, 0xe5, 0x34, 0x25, + 0xa6, 0xbd, 0xc5, 0xc2, 0x66, 0x37, 0x3e, 0xbf, 0x6e, 0x6e, 0xf9, 0xf2, 0x69, 0x98, 0x30, 0x7d, + 0x7b, 0xe8, 0xdd, 0xe6, 0x43, 0x12, 0xe7, 0xb2, 0x39, 0xf9, 0xc7, 0x65, 0x7d, 0x23, 0x2f, 0xbf, + 0x91, 0x38, 0xbf, 0x70, 0xa8, 0x89, 0xf3, 0x07, 0x8e, 0x26, 0x71, 0xfe, 0xe4, 0x61, 0x24, 0xce, + 0x3f, 0xbe, 0xaf, 0xc4, 0xf9, 0xda, 0xc3, 0x05, 0x83, 0x77, 0x78, 0xb8, 0x60, 0x16, 0x8e, 0xc9, + 0xbb, 0x3f, 0x44, 0xe4, 0x26, 0xe7, 0x6e, 0xff, 0x33, 0xa2, 0xc9, 0xb1, 0x79, 0xb3, 0x18, 0x67, + 0xeb, 0xd3, 0x4d, 0x56, 0x0c, 0x58, 0xcb, 0x21, 0x5b, 0xaf, 0x1a, 0x99, 0x4b, 0x8b, 0xa9, 0xcf, + 0x82, 0x45, 0xc9, 0x68, 0xe7, 0x22, 0x83, 0xdd, 0x96, 0xff, 0x60, 0xde, 0x03, 0xf4, 0x22, 0x94, + 0xc3, 0xcd, 0xcd, 0x66, 0xe8, 0xd5, 0xd2, 0xec, 0xfe, 0x32, 0x2e, 0x81, 0xdf, 0xdd, 0x54, 0xc9, + 0x60, 0x57, 0x7b, 0xd4, 0xc3, 0x3d, 0x31, 0xa0, 0x37, 0xa9, 0x60, 0x92, 0x84, 0x11, 0xa9, 0xa5, + 0xb6, 0x9a, 0x51, 0x36, 0x66, 0x62, 0x7d, 0xcc, 0x15, 0x93, 0x0e, 0x1f, 0xbd, 0xfa, 0x28, 0x99, + 0x52, 0x9c, 0xed, 0x16, 0x8a, 0xe0, 0x74, 0x3b, 0xcf, 0x54, 0x14, 0x8b, 0x1b, 0x4b, 0x7b, 0x19, + 0xac, 0xe4, 0xd6, 0x3d, 0x9d, 0x6b, 0x6c, 0x8a, 0x71, 0x0f, 0xcc, 0x7a, 0x06, 0xfe, 0x91, 0xa3, + 0xc9, 0xc0, 0xff, 0x71, 0x00, 0x75, 0x49, 0x5d, 0x1a, 0x1f, 0x96, 0xad, 0x5c, 0xa5, 0xe1, 0x38, + 0xb5, 0x47, 0x4f, 0x15, 0x19, 0xac, 0x91, 0x44, 0xff, 0x37, 0xf7, 0x89, 0x0a, 0x6e, 0x61, 0xa9, + 0x5b, 0x5f, 0x13, 0x6f, 0xbb, 0x67, 0x2a, 0xfe, 0x89, 0x03, 0x53, 0x7c, 0xe5, 0x65, 0x85, 0x7b, + 0x2a, 0x5a, 0x88, 0xbb, 0x3d, 0xb6, 0x43, 0x57, 0x58, 0x14, 0x5f, 0xc5, 0xa0, 0xca, 0x1c, 0xdd, + 0x7b, 0xf4, 0x04, 0x7d, 0x35, 0x47, 0xa5, 0x38, 0x66, 0xcb, 0x66, 0x99, 0xff, 0xd0, 0xc0, 0x89, + 0x5b, 0xfd, 0x68, 0x11, 0xff, 0xac, 0xa7, 0x49, 0x15, 0xb1, 0xee, 0xfd, 0xd2, 0x21, 0x99, 0x54, + 0xf5, 0xd7, 0x10, 0xf6, 0x65, 0x58, 0xfd, 0xa2, 0x03, 0x93, 0x5e, 0x26, 0xd4, 0x84, 0xd9, 0x81, + 0xac, 0xd8, 0xa4, 0x66, 0xa3, 0x34, 0x7e, 0x85, 0x09, 0x79, 0xd9, 0xa8, 0x16, 0xdc, 0x45, 0x1c, + 0xfd, 0xc8, 0x81, 0xb3, 0x89, 0x17, 0x6f, 0xf1, 0x5c, 0xc3, 0x71, 0x7a, 0x57, 0x57, 0x74, 0xee, + 0x24, 0xdb, 0x8d, 0x2f, 0x5b, 0xdf, 0x8d, 0xeb, 0xbd, 0x69, 0xf2, 0x7d, 0xf9, 0xb0, 0xd8, 0x97, + 0x67, 0xf7, 0xa8, 0x89, 0xf7, 0xea, 0xfa, 0xd4, 0x67, 0x1c, 0xfe, 0x26, 0x55, 0x4f, 0x91, 0x6f, + 0xc3, 0x14, 0xf9, 0xae, 0xd8, 0x7c, 0x15, 0x47, 0x97, 0x3d, 0x7f, 0xd5, 0x81, 0x93, 0x79, 0x27, + 0x52, 0x4e, 0x97, 0x3e, 0x6a, 0x76, 0xc9, 0xa2, 0x96, 0xa5, 0x77, 0xc8, 0xca, 0xa3, 0x1c, 0x53, + 0x57, 0xe1, 0xa1, 0x3b, 0x7d, 0xc5, 0x3b, 0xe1, 0x1b, 0xd1, 0xc5, 0xe2, 0x3f, 0x19, 0xd5, 0xbc, + 0x90, 0x09, 0x69, 0x5b, 0x8f, 0xe1, 0x0e, 0x60, 0xc8, 0x0f, 0x9a, 0x7e, 0x40, 0xc4, 0x7d, 0x4d, + 0x9b, 0x3a, 0xac, 0x78, 0x54, 0x87, 0x62, 0xc7, 0x82, 0xca, 0x3d, 0x76, 0x4a, 0x66, 0x9f, 0x29, + 0x1b, 0x3c, 0xfa, 0x67, 0xca, 0x6e, 0xc0, 0xe8, 0x0d, 0x3f, 0x69, 0xb0, 0x60, 0x0a, 0xe1, 0xeb, + 0xb3, 0x70, 0xcf, 0x91, 0xa2, 0x4b, 0xc7, 0x7e, 0x5d, 0x12, 0xc0, 0x29, 0x2d, 0x74, 0x9e, 0x13, + 0x66, 0x91, 0xdb, 0xd9, 0x90, 0xda, 0xeb, 0xb2, 0x00, 0xa7, 0x75, 0xe8, 0x64, 0x8d, 0xd1, 0x5f, + 0x32, 0x21, 0x92, 0xc8, 0xdb, 0x6b, 0x23, 0x1f, 0xa3, 0xc0, 0xc8, 0x6f, 0x13, 0x5f, 0xd7, 0x68, + 0x60, 0x83, 0xa2, 0x4a, 0x9d, 0x3c, 0xd2, 0x33, 0x75, 0xf2, 0x6b, 0x4c, 0x60, 0x4b, 0xfc, 0xa0, + 0x43, 0x56, 0x03, 0x11, 0xef, 0x7d, 0xc5, 0xce, 0xdd, 0x67, 0x8e, 0x93, 0xab, 0xe0, 0xe9, 0x6f, + 0xac, 0xd1, 0xd3, 0x5c, 0x2e, 0xa5, 0x3d, 0x5d, 0x2e, 0xa9, 0xc9, 0x65, 0xcc, 0xba, 0xc9, 0x25, + 0x21, 0x6d, 0x2b, 0x26, 0x97, 0xb7, 0x95, 0x39, 0xe0, 0xcf, 0x1d, 0x40, 0x4a, 0xee, 0x52, 0x0c, + 0xf5, 0x08, 0x82, 0x2a, 0x3f, 0xe1, 0x00, 0x04, 0xea, 0x31, 0x4b, 0xbb, 0xa7, 0x20, 0xc7, 0x99, + 0x76, 0x20, 0x85, 0x61, 0x8d, 0xa6, 0xfb, 0x3f, 0x9d, 0x34, 0x76, 0x39, 0x1d, 0xfb, 0x11, 0x04, + 0x91, 0xed, 0x98, 0x41, 0x64, 0xeb, 0x16, 0x4d, 0xf7, 0x6a, 0x18, 0x3d, 0xc2, 0xc9, 0x7e, 0x5a, + 0x80, 0x63, 0x7a, 0xe5, 0x0a, 0x39, 0x8a, 0x8f, 0x7d, 0xc3, 0x88, 0xa0, 0xbd, 0x66, 0x77, 0xbc, + 0x15, 0xe1, 0x01, 0xca, 0x8b, 0xd6, 0xfe, 0x78, 0x26, 0x5a, 0xfb, 0xba, 0x7d, 0xd2, 0x7b, 0x87, + 0x6c, 0xff, 0x37, 0x07, 0x4e, 0x64, 0x5a, 0x1c, 0xc1, 0x02, 0xdb, 0x36, 0x17, 0xd8, 0xb3, 0xd6, + 0x47, 0xdd, 0x63, 0x75, 0x7d, 0xbb, 0xd0, 0x35, 0x5a, 0xa6, 0xc4, 0x7d, 0xda, 0x81, 0x22, 0x95, + 0x96, 0x65, 0x3c, 0xd7, 0x47, 0x0f, 0x65, 0x05, 0x30, 0xb9, 0x5e, 0x70, 0x67, 0xd5, 0x3f, 0x06, + 0xc3, 0x9c, 0xfa, 0xd4, 0xa7, 0x1c, 0x80, 0xb4, 0xd2, 0xbd, 0x12, 0x81, 0xdd, 0xef, 0x16, 0xe0, + 0x54, 0xee, 0x32, 0x42, 0x9f, 0x55, 0x16, 0x39, 0xc7, 0x76, 0xb4, 0xa2, 0x41, 0x48, 0x37, 0xcc, + 0x8d, 0x1b, 0x86, 0x39, 0x61, 0x8f, 0xbb, 0x57, 0x0a, 0x8c, 0x60, 0xd3, 0xda, 0x64, 0xfd, 0xc4, + 0x49, 0x03, 0x60, 0x55, 0x5e, 0xa3, 0xbf, 0x84, 0x97, 0x78, 0xdc, 0x9f, 0x6a, 0x37, 0x1c, 0xe4, + 0x40, 0x8f, 0x80, 0x57, 0xdc, 0x30, 0x79, 0x05, 0xb6, 0xef, 0x47, 0xee, 0xc1, 0x2c, 0x5e, 0x86, + 0x3c, 0xc7, 0x72, 0x7f, 0x49, 0x11, 0x8d, 0xeb, 0xb0, 0x85, 0xbe, 0xaf, 0xc3, 0x8e, 0x43, 0xe9, + 0x05, 0x5f, 0x65, 0xd3, 0x9c, 0x9b, 0xf9, 0xde, 0x8f, 0xcf, 0xdd, 0xf7, 0xfd, 0x1f, 0x9f, 0xbb, + 0xef, 0x47, 0x3f, 0x3e, 0x77, 0xdf, 0x27, 0x6e, 0x9d, 0x73, 0xbe, 0x77, 0xeb, 0x9c, 0xf3, 0xfd, + 0x5b, 0xe7, 0x9c, 0x1f, 0xdd, 0x3a, 0xe7, 0xfc, 0xc7, 0x5b, 0xe7, 0x9c, 0xbf, 0xf7, 0x9f, 0xce, + 0xdd, 0xf7, 0xc2, 0x88, 0x1c, 0xd8, 0x5f, 0x04, 0x00, 0x00, 0xff, 0xff, 0x00, 0x23, 0x7a, 0xc2, + 0xb6, 0xd6, 0x00, 0x00, } func (m *Amount) Marshal() (dAtA []byte, err error) { @@ -10232,18 +10230,11 @@ func (m *PodGC) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l - if m.DeleteDelayDuration != nil { - { - size, err := m.DeleteDelayDuration.MarshalToSizedBuffer(dAtA[:i]) - if err != nil { - return 0, err - } - i -= size - i = encodeVarintGenerated(dAtA, i, uint64(size)) - } - i-- - dAtA[i] = 0x1a - } + i -= len(m.DeleteDelayDuration) + copy(dAtA[i:], m.DeleteDelayDuration) + i = encodeVarintGenerated(dAtA, i, uint64(len(m.DeleteDelayDuration))) + i-- + dAtA[i] = 0x1a if m.LabelSelector != nil { { size, err := m.LabelSelector.MarshalToSizedBuffer(dAtA[:i]) @@ -16000,10 +15991,8 @@ func (m *PodGC) Size() (n int) { l = m.LabelSelector.Size() n += 1 + l + sovGenerated(uint64(l)) } - if m.DeleteDelayDuration != nil { - l = m.DeleteDelayDuration.Size() - n += 1 + l + sovGenerated(uint64(l)) - } + l = len(m.DeleteDelayDuration) + n += 1 + l + sovGenerated(uint64(l)) return n } @@ -18738,7 +18727,7 @@ func (this *PodGC) String() string { s := strings.Join([]string{`&PodGC{`, `Strategy:` + fmt.Sprintf("%v", this.Strategy) + `,`, `LabelSelector:` + strings.Replace(fmt.Sprintf("%v", this.LabelSelector), "LabelSelector", "v11.LabelSelector", 1) + `,`, - `DeleteDelayDuration:` + strings.Replace(fmt.Sprintf("%v", this.DeleteDelayDuration), "Duration", "v11.Duration", 1) + `,`, + `DeleteDelayDuration:` + fmt.Sprintf("%v", this.DeleteDelayDuration) + `,`, `}`, }, "") return s @@ -35563,7 +35552,7 @@ func (m *PodGC) Unmarshal(dAtA []byte) error { if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field DeleteDelayDuration", wireType) } - var msglen int + var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowGenerated @@ -35573,27 +35562,23 @@ func (m *PodGC) Unmarshal(dAtA []byte) error { } b := dAtA[iNdEx] iNdEx++ - msglen |= int(b&0x7F) << shift + stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } - if msglen < 0 { + intStringLen := int(stringLen) + if intStringLen < 0 { return ErrInvalidLengthGenerated } - postIndex := iNdEx + msglen + postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthGenerated } if postIndex > l { return io.ErrUnexpectedEOF } - if m.DeleteDelayDuration == nil { - m.DeleteDelayDuration = &v11.Duration{} - } - if err := m.DeleteDelayDuration.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { - return err - } + m.DeleteDelayDuration = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex default: iNdEx = preIndex diff --git a/pkg/apis/workflow/v1alpha1/generated.proto b/pkg/apis/workflow/v1alpha1/generated.proto index 75daf99022cb..ec331f86d739 100644 --- a/pkg/apis/workflow/v1alpha1/generated.proto +++ b/pkg/apis/workflow/v1alpha1/generated.proto @@ -1263,7 +1263,7 @@ message PodGC { optional k8s.io.apimachinery.pkg.apis.meta.v1.LabelSelector labelSelector = 2; // DeleteDelayDuration specifies the duration before pods in the GC queue get deleted. - optional k8s.io.apimachinery.pkg.apis.meta.v1.Duration deleteDelayDuration = 3; + optional string deleteDelayDuration = 3; } // Prometheus is a prometheus metric to be emitted diff --git a/pkg/apis/workflow/v1alpha1/openapi_generated.go b/pkg/apis/workflow/v1alpha1/openapi_generated.go index 56d15884603e..46e85c44e60f 100644 --- a/pkg/apis/workflow/v1alpha1/openapi_generated.go +++ b/pkg/apis/workflow/v1alpha1/openapi_generated.go @@ -4831,14 +4831,15 @@ func schema_pkg_apis_workflow_v1alpha1_PodGC(ref common.ReferenceCallback) commo "deleteDelayDuration": { SchemaProps: spec.SchemaProps{ Description: "DeleteDelayDuration specifies the duration before pods in the GC queue get deleted.", - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Duration"), + Type: []string{"string"}, + Format: "", }, }, }, }, }, Dependencies: []string{ - "k8s.io/apimachinery/pkg/apis/meta/v1.Duration", "k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector"}, + "k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector"}, } } diff --git a/pkg/apis/workflow/v1alpha1/utils.go b/pkg/apis/workflow/v1alpha1/utils.go index 2ddc06fd4215..c0f348df709c 100644 --- a/pkg/apis/workflow/v1alpha1/utils.go +++ b/pkg/apis/workflow/v1alpha1/utils.go @@ -7,14 +7,14 @@ import ( ) func ParseStringToDuration(durationString string) (time.Duration, error) { - var suspendDuration time.Duration + var duration time.Duration // If no units are attached, treat as seconds if val, err := strconv.Atoi(durationString); err == nil { - suspendDuration = time.Duration(val) * time.Second - } else if duration, err := time.ParseDuration(durationString); err == nil { - suspendDuration = duration + duration = time.Duration(val) * time.Second + } else if parsed, err := time.ParseDuration(durationString); err == nil { + duration = parsed } else { return 0, fmt.Errorf("unable to parse %s as a duration: %w", durationString, err) } - return suspendDuration, nil + return duration, nil } diff --git a/pkg/apis/workflow/v1alpha1/workflow_template_types_test.go b/pkg/apis/workflow/v1alpha1/workflow_template_types_test.go index 2b7738e28b7b..43050643e61e 100644 --- a/pkg/apis/workflow/v1alpha1/workflow_template_types_test.go +++ b/pkg/apis/workflow/v1alpha1/workflow_template_types_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -15,9 +16,8 @@ func TestWorkflowTemplates(t *testing.T) { {ObjectMeta: v1.ObjectMeta{Name: "0"}}, } sort.Sort(tmpls) - if assert.Len(t, tmpls, 3) { - assert.Equal(t, "0", tmpls[0].Name) - assert.Equal(t, "1", tmpls[1].Name) - assert.Equal(t, "2", tmpls[2].Name) - } + require.Len(t, tmpls, 3) + assert.Equal(t, "0", tmpls[0].Name) + assert.Equal(t, "1", tmpls[1].Name) + assert.Equal(t, "2", tmpls[2].Name) } diff --git a/pkg/apis/workflow/v1alpha1/workflow_types.go b/pkg/apis/workflow/v1alpha1/workflow_types.go index a807ddf9a8b6..096d1c5b3b59 100644 --- a/pkg/apis/workflow/v1alpha1/workflow_types.go +++ b/pkg/apis/workflow/v1alpha1/workflow_types.go @@ -1018,7 +1018,7 @@ type PodGC struct { // LabelSelector is the label selector to check if the pods match the labels before being added to the pod GC queue. LabelSelector *metav1.LabelSelector `json:"labelSelector,omitempty" protobuf:"bytes,2,opt,name=labelSelector"` // DeleteDelayDuration specifies the duration before pods in the GC queue get deleted. - DeleteDelayDuration *metav1.Duration `json:"deleteDelayDuration,omitempty" protobuf:"bytes,3,opt,name=deleteDelayDuration"` + DeleteDelayDuration string `json:"deleteDelayDuration,omitempty" protobuf:"bytes,3,opt,name=deleteDelayDuration"` } // GetLabelSelector gets the label selector from podGC. @@ -1039,6 +1039,13 @@ func (podGC *PodGC) GetStrategy() PodGCStrategy { return PodGCOnPodNone } +func (podGC *PodGC) GetDeleteDelayDuration() (time.Duration, error) { + if podGC == nil || podGC.DeleteDelayDuration == "" { + return -1, nil // negative return means the field was omitted + } + return ParseStringToDuration(podGC.DeleteDelayDuration) +} + // WorkflowLevelArtifactGC describes how to delete artifacts from completed Workflows - this spec is used on the Workflow level type WorkflowLevelArtifactGC struct { // ArtifactGC is an embedded struct @@ -2402,6 +2409,11 @@ func (n NodeStatus) IsExitNode() bool { return strings.HasSuffix(n.DisplayName, ".onExit") } +// IsPodDeleted returns whether node is error with pod deleted. +func (n NodeStatus) IsPodDeleted() bool { + return n.Phase == NodeError && n.Message == "pod deleted" +} + func (n NodeStatus) Succeeded() bool { return n.Phase == NodeSucceeded } diff --git a/pkg/apis/workflow/v1alpha1/workflow_types_test.go b/pkg/apis/workflow/v1alpha1/workflow_types_test.go index 39da1b038c00..fe75ecf7cfe4 100644 --- a/pkg/apis/workflow/v1alpha1/workflow_types_test.go +++ b/pkg/apis/workflow/v1alpha1/workflow_types_test.go @@ -894,9 +894,7 @@ func TestPrometheus_GetDescIsStable(t *testing.T) { } stableDesc := metric.GetKey() for i := 0; i < 10; i++ { - if !assert.Equal(t, stableDesc, metric.GetKey()) { - break - } + require.Equal(t, stableDesc, metric.GetKey()) } } diff --git a/pkg/apis/workflow/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/workflow/v1alpha1/zz_generated.deepcopy.go index f71aca040c98..149a3951cfae 100644 --- a/pkg/apis/workflow/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/workflow/v1alpha1/zz_generated.deepcopy.go @@ -2527,11 +2527,6 @@ func (in *PodGC) DeepCopyInto(out *PodGC) { *out = new(metav1.LabelSelector) (*in).DeepCopyInto(*out) } - if in.DeleteDelayDuration != nil { - in, out := &in.DeleteDelayDuration, &out.DeleteDelayDuration - *out = new(metav1.Duration) - **out = **in - } return } diff --git a/sdks/java/client/docs/Duration.md b/sdks/java/client/docs/Duration.md deleted file mode 100644 index e6f5fc5176a5..000000000000 --- a/sdks/java/client/docs/Duration.md +++ /dev/null @@ -1,14 +0,0 @@ - - -# Duration - -Duration is a wrapper around time.Duration which supports correct marshaling to YAML and JSON. In particular, it marshals into strings, which can be used as map keys in json. - -## Properties - -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**duration** | **String** | | [optional] - - - diff --git a/sdks/java/client/docs/IoArgoprojWorkflowV1alpha1PodGC.md b/sdks/java/client/docs/IoArgoprojWorkflowV1alpha1PodGC.md index d2bdd24a81b1..e539f998cd23 100644 --- a/sdks/java/client/docs/IoArgoprojWorkflowV1alpha1PodGC.md +++ b/sdks/java/client/docs/IoArgoprojWorkflowV1alpha1PodGC.md @@ -8,7 +8,7 @@ PodGC describes how to delete completed pods as they complete Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**deleteDelayDuration** | [**Duration**](Duration.md) | | [optional] +**deleteDelayDuration** | **String** | DeleteDelayDuration specifies the duration before pods in the GC queue get deleted. | [optional] **labelSelector** | [**LabelSelector**](LabelSelector.md) | | [optional] **strategy** | **String** | Strategy is the strategy to use. One of \"OnPodCompletion\", \"OnPodSuccess\", \"OnWorkflowCompletion\", \"OnWorkflowSuccess\". If unset, does not delete Pods | [optional] diff --git a/sdks/python/client/argo_workflows/model/duration.py b/sdks/python/client/argo_workflows/model/duration.py deleted file mode 100644 index 624f4e17cad6..000000000000 --- a/sdks/python/client/argo_workflows/model/duration.py +++ /dev/null @@ -1,255 +0,0 @@ -""" - Argo Workflows API - - Argo Workflows is an open source container-native workflow engine for orchestrating parallel jobs on Kubernetes. For more information, please see https://argo-workflows.readthedocs.io/en/latest/ # noqa: E501 - - The version of the OpenAPI document: VERSION - Generated by: https://openapi-generator.tech -""" - - -import re # noqa: F401 -import sys # noqa: F401 - -from argo_workflows.model_utils import ( # noqa: F401 - ApiTypeError, - ModelComposed, - ModelNormal, - ModelSimple, - cached_property, - change_keys_js_to_python, - convert_js_args_to_python_args, - date, - datetime, - file_type, - none_type, - validate_get_composed_info, - OpenApiModel -) -from argo_workflows.exceptions import ApiAttributeError - - - -class Duration(ModelNormal): - """NOTE: This class is auto generated by OpenAPI Generator. - Ref: https://openapi-generator.tech - - Do not edit the class manually. - - Attributes: - allowed_values (dict): The key is the tuple path to the attribute - and the for var_name this is (var_name,). The value is a dict - with a capitalized key describing the allowed value and an allowed - value. These dicts store the allowed enum values. - attribute_map (dict): The key is attribute name - and the value is json key in definition. - discriminator_value_class_map (dict): A dict to go from the discriminator - variable value to the discriminator class name. - validations (dict): The key is the tuple path to the attribute - and the for var_name this is (var_name,). The value is a dict - that stores validations for max_length, min_length, max_items, - min_items, exclusive_maximum, inclusive_maximum, exclusive_minimum, - inclusive_minimum, and regex. - additional_properties_type (tuple): A tuple of classes accepted - as additional properties values. - """ - - allowed_values = { - } - - validations = { - } - - @cached_property - def additional_properties_type(): - """ - This must be a method because a model may have properties that are - of type self, this must run after the class is loaded - """ - return (bool, date, datetime, dict, float, int, list, str, none_type,) # noqa: E501 - - _nullable = False - - @cached_property - def openapi_types(): - """ - This must be a method because a model may have properties that are - of type self, this must run after the class is loaded - - Returns - openapi_types (dict): The key is attribute name - and the value is attribute type. - """ - return { - 'duration': (str,), # noqa: E501 - } - - @cached_property - def discriminator(): - return None - - - attribute_map = { - 'duration': 'duration', # noqa: E501 - } - - read_only_vars = { - } - - _composed_schemas = {} - - @classmethod - @convert_js_args_to_python_args - def _from_openapi_data(cls, *args, **kwargs): # noqa: E501 - """Duration - a model defined in OpenAPI - - Keyword Args: - _check_type (bool): if True, values for parameters in openapi_types - will be type checked and a TypeError will be - raised if the wrong type is input. - Defaults to True - _path_to_item (tuple/list): This is a list of keys or values to - drill down to the model in received_data - when deserializing a response - _spec_property_naming (bool): True if the variable names in the input data - are serialized names, as specified in the OpenAPI document. - False if the variable names in the input data - are pythonic names, e.g. snake case (default) - _configuration (Configuration): the instance to use when - deserializing a file_type parameter. - If passed, type conversion is attempted - If omitted no type conversion is done. - _visited_composed_classes (tuple): This stores a tuple of - classes that we have traveled through so that - if we see that class again we will not use its - discriminator again. - When traveling through a discriminator, the - composed schema that is - is traveled through is added to this set. - For example if Animal has a discriminator - petType and we pass in "Dog", and the class Dog - allOf includes Animal, we move through Animal - once using the discriminator, and pick Dog. - Then in Dog, we will make an instance of the - Animal class but this time we won't travel - through its discriminator because we passed in - _visited_composed_classes = (Animal,) - duration (str): [optional] # noqa: E501 - """ - - _check_type = kwargs.pop('_check_type', True) - _spec_property_naming = kwargs.pop('_spec_property_naming', False) - _path_to_item = kwargs.pop('_path_to_item', ()) - _configuration = kwargs.pop('_configuration', None) - _visited_composed_classes = kwargs.pop('_visited_composed_classes', ()) - - self = super(OpenApiModel, cls).__new__(cls) - - if args: - raise ApiTypeError( - "Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." % ( - args, - self.__class__.__name__, - ), - path_to_item=_path_to_item, - valid_classes=(self.__class__,), - ) - - self._data_store = {} - self._check_type = _check_type - self._spec_property_naming = _spec_property_naming - self._path_to_item = _path_to_item - self._configuration = _configuration - self._visited_composed_classes = _visited_composed_classes + (self.__class__,) - - for var_name, var_value in kwargs.items(): - if var_name not in self.attribute_map and \ - self._configuration is not None and \ - self._configuration.discard_unknown_keys and \ - self.additional_properties_type is None: - # discard variable. - continue - setattr(self, var_name, var_value) - return self - - required_properties = set([ - '_data_store', - '_check_type', - '_spec_property_naming', - '_path_to_item', - '_configuration', - '_visited_composed_classes', - ]) - - @convert_js_args_to_python_args - def __init__(self, *args, **kwargs): # noqa: E501 - """Duration - a model defined in OpenAPI - - Keyword Args: - _check_type (bool): if True, values for parameters in openapi_types - will be type checked and a TypeError will be - raised if the wrong type is input. - Defaults to True - _path_to_item (tuple/list): This is a list of keys or values to - drill down to the model in received_data - when deserializing a response - _spec_property_naming (bool): True if the variable names in the input data - are serialized names, as specified in the OpenAPI document. - False if the variable names in the input data - are pythonic names, e.g. snake case (default) - _configuration (Configuration): the instance to use when - deserializing a file_type parameter. - If passed, type conversion is attempted - If omitted no type conversion is done. - _visited_composed_classes (tuple): This stores a tuple of - classes that we have traveled through so that - if we see that class again we will not use its - discriminator again. - When traveling through a discriminator, the - composed schema that is - is traveled through is added to this set. - For example if Animal has a discriminator - petType and we pass in "Dog", and the class Dog - allOf includes Animal, we move through Animal - once using the discriminator, and pick Dog. - Then in Dog, we will make an instance of the - Animal class but this time we won't travel - through its discriminator because we passed in - _visited_composed_classes = (Animal,) - duration (str): [optional] # noqa: E501 - """ - - _check_type = kwargs.pop('_check_type', True) - _spec_property_naming = kwargs.pop('_spec_property_naming', False) - _path_to_item = kwargs.pop('_path_to_item', ()) - _configuration = kwargs.pop('_configuration', None) - _visited_composed_classes = kwargs.pop('_visited_composed_classes', ()) - - if args: - raise ApiTypeError( - "Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." % ( - args, - self.__class__.__name__, - ), - path_to_item=_path_to_item, - valid_classes=(self.__class__,), - ) - - self._data_store = {} - self._check_type = _check_type - self._spec_property_naming = _spec_property_naming - self._path_to_item = _path_to_item - self._configuration = _configuration - self._visited_composed_classes = _visited_composed_classes + (self.__class__,) - - for var_name, var_value in kwargs.items(): - if var_name not in self.attribute_map and \ - self._configuration is not None and \ - self._configuration.discard_unknown_keys and \ - self.additional_properties_type is None: - # discard variable. - continue - setattr(self, var_name, var_value) - if var_name in self.read_only_vars: - raise ApiAttributeError(f"`{var_name}` is a read-only attribute. Use `from_openapi_data` to instantiate " - f"class with read only attributes.") diff --git a/sdks/python/client/argo_workflows/model/io_argoproj_workflow_v1alpha1_pod_gc.py b/sdks/python/client/argo_workflows/model/io_argoproj_workflow_v1alpha1_pod_gc.py index 06ccce15c88d..637d10088cc1 100644 --- a/sdks/python/client/argo_workflows/model/io_argoproj_workflow_v1alpha1_pod_gc.py +++ b/sdks/python/client/argo_workflows/model/io_argoproj_workflow_v1alpha1_pod_gc.py @@ -30,9 +30,7 @@ def lazy_import(): - from argo_workflows.model.duration import Duration from argo_workflows.model.label_selector import LabelSelector - globals()['Duration'] = Duration globals()['LabelSelector'] = LabelSelector @@ -89,7 +87,7 @@ def openapi_types(): """ lazy_import() return { - 'delete_delay_duration': (Duration,), # noqa: E501 + 'delete_delay_duration': (str,), # noqa: E501 'label_selector': (LabelSelector,), # noqa: E501 'strategy': (str,), # noqa: E501 } @@ -146,7 +144,7 @@ def _from_openapi_data(cls, *args, **kwargs): # noqa: E501 Animal class but this time we won't travel through its discriminator because we passed in _visited_composed_classes = (Animal,) - delete_delay_duration (Duration): [optional] # noqa: E501 + delete_delay_duration (str): DeleteDelayDuration specifies the duration before pods in the GC queue get deleted.. [optional] # noqa: E501 label_selector (LabelSelector): [optional] # noqa: E501 strategy (str): Strategy is the strategy to use. One of \"OnPodCompletion\", \"OnPodSuccess\", \"OnWorkflowCompletion\", \"OnWorkflowSuccess\". If unset, does not delete Pods. [optional] # noqa: E501 """ @@ -230,7 +228,7 @@ def __init__(self, *args, **kwargs): # noqa: E501 Animal class but this time we won't travel through its discriminator because we passed in _visited_composed_classes = (Animal,) - delete_delay_duration (Duration): [optional] # noqa: E501 + delete_delay_duration (str): DeleteDelayDuration specifies the duration before pods in the GC queue get deleted.. [optional] # noqa: E501 label_selector (LabelSelector): [optional] # noqa: E501 strategy (str): Strategy is the strategy to use. One of \"OnPodCompletion\", \"OnPodSuccess\", \"OnWorkflowCompletion\", \"OnWorkflowSuccess\". If unset, does not delete Pods. [optional] # noqa: E501 """ diff --git a/sdks/python/client/argo_workflows/models/__init__.py b/sdks/python/client/argo_workflows/models/__init__.py index 54f6ea4c1319..6e5421d79d19 100644 --- a/sdks/python/client/argo_workflows/models/__init__.py +++ b/sdks/python/client/argo_workflows/models/__init__.py @@ -27,7 +27,6 @@ from argo_workflows.model.downward_api_projection import DownwardAPIProjection from argo_workflows.model.downward_api_volume_file import DownwardAPIVolumeFile from argo_workflows.model.downward_api_volume_source import DownwardAPIVolumeSource -from argo_workflows.model.duration import Duration from argo_workflows.model.empty_dir_volume_source import EmptyDirVolumeSource from argo_workflows.model.env_from_source import EnvFromSource from argo_workflows.model.env_var import EnvVar diff --git a/sdks/python/client/docs/ClusterWorkflowTemplateServiceApi.md b/sdks/python/client/docs/ClusterWorkflowTemplateServiceApi.md index 5f2d50121d95..7fa0b7e482d9 100644 --- a/sdks/python/client/docs/ClusterWorkflowTemplateServiceApi.md +++ b/sdks/python/client/docs/ClusterWorkflowTemplateServiceApi.md @@ -974,9 +974,7 @@ with argo_workflows.ApiClient(configuration) as api_client: ), ), pod_gc=IoArgoprojWorkflowV1alpha1PodGC( - delete_delay_duration=Duration( - duration="duration_example", - ), + delete_delay_duration="delete_delay_duration_example", label_selector=LabelSelector( match_expressions=[ LabelSelectorRequirement( @@ -11908,9 +11906,7 @@ with argo_workflows.ApiClient(configuration) as api_client: ), ), pod_gc=IoArgoprojWorkflowV1alpha1PodGC( - delete_delay_duration=Duration( - duration="duration_example", - ), + delete_delay_duration="delete_delay_duration_example", label_selector=LabelSelector( match_expressions=[ LabelSelectorRequirement( @@ -22746,9 +22742,7 @@ with argo_workflows.ApiClient(configuration) as api_client: ), ), pod_gc=IoArgoprojWorkflowV1alpha1PodGC( - delete_delay_duration=Duration( - duration="duration_example", - ), + delete_delay_duration="delete_delay_duration_example", label_selector=LabelSelector( match_expressions=[ LabelSelectorRequirement( diff --git a/sdks/python/client/docs/CronWorkflowServiceApi.md b/sdks/python/client/docs/CronWorkflowServiceApi.md index 455b2749c5cd..aa41b5aef828 100644 --- a/sdks/python/client/docs/CronWorkflowServiceApi.md +++ b/sdks/python/client/docs/CronWorkflowServiceApi.md @@ -1034,9 +1034,7 @@ with argo_workflows.ApiClient(configuration) as api_client: ), ), pod_gc=IoArgoprojWorkflowV1alpha1PodGC( - delete_delay_duration=Duration( - duration="duration_example", - ), + delete_delay_duration="delete_delay_duration_example", label_selector=LabelSelector( match_expressions=[ LabelSelectorRequirement( @@ -12050,9 +12048,7 @@ with argo_workflows.ApiClient(configuration) as api_client: ), ), pod_gc=IoArgoprojWorkflowV1alpha1PodGC( - delete_delay_duration=Duration( - duration="duration_example", - ), + delete_delay_duration="delete_delay_duration_example", label_selector=LabelSelector( match_expressions=[ LabelSelectorRequirement( @@ -23151,9 +23147,7 @@ with argo_workflows.ApiClient(configuration) as api_client: ), ), pod_gc=IoArgoprojWorkflowV1alpha1PodGC( - delete_delay_duration=Duration( - duration="duration_example", - ), + delete_delay_duration="delete_delay_duration_example", label_selector=LabelSelector( match_expressions=[ LabelSelectorRequirement( diff --git a/sdks/python/client/docs/Duration.md b/sdks/python/client/docs/Duration.md deleted file mode 100644 index 599dd08ebc7f..000000000000 --- a/sdks/python/client/docs/Duration.md +++ /dev/null @@ -1,13 +0,0 @@ -# Duration - -Duration is a wrapper around time.Duration which supports correct marshaling to YAML and JSON. In particular, it marshals into strings, which can be used as map keys in json. - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**duration** | **str** | | [optional] -**any string name** | **bool, date, datetime, dict, float, int, list, str, none_type** | any string name can be used but the value must be the correct type | [optional] - -[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) - - diff --git a/sdks/python/client/docs/IoArgoprojWorkflowV1alpha1PodGC.md b/sdks/python/client/docs/IoArgoprojWorkflowV1alpha1PodGC.md index 5d30ba94296a..b1408987762d 100644 --- a/sdks/python/client/docs/IoArgoprojWorkflowV1alpha1PodGC.md +++ b/sdks/python/client/docs/IoArgoprojWorkflowV1alpha1PodGC.md @@ -5,7 +5,7 @@ PodGC describes how to delete completed pods as they complete ## Properties Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**delete_delay_duration** | [**Duration**](Duration.md) | | [optional] +**delete_delay_duration** | **str** | DeleteDelayDuration specifies the duration before pods in the GC queue get deleted. | [optional] **label_selector** | [**LabelSelector**](LabelSelector.md) | | [optional] **strategy** | **str** | Strategy is the strategy to use. One of \"OnPodCompletion\", \"OnPodSuccess\", \"OnWorkflowCompletion\", \"OnWorkflowSuccess\". If unset, does not delete Pods | [optional] **any string name** | **bool, date, datetime, dict, float, int, list, str, none_type** | any string name can be used but the value must be the correct type | [optional] diff --git a/sdks/python/client/docs/WorkflowServiceApi.md b/sdks/python/client/docs/WorkflowServiceApi.md index 4296e4211333..66b6889994de 100644 --- a/sdks/python/client/docs/WorkflowServiceApi.md +++ b/sdks/python/client/docs/WorkflowServiceApi.md @@ -989,9 +989,7 @@ with argo_workflows.ApiClient(configuration) as api_client: ), ), pod_gc=IoArgoprojWorkflowV1alpha1PodGC( - delete_delay_duration=Duration( - duration="duration_example", - ), + delete_delay_duration="delete_delay_duration_example", label_selector=LabelSelector( match_expressions=[ LabelSelectorRequirement( @@ -17567,9 +17565,7 @@ with argo_workflows.ApiClient(configuration) as api_client: ), ), pod_gc=IoArgoprojWorkflowV1alpha1PodGC( - delete_delay_duration=Duration( - duration="duration_example", - ), + delete_delay_duration="delete_delay_duration_example", label_selector=LabelSelector( match_expressions=[ LabelSelectorRequirement( @@ -28543,9 +28539,7 @@ with argo_workflows.ApiClient(configuration) as api_client: ), ), pod_gc=IoArgoprojWorkflowV1alpha1PodGC( - delete_delay_duration=Duration( - duration="duration_example", - ), + delete_delay_duration="delete_delay_duration_example", label_selector=LabelSelector( match_expressions=[ LabelSelectorRequirement( @@ -45121,9 +45115,7 @@ with argo_workflows.ApiClient(configuration) as api_client: ), ), pod_gc=IoArgoprojWorkflowV1alpha1PodGC( - delete_delay_duration=Duration( - duration="duration_example", - ), + delete_delay_duration="delete_delay_duration_example", label_selector=LabelSelector( match_expressions=[ LabelSelectorRequirement( diff --git a/sdks/python/client/docs/WorkflowTemplateServiceApi.md b/sdks/python/client/docs/WorkflowTemplateServiceApi.md index 8f9032d97d57..01f968cb76a7 100644 --- a/sdks/python/client/docs/WorkflowTemplateServiceApi.md +++ b/sdks/python/client/docs/WorkflowTemplateServiceApi.md @@ -976,9 +976,7 @@ with argo_workflows.ApiClient(configuration) as api_client: ), ), pod_gc=IoArgoprojWorkflowV1alpha1PodGC( - delete_delay_duration=Duration( - duration="duration_example", - ), + delete_delay_duration="delete_delay_duration_example", label_selector=LabelSelector( match_expressions=[ LabelSelectorRequirement( @@ -11917,9 +11915,7 @@ with argo_workflows.ApiClient(configuration) as api_client: ), ), pod_gc=IoArgoprojWorkflowV1alpha1PodGC( - delete_delay_duration=Duration( - duration="duration_example", - ), + delete_delay_duration="delete_delay_duration_example", label_selector=LabelSelector( match_expressions=[ LabelSelectorRequirement( @@ -22769,9 +22765,7 @@ with argo_workflows.ApiClient(configuration) as api_client: ), ), pod_gc=IoArgoprojWorkflowV1alpha1PodGC( - delete_delay_duration=Duration( - duration="duration_example", - ), + delete_delay_duration="delete_delay_duration_example", label_selector=LabelSelector( match_expressions=[ LabelSelectorRequirement( diff --git a/server/artifacts/artifact_server_test.go b/server/artifacts/artifact_server_test.go index 9cc9edac8a9c..e3a6fa920091 100644 --- a/server/artifacts/artifact_server_test.go +++ b/server/artifacts/artifact_server_test.go @@ -516,14 +516,13 @@ func TestArtifactServer_GetOutputArtifact(t *testing.T) { recorder := httptest.NewRecorder() s.GetOutputArtifact(recorder, r) - if assert.Equal(t, 200, recorder.Result().StatusCode) { - assert.Equal(t, fmt.Sprintf(`filename="%s"`, tt.fileName), recorder.Header().Get("Content-Disposition")) - all, err := io.ReadAll(recorder.Result().Body) - if err != nil { - panic(fmt.Sprintf("failed to read http body: %v", err)) - } - assert.Equal(t, "my-data", string(all)) + require.Equal(t, 200, recorder.Result().StatusCode) + assert.Equal(t, fmt.Sprintf(`filename="%s"`, tt.fileName), recorder.Header().Get("Content-Disposition")) + all, err := io.ReadAll(recorder.Result().Body) + if err != nil { + panic(fmt.Sprintf("failed to read http body: %v", err)) } + assert.Equal(t, "my-data", string(all)) }) } } @@ -548,14 +547,13 @@ func TestArtifactServer_GetOutputArtifactWithTemplate(t *testing.T) { recorder := httptest.NewRecorder() s.GetOutputArtifact(recorder, r) - if assert.Equal(t, 200, recorder.Result().StatusCode) { - assert.Equal(t, fmt.Sprintf(`filename="%s"`, tt.fileName), recorder.Header().Get("Content-Disposition")) - all, err := io.ReadAll(recorder.Result().Body) - if err != nil { - panic(fmt.Sprintf("failed to read http body: %v", err)) - } - assert.Equal(t, "my-data", string(all)) + require.Equal(t, 200, recorder.Result().StatusCode) + assert.Equal(t, fmt.Sprintf(`filename="%s"`, tt.fileName), recorder.Header().Get("Content-Disposition")) + all, err := io.ReadAll(recorder.Result().Body) + if err != nil { + panic(fmt.Sprintf("failed to read http body: %v", err)) } + assert.Equal(t, "my-data", string(all)) }) } } @@ -580,14 +578,13 @@ func TestArtifactServer_GetOutputArtifactWithInlineTemplate(t *testing.T) { recorder := httptest.NewRecorder() s.GetOutputArtifact(recorder, r) - if assert.Equal(t, 200, recorder.Result().StatusCode) { - assert.Equal(t, fmt.Sprintf(`filename="%s"`, tt.fileName), recorder.Header().Get("Content-Disposition")) - all, err := io.ReadAll(recorder.Result().Body) - if err != nil { - panic(fmt.Sprintf("failed to read http body: %v", err)) - } - assert.Equal(t, "my-data", string(all)) + require.Equal(t, 200, recorder.Result().StatusCode) + assert.Equal(t, fmt.Sprintf(`filename="%s"`, tt.fileName), recorder.Header().Get("Content-Disposition")) + all, err := io.ReadAll(recorder.Result().Body) + if err != nil { + panic(fmt.Sprintf("failed to read http body: %v", err)) } + assert.Equal(t, "my-data", string(all)) }) } } @@ -611,14 +608,13 @@ func TestArtifactServer_GetInputArtifact(t *testing.T) { r.URL = mustParse(fmt.Sprintf("/input-artifacts/my-ns/my-wf/my-node-1/%s", tt.artifactName)) recorder := httptest.NewRecorder() s.GetInputArtifact(recorder, r) - if assert.Equal(t, 200, recorder.Result().StatusCode) { - assert.Equal(t, fmt.Sprintf(`filename="%s"`, tt.fileName), recorder.Result().Header.Get("Content-Disposition")) - all, err := io.ReadAll(recorder.Result().Body) - if err != nil { - panic(fmt.Sprintf("failed to read http body: %v", err)) - } - assert.Equal(t, "my-data", string(all)) + require.Equal(t, 200, recorder.Result().StatusCode) + assert.Equal(t, fmt.Sprintf(`filename="%s"`, tt.fileName), recorder.Result().Header.Get("Content-Disposition")) + all, err := io.ReadAll(recorder.Result().Body) + if err != nil { + panic(fmt.Sprintf("failed to read http body: %v", err)) } + assert.Equal(t, "my-data", string(all)) }) } } diff --git a/server/auth/gatekeeper_test.go b/server/auth/gatekeeper_test.go index fe938d1dc0ea..3dcaacebcc71 100644 --- a/server/auth/gatekeeper_test.go +++ b/server/auth/gatekeeper_test.go @@ -154,9 +154,8 @@ func TestServer_GetWFClient(t *testing.T) { require.NoError(t, err) assert.Equal(t, wfClient, GetWfClient(ctx)) assert.Equal(t, kubeClient, GetKubeClient(ctx)) - if assert.NotNil(t, GetClaims(ctx)) { - assert.Equal(t, "my-sub", GetClaims(ctx).Subject) - } + require.NotNil(t, GetClaims(ctx)) + assert.Equal(t, "my-sub", GetClaims(ctx).Subject) }) hook := &test.Hook{} log.AddHook(hook) @@ -172,11 +171,10 @@ func TestServer_GetWFClient(t *testing.T) { assert.NotEqual(t, clients, GetWfClient(ctx)) assert.NotEqual(t, kubeClient, GetKubeClient(ctx)) claims := GetClaims(ctx) - if assert.NotNil(t, claims) { - assert.Equal(t, []string{"my-group", "other-group"}, claims.Groups) - assert.Equal(t, "my-sa", claims.ServiceAccountName) - assert.Equal(t, "my-ns", claims.ServiceAccountNamespace) - } + require.NotNil(t, claims) + assert.Equal(t, []string{"my-group", "other-group"}, claims.Groups) + assert.Equal(t, "my-sa", claims.ServiceAccountName) + assert.Equal(t, "my-ns", claims.ServiceAccountNamespace) assert.Equal(t, "my-sa", hook.LastEntry().Data["serviceAccount"]) }) t.Run("SSO+RBAC, Namespace delegation ON, precedence=2, Delegated", func(t *testing.T) { @@ -191,11 +189,10 @@ func TestServer_GetWFClient(t *testing.T) { assert.NotEqual(t, clients, GetWfClient(ctx)) assert.NotEqual(t, kubeClient, GetKubeClient(ctx)) claims := GetClaims(ctx) - if assert.NotNil(t, claims) { - assert.Equal(t, []string{"my-group", "other-group"}, claims.Groups) - assert.Equal(t, "user1-sa", claims.ServiceAccountName) - assert.Equal(t, "user1-ns", claims.ServiceAccountNamespace) - } + require.NotNil(t, claims) + assert.Equal(t, []string{"my-group", "other-group"}, claims.Groups) + assert.Equal(t, "user1-sa", claims.ServiceAccountName) + assert.Equal(t, "user1-ns", claims.ServiceAccountNamespace) assert.Equal(t, "user1-sa", hook.LastEntry().Data["serviceAccount"]) }) t.Run("SSO+RBAC, Namespace delegation OFF, precedence=2, Not Delegated", func(t *testing.T) { @@ -209,11 +206,10 @@ func TestServer_GetWFClient(t *testing.T) { assert.NotEqual(t, clients, GetWfClient(ctx)) assert.NotEqual(t, kubeClient, GetKubeClient(ctx)) claims := GetClaims(ctx) - if assert.NotNil(t, claims) { - assert.Equal(t, []string{"my-group", "other-group"}, claims.Groups) - assert.Equal(t, "my-sa", claims.ServiceAccountName) - assert.Equal(t, "my-ns", claims.ServiceAccountNamespace) - } + require.NotNil(t, claims) + assert.Equal(t, []string{"my-group", "other-group"}, claims.Groups) + assert.Equal(t, "my-sa", claims.ServiceAccountName) + assert.Equal(t, "my-ns", claims.ServiceAccountNamespace) assert.Equal(t, "my-sa", hook.LastEntry().Data["serviceAccount"]) }) t.Run("SSO+RBAC, Namespace delegation ON, precedence=0, Not delegated", func(t *testing.T) { @@ -228,11 +224,10 @@ func TestServer_GetWFClient(t *testing.T) { assert.NotEqual(t, clients, GetWfClient(ctx)) assert.NotEqual(t, kubeClient, GetKubeClient(ctx)) claims := GetClaims(ctx) - if assert.NotNil(t, claims) { - assert.Equal(t, []string{"my-group", "other-group"}, claims.Groups) - assert.Equal(t, "my-sa", claims.ServiceAccountName) - assert.Equal(t, "my-ns", claims.ServiceAccountNamespace) - } + require.NotNil(t, claims) + assert.Equal(t, []string{"my-group", "other-group"}, claims.Groups) + assert.Equal(t, "my-sa", claims.ServiceAccountName) + assert.Equal(t, "my-ns", claims.ServiceAccountNamespace) assert.Equal(t, "my-sa", hook.LastEntry().Data["serviceAccount"]) }) t.Run("SSO+RBAC, Namespace delegation ON, precedence=1, Not delegated", func(t *testing.T) { @@ -247,11 +242,10 @@ func TestServer_GetWFClient(t *testing.T) { assert.NotEqual(t, clients, GetWfClient(ctx)) assert.NotEqual(t, kubeClient, GetKubeClient(ctx)) claims := GetClaims(ctx) - if assert.NotNil(t, claims) { - assert.Equal(t, []string{"my-group", "other-group"}, claims.Groups) - assert.Equal(t, "my-sa", claims.ServiceAccountName) - assert.Equal(t, "my-ns", claims.ServiceAccountNamespace) - } + require.NotNil(t, claims) + assert.Equal(t, []string{"my-group", "other-group"}, claims.Groups) + assert.Equal(t, "my-sa", claims.ServiceAccountName) + assert.Equal(t, "my-ns", claims.ServiceAccountNamespace) assert.Equal(t, "my-sa", hook.LastEntry().Data["serviceAccount"]) }) t.Run("SSO+RBAC,precedence=0", func(t *testing.T) { diff --git a/server/auth/mode_test.go b/server/auth/mode_test.go index 8d9ce8ef9158..72c1434fc8b4 100644 --- a/server/auth/mode_test.go +++ b/server/auth/mode_test.go @@ -42,21 +42,18 @@ func TestModes_GetMode(t *testing.T) { } t.Run("Client", func(t *testing.T) { mode, valid := m.GetMode("Bearer ") - if assert.True(t, valid) { - assert.Equal(t, Client, mode) - } + require.True(t, valid) + assert.Equal(t, Client, mode) }) t.Run("Server", func(t *testing.T) { mode, valid := m.GetMode("") - if assert.True(t, valid) { - assert.Equal(t, Server, mode) - } + require.True(t, valid) + assert.Equal(t, Server, mode) }) t.Run("SSO", func(t *testing.T) { mode, valid := m.GetMode("Bearer v2:") - if assert.True(t, valid) { - assert.Equal(t, SSO, mode) - } + require.True(t, valid) + assert.Equal(t, SSO, mode) }) m = Modes{ @@ -66,8 +63,7 @@ func TestModes_GetMode(t *testing.T) { } t.Run("Server and Auth", func(t *testing.T) { mode, valid := m.GetMode("Bearer ") - if assert.True(t, valid) { - assert.Equal(t, Server, mode) - } + require.True(t, valid) + assert.Equal(t, Server, mode) }) } diff --git a/server/event/dispatch/operation_test.go b/server/event/dispatch/operation_test.go index e99508577fb8..ca37c5c22522 100644 --- a/server/event/dispatch/operation_test.go +++ b/server/event/dispatch/operation_test.go @@ -33,9 +33,8 @@ func Test_metaData(t *testing.T) { "ignored": []string{"false"}, }) data := metaData(ctx) - if assert.Len(t, data, 1) { - assert.Equal(t, []string{"true"}, data["x-valid"]) - } + require.Len(t, data, 1) + assert.Equal(t, []string{"true"}, data["x-valid"]) }) } @@ -178,17 +177,17 @@ func TestNewOperation(t *testing.T) { // assert list, err := client.ArgoprojV1alpha1().Workflows("my-ns").List(ctx, metav1.ListOptions{}) require.NoError(t, err) - if assert.Len(t, list.Items, 4) { - for _, wf := range list.Items { - assert.Equal(t, "my-instanceid", wf.Labels[common.LabelKeyControllerInstanceID]) - assert.Equal(t, "my-sub", wf.Labels[common.LabelKeyCreator]) - assert.Contains(t, wf.Labels, common.LabelKeyWorkflowEventBinding) - assert.Contains(t, "my-param", wf.Spec.Arguments.Parameters[0].Name) - paramValues = append(paramValues, string(*wf.Spec.Arguments.Parameters[0].Value)) - } - sort.Strings(paramValues) - assert.Equal(t, expectedParamValues, paramValues) + require.Len(t, list.Items, 4) + for _, wf := range list.Items { + assert.Equal(t, "my-instanceid", wf.Labels[common.LabelKeyControllerInstanceID]) + assert.Equal(t, "my-sub", wf.Labels[common.LabelKeyCreator]) + assert.Contains(t, wf.Labels, common.LabelKeyWorkflowEventBinding) + assert.Contains(t, "my-param", wf.Spec.Arguments.Parameters[0].Name) + paramValues = append(paramValues, string(*wf.Spec.Arguments.Parameters[0].Value)) } + sort.Strings(paramValues) + assert.Equal(t, expectedParamValues, paramValues) + assert.Contains(t, "Warning WorkflowEventBindingError failed to dispatch event: failed to evaluate workflow template expression: unexpected token EOF", <-recorder.Events) assert.Equal(t, "Warning WorkflowEventBindingError failed to dispatch event: failed to get workflow template: workflowtemplates.argoproj.io \"not-found\" not found", <-recorder.Events) assert.Equal(t, "Warning WorkflowEventBindingError failed to dispatch event: failed to validate workflow template instanceid: 'my-wft-3' is not managed by the current Argo Server", <-recorder.Events) diff --git a/test/e2e/agent_test.go b/test/e2e/agent_test.go index 9b77c75e7d3b..2a071f4b4f4f 100644 --- a/test/e2e/agent_test.go +++ b/test/e2e/agent_test.go @@ -8,6 +8,7 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -75,20 +76,19 @@ spec: finishedTimes = append(finishedTimes, node.FinishedAt.Time) } - if assert.Len(t, finishedTimes, 4) { - sort.Slice(finishedTimes, func(i, j int) bool { - return finishedTimes[i].Before(finishedTimes[j]) - }) - // Everything finished with a two second tolerance window - assert.Less(t, finishedTimes[3].Sub(finishedTimes[0]), time.Duration(2)*time.Second) - } - if assert.Len(t, startTimes, 4) { - sort.Slice(startTimes, func(i, j int) bool { - return startTimes[i].Before(startTimes[j]) - }) - // Everything started with same time - assert.Equal(t, time.Duration(0), startTimes[3].Sub(startTimes[0])) - } + require.Len(t, finishedTimes, 4) + sort.Slice(finishedTimes, func(i, j int) bool { + return finishedTimes[i].Before(finishedTimes[j]) + }) + // Everything finished with a two second tolerance window + assert.Less(t, finishedTimes[3].Sub(finishedTimes[0]), time.Duration(2)*time.Second) + + require.Len(t, startTimes, 4) + sort.Slice(startTimes, func(i, j int) bool { + return startTimes[i].Before(startTimes[j]) + }) + // Everything started with same time + assert.Equal(t, time.Duration(0), startTimes[3].Sub(startTimes[0])) }) } @@ -145,21 +145,20 @@ spec: assert.Equal(t, wfv1.WorkflowFailed, status.Phase) containsFails := status.Nodes.FindByDisplayName("http-body-contains-google-fails") - if assert.NotNil(t, containsFails) { - assert.Equal(t, wfv1.NodeFailed, containsFails.Phase) - } + require.NotNil(t, containsFails) + assert.Equal(t, wfv1.NodeFailed, containsFails.Phase) + containsSucceeds := status.Nodes.FindByDisplayName("http-body-contains-google-succeeds") - if assert.NotNil(t, containsFails) { - assert.Equal(t, wfv1.NodeSucceeded, containsSucceeds.Phase) - } + require.NotNil(t, containsFails) + assert.Equal(t, wfv1.NodeSucceeded, containsSucceeds.Phase) + statusFails := status.Nodes.FindByDisplayName("http-status-is-201-fails") - if assert.NotNil(t, statusFails) { - assert.Equal(t, wfv1.NodeFailed, statusFails.Phase) - } + require.NotNil(t, statusFails) + assert.Equal(t, wfv1.NodeFailed, statusFails.Phase) + statusSucceeds := status.Nodes.FindByDisplayName("http-status-is-201-succeeds") - if assert.NotNil(t, statusFails) { - assert.Equal(t, wfv1.NodeSucceeded, statusSucceeds.Phase) - } + require.NotNil(t, statusFails) + assert.Equal(t, wfv1.NodeSucceeded, statusSucceeds.Phase) }) } diff --git a/test/e2e/artifacts_test.go b/test/e2e/artifacts_test.go index 28a43b023a47..11ca54bcde01 100644 --- a/test/e2e/artifacts_test.go +++ b/test/e2e/artifacts_test.go @@ -675,10 +675,9 @@ func (s *ArtifactsSuite) TestOutputResult() { Then(). ExpectWorkflow(func(t *testing.T, _ *metav1.ObjectMeta, status *wfv1.WorkflowStatus) { n := status.Nodes.FindByDisplayName("a") - if assert.NotNil(t, n) { - assert.NotNil(t, n.Outputs.ExitCode) - assert.NotNil(t, n.Outputs.Result) - } + require.NotNil(t, n) + assert.NotNil(t, n.Outputs.ExitCode) + assert.NotNil(t, n.Outputs.Result) }) } @@ -752,9 +751,8 @@ spec: }, }, } - if assert.NotNil(t, n) { - assert.Equal(t, expectedOutputs, n.Outputs) - } + require.NotNil(t, n) + assert.Equal(t, expectedOutputs, n.Outputs) }) }) } diff --git a/test/e2e/cli_test.go b/test/e2e/cli_test.go index 34946155fc5e..c981c9caf8c9 100644 --- a/test/e2e/cli_test.go +++ b/test/e2e/cli_test.go @@ -1866,13 +1866,12 @@ spec: ExpectWorkflow(func(t *testing.T, _ *metav1.ObjectMeta, status *wfv1.WorkflowStatus) { assert.Equal(t, wfv1.WorkflowSucceeded, status.Phase) nodeStatus := status.Nodes.FindByDisplayName("release") - if assert.NotNil(t, nodeStatus) { - assert.Equal(t, "Hello, World!", nodeStatus.Inputs.Parameters[0].Value.String()) - } + require.NotNil(t, nodeStatus) + assert.Equal(t, "Hello, World!", nodeStatus.Inputs.Parameters[0].Value.String()) + nodeStatus = status.Nodes.FindByDisplayName("approve") - if assert.NotNil(t, nodeStatus) { - assert.Equal(t, "Test message; Resumed by: map[User:system:serviceaccount:argo:argo-server]", nodeStatus.Message) - } + require.NotNil(t, nodeStatus) + assert.Equal(t, "Test message; Resumed by: map[User:system:serviceaccount:argo:argo-server]", nodeStatus.Message) }) } diff --git a/test/e2e/cron_test.go b/test/e2e/cron_test.go index cd9f9c5169cb..9eb439a154b8 100644 --- a/test/e2e/cron_test.go +++ b/test/e2e/cron_test.go @@ -11,6 +11,7 @@ import ( "github.com/argoproj/pkg/humanize" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" corev1 "k8s.io/api/core/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -277,9 +278,8 @@ spec: Then(). ExpectCron(func(t *testing.T, cronWf *wfv1.CronWorkflow) { assert.Len(t, cronWf.Status.Active, 1) - if assert.NotNil(t, cronWf.Status.LastScheduledTime) { - assert.True(t, cronWf.Status.LastScheduledTime.Time.After(time.Now().Add(-1*time.Minute))) - } + require.NotNil(t, cronWf.Status.LastScheduledTime) + assert.True(t, cronWf.Status.LastScheduledTime.Time.After(time.Now().Add(-1*time.Minute))) }) }) s.Run("TestSuccessfulJobHistoryLimit", func() { diff --git a/test/e2e/daemon_pod_test.go b/test/e2e/daemon_pod_test.go index 749a3a845aec..c92bcd29bd51 100644 --- a/test/e2e/daemon_pod_test.go +++ b/test/e2e/daemon_pod_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -171,9 +172,8 @@ func (s *DaemonPodSuite) TestMarkDaemonedPodSucceeded() { Then(). ExpectWorkflow(func(t *testing.T, metadata *v1.ObjectMeta, status *v1alpha1.WorkflowStatus) { node := status.Nodes.FindByDisplayName("daemoned") - if assert.NotNil(t, node) { - assert.Equal(t, v1alpha1.NodeSucceeded, node.Phase) - } + require.NotNil(t, node) + assert.Equal(t, v1alpha1.NodeSucceeded, node.Phase) }) } diff --git a/test/e2e/executor_plugins_test.go b/test/e2e/executor_plugins_test.go index b4b9f86d1cac..ea662022c42d 100644 --- a/test/e2e/executor_plugins_test.go +++ b/test/e2e/executor_plugins_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" apiv1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1" @@ -34,51 +35,45 @@ func (s *ExecutorPluginsSuite) TestTemplateExecutor() { assert.Len(t, n.Outputs.Parameters, 1) }). ExpectPods(func(t *testing.T, pods []apiv1.Pod) { - if assert.Len(t, pods, 1) { - pod := pods[0] - spec := pod.Spec - assert.Equal(t, pointer.Bool(false), spec.AutomountServiceAccountToken) - assert.Equal(t, &apiv1.PodSecurityContext{ - RunAsUser: pointer.Int64(8737), - RunAsNonRoot: pointer.Bool(true), - SeccompProfile: &v1.SeccompProfile{Type: "RuntimeDefault"}, - }, spec.SecurityContext) - if assert.Len(t, spec.Volumes, 4) { - assert.Contains(t, spec.Volumes[0].Name, "kube-api-access-") - assert.Equal(t, "var-run-argo", spec.Volumes[1].Name) - assert.Contains(t, spec.Volumes[2].Name, "kube-api-access-") - assert.Equal(t, "argo-workflows-agent-ca-certificates", spec.Volumes[3].Name) - } - if assert.Len(t, spec.Containers, 2) { - { - plug := spec.Containers[0] - if assert.Equal(t, "hello-executor-plugin", plug.Name) { - if assert.Len(t, plug.VolumeMounts, 2) { - assert.Equal(t, "var-run-argo", plug.VolumeMounts[0].Name) - assert.Contains(t, plug.VolumeMounts[1].Name, "kube-api-access-") - } - } - } - { - agent := spec.Containers[1] - if assert.Equal(t, "main", agent.Name) { - if assert.Len(t, agent.VolumeMounts, 3) { - assert.Equal(t, "var-run-argo", agent.VolumeMounts[0].Name) - assert.Contains(t, agent.VolumeMounts[1].Name, "kube-api-access-") - assert.Equal(t, "argo-workflows-agent-ca-certificates", agent.VolumeMounts[2].Name) - } - assert.Equal(t, &apiv1.SecurityContext{ - RunAsUser: pointer.Int64(8737), - RunAsNonRoot: pointer.Bool(true), - AllowPrivilegeEscalation: pointer.Bool(false), - ReadOnlyRootFilesystem: pointer.Bool(true), - Privileged: pointer.Bool(false), - Capabilities: &apiv1.Capabilities{Drop: []apiv1.Capability{"ALL"}}, - SeccompProfile: &v1.SeccompProfile{Type: "RuntimeDefault"}, - }, agent.SecurityContext) - } - } - } + require.Len(t, pods, 1) + pod := pods[0] + spec := pod.Spec + assert.Equal(t, pointer.Bool(false), spec.AutomountServiceAccountToken) + assert.Equal(t, &apiv1.PodSecurityContext{ + RunAsUser: pointer.Int64(8737), + RunAsNonRoot: pointer.Bool(true), + SeccompProfile: &v1.SeccompProfile{Type: "RuntimeDefault"}, + }, spec.SecurityContext) + require.Len(t, spec.Volumes, 4) + assert.Contains(t, spec.Volumes[0].Name, "kube-api-access-") + assert.Equal(t, "var-run-argo", spec.Volumes[1].Name) + assert.Contains(t, spec.Volumes[2].Name, "kube-api-access-") + assert.Equal(t, "argo-workflows-agent-ca-certificates", spec.Volumes[3].Name) + + require.Len(t, spec.Containers, 2) + { + plug := spec.Containers[0] + require.Equal(t, "hello-executor-plugin", plug.Name) + require.Len(t, plug.VolumeMounts, 2) + assert.Equal(t, "var-run-argo", plug.VolumeMounts[0].Name) + assert.Contains(t, plug.VolumeMounts[1].Name, "kube-api-access-") + } + { + agent := spec.Containers[1] + require.Equal(t, "main", agent.Name) + require.Len(t, agent.VolumeMounts, 3) + assert.Equal(t, "var-run-argo", agent.VolumeMounts[0].Name) + assert.Contains(t, agent.VolumeMounts[1].Name, "kube-api-access-") + assert.Equal(t, "argo-workflows-agent-ca-certificates", agent.VolumeMounts[2].Name) + assert.Equal(t, &apiv1.SecurityContext{ + RunAsUser: pointer.Int64(8737), + RunAsNonRoot: pointer.Bool(true), + AllowPrivilegeEscalation: pointer.Bool(false), + ReadOnlyRootFilesystem: pointer.Bool(true), + Privileged: pointer.Bool(false), + Capabilities: &apiv1.Capabilities{Drop: []apiv1.Capability{"ALL"}}, + SeccompProfile: &v1.SeccompProfile{Type: "RuntimeDefault"}, + }, agent.SecurityContext) } }). ExpectWorkflowTaskSet(func(t *testing.T, wfts *wfv1.WorkflowTaskSet) { diff --git a/test/e2e/functional/param-aggregation-fromoutputs.yaml b/test/e2e/functional/param-aggregation-fromoutputs.yaml new file mode 100644 index 000000000000..74ac9e432dff --- /dev/null +++ b/test/e2e/functional/param-aggregation-fromoutputs.yaml @@ -0,0 +1,69 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Workflow +metadata: + generateName: double-fan-out-using-param- +spec: + entrypoint: main-dag + templates: + - inputs: + parameters: + - name: param1 + name: operation + outputs: + parameters: + - name: output1 + valueFrom: + path: /tmp/fan_out.json + script: + command: + - python3 + image: python:3.11 + source: |- + import os + import sys + sys.path.append(os.getcwd()) + import json + try: param1 = json.loads(r'''{{inputs.parameters.param1}}''') + except: param1 = r'''{{inputs.parameters.param1}}''' + + with open('/tmp/fan_out.json', 'w') as f: + json.dump(param1, f) + print(json.dumps(param1)) + - dag: + tasks: + - arguments: + parameters: + - name: param1 + value: '{{item}}' + name: task3 + template: operation + withParam: '{{inputs.parameters.param1}}' + inputs: + parameters: + - name: param1 + name: secondary-dag + - dag: + tasks: + - arguments: + parameters: + - name: param1 + value: '[[{"key1": "value1"}, {"key2": "value2"}, {"key3": "value3"}], [{"key4": "value4"}, {"key5": "value5"}]]' + name: task1 + template: operation + - arguments: + parameters: + - name: param1 + value: '{{item}}' + depends: task1 + name: task2 + template: operation + withParam: '{{tasks.task1.outputs.parameters.output1}}' + - arguments: + parameters: + - name: param1 + value: '{{item}}' + depends: task2 + name: task3-dag + template: secondary-dag + withParam: '{{tasks.task2.outputs.parameters.output1}}' + name: main-dag diff --git a/test/e2e/functional_test.go b/test/e2e/functional_test.go index b8b27ac657b1..906eeac31770 100644 --- a/test/e2e/functional_test.go +++ b/test/e2e/functional_test.go @@ -237,11 +237,10 @@ spec: ExpectWorkflow(func(t *testing.T, _ *metav1.ObjectMeta, status *wfv1.WorkflowStatus) { assert.Len(t, status.Nodes, 7) nodeStatus := status.Nodes.FindByDisplayName("B") - if assert.NotNil(t, nodeStatus) { - assert.Equal(t, wfv1.NodeFailed, nodeStatus.Phase) - assert.Len(t, nodeStatus.Children, 1) - assert.Len(t, nodeStatus.OutboundNodes, 1) - } + require.NotNil(t, nodeStatus) + assert.Equal(t, wfv1.NodeFailed, nodeStatus.Phase) + assert.Len(t, nodeStatus.Children, 1) + assert.Len(t, nodeStatus.OutboundNodes, 1) }) } @@ -418,12 +417,12 @@ func (s *FunctionalSuite) TestArtifactRepositoryRef() { Then(). ExpectWorkflow(func(t *testing.T, metadata *metav1.ObjectMeta, status *wfv1.WorkflowStatus) { assert.Equal(t, wfv1.WorkflowSucceeded, status.Phase) - if assert.NotEmpty(t, status.ArtifactRepositoryRef) { - assert.Equal(t, "argo", status.ArtifactRepositoryRef.Namespace) - assert.Equal(t, "artifact-repositories", status.ArtifactRepositoryRef.ConfigMap) - assert.Equal(t, "my-key", status.ArtifactRepositoryRef.Key) - assert.False(t, status.ArtifactRepositoryRef.Default) - } + require.NotEmpty(t, status.ArtifactRepositoryRef) + assert.Equal(t, "argo", status.ArtifactRepositoryRef.Namespace) + assert.Equal(t, "artifact-repositories", status.ArtifactRepositoryRef.ConfigMap) + assert.Equal(t, "my-key", status.ArtifactRepositoryRef.Key) + assert.False(t, status.ArtifactRepositoryRef.Default) + // these should never be set because we must get them from the artifactRepositoryRef generated := status.Nodes.FindByDisplayName("generate").Outputs.Artifacts[0].S3 assert.Empty(t, generated.Bucket) @@ -443,11 +442,10 @@ func (s *FunctionalSuite) TestLoopEmptyParam() { Then(). ExpectWorkflow(func(t *testing.T, _ *metav1.ObjectMeta, status *wfv1.WorkflowStatus) { assert.Equal(t, wfv1.WorkflowSucceeded, status.Phase) - if assert.Len(t, status.Nodes, 5) { - nodeStatus := status.Nodes.FindByDisplayName("sleep") - assert.Equal(t, wfv1.NodeSkipped, nodeStatus.Phase) - assert.Equal(t, "Skipped, empty params", nodeStatus.Message) - } + require.Len(t, status.Nodes, 5) + nodeStatus := status.Nodes.FindByDisplayName("sleep") + assert.Equal(t, wfv1.NodeSkipped, nodeStatus.Phase) + assert.Equal(t, "Skipped, empty params", nodeStatus.Message) }) } @@ -460,11 +458,10 @@ func (s *FunctionalSuite) TestDAGEmptyParam() { Then(). ExpectWorkflow(func(t *testing.T, _ *metav1.ObjectMeta, status *wfv1.WorkflowStatus) { assert.Equal(t, wfv1.WorkflowSucceeded, status.Phase) - if assert.Len(t, status.Nodes, 3) { - nodeStatus := status.Nodes.FindByDisplayName("sleep") - assert.Equal(t, wfv1.NodeSkipped, nodeStatus.Phase) - assert.Equal(t, "Skipped, empty params", nodeStatus.Message) - } + require.Len(t, status.Nodes, 3) + nodeStatus := status.Nodes.FindByDisplayName("sleep") + assert.Equal(t, wfv1.NodeSkipped, nodeStatus.Phase) + assert.Equal(t, "Skipped, empty params", nodeStatus.Message) }) } @@ -566,9 +563,25 @@ func (s *FunctionalSuite) TestParameterAggregation() { ExpectWorkflow(func(t *testing.T, _ *metav1.ObjectMeta, status *wfv1.WorkflowStatus) { assert.Equal(t, wfv1.WorkflowSucceeded, status.Phase) nodeStatus := status.Nodes.FindByDisplayName("print(0:res:1)") - if assert.NotNil(t, nodeStatus) { - assert.Equal(t, wfv1.NodeSucceeded, nodeStatus.Phase) - } + require.NotNil(t, nodeStatus) + assert.Equal(t, wfv1.NodeSucceeded, nodeStatus.Phase) + }) +} + +func (s *FunctionalSuite) TestParameterAggregationFromOutputs() { + s.Given(). + Workflow("@functional/param-aggregation-fromoutputs.yaml"). + When(). + SubmitWorkflow(). + WaitForWorkflow(time.Second * 90). + Then(). + ExpectWorkflow(func(t *testing.T, _ *metav1.ObjectMeta, status *wfv1.WorkflowStatus) { + assert.Equal(t, wfv1.WorkflowSucceeded, status.Phase) + assert.NotNil(t, status.Nodes.FindByDisplayName("task3(0:key1:value1)")) + assert.NotNil(t, status.Nodes.FindByDisplayName("task3(1:key2:value2)")) + assert.NotNil(t, status.Nodes.FindByDisplayName("task3(2:key3:value3)")) + assert.NotNil(t, status.Nodes.FindByDisplayName("task3(0:key4:value4)")) + assert.NotNil(t, status.Nodes.FindByDisplayName("task3(1:key5:value5)")) }) } @@ -911,9 +924,9 @@ func (s *FunctionalSuite) TestDataTransformation() { ExpectWorkflow(func(t *testing.T, metadata *metav1.ObjectMeta, status *wfv1.WorkflowStatus) { assert.Equal(t, wfv1.WorkflowSucceeded, status.Phase) paths := status.Nodes.FindByDisplayName("get-artifact-path") - if assert.NotNil(t, paths) { - assert.Equal(t, `["foo/script.py","script.py"]`, *paths.Outputs.Result) - } + require.NotNil(t, paths) + assert.Equal(t, `["foo/script.py","script.py"]`, *paths.Outputs.Result) + assert.NotNil(t, status.Nodes.FindByDisplayName("process-artifact(0:foo/script.py)")) assert.NotNil(t, status.Nodes.FindByDisplayName("process-artifact(1:script.py)")) for _, value := range status.TaskResultsCompletionStatus { diff --git a/test/e2e/resource_template_test.go b/test/e2e/resource_template_test.go index 1d47d7e48d66..f2f319920be0 100644 --- a/test/e2e/resource_template_test.go +++ b/test/e2e/resource_template_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -151,13 +152,11 @@ func (s *ResourceTemplateSuite) TestResourceTemplateWithOutputs() { Then(). ExpectWorkflow(func(t *testing.T, md *metav1.ObjectMeta, status *wfv1.WorkflowStatus) { outputs := status.Nodes[md.Name].Outputs - if assert.NotNil(t, outputs) { - parameters := outputs.Parameters - if assert.Len(t, parameters, 2) { - assert.Equal(t, "my-pod", parameters[0].Value.String(), "metadata.name is capture for json") - assert.Equal(t, "my-pod", parameters[1].Value.String(), "metadata.name is capture for jq") - } - } + require.NotNil(t, outputs) + parameters := outputs.Parameters + require.Len(t, parameters, 2) + assert.Equal(t, "my-pod", parameters[0].Value.String(), "metadata.name is capture for json") + assert.Equal(t, "my-pod", parameters[1].Value.String(), "metadata.name is capture for jq") for _, value := range status.TaskResultsCompletionStatus { assert.True(t, value) } diff --git a/test/e2e/signals_test.go b/test/e2e/signals_test.go index 4de48d5c2b9a..132e99a60c49 100644 --- a/test/e2e/signals_test.go +++ b/test/e2e/signals_test.go @@ -7,6 +7,7 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -35,17 +36,16 @@ func (s *SignalsSuite) TestStopBehavior() { ExpectWorkflow(func(t *testing.T, m *metav1.ObjectMeta, status *wfv1.WorkflowStatus) { assert.Contains(t, []wfv1.WorkflowPhase{wfv1.WorkflowFailed, wfv1.WorkflowError}, status.Phase) nodeStatus := status.Nodes.FindByDisplayName("A") - if assert.NotNil(t, nodeStatus) { - assert.Contains(t, []wfv1.NodePhase{wfv1.NodeFailed, wfv1.NodeError}, nodeStatus.Phase) - } + require.NotNil(t, nodeStatus) + assert.Contains(t, []wfv1.NodePhase{wfv1.NodeFailed, wfv1.NodeError}, nodeStatus.Phase) + nodeStatus = status.Nodes.FindByDisplayName("A.onExit") - if assert.NotNil(t, nodeStatus) { - assert.Equal(t, wfv1.NodeSucceeded, nodeStatus.Phase) - } + require.NotNil(t, nodeStatus) + assert.Equal(t, wfv1.NodeSucceeded, nodeStatus.Phase) + nodeStatus = status.Nodes.FindByDisplayName(m.Name + ".onExit") - if assert.NotNil(t, nodeStatus) { - assert.Equal(t, wfv1.NodeSucceeded, nodeStatus.Phase) - } + require.NotNil(t, nodeStatus) + assert.Equal(t, wfv1.NodeSucceeded, nodeStatus.Phase) }) } @@ -61,9 +61,8 @@ func (s *SignalsSuite) TestStopBehaviorWithDaemon() { ExpectWorkflow(func(t *testing.T, m *metav1.ObjectMeta, status *wfv1.WorkflowStatus) { assert.Contains(t, []wfv1.WorkflowPhase{wfv1.WorkflowFailed, wfv1.WorkflowError}, status.Phase) nodeStatus := status.Nodes.FindByDisplayName("Daemon") - if assert.NotNil(t, nodeStatus) { - assert.Equal(t, wfv1.NodeSucceeded, nodeStatus.Phase) - } + require.NotNil(t, nodeStatus) + assert.Equal(t, wfv1.NodeSucceeded, nodeStatus.Phase) }) } @@ -79,9 +78,8 @@ func (s *SignalsSuite) TestTerminateBehavior() { ExpectWorkflow(func(t *testing.T, m *metav1.ObjectMeta, status *wfv1.WorkflowStatus) { assert.Contains(t, []wfv1.WorkflowPhase{wfv1.WorkflowFailed, wfv1.WorkflowError}, status.Phase) nodeStatus := status.Nodes.FindByDisplayName("A") - if assert.NotNil(t, nodeStatus) { - assert.Contains(t, []wfv1.NodePhase{wfv1.NodeFailed, wfv1.NodeError}, nodeStatus.Phase) - } + require.NotNil(t, nodeStatus) + assert.Contains(t, []wfv1.NodePhase{wfv1.NodeFailed, wfv1.NodeError}, nodeStatus.Phase) nodeStatus = status.Nodes.FindByDisplayName("A.onExit") assert.Nil(t, nodeStatus) nodeStatus = status.Nodes.FindByDisplayName(m.Name + ".onExit") @@ -102,9 +100,8 @@ func (s *SignalsSuite) TestDoNotCreatePodsUnderStopBehavior() { ExpectWorkflow(func(t *testing.T, m *metav1.ObjectMeta, status *wfv1.WorkflowStatus) { assert.Equal(t, wfv1.WorkflowFailed, status.Phase) nodeStatus := status.Nodes.FindByDisplayName("A") - if assert.NotNil(t, nodeStatus) { - assert.Equal(t, wfv1.NodeFailed, nodeStatus.Phase) - } + require.NotNil(t, nodeStatus) + assert.Equal(t, wfv1.NodeFailed, nodeStatus.Phase) nodeStatus = status.Nodes.FindByDisplayName("B") assert.Nil(t, nodeStatus) }) @@ -159,15 +156,14 @@ func (s *SignalsSuite) TestSignaledContainerSet() { assert.Equal(t, wfv1.WorkflowFailed, status.Phase) assert.Contains(t, status.Message, "(exit code 137)") one := status.Nodes.FindByDisplayName("one") - if assert.NotNil(t, one) { - assert.Equal(t, wfv1.NodeFailed, one.Phase) - assert.Contains(t, one.Message, "(exit code 137)") - } + require.NotNil(t, one) + assert.Equal(t, wfv1.NodeFailed, one.Phase) + assert.Contains(t, one.Message, "(exit code 137)") + two := status.Nodes.FindByDisplayName("two") - if assert.NotNil(t, two) { - assert.Equal(t, wfv1.NodeFailed, two.Phase) - assert.Contains(t, two.Message, "(exit code 143)") - } + require.NotNil(t, two) + assert.Equal(t, wfv1.NodeFailed, two.Phase) + assert.Contains(t, two.Message, "(exit code 143)") }) } diff --git a/ui/package.json b/ui/package.json index 55c7c94cdd6b..2c1037e86ba2 100644 --- a/ui/package.json +++ b/ui/package.json @@ -80,7 +80,7 @@ "ts-jest": "^26.4.4", "ts-node": "^9.1.1", "typescript": "^4.6.4", - "webpack": "^5.89.0", + "webpack": "^5.94.0", "webpack-cli": "^5.1.4", "webpack-dev-server": "^4.15.0", "yarn-deduplicate": "^6.0.2" diff --git a/ui/src/app/cluster-workflow-templates/cluster-workflow-template-list.tsx b/ui/src/app/cluster-workflow-templates/cluster-workflow-template-list.tsx index 2e52d4591658..c5cffe6c2f96 100644 --- a/ui/src/app/cluster-workflow-templates/cluster-workflow-template-list.tsx +++ b/ui/src/app/cluster-workflow-templates/cluster-workflow-template-list.tsx @@ -10,7 +10,7 @@ import {ErrorNotice} from '../shared/components/error-notice'; import {ExampleManifests} from '../shared/components/example-manifests'; import {InfoIcon} from '../shared/components/fa-icons'; import {Loading} from '../shared/components/loading'; -import {Timestamp} from '../shared/components/timestamp'; +import {Timestamp, TimestampSwitch} from '../shared/components/timestamp'; import {useCollectEvent} from '../shared/use-collect-event'; import {ZeroState} from '../shared/components/zero-state'; import {Context} from '../shared/context'; @@ -20,6 +20,7 @@ import {services} from '../shared/services'; import {ClusterWorkflowTemplateCreator} from './cluster-workflow-template-creator'; import './cluster-workflow-template-list.scss'; +import useTimestamp, {TIMESTAMP_KEYS} from '../shared/use-timestamp'; export function ClusterWorkflowTemplateList({history, location}: RouteComponentProps) { const {navigation} = useContext(Context); @@ -51,6 +52,8 @@ export function ClusterWorkflowTemplateList({history, location}: RouteComponentP useCollectEvent('openedClusterWorkflowTemplateList'); + const [storedDisplayISOFormat, setStoredDisplayISOFormat] = useTimestamp(TIMESTAMP_KEYS.CLUSTER_WORKFLOW_TEMPLATE_LIST); + function renderTemplates() { if (error) { return ; @@ -75,7 +78,9 @@ export function ClusterWorkflowTemplateList({history, location}: RouteComponentP
NAME
-
CREATED
+
+ CREATED +
{templates.map(t => ( @@ -84,7 +89,7 @@ export function ClusterWorkflowTemplateList({history, location}: RouteComponentP
{t.metadata.name}
- +
))} diff --git a/ui/src/app/cron-workflows/cron-workflow-list.tsx b/ui/src/app/cron-workflows/cron-workflow-list.tsx index 48809f7d5871..cbcf08627e84 100644 --- a/ui/src/app/cron-workflows/cron-workflow-list.tsx +++ b/ui/src/app/cron-workflows/cron-workflow-list.tsx @@ -12,7 +12,7 @@ import {ErrorNotice} from '../shared/components/error-notice'; import {ExampleManifests} from '../shared/components/example-manifests'; import {InfoIcon} from '../shared/components/fa-icons'; import {Loading} from '../shared/components/loading'; -import {Timestamp} from '../shared/components/timestamp'; +import {Timestamp, TimestampSwitch} from '../shared/components/timestamp'; import {useCollectEvent} from '../shared/use-collect-event'; import {ZeroState} from '../shared/components/zero-state'; import {Context} from '../shared/context'; @@ -27,6 +27,7 @@ import {CronWorkflowFilters} from './cron-workflow-filters'; import {PrettySchedule} from './pretty-schedule'; import './cron-workflow-list.scss'; +import useTimestamp, {TIMESTAMP_KEYS} from '../shared/use-timestamp'; const learnMore = Learn more; @@ -40,6 +41,9 @@ export function CronWorkflowList({match, location, history}: RouteComponentProps const [labels, setLabels] = useState([]); const [states, setStates] = useState(['Running', 'Suspended']); // check all by default + const [storedDisplayISOFormatCreation, setStoredDisplayISOFormatCreation] = useTimestamp(TIMESTAMP_KEYS.CRON_WORKFLOW_LIST_CREATION); + const [storedDisplayISOFormatNextScheduled, setStoredDisplayISOFormatNextScheduled] = useTimestamp(TIMESTAMP_KEYS.CRON_WORKFLOW_LIST_NEXT_SCHEDULED); + useEffect( useQueryParams(history, p => { setSidePanel(p.get('sidePanel') === 'true'); @@ -135,13 +139,22 @@ export function CronWorkflowList({match, location, history}: RouteComponentProps
-
NAME
+
NAME
NAMESPACE
TimeZone
SCHEDULE
-
-
CREATED
-
NEXT RUN
+
+
+ CREATED{' '} + +
+
+ NEXT RUN{' '} + +
{cronWorkflows.map(w => (
{w.spec.suspend ? : }
-
+
{w.metadata.annotations?.[ANNOTATION_TITLE] ?? w.metadata.name} {w.metadata.annotations?.[ANNOTATION_DESCRIPTION] ?

{w.metadata.annotations[ANNOTATION_DESCRIPTION]}

: null}
-
{w.metadata.namespace}
+
{w.metadata.namespace}
{w.spec.timezone}
{w.spec.schedule != '' @@ -179,11 +192,17 @@ export function CronWorkflowList({match, location, history}: RouteComponentProps )}
-
- +
+
-
- {w.spec.suspend ? '' : {() => }} +
+ {w.spec.suspend ? ( + '' + ) : ( + + {() => } + + )}
))} diff --git a/ui/src/app/cron-workflows/cron-workflow-status-viewer.tsx b/ui/src/app/cron-workflows/cron-workflow-status-viewer.tsx index e3a72e9a6242..6a82045871d4 100644 --- a/ui/src/app/cron-workflows/cron-workflow-status-viewer.tsx +++ b/ui/src/app/cron-workflows/cron-workflow-status-viewer.tsx @@ -6,6 +6,7 @@ import {Timestamp} from '../shared/components/timestamp'; import {ConditionsPanel} from '../shared/conditions-panel'; import {WorkflowLink} from '../workflows/components/workflow-link'; import {PrettySchedule} from './pretty-schedule'; +import {TIMESTAMP_KEYS} from '../shared/use-timestamp'; export function CronWorkflowStatusViewer({spec, status}: {spec: CronWorkflowSpec; status: CronWorkflowStatus}) { if (status === null) { @@ -35,7 +36,7 @@ export function CronWorkflowStatusViewer({spec, status}: {spec: CronWorkflowSpec ) }, - {title: 'Last Scheduled Time', value: }, + {title: 'Last Scheduled Time', value: }, {title: 'Conditions', value: } ].map(attr => (
diff --git a/ui/src/app/event-sources/event-source-list.tsx b/ui/src/app/event-sources/event-source-list.tsx index a2796cfb7769..f6824cfc66a6 100644 --- a/ui/src/app/event-sources/event-source-list.tsx +++ b/ui/src/app/event-sources/event-source-list.tsx @@ -14,7 +14,7 @@ import {ErrorNotice} from '../shared/components/error-notice'; import {Node} from '../shared/components/graph/types'; import {Loading} from '../shared/components/loading'; import {NamespaceFilter} from '../shared/components/namespace-filter'; -import {Timestamp} from '../shared/components/timestamp'; +import {Timestamp, TimestampSwitch} from '../shared/components/timestamp'; import {useCollectEvent} from '../shared/use-collect-event'; import {ZeroState} from '../shared/components/zero-state'; import {Context} from '../shared/context'; @@ -26,6 +26,7 @@ import {Utils} from '../shared/utils'; import {EventsPanel} from '../workflows/components/events-panel'; import {EventSourceCreator} from './event-source-creator'; import {EventSourceLogsViewer} from './event-source-log-viewer'; +import useTimestamp, {TIMESTAMP_KEYS} from '../shared/use-timestamp'; const learnMore = Learn more; @@ -88,6 +89,8 @@ export function EventSourceList({match, location, history}: RouteComponentProps< useCollectEvent('openedEventSourceList'); + const [storedDisplayISOFormat, setStoredDisplayISOFormat] = useTimestamp(TIMESTAMP_KEYS.EVENT_SOURCE_LIST_CREATION); + return (
NAME
NAMESPACE
-
CREATED
+
+ CREATED +
LOGS
{eventSources.map(es => ( @@ -140,7 +145,7 @@ export function EventSourceList({match, location, history}: RouteComponentProps<
{es.metadata.name}
{es.metadata.namespace}
- +
Learn more; @@ -83,6 +84,8 @@ export function SensorList({match, location, history}: RouteComponentProps) const loading = !error && !sensors; const zeroState = (sensors || []).length === 0; + const [storedDisplayISOFormat, setStoredDisplayISOFormat] = useTimestamp(TIMESTAMP_KEYS.SENSOR_LIST_CREATION); + return ( )
NAME
NAMESPACE
-
CREATED
+
+ CREATED +
LOGS
{sensors.map(s => ( @@ -138,7 +143,7 @@ export function SensorList({match, location, history}: RouteComponentProps)
{s.metadata.name}
{s.metadata.namespace}
- +
-; -export function Timestamp({date}: {date: Date | string | number}) { - const tooltip = (utc: Date | string | number) => { - return utc.toString() + '\n' + new Date(utc.toString()).toLocaleString(); - }; return ( - {date === null || date === undefined ? ( - '-' - ) : ( - - {() => ago(new Date(date))} - - )} + + {displayISOFormatValue ? ( + new Date(date.toString()).toISOString() + ) : ( + <> + {displayLocalDateTime ? ( + <> + {new Date(date.toString()).toLocaleString()} ({() => ago(new Date(date))}) + + ) : ( + {() => ago(new Date(date))} + )} + + )} + + {timestampKey ? : null} ); } + +export function TimestampSwitch({storedDisplayISOFormat, setStoredDisplayISOFormat}: {storedDisplayISOFormat: boolean; setStoredDisplayISOFormat: (value: boolean) => void}) { + return ( + + + { + e.stopPropagation(); + e.preventDefault(); + setStoredDisplayISOFormat(!storedDisplayISOFormat); + }} + /> + + + ); +} diff --git a/ui/src/app/shared/use-timestamp.ts b/ui/src/app/shared/use-timestamp.ts new file mode 100644 index 000000000000..53085de8a8c0 --- /dev/null +++ b/ui/src/app/shared/use-timestamp.ts @@ -0,0 +1,39 @@ +import {useState} from 'react'; +import {ScopedLocalStorage} from './scoped-local-storage'; + +export enum TIMESTAMP_KEYS { + WORKFLOW_NODE_STARTED = 'workflowNodeStarted', + WORKFLOW_NODE_FINISHED = 'workflowNodeFinished', + CLUSTER_WORKFLOW_TEMPLATE_LIST = 'clusterWorkflowTemplateList', + CRON_WORKFLOW_LIST_CREATION = 'cronWorkflowListCreation', + CRON_WORKFLOW_LIST_NEXT_SCHEDULED = 'cronWorkflowListNextScheduled', + CRON_WORKFLOW_STATUS_LAST_SCHEDULED = 'cronWorkflowStatusLastScheduled', + EVENT_SOURCE_LIST_CREATION = 'eventSourceListCreation', + SENSOR_LIST_CREATION = 'sensorListCreation', + EVENTS_PANEL_LAST = 'eventsPanelLast', + WORKFLOW_ARTIFACTS_CREATED = 'workflowArtifactsCreated', + WORKFLOW_SUMMARY_PANEL_START = 'workflowSummaryPanelStart', + WORKFLOW_SUMMARY_PANEL_END = 'workflowSummaryPanelEnd', + WORKFLOW_NODE_ARTIFACT_CREATED = 'workflowNodeArtifactCreated', + WORKFLOW_TEMPLATE_LIST_CREATION = 'workflowTemplateListCreation', + WORKFLOWS_ROW_STARTED = 'workflowsRowStarted', + WORKFLOWS_ROW_FINISHED = 'workflowsRowFinished', + CRON_ROW_STARTED = 'cronRowStarted', + CRON_ROW_FINISHED = 'cronRowFinished' +} + +const storage = new ScopedLocalStorage('Timestamp'); + +// key is used to store the preference in local storage +const useTimestamp = (timestampKey: TIMESTAMP_KEYS): [boolean, (value: boolean) => void] => { + const [storedDisplayISOFormat, setStoredDisplayISOFormat] = useState(storage.getItem(`displayISOFormat-${timestampKey}`, false)); + + const handleStoredDisplayISOFormatChange = (value: boolean) => { + setStoredDisplayISOFormat(value); + storage.setItem(`displayISOFormat-${timestampKey}`, value, false); + }; + + return [storedDisplayISOFormat, handleStoredDisplayISOFormatChange]; +}; + +export default useTimestamp; diff --git a/ui/src/app/workflow-templates/workflow-template-list.tsx b/ui/src/app/workflow-templates/workflow-template-list.tsx index 7f40a15db439..8e11dc92c348 100644 --- a/ui/src/app/workflow-templates/workflow-template-list.tsx +++ b/ui/src/app/workflow-templates/workflow-template-list.tsx @@ -12,7 +12,7 @@ import {ExampleManifests} from '../shared/components/example-manifests'; import {InfoIcon} from '../shared/components/fa-icons'; import {Loading} from '../shared/components/loading'; import {PaginationPanel} from '../shared/components/pagination-panel'; -import {Timestamp} from '../shared/components/timestamp'; +import {Timestamp, TimestampSwitch} from '../shared/components/timestamp'; import {useCollectEvent} from '../shared/use-collect-event'; import {ZeroState} from '../shared/components/zero-state'; import {Context} from '../shared/context'; @@ -27,6 +27,7 @@ import {WorkflowTemplateCreator} from './workflow-template-creator'; import {WorkflowTemplateFilters} from './workflow-template-filters'; import './workflow-template-list.scss'; +import useTimestamp, {TIMESTAMP_KEYS} from '../shared/use-timestamp'; const learnMore = Learn more; @@ -85,6 +86,8 @@ export function WorkflowTemplateList({match, location, history}: RouteComponentP useCollectEvent('openedWorkflowTemplateList'); + const [storedDisplayISOFormat, setStoredDisplayISOFormat] = useTimestamp(TIMESTAMP_KEYS.WORKFLOW_TEMPLATE_LIST_CREATION); + return (
NAME
NAMESPACE
-
CREATED
+
+ CREATED +
{templates.map(t => (
{t.metadata.namespace}
- +
))} diff --git a/ui/src/app/workflows/components/events-panel.tsx b/ui/src/app/workflows/components/events-panel.tsx index 3e1fd61bd67c..dae92540688b 100644 --- a/ui/src/app/workflows/components/events-panel.tsx +++ b/ui/src/app/workflows/components/events-panel.tsx @@ -4,11 +4,12 @@ import {map} from 'rxjs/operators'; import {Event} from '../../../models'; import {ErrorNotice} from '../../shared/components/error-notice'; import {Notice} from '../../shared/components/notice'; -import {Timestamp} from '../../shared/components/timestamp'; +import {Timestamp, TimestampSwitch} from '../../shared/components/timestamp'; import {ToggleButton} from '../../shared/components/toggle-button'; import debounce from '../../shared/debounce'; import {ListWatch} from '../../shared/list-watch'; import {services} from '../../shared/services'; +import useTimestamp, {TIMESTAMP_KEYS} from '../../shared/use-timestamp'; export function EventsPanel({namespace, name, kind}: {namespace: string; name: string; kind: string}) { const [showAll, setShowAll] = useState(false); @@ -87,6 +88,8 @@ export function EventsPanel({namespace, name, kind}: {namespace: string; name: s }; }); + const [storedDisplayISOFormat, setStoredDisplayISOFormat] = useTimestamp(TIMESTAMP_KEYS.EVENTS_PANEL_LAST); + return ( <>
@@ -106,7 +109,9 @@ export function EventsPanel({namespace, name, kind}: {namespace: string; name: s
Type
-
Last Seen
+
+ Last Seen +
Reason
Object
Message
@@ -120,7 +125,7 @@ export function EventsPanel({namespace, name, kind}: {namespace: string; name: s {e.type === 'Normal' ? : }
- +
{e.reason}
diff --git a/ui/src/app/workflows/components/workflow-artifacts.tsx b/ui/src/app/workflows/components/workflow-artifacts.tsx index 9b700ec8998b..1f9c820822c1 100644 --- a/ui/src/app/workflows/components/workflow-artifacts.tsx +++ b/ui/src/app/workflows/components/workflow-artifacts.tsx @@ -3,6 +3,7 @@ import * as React from 'react'; import * as models from '../../../models'; import {Timestamp} from '../../shared/components/timestamp'; import {services} from '../../shared/services'; +import {TIMESTAMP_KEYS} from '../../shared/use-timestamp'; interface Props { workflow: models.Workflow; @@ -53,7 +54,7 @@ export function WorkflowArtifacts(props: Props) {
{artifact.stepName}
{artifact.path}
- +
))} diff --git a/ui/src/app/workflows/components/workflow-details-list/workflow-details-list.tsx b/ui/src/app/workflows/components/workflow-details-list/workflow-details-list.tsx index 40dbb9e427b4..68f8bef5e90b 100644 --- a/ui/src/app/workflows/components/workflow-details-list/workflow-details-list.tsx +++ b/ui/src/app/workflows/components/workflow-details-list/workflow-details-list.tsx @@ -4,6 +4,8 @@ import * as models from '../../../../models'; import {WorkflowsRow} from '../../../workflows/components/workflows-row/workflows-row'; import './workflow-details-list.scss'; +import useTimestamp, {TIMESTAMP_KEYS} from '../../../shared/use-timestamp'; +import {TimestampSwitch} from '../../../shared/components/timestamp'; interface WorkflowDetailsList { workflows: models.Workflow[]; @@ -11,6 +13,8 @@ interface WorkflowDetailsList { } export function WorkflowDetailsList(props: WorkflowDetailsList) { + const [storedDisplayISOFormatStart, setStoredDisplayISOFormatStart] = useTimestamp(TIMESTAMP_KEYS.CRON_ROW_STARTED); + const [storedDisplayISOFormatFinished, setStoredDisplayISOFormatFinished] = useTimestamp(TIMESTAMP_KEYS.CRON_ROW_FINISHED); return (
@@ -18,8 +22,12 @@ export function WorkflowDetailsList(props: WorkflowDetailsList) {
NAME
NAMESPACE
-
STARTED
-
FINISHED
+
+ STARTED +
+
+ FINISHED +
DURATION
PROGRESS
MESSAGE
@@ -36,7 +44,18 @@ export function WorkflowDetailsList(props: WorkflowDetailsList) {
{/* checkboxes are not visible and are unused in details pages */} {props.workflows.map(wf => { - return ; + return ( + + ); })}
); diff --git a/ui/src/app/workflows/components/workflow-drawer/workflow-drawer.tsx b/ui/src/app/workflows/components/workflow-drawer/workflow-drawer.tsx index 3ffa8d7a709d..d35e22cde686 100644 --- a/ui/src/app/workflows/components/workflow-drawer/workflow-drawer.tsx +++ b/ui/src/app/workflows/components/workflow-drawer/workflow-drawer.tsx @@ -41,6 +41,10 @@ export function WorkflowDrawer(props: WorkflowDrawerProps) {
{wf.status.message}
)} +
+
NAME
+
{wf.metadata.name}
+
{!wf.status || !wf.status.conditions ? null : (
CONDITIONS
diff --git a/ui/src/app/workflows/components/workflow-node-info/workflow-node-info.tsx b/ui/src/app/workflows/components/workflow-node-info/workflow-node-info.tsx index 8d5c70a4ec34..7c4dc96f0a19 100644 --- a/ui/src/app/workflows/components/workflow-node-info/workflow-node-info.tsx +++ b/ui/src/app/workflows/components/workflow-node-info/workflow-node-info.tsx @@ -21,6 +21,7 @@ import {services} from '../../../shared/services'; import {getResolvedTemplates} from '../../../shared/template-resolution'; import './workflow-node-info.scss'; +import {TIMESTAMP_KEYS} from '../../../shared/use-timestamp'; function nodeDuration(node: models.NodeStatus, now: moment.Moment) { const endTime = node.finishedAt ? moment(node.finishedAt) : now; @@ -82,20 +83,16 @@ const AttributeRows = (props: {attributes: {title: string; value: any}[]}) => (
); -function DisplayWorkflowTime(props: {date: Date | string | number}) { +function DisplayWorkflowTime(props: {date: Date | string | number; timestampKey: TIMESTAMP_KEYS}) { const {date} = props; - const getLocalDateTime = (utc: Date | string | number) => { - return new Date(utc.toString()).toLocaleString(); - }; + + if (date === null || date === undefined) return
-
; + return (
- {date === null || date === undefined ? ( - '-' - ) : ( - - {getLocalDateTime(date)} () - - )} + + +
); } @@ -120,8 +117,8 @@ function WorkflowNodeSummary(props: Props) { } ] : []), - {title: 'START TIME', value: }, - {title: 'END TIME', value: }, + {title: 'START TIME', value: }, + {title: 'END TIME', value: }, { title: 'DURATION', value: {now => } @@ -450,7 +447,7 @@ function WorkflowNodeArtifacts(props: {workflow: Workflow; node: NodeStatus; arc {artifact.path} - +
diff --git a/ui/src/app/workflows/components/workflow-summary-panel.tsx b/ui/src/app/workflows/components/workflow-summary-panel.tsx index 36cade2327e8..85d66735b7b7 100644 --- a/ui/src/app/workflows/components/workflow-summary-panel.tsx +++ b/ui/src/app/workflows/components/workflow-summary-panel.tsx @@ -13,6 +13,7 @@ import {ResourcesDuration} from '../../shared/resources-duration'; import {WorkflowCreatorInfo} from './workflow-creator-info/workflow-creator-info'; import {WorkflowFrom} from './workflow-from'; import {WorkflowLabels} from './workflow-labels/workflow-labels'; +import {TIMESTAMP_KEYS} from '../../shared/use-timestamp'; export const WorkflowSummaryPanel = (props: {workflow: Workflow}) => ( @@ -36,8 +37,8 @@ export const WorkflowSummaryPanel = (props: {workflow: Workflow}) => ( ) }, - {title: 'Started', value: }, - {title: 'Finished ', value: }, + {title: 'Started', value: }, + {title: 'Finished ', value: }, { title: 'Duration', value: ( diff --git a/ui/src/app/workflows/components/workflows-list/workflows-list.scss b/ui/src/app/workflows/components/workflows-list/workflows-list.scss index 01b94241d786..85ec8f2cb346 100644 --- a/ui/src/app/workflows/components/workflows-list/workflows-list.scss +++ b/ui/src/app/workflows/components/workflows-list/workflows-list.scss @@ -32,6 +32,14 @@ height: 100%; } + &__timestamp { + white-space: break-spaces !important; + line-height: normal; + align-items: center; + display: flex; + word-break: break-word; + } + &__title { font-weight: bolder; font-size: 15px; diff --git a/ui/src/app/workflows/components/workflows-list/workflows-list.tsx b/ui/src/app/workflows/components/workflows-list/workflows-list.tsx index d1c9b3ab6630..e994cef30955 100644 --- a/ui/src/app/workflows/components/workflows-list/workflows-list.tsx +++ b/ui/src/app/workflows/components/workflows-list/workflows-list.tsx @@ -29,6 +29,8 @@ import {WorkflowsSummaryContainer} from '../workflows-summary-container/workflow import {WorkflowsToolbar} from '../workflows-toolbar/workflows-toolbar'; import './workflows-list.scss'; +import useTimestamp, {TIMESTAMP_KEYS} from '../../../shared/use-timestamp'; +import {TimestampSwitch} from '../../../shared/components/timestamp'; interface WorkflowListRenderOptions { paginationLimit: number; @@ -160,6 +162,9 @@ export function WorkflowsList({match, location, history}: RouteComponentProps
NAME
NAMESPACE
-
STARTED
-
FINISHED
+
+ STARTED{' '} + +
+
+ FINISHED{' '} + +
DURATION
PROGRESS
MESSAGE
@@ -305,6 +319,8 @@ export function WorkflowsList({match, location, history}: RouteComponentProps ); })} diff --git a/ui/src/app/workflows/components/workflows-row/workflows-row.tsx b/ui/src/app/workflows/components/workflows-row/workflows-row.tsx index b7aba72bf9cb..8e29fd2bc56f 100644 --- a/ui/src/app/workflows/components/workflows-row/workflows-row.tsx +++ b/ui/src/app/workflows/components/workflows-row/workflows-row.tsx @@ -22,6 +22,8 @@ interface WorkflowsRowProps { select: (wf: Workflow) => void; checked: boolean; columns: models.Column[]; + displayISOFormatStart: boolean; + displayISOFormatFinished: boolean; } export function WorkflowsRow(props: WorkflowsRowProps) { @@ -60,11 +62,11 @@ export function WorkflowsRow(props: WorkflowsRowProps) {
{hasAnnotation ? : markdown}
{wf.metadata.namespace}
-
- +
+
-
- +
+
{() => } diff --git a/ui/yarn.lock b/ui/yarn.lock index f199c8c09ded..390aa23b8844 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -1416,10 +1416,10 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== -"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.9": - version "0.3.20" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz#72e45707cf240fa6b081d0366f8265b0cd10197f" - integrity sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q== +"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.20", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== dependencies: "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" @@ -1991,23 +1991,7 @@ dependencies: "@types/ms" "*" -"@types/eslint-scope@^3.7.3": - version "3.7.7" - resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5" - integrity sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg== - dependencies: - "@types/eslint" "*" - "@types/estree" "*" - -"@types/eslint@*": - version "8.56.0" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.56.0.tgz#e28d045b8e530a33c9cbcfbf02332df0d1380a2c" - integrity sha512-FlsN0p4FhuYRjIxpbdXovvHQhtlG05O1GG/RNWvdAxTboR438IOTwmrY/vLA+Xfgg06BTkP045M3vpFwTMv1dg== - dependencies: - "@types/estree" "*" - "@types/json-schema" "*" - -"@types/estree@*", "@types/estree@^1.0.0": +"@types/estree@^1.0.5": version "1.0.5" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== @@ -2100,7 +2084,7 @@ resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.9.tgz#cd82382c4f902fed9691a2ed79ec68c5898af4c2" integrity sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg== -"@types/json-schema@*", "@types/json-schema@^7.0.12", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": +"@types/json-schema@^7.0.12", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== @@ -2471,10 +2455,10 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -"@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24" - integrity sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q== +"@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.12.1.tgz#bb16a0e8b1914f979f45864c23819cc3e3f0d4bb" + integrity sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg== dependencies: "@webassemblyjs/helper-numbers" "1.11.6" "@webassemblyjs/helper-wasm-bytecode" "1.11.6" @@ -2489,10 +2473,10 @@ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== -"@webassemblyjs/helper-buffer@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz#b66d73c43e296fd5e88006f18524feb0f2c7c093" - integrity sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA== +"@webassemblyjs/helper-buffer@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz#6df20d272ea5439bf20ab3492b7fb70e9bfcb3f6" + integrity sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw== "@webassemblyjs/helper-numbers@1.11.6": version "1.11.6" @@ -2508,15 +2492,15 @@ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== -"@webassemblyjs/helper-wasm-section@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz#ff97f3863c55ee7f580fd5c41a381e9def4aa577" - integrity sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g== +"@webassemblyjs/helper-wasm-section@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz#3da623233ae1a60409b509a52ade9bc22a37f7bf" + integrity sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g== dependencies: - "@webassemblyjs/ast" "1.11.6" - "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" "@webassemblyjs/helper-wasm-bytecode" "1.11.6" - "@webassemblyjs/wasm-gen" "1.11.6" + "@webassemblyjs/wasm-gen" "1.12.1" "@webassemblyjs/ieee754@1.11.6": version "1.11.6" @@ -2537,59 +2521,59 @@ resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== -"@webassemblyjs/wasm-edit@^1.11.5": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz#c72fa8220524c9b416249f3d94c2958dfe70ceab" - integrity sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw== +"@webassemblyjs/wasm-edit@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz#9f9f3ff52a14c980939be0ef9d5df9ebc678ae3b" + integrity sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g== dependencies: - "@webassemblyjs/ast" "1.11.6" - "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" "@webassemblyjs/helper-wasm-bytecode" "1.11.6" - "@webassemblyjs/helper-wasm-section" "1.11.6" - "@webassemblyjs/wasm-gen" "1.11.6" - "@webassemblyjs/wasm-opt" "1.11.6" - "@webassemblyjs/wasm-parser" "1.11.6" - "@webassemblyjs/wast-printer" "1.11.6" - -"@webassemblyjs/wasm-gen@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz#fb5283e0e8b4551cc4e9c3c0d7184a65faf7c268" - integrity sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA== - dependencies: - "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-wasm-section" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-opt" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" + "@webassemblyjs/wast-printer" "1.12.1" + +"@webassemblyjs/wasm-gen@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz#a6520601da1b5700448273666a71ad0a45d78547" + integrity sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w== + dependencies: + "@webassemblyjs/ast" "1.12.1" "@webassemblyjs/helper-wasm-bytecode" "1.11.6" "@webassemblyjs/ieee754" "1.11.6" "@webassemblyjs/leb128" "1.11.6" "@webassemblyjs/utf8" "1.11.6" -"@webassemblyjs/wasm-opt@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz#d9a22d651248422ca498b09aa3232a81041487c2" - integrity sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g== +"@webassemblyjs/wasm-opt@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz#9e6e81475dfcfb62dab574ac2dda38226c232bc5" + integrity sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg== dependencies: - "@webassemblyjs/ast" "1.11.6" - "@webassemblyjs/helper-buffer" "1.11.6" - "@webassemblyjs/wasm-gen" "1.11.6" - "@webassemblyjs/wasm-parser" "1.11.6" + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" -"@webassemblyjs/wasm-parser@1.11.6", "@webassemblyjs/wasm-parser@^1.11.5": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz#bb85378c527df824004812bbdb784eea539174a1" - integrity sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ== +"@webassemblyjs/wasm-parser@1.12.1", "@webassemblyjs/wasm-parser@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz#c47acb90e6f083391e3fa61d113650eea1e95937" + integrity sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ== dependencies: - "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/ast" "1.12.1" "@webassemblyjs/helper-api-error" "1.11.6" "@webassemblyjs/helper-wasm-bytecode" "1.11.6" "@webassemblyjs/ieee754" "1.11.6" "@webassemblyjs/leb128" "1.11.6" "@webassemblyjs/utf8" "1.11.6" -"@webassemblyjs/wast-printer@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz#a7bf8dd7e362aeb1668ff43f35cb849f188eff20" - integrity sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A== +"@webassemblyjs/wast-printer@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz#bcecf661d7d1abdaf989d8341a4833e33e2b31ac" + integrity sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA== dependencies: - "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/ast" "1.12.1" "@xtuc/long" "4.2.2" "@webpack-cli/configtest@^2.1.1": @@ -2643,10 +2627,10 @@ acorn-globals@^6.0.0: acorn "^7.1.1" acorn-walk "^7.1.1" -acorn-import-assertions@^1.9.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac" - integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA== +acorn-import-attributes@^1.9.5: + version "1.9.5" + resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef" + integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ== acorn-jsx@^5.3.2: version "5.3.2" @@ -3216,15 +3200,15 @@ browser-process-hrtime@^1.0.0: resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== -browserslist@^4.14.5, browserslist@^4.22.2: - version "4.22.2" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.22.2.tgz#704c4943072bd81ea18997f3bd2180e89c77874b" - integrity sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A== +browserslist@^4.21.10, browserslist@^4.22.2: + version "4.23.3" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.3.tgz#debb029d3c93ebc97ffbc8d9cbb03403e227c800" + integrity sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA== dependencies: - caniuse-lite "^1.0.30001565" - electron-to-chromium "^1.4.601" - node-releases "^2.0.14" - update-browserslist-db "^1.0.13" + caniuse-lite "^1.0.30001646" + electron-to-chromium "^1.5.4" + node-releases "^2.0.18" + update-browserslist-db "^1.1.0" bs-logger@0.x: version "0.2.6" @@ -3317,10 +3301,10 @@ camelcase@^6.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== -caniuse-lite@^1.0.30001565: - version "1.0.30001571" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001571.tgz#4182e93d696ff42930f4af7eba515ddeb57917ac" - integrity sha512-tYq/6MoXhdezDLFZuCO/TKboTzuQ/xR5cFdgXPfDtM7/kchBO3b4VWghE/OAi/DV7tTdhmLjZiZBZi1fA/GheQ== +caniuse-lite@^1.0.30001646: + version "1.0.30001655" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001655.tgz#0ce881f5a19a2dcfda2ecd927df4d5c1684b982f" + integrity sha512-jRGVy3iSGO5Uutn2owlb5gR6qsGngTw9ZTb4ali9f3glshcNmJ2noam4Mo9zia5P9Dk3jNNydy7vQjuE5dQmfg== capture-exit@^2.0.0: version "2.0.0" @@ -4108,10 +4092,10 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== -electron-to-chromium@^1.4.601: - version "1.4.616" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.616.tgz#4bddbc2c76e1e9dbf449ecd5da3d8119826ea4fb" - integrity sha512-1n7zWYh8eS0L9Uy+GskE0lkBUNK83cXTVJI0pU3mGprFsbfSdAc15VTFbo+A+Bq4pwstmL30AVcEU3Fo463lNg== +electron-to-chromium@^1.5.4: + version "1.5.13" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz#1abf0410c5344b2b829b7247e031f02810d442e6" + integrity sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q== emittery@^0.7.1: version "0.7.2" @@ -4140,10 +4124,10 @@ end-of-stream@^1.1.0, end-of-stream@^1.4.1: dependencies: once "^1.4.0" -enhanced-resolve@^5.15.0: - version "5.15.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35" - integrity sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg== +enhanced-resolve@^5.17.1: + version "5.17.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" + integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" @@ -4299,10 +4283,10 @@ esbuild@^0.19.0: "@esbuild/win32-ia32" "0.19.11" "@esbuild/win32-x64" "0.19.11" -escalade@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" - integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== +escalade@^3.1.2: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== escape-html@~1.0.3: version "1.0.3" @@ -5075,7 +5059,7 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -graceful-fs@^4.1.2, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: +graceful-fs@^4.1.2, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -7379,10 +7363,10 @@ node-notifier@^8.0.0: uuid "^8.3.0" which "^2.0.2" -node-releases@^2.0.14: - version "2.0.14" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" - integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== +node-releases@^2.0.18: + version "2.0.18" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" + integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== normalize-package-data@^2.5.0: version "2.5.0" @@ -7761,10 +7745,10 @@ path-type@^5.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-5.0.0.tgz#14b01ed7aea7ddf9c7c3f46181d4d04f9c785bb8" integrity sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg== -picocolors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" - integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== +picocolors@^1.0.0, picocolors@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" + integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: version "2.3.1" @@ -9404,21 +9388,21 @@ terminal-link@^2.0.0: ansi-escapes "^4.2.1" supports-hyperlinks "^2.0.0" -terser-webpack-plugin@^5.3.7: - version "5.3.9" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz#832536999c51b46d468067f9e37662a3b96adfe1" - integrity sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA== +terser-webpack-plugin@^5.3.10: + version "5.3.10" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz#904f4c9193c6fd2a03f693a2150c62a92f40d199" + integrity sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w== dependencies: - "@jridgewell/trace-mapping" "^0.3.17" + "@jridgewell/trace-mapping" "^0.3.20" jest-worker "^27.4.5" schema-utils "^3.1.1" serialize-javascript "^6.0.1" - terser "^5.16.8" + terser "^5.26.0" -terser@^5.10.0, terser@^5.16.8: - version "5.26.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.26.0.tgz#ee9f05d929f4189a9c28a0feb889d96d50126fe1" - integrity sha512-dytTGoE2oHgbNV9nTzgBEPaqAWvcJNl66VZ0BkJqlvp71IjO8CxdBx/ykCNb47cLnCmCvRZ6ZR0tLkqvZCdVBQ== +terser@^5.10.0, terser@^5.26.0: + version "5.31.6" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.31.6.tgz#c63858a0f0703988d0266a82fcbf2d7ba76422b1" + integrity sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg== dependencies: "@jridgewell/source-map" "^0.3.3" acorn "^8.8.2" @@ -9867,13 +9851,13 @@ untildify@^4.0.0: resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== -update-browserslist-db@^1.0.13: - version "1.0.13" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4" - integrity sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg== +update-browserslist-db@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz#7ca61c0d8650766090728046e416a8cde682859e" + integrity sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ== dependencies: - escalade "^3.1.1" - picocolors "^1.0.0" + escalade "^3.1.2" + picocolors "^1.0.1" uri-js@^4.2.2: version "4.4.1" @@ -10013,10 +9997,10 @@ warning@^4.0.1, warning@^4.0.2: dependencies: loose-envify "^1.0.0" -watchpack@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" - integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== +watchpack@^2.4.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.2.tgz#2feeaed67412e7c33184e5a79ca738fbd38564da" + integrity sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw== dependencies: glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" @@ -10136,34 +10120,33 @@ webpack-sources@^3.2.3: resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== -webpack@^5.89.0: - version "5.89.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.89.0.tgz#56b8bf9a34356e93a6625770006490bf3a7f32dc" - integrity sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw== +webpack@^5.94.0: + version "5.94.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.94.0.tgz#77a6089c716e7ab90c1c67574a28da518a20970f" + integrity sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg== dependencies: - "@types/eslint-scope" "^3.7.3" - "@types/estree" "^1.0.0" - "@webassemblyjs/ast" "^1.11.5" - "@webassemblyjs/wasm-edit" "^1.11.5" - "@webassemblyjs/wasm-parser" "^1.11.5" + "@types/estree" "^1.0.5" + "@webassemblyjs/ast" "^1.12.1" + "@webassemblyjs/wasm-edit" "^1.12.1" + "@webassemblyjs/wasm-parser" "^1.12.1" acorn "^8.7.1" - acorn-import-assertions "^1.9.0" - browserslist "^4.14.5" + acorn-import-attributes "^1.9.5" + browserslist "^4.21.10" chrome-trace-event "^1.0.2" - enhanced-resolve "^5.15.0" + enhanced-resolve "^5.17.1" es-module-lexer "^1.2.1" eslint-scope "5.1.1" events "^3.2.0" glob-to-regexp "^0.4.1" - graceful-fs "^4.2.9" + graceful-fs "^4.2.11" json-parse-even-better-errors "^2.3.1" loader-runner "^4.2.0" mime-types "^2.1.27" neo-async "^2.6.2" schema-utils "^3.2.0" tapable "^2.1.1" - terser-webpack-plugin "^5.3.7" - watchpack "^2.4.0" + terser-webpack-plugin "^5.3.10" + watchpack "^2.4.1" webpack-sources "^3.2.3" websocket-driver@>=0.5.1, websocket-driver@^0.7.4: diff --git a/util/kubeconfig/kubeconfig_test.go b/util/kubeconfig/kubeconfig_test.go index 5d9d940b1ecc..6ed4163263c0 100644 --- a/util/kubeconfig/kubeconfig_test.go +++ b/util/kubeconfig/kubeconfig_test.go @@ -41,10 +41,10 @@ func Test_BasicAuthString(t *testing.T) { assert.True(t, IsBasicAuthScheme(authString)) token := strings.TrimSpace(strings.TrimPrefix(authString, BasicAuthScheme)) uname, pwd, ok := decodeBasicAuthToken(token) - if assert.True(t, ok) { - assert.Equal(t, "admin", uname) - assert.Equal(t, "admin", pwd) - } + require.True(t, ok) + assert.Equal(t, "admin", uname) + assert.Equal(t, "admin", pwd) + file, err := os.CreateTemp("", "config.yaml") require.NoError(t, err) _, err = file.WriteString(config) diff --git a/util/telemetry/attributes.go b/util/telemetry/attributes.go new file mode 100644 index 000000000000..fad80c8bec07 --- /dev/null +++ b/util/telemetry/attributes.go @@ -0,0 +1,43 @@ +package telemetry + +const ( + AttribBuildVersion string = `version` + AttribBuildPlatform string = `platform` + AttribBuildGoVersion string = `go_version` + AttribBuildDate string = `build_date` + AttribBuildCompiler string = `compiler` + AttribBuildGitCommit string = `git_commit` + AttribBuildGitTreeState string = `git_treestate` + AttribBuildGitTag string = `git_tag` + + AttribCronWFName string = `name` + + AttribErrorCause string = "cause" + + AttribLogLevel string = `level` + + AttribNodePhase string = `node_phase` + + AttribPodPhase string = `phase` + AttribPodNamespace string = `namespace` + AttribPodPendingReason string = `reason` + + AttribQueueName string = `queue_name` + + AttribRecentlyStarted string = `recently_started` + + AttribRequestKind = `kind` + AttribRequestVerb = `verb` + AttribRequestCode = `status_code` + + AttribTemplateName string = `name` + AttribTemplateNamespace string = `namespace` + AttribTemplateCluster string = `cluster_scope` + + AttribWorkerType string = `worker_type` + + AttribWorkflowNamespace string = `namespace` + AttribWorkflowPhase string = `phase` + AttribWorkflowStatus = `status` + AttribWorkflowType = `type` +) diff --git a/workflow/metrics/exporter_prometheus.go b/util/telemetry/exporter_prometheus.go similarity index 95% rename from workflow/metrics/exporter_prometheus.go rename to util/telemetry/exporter_prometheus.go index cbea7d80de56..1ed45a90c91e 100644 --- a/workflow/metrics/exporter_prometheus.go +++ b/util/telemetry/exporter_prometheus.go @@ -1,4 +1,4 @@ -package metrics +package telemetry import ( "context" @@ -20,8 +20,8 @@ import ( ) const ( - defaultPrometheusServerPort = 9090 - defaultPrometheusServerPath = "/metrics" + DefaultPrometheusServerPort = 9090 + DefaultPrometheusServerPath = "/metrics" ) func (config *Config) prometheusMetricsExporter(namespace string) (*prometheus.Exporter, error) { @@ -39,14 +39,14 @@ func (config *Config) prometheusMetricsExporter(namespace string) (*prometheus.E func (config *Config) path() string { if config.Path == "" { - return defaultPrometheusServerPath + return DefaultPrometheusServerPath } return config.Path } func (config *Config) port() int { if config.Port == 0 { - return defaultPrometheusServerPort + return DefaultPrometheusServerPort } return config.Port } diff --git a/workflow/metrics/exporter_prometheus_test.go b/util/telemetry/exporter_prometheus_test.go similarity index 57% rename from workflow/metrics/exporter_prometheus_test.go rename to util/telemetry/exporter_prometheus_test.go index c80a3aa45057..05b34ea1eeff 100644 --- a/workflow/metrics/exporter_prometheus_test.go +++ b/util/telemetry/exporter_prometheus_test.go @@ -1,6 +1,6 @@ //go:build !windows -package metrics +package telemetry import ( "context" @@ -14,19 +14,22 @@ import ( "github.com/stretchr/testify/require" ) +// testScopeName is the name that the metrics running under test will have +const testScopeName string = "argo-workflows-test" + func TestDisablePrometheusServer(t *testing.T) { config := Config{ Enabled: false, - Path: defaultPrometheusServerPath, - Port: defaultPrometheusServerPort, + Path: DefaultPrometheusServerPath, + Port: DefaultPrometheusServerPort, } ctx, cancel := context.WithCancel(context.Background()) defer cancel() - m, err := New(ctx, TestScopeName, &config, Callbacks{}) + m, err := NewMetrics(ctx, testScopeName, testScopeName, &config) require.NoError(t, err) go m.RunPrometheusServer(ctx, false) time.Sleep(1 * time.Second) // to confirm that the server doesn't start, even if we wait - resp, err := http.Get(fmt.Sprintf("http://localhost:%d%s", defaultPrometheusServerPort, defaultPrometheusServerPath)) + resp, err := http.Get(fmt.Sprintf("http://localhost:%d%s", DefaultPrometheusServerPort, DefaultPrometheusServerPath)) if resp != nil { defer resp.Body.Close() } @@ -37,16 +40,16 @@ func TestDisablePrometheusServer(t *testing.T) { func TestPrometheusServer(t *testing.T) { config := Config{ Enabled: true, - Path: defaultPrometheusServerPath, - Port: defaultPrometheusServerPort, + Path: DefaultPrometheusServerPath, + Port: DefaultPrometheusServerPort, } ctx, cancel := context.WithCancel(context.Background()) defer cancel() - m, err := New(ctx, TestScopeName, &config, Callbacks{}) + m, err := NewMetrics(ctx, testScopeName, testScopeName, &config) require.NoError(t, err) go m.RunPrometheusServer(ctx, false) time.Sleep(1 * time.Second) - resp, err := http.Get(fmt.Sprintf("http://localhost:%d%s", defaultPrometheusServerPort, defaultPrometheusServerPath)) + resp, err := http.Get(fmt.Sprintf("http://localhost:%d%s", DefaultPrometheusServerPort, DefaultPrometheusServerPath)) require.NoError(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) @@ -57,22 +60,25 @@ func TestPrometheusServer(t *testing.T) { bodyString := string(bodyBytes) assert.NotEmpty(t, bodyString) + + cancel() // Explicit cancel as sometimes in github CI port 9090 is still busy + time.Sleep(1 * time.Second) // Wait for prometheus server } func TestDummyPrometheusServer(t *testing.T) { config := Config{ Enabled: true, - Path: defaultPrometheusServerPath, - Port: defaultPrometheusServerPort, + Path: DefaultPrometheusServerPath, + Port: DefaultPrometheusServerPort, Secure: false, } ctx, cancel := context.WithCancel(context.Background()) defer cancel() - m, err := New(ctx, TestScopeName, &config, Callbacks{}) + m, err := NewMetrics(ctx, testScopeName, testScopeName, &config) require.NoError(t, err) go m.RunPrometheusServer(ctx, true) time.Sleep(1 * time.Second) - resp, err := http.Get(fmt.Sprintf("http://localhost:%d%s", defaultPrometheusServerPort, defaultPrometheusServerPath)) + resp, err := http.Get(fmt.Sprintf("http://localhost:%d%s", DefaultPrometheusServerPort, DefaultPrometheusServerPath)) require.NoError(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) @@ -84,4 +90,7 @@ func TestDummyPrometheusServer(t *testing.T) { bodyString := string(bodyBytes) assert.Empty(t, bodyString) // expect the dummy metrics server to provide no metrics responses + + cancel() // Explicit cancel as sometimes in github CI port 9090 is still busy + time.Sleep(1 * time.Second) // Wait for prometheus server } diff --git a/util/telemetry/helpers_test.go b/util/telemetry/helpers_test.go new file mode 100644 index 000000000000..2aedaed192d6 --- /dev/null +++ b/util/telemetry/helpers_test.go @@ -0,0 +1,65 @@ +package telemetry + +import ( + "context" + + "go.opentelemetry.io/otel/sdk/metric" +) + +func createDefaultTestMetrics() (*Metrics, *TestMetricsExporter, error) { + config := Config{ + Enabled: true, + } + return createTestMetrics(&config) +} + +func createTestMetrics(config *Config) (*Metrics, *TestMetricsExporter, error) { + ctx /* with cancel*/ := context.Background() + te := NewTestMetricsExporter() + + m, err := NewMetrics(ctx, TestScopeName, TestScopeName, config, metric.WithReader(te)) + if err != nil { + return nil, nil, err + } + err = m.Populate(ctx, AddVersion, addTestingCounter, addTestingHistogram) + return m, te, err +} + +const ( + nameTestingHistogram = `testing_histogram` + nameTestingCounter = `testing_counter` + errorCauseTestingA = "TestingA" + errorCauseTestingB = "TestingB" +) + +func addTestingHistogram(_ context.Context, m *Metrics) error { + // The buckets here are only the 'defaults' and can be overridden with configmap defaults + return m.CreateInstrument(Float64Histogram, + nameTestingHistogram, + "Testing Metric", + "s", + WithDefaultBuckets([]float64{0.0, 1.0, 5.0, 10.0}), + WithAsBuiltIn(), + ) +} + +func (m *Metrics) TestingHistogramRecord(ctx context.Context, value float64) { + m.Record(ctx, nameTestingHistogram, value, InstAttribs{}) +} + +func addTestingCounter(ctx context.Context, m *Metrics) error { + return m.CreateInstrument(Int64Counter, + nameTestingCounter, + "Testing Error Counting Metric", + "{errors}", + WithAsBuiltIn(), + ) +} + +func (m *Metrics) TestingErrorA(ctx context.Context) { + m.AddInt(ctx, nameTestingCounter, 1, InstAttribs{{Name: AttribErrorCause, Value: errorCauseTestingB}}) +} + +func (m *Metrics) TestingErrorB(ctx context.Context) { + m.AddInt(ctx, nameTestingCounter, 1, InstAttribs{{Name: AttribErrorCause, Value: errorCauseTestingB}}) +} diff --git a/workflow/metrics/instrument.go b/util/telemetry/instrument.go similarity index 73% rename from workflow/metrics/instrument.go rename to util/telemetry/instrument.go index 35ba4ea28acd..6831dd5be804 100644 --- a/workflow/metrics/instrument.go +++ b/util/telemetry/instrument.go @@ -1,4 +1,4 @@ -package metrics +package telemetry import ( "fmt" @@ -9,7 +9,7 @@ import ( "github.com/argoproj/argo-workflows/v3/util/help" ) -type instrument struct { +type Instrument struct { name string description string otel interface{} @@ -17,7 +17,7 @@ type instrument struct { } func (m *Metrics) preCreateCheck(name string) error { - if _, exists := m.allInstruments[name]; exists { + if _, exists := m.AllInstruments[name]; exists { return fmt.Errorf("Instrument called %s already exists", name) } return nil @@ -30,13 +30,13 @@ func addHelpLink(name, description string) string { type instrumentType int const ( - float64ObservableGauge instrumentType = iota - float64Histogram - float64UpDownCounter - float64ObservableUpDownCounter - int64ObservableGauge - int64UpDownCounter - int64Counter + Float64ObservableGauge instrumentType = iota + Float64Histogram + Float64UpDownCounter + Float64ObservableUpDownCounter + Int64ObservableGauge + Int64UpDownCounter + Int64Counter ) // InstrumentOption applies options to all instruments. @@ -47,13 +47,13 @@ type instrumentOptions struct { type instrumentOption func(*instrumentOptions) -func withAsBuiltIn() instrumentOption { +func WithAsBuiltIn() instrumentOption { return func(o *instrumentOptions) { o.builtIn = true } } -func withDefaultBuckets(buckets []float64) instrumentOption { +func WithDefaultBuckets(buckets []float64) instrumentOption { return func(o *instrumentOptions) { o.defaultBuckets = buckets } @@ -67,10 +67,10 @@ func collectOptions(options ...instrumentOption) instrumentOptions { return o } -func (m *Metrics) createInstrument(instType instrumentType, name, desc, unit string, options ...instrumentOption) error { +func (m *Metrics) CreateInstrument(instType instrumentType, name, desc, unit string, options ...instrumentOption) error { opts := collectOptions(options...) - m.mutex.Lock() - defer m.mutex.Unlock() + m.Mutex.Lock() + defer m.Mutex.Unlock() err := m.preCreateCheck(name) if err != nil { return err @@ -81,14 +81,14 @@ func (m *Metrics) createInstrument(instType instrumentType, name, desc, unit str } var instPtr interface{} switch instType { - case float64ObservableGauge: + case Float64ObservableGauge: inst, insterr := (*m.otelMeter).Float64ObservableGauge(name, metric.WithDescription(desc), metric.WithUnit(unit), ) instPtr = &inst err = insterr - case float64Histogram: + case Float64Histogram: inst, insterr := (*m.otelMeter).Float64Histogram(name, metric.WithDescription(desc), metric.WithUnit(unit), @@ -96,35 +96,35 @@ func (m *Metrics) createInstrument(instType instrumentType, name, desc, unit str ) instPtr = &inst err = insterr - case float64UpDownCounter: + case Float64UpDownCounter: inst, insterr := (*m.otelMeter).Float64UpDownCounter(name, metric.WithDescription(desc), metric.WithUnit(unit), ) instPtr = &inst err = insterr - case float64ObservableUpDownCounter: + case Float64ObservableUpDownCounter: inst, insterr := (*m.otelMeter).Float64ObservableUpDownCounter(name, metric.WithDescription(desc), metric.WithUnit(unit), ) instPtr = &inst err = insterr - case int64ObservableGauge: + case Int64ObservableGauge: inst, insterr := (*m.otelMeter).Int64ObservableGauge(name, metric.WithDescription(desc), metric.WithUnit(unit), ) instPtr = &inst err = insterr - case int64UpDownCounter: + case Int64UpDownCounter: inst, insterr := (*m.otelMeter).Int64UpDownCounter(name, metric.WithDescription(desc), metric.WithUnit(unit), ) instPtr = &inst err = insterr - case int64Counter: + case Int64Counter: inst, insterr := (*m.otelMeter).Int64Counter(name, metric.WithDescription(desc), metric.WithUnit(unit), @@ -137,7 +137,7 @@ func (m *Metrics) createInstrument(instType instrumentType, name, desc, unit str if err != nil { return err } - m.allInstruments[name] = &instrument{ + m.AllInstruments[name] = &Instrument{ name: name, description: desc, otel: instPtr, @@ -155,3 +155,23 @@ func (m *Metrics) buckets(name string, defaultBuckets []float64) []float64 { } return defaultBuckets } + +func (i *Instrument) GetName() string { + return i.name +} + +func (i *Instrument) GetDescription() string { + return i.description +} + +func (i *Instrument) GetOtel() interface{} { + return i.otel +} + +func (i *Instrument) SetUserdata(data interface{}) { + i.userdata = data +} + +func (i *Instrument) GetUserdata() interface{} { + return i.userdata +} diff --git a/util/telemetry/metrics.go b/util/telemetry/metrics.go new file mode 100644 index 000000000000..2a6be32c38fd --- /dev/null +++ b/util/telemetry/metrics.go @@ -0,0 +1,120 @@ +package telemetry + +import ( + "context" + "os" + "sync" + "time" + + log "github.com/sirupsen/logrus" + "go.opentelemetry.io/otel" + + wfconfig "github.com/argoproj/argo-workflows/v3/config" + + "go.opentelemetry.io/contrib/instrumentation/runtime" + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" + "go.opentelemetry.io/otel/metric" + metricsdk "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/metric/metricdata" + "go.opentelemetry.io/otel/sdk/resource" + semconv "go.opentelemetry.io/otel/semconv/v1.24.0" +) + +type Config struct { + Enabled bool + Path string + Port int + TTL time.Duration + IgnoreErrors bool + Secure bool + Modifiers map[string]Modifier + Temporality wfconfig.MetricsTemporality +} + +type Metrics struct { + // Ensures mutual exclusion in workflows map + Mutex sync.RWMutex + + // Evil context for compatibility with legacy context free interfaces + Ctx context.Context + otelMeter *metric.Meter + config *Config + + AllInstruments map[string]*Instrument +} + +func NewMetrics(ctx context.Context, serviceName, prometheusName string, config *Config, extraOpts ...metricsdk.Option) (*Metrics, error) { + res := resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceName(serviceName), + ) + + options := make([]metricsdk.Option, 0) + options = append(options, metricsdk.WithResource(res)) + _, otlpEnabled := os.LookupEnv(`OTEL_EXPORTER_OTLP_ENDPOINT`) + _, otlpMetricsEnabled := os.LookupEnv(`OTEL_EXPORTER_OTLP_METRICS_ENDPOINT`) + if otlpEnabled || otlpMetricsEnabled { + log.Info("Starting OTLP metrics exporter") + otelExporter, err := otlpmetricgrpc.New(ctx, otlpmetricgrpc.WithTemporalitySelector(getTemporality(config))) + if err != nil { + return nil, err + } + options = append(options, metricsdk.WithReader(metricsdk.NewPeriodicReader(otelExporter))) + } + + if config.Enabled { + log.Info("Starting Prometheus metrics exporter") + promExporter, err := config.prometheusMetricsExporter(prometheusName) + if err != nil { + return nil, err + } + options = append(options, metricsdk.WithReader(promExporter)) + } + options = append(options, extraOpts...) + options = append(options, view(config)) + + provider := metricsdk.NewMeterProvider(options...) + otel.SetMeterProvider(provider) + + // Add runtime metrics + err := runtime.Start(runtime.WithMinimumReadMemStatsInterval(time.Second)) + if err != nil { + return nil, err + } + + meter := provider.Meter(serviceName) + metrics := &Metrics{ + Ctx: ctx, + otelMeter: &meter, + config: config, + AllInstruments: make(map[string]*Instrument), + } + + return metrics, nil +} + +type AddMetric func(context.Context, *Metrics) error + +func (m *Metrics) Populate(ctx context.Context, adders ...AddMetric) error { + for _, adder := range adders { + if err := adder(ctx, m); err != nil { + return err + } + } + return nil +} + +func getTemporality(config *Config) metricsdk.TemporalitySelector { + switch config.Temporality { + case wfconfig.MetricsTemporalityCumulative: + return func(metricsdk.InstrumentKind) metricdata.Temporality { + return metricdata.CumulativeTemporality + } + case wfconfig.MetricsTemporalityDelta: + return func(metricsdk.InstrumentKind) metricdata.Temporality { + return metricdata.DeltaTemporality + } + default: + return metricsdk.DefaultTemporalitySelector + } +} diff --git a/workflow/metrics/modifiers.go b/util/telemetry/modifiers.go similarity index 95% rename from workflow/metrics/modifiers.go rename to util/telemetry/modifiers.go index 984c21867361..f752f6184085 100644 --- a/workflow/metrics/modifiers.go +++ b/util/telemetry/modifiers.go @@ -1,4 +1,4 @@ -package metrics +package telemetry import ( "go.opentelemetry.io/otel/attribute" @@ -12,7 +12,7 @@ type Modifier struct { HistogramBuckets []float64 } -// Create an opentelemetry 'view' which disables whole metrics or aggregates across labels +// Create an opentelemetry 'view' which disables whole metrics or aggregates across attributes func view(config *Config) metricsdk.Option { views := make([]metricsdk.View, 0) for metric, modifier := range config.Modifiers { diff --git a/workflow/metrics/modifiers_test.go b/util/telemetry/modifiers_test.go similarity index 63% rename from workflow/metrics/modifiers_test.go rename to util/telemetry/modifiers_test.go index 818432e35778..3f588065a3a3 100644 --- a/workflow/metrics/modifiers_test.go +++ b/util/telemetry/modifiers_test.go @@ -1,4 +1,4 @@ -package metrics +package telemetry import ( "context" @@ -13,42 +13,38 @@ func TestViewDisable(t *testing.T) { // Same metric as TestMetrics, but disabled by a view m, te, err := createTestMetrics(&Config{ Modifiers: map[string]Modifier{ - nameOperationDuration: { + nameTestingHistogram: { Disabled: true, }, }, - }, - Callbacks{}, - ) + }) require.NoError(t, err) - m.OperationCompleted(m.ctx, 5) + m.TestingHistogramRecord(m.Ctx, 5) attribs := attribute.NewSet() - _, err = te.GetFloat64HistogramData(nameOperationDuration, &attribs) + _, err = te.GetFloat64HistogramData(nameTestingHistogram, &attribs) require.Error(t, err) } func TestViewDisabledAttributes(t *testing.T) { - // Disable the error cause label + // Disable the error cause attribute m, te, err := createTestMetrics(&Config{ Modifiers: map[string]Modifier{ - nameErrorCount: { - DisabledAttributes: []string{labelErrorCause}, + nameTestingCounter: { + DisabledAttributes: []string{AttribErrorCause}, }, }, - }, - Callbacks{}, - ) + }) require.NoError(t, err) // Submit a couple of errors - m.OperationPanic(context.Background()) - m.CronWorkflowSubmissionError(context.Background()) + m.TestingErrorA(context.Background()) + m.TestingErrorB(context.Background()) // See if we can find this with the attributes, we should not be able to - attribsFail := attribute.NewSet(attribute.String(labelErrorCause, string(ErrorCauseOperationPanic))) - _, err = te.GetInt64CounterValue(nameErrorCount, &attribsFail) + attribsFail := attribute.NewSet(attribute.String(AttribErrorCause, string(errorCauseTestingA))) + _, err = te.GetInt64CounterValue(nameTestingCounter, &attribsFail) require.Error(t, err) // Find a sum of all error types attribsSuccess := attribute.NewSet() - val, err := te.GetInt64CounterValue(nameErrorCount, &attribsSuccess) + val, err := te.GetInt64CounterValue(nameTestingCounter, &attribsSuccess) require.NoError(t, err) // Sum of the two submitted errors is 2 assert.Equal(t, int64(2), val) @@ -59,17 +55,15 @@ func TestViewHistogramBuckets(t *testing.T) { bounds := []float64{1.0, 3.0, 5.0, 10.0} m, te, err := createTestMetrics(&Config{ Modifiers: map[string]Modifier{ - nameOperationDuration: { + nameTestingHistogram: { HistogramBuckets: bounds, }, }, - }, - Callbacks{}, - ) + }) require.NoError(t, err) - m.OperationCompleted(m.ctx, 5) + m.TestingHistogramRecord(m.Ctx, 5) attribs := attribute.NewSet() - val, err := te.GetFloat64HistogramData(nameOperationDuration, &attribs) + val, err := te.GetFloat64HistogramData(nameTestingHistogram, &attribs) require.NoError(t, err) assert.Equal(t, bounds, val.Bounds) assert.Equal(t, []uint64{0, 0, 1, 0, 0}, val.BucketCounts) diff --git a/workflow/metrics/operators.go b/util/telemetry/operators.go similarity index 52% rename from workflow/metrics/operators.go rename to util/telemetry/operators.go index afe7b33c5459..f99f7426d5d6 100644 --- a/workflow/metrics/operators.go +++ b/util/telemetry/operators.go @@ -1,4 +1,4 @@ -package metrics +package telemetry import ( "context" @@ -9,43 +9,43 @@ import ( "go.opentelemetry.io/otel/metric" ) -func (m *Metrics) addInt(ctx context.Context, name string, val int64, labels instAttribs) { - if instrument, ok := m.allInstruments[name]; ok { - instrument.addInt(ctx, val, labels) +func (m *Metrics) AddInt(ctx context.Context, name string, val int64, attribs InstAttribs) { + if instrument, ok := m.AllInstruments[name]; ok { + instrument.AddInt(ctx, val, attribs) } else { log.Errorf("Metrics addInt() to non-existent metric %s", name) } } -func (i *instrument) addInt(ctx context.Context, val int64, labels instAttribs) { +func (i *Instrument) AddInt(ctx context.Context, val int64, attribs InstAttribs) { switch inst := i.otel.(type) { case *metric.Int64UpDownCounter: - (*inst).Add(ctx, val, i.attributes(labels)) + (*inst).Add(ctx, val, i.attributes(attribs)) case *metric.Int64Counter: - (*inst).Add(ctx, val, i.attributes(labels)) + (*inst).Add(ctx, val, i.attributes(attribs)) default: log.Errorf("Metrics addInt() to invalid type %s (%t)", i.name, i.otel) } } -func (m *Metrics) record(ctx context.Context, name string, val float64, labels instAttribs) { - if instrument, ok := m.allInstruments[name]; ok { - instrument.record(ctx, val, labels) +func (m *Metrics) Record(ctx context.Context, name string, val float64, attribs InstAttribs) { + if instrument, ok := m.AllInstruments[name]; ok { + instrument.Record(ctx, val, attribs) } else { log.Errorf("Metrics record() to non-existent metric %s", name) } } -func (i *instrument) record(ctx context.Context, val float64, labels instAttribs) { +func (i *Instrument) Record(ctx context.Context, val float64, attribs InstAttribs) { switch inst := i.otel.(type) { case *metric.Float64Histogram: - (*inst).Record(ctx, val, i.attributes(labels)) + (*inst).Record(ctx, val, i.attributes(attribs)) default: log.Errorf("Metrics record() to invalid type %s (%t)", i.name, i.otel) } } -func (i *instrument) registerCallback(m *Metrics, f metric.Callback) error { +func (i *Instrument) RegisterCallback(m *Metrics, f metric.Callback) error { switch inst := i.otel.(type) { case *metric.Float64ObservableUpDownCounter: _, err := (*m.otelMeter).RegisterCallback(f, *inst) @@ -61,46 +61,46 @@ func (i *instrument) registerCallback(m *Metrics, f metric.Callback) error { } } -func (i *instrument) observeInt(o metric.Observer, val int64, labels instAttribs) { +func (i *Instrument) ObserveInt(o metric.Observer, val int64, attribs InstAttribs) { switch inst := i.otel.(type) { case *metric.Int64ObservableGauge: - o.ObserveInt64(*inst, val, i.attributes(labels)) + o.ObserveInt64(*inst, val, i.attributes(attribs)) default: log.Errorf("Metrics observeFloat() to invalid type %s (%t)", i.name, i.otel) } } -func (i *instrument) observeFloat(o metric.Observer, val float64, labels instAttribs) { +func (i *Instrument) ObserveFloat(o metric.Observer, val float64, attribs InstAttribs) { switch inst := i.otel.(type) { case *metric.Float64ObservableGauge: - o.ObserveFloat64(*inst, val, i.attributes(labels)) + o.ObserveFloat64(*inst, val, i.attributes(attribs)) case *metric.Float64ObservableUpDownCounter: - o.ObserveFloat64(*inst, val, i.attributes(labels)) + o.ObserveFloat64(*inst, val, i.attributes(attribs)) default: log.Errorf("Metrics observeFloat() to invalid type %s (%t)", i.name, i.otel) } } -type instAttribs []instAttrib -type instAttrib struct { - name string - value interface{} +type InstAttribs []InstAttrib +type InstAttrib struct { + Name string + Value interface{} } -func (i *instrument) attributes(labels instAttribs) metric.MeasurementOption { +func (i *Instrument) attributes(labels InstAttribs) metric.MeasurementOption { attribs := make([]attribute.KeyValue, 0) for _, label := range labels { - switch value := label.value.(type) { + switch value := label.Value.(type) { case string: - attribs = append(attribs, attribute.String(label.name, value)) + attribs = append(attribs, attribute.String(label.Name, value)) case bool: - attribs = append(attribs, attribute.Bool(label.name, value)) + attribs = append(attribs, attribute.Bool(label.Name, value)) case int: - attribs = append(attribs, attribute.Int(label.name, value)) + attribs = append(attribs, attribute.Int(label.Name, value)) case int64: - attribs = append(attribs, attribute.Int64(label.name, value)) + attribs = append(attribs, attribute.Int64(label.Name, value)) case float64: - attribs = append(attribs, attribute.Float64(label.name, value)) + attribs = append(attribs, attribute.Float64(label.Name, value)) default: log.Errorf("Attempt to use label of unhandled type in metric %s", i.name) } diff --git a/workflow/metrics/test_exporter.go b/util/telemetry/test_metrics_exporter.go similarity index 61% rename from workflow/metrics/test_exporter.go rename to util/telemetry/test_metrics_exporter.go index 071e436386ac..4a49b1965306 100644 --- a/workflow/metrics/test_exporter.go +++ b/util/telemetry/test_metrics_exporter.go @@ -1,84 +1,39 @@ -package metrics +package telemetry import ( "context" "fmt" - "time" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric/metricdata" - "k8s.io/client-go/util/workqueue" ) +// TestScopeName is the name that the metrics running under test will have +const TestScopeName string = "argo-workflows-test" + // TestExporter is an opentelemetry metrics exporter, purely for use within // tests. It is not possible to query the values of an instrument via the otel // SDK, so this exporter provides methods by which you can request // metrics by name+attributes and therefore inspect whether they exist, and // their values for the purposes of testing only. // This is a public structure as it is used outside of this module also. -type TestExporter struct { +type TestMetricsExporter struct { metric.Reader } -// TestScopeName is the name that the metrics running under test will have -const TestScopeName string = "argo-workflows-test" - -var _ metric.Reader = &TestExporter{} - -var sharedMetrics *Metrics = nil -var sharedTE *TestExporter = nil - -// getSharedMetrics returns a singleton metrics with test exporter -// This is necessary because only the first call to workqueue.SetProvider -// takes effect within a single binary -// This can be fixed when we update to client-go 0.27 or later and we can -// create workqueues with https://godocs.io/k8s.io/client-go/util/workqueue#NewRateLimitingQueueWithConfig -func getSharedMetrics() (*Metrics, *TestExporter, error) { - if sharedMetrics == nil { - config := Config{ - Enabled: true, - TTL: 1 * time.Second, - } - var err error - sharedMetrics, sharedTE, err = createTestMetrics(&config, Callbacks{}) - if err != nil { - return nil, nil, err - } - - workqueue.SetProvider(sharedMetrics) - } - return sharedMetrics, sharedTE, nil -} - -// CreateDefaultTestMetrics creates a boring testExporter enabled -// metrics, suitable for many tests -func CreateDefaultTestMetrics() (*Metrics, *TestExporter, error) { - config := Config{ - Enabled: true, - } - return createTestMetrics(&config, Callbacks{}) -} - -func createTestMetrics(config *Config, callbacks Callbacks) (*Metrics, *TestExporter, error) { - ctx /* with cancel*/ := context.Background() - te := newTestExporter() - - m, err := New(ctx, TestScopeName, config, callbacks, metric.WithReader(te)) - return m, te, err - -} +var _ metric.Reader = &TestMetricsExporter{} -func newTestExporter() *TestExporter { +func NewTestMetricsExporter() *TestMetricsExporter { reader := metric.NewManualReader() - e := &TestExporter{ + e := &TestMetricsExporter{ Reader: reader, } return e } -func (t *TestExporter) getOurMetrics() (*[]metricdata.Metrics, error) { +func (t *TestMetricsExporter) getOurMetrics() (*[]metricdata.Metrics, error) { metrics := metricdata.ResourceMetrics{} err := t.Collect(context.TODO(), &metrics) if err != nil { @@ -92,7 +47,7 @@ func (t *TestExporter) getOurMetrics() (*[]metricdata.Metrics, error) { return nil, fmt.Errorf("%s scope not found", TestScopeName) } -func (t *TestExporter) getNamedMetric(name string) (*metricdata.Metrics, error) { +func (t *TestMetricsExporter) getNamedMetric(name string) (*metricdata.Metrics, error) { mtcs, err := t.getOurMetrics() if err != nil { return nil, err @@ -105,7 +60,7 @@ func (t *TestExporter) getNamedMetric(name string) (*metricdata.Metrics, error) return nil, fmt.Errorf("%s named metric not found in %v", name, mtcs) } -func (t *TestExporter) getNamedInt64CounterData(name string, attribs *attribute.Set) (*metricdata.DataPoint[int64], error) { +func (t *TestMetricsExporter) getNamedInt64CounterData(name string, attribs *attribute.Set) (*metricdata.DataPoint[int64], error) { mtc, err := t.getNamedMetric(name) if err != nil { return nil, err @@ -122,7 +77,7 @@ func (t *TestExporter) getNamedInt64CounterData(name string, attribs *attribute. return nil, fmt.Errorf("%s type counter[int64] not found in %v", name, mtc) } -func (t *TestExporter) getNamedFloat64GaugeData(name string, attribs *attribute.Set) (*metricdata.DataPoint[float64], error) { +func (t *TestMetricsExporter) getNamedFloat64GaugeData(name string, attribs *attribute.Set) (*metricdata.DataPoint[float64], error) { mtc, err := t.getNamedMetric(name) if err != nil { return nil, err @@ -139,7 +94,7 @@ func (t *TestExporter) getNamedFloat64GaugeData(name string, attribs *attribute. return nil, fmt.Errorf("%s type gauge[float64] not found in %v", name, mtc) } -func (t *TestExporter) getNamedInt64GaugeData(name string, attribs *attribute.Set) (*metricdata.DataPoint[int64], error) { +func (t *TestMetricsExporter) getNamedInt64GaugeData(name string, attribs *attribute.Set) (*metricdata.DataPoint[int64], error) { mtc, err := t.getNamedMetric(name) if err != nil { return nil, err @@ -158,7 +113,7 @@ func (t *TestExporter) getNamedInt64GaugeData(name string, attribs *attribute.Se return nil, fmt.Errorf("%s named gauge[float64] with attribs %v not found in %v", name, attribs, mtc) } -func (t *TestExporter) getNamedFloat64CounterData(name string, attribs *attribute.Set) (*metricdata.DataPoint[float64], error) { +func (t *TestMetricsExporter) getNamedFloat64CounterData(name string, attribs *attribute.Set) (*metricdata.DataPoint[float64], error) { mtc, err := t.getNamedMetric(name) if err != nil { return nil, err @@ -175,7 +130,7 @@ func (t *TestExporter) getNamedFloat64CounterData(name string, attribs *attribut return nil, fmt.Errorf("%s type counter[float64] not found in %v", name, mtc) } -func (t *TestExporter) getNamedFloat64HistogramData(name string, attribs *attribute.Set) (*metricdata.HistogramDataPoint[float64], error) { +func (t *TestMetricsExporter) getNamedFloat64HistogramData(name string, attribs *attribute.Set) (*metricdata.HistogramDataPoint[float64], error) { mtc, err := t.getNamedMetric(name) if err != nil { return nil, err @@ -193,13 +148,13 @@ func (t *TestExporter) getNamedFloat64HistogramData(name string, attribs *attrib } // GetFloat64HistogramData returns an otel histogram float64 data point for test reads -func (t *TestExporter) GetFloat64HistogramData(name string, attribs *attribute.Set) (*metricdata.HistogramDataPoint[float64], error) { +func (t *TestMetricsExporter) GetFloat64HistogramData(name string, attribs *attribute.Set) (*metricdata.HistogramDataPoint[float64], error) { data, err := t.getNamedFloat64HistogramData(name, attribs) return data, err } // GetInt64CounterValue returns an otel int64 counter value for test reads -func (t *TestExporter) GetInt64CounterValue(name string, attribs *attribute.Set) (int64, error) { +func (t *TestMetricsExporter) GetInt64CounterValue(name string, attribs *attribute.Set) (int64, error) { counter, err := t.getNamedInt64CounterData(name, attribs) if err != nil { return 0, err @@ -208,7 +163,7 @@ func (t *TestExporter) GetInt64CounterValue(name string, attribs *attribute.Set) } // GetFloat64GaugeValue returns an otel float64 gauge value for test reads -func (t *TestExporter) GetFloat64GaugeValue(name string, attribs *attribute.Set) (float64, error) { +func (t *TestMetricsExporter) GetFloat64GaugeValue(name string, attribs *attribute.Set) (float64, error) { gauge, err := t.getNamedFloat64GaugeData(name, attribs) if err != nil { return 0, err @@ -217,7 +172,7 @@ func (t *TestExporter) GetFloat64GaugeValue(name string, attribs *attribute.Set) } // GetInt64GaugeValue returns an otel int64 gauge value for test reads -func (t *TestExporter) GetInt64GaugeValue(name string, attribs *attribute.Set) (int64, error) { +func (t *TestMetricsExporter) GetInt64GaugeValue(name string, attribs *attribute.Set) (int64, error) { gauge, err := t.getNamedInt64GaugeData(name, attribs) if err != nil { return 0, err @@ -226,7 +181,7 @@ func (t *TestExporter) GetInt64GaugeValue(name string, attribs *attribute.Set) ( } // GetFloat64CounterValue returns an otel float64 counter value for test reads -func (t *TestExporter) GetFloat64CounterValue(name string, attribs *attribute.Set) (float64, error) { +func (t *TestMetricsExporter) GetFloat64CounterValue(name string, attribs *attribute.Set) (float64, error) { counter, err := t.getNamedFloat64CounterData(name, attribs) if err != nil { return 0, err diff --git a/util/telemetry/version.go b/util/telemetry/version.go new file mode 100644 index 000000000000..055aa038bf74 --- /dev/null +++ b/util/telemetry/version.go @@ -0,0 +1,33 @@ +package telemetry + +import ( + "context" + + "github.com/argoproj/argo-workflows/v3" +) + +func AddVersion(ctx context.Context, m *Metrics) error { + const nameVersion = `version` + err := m.CreateInstrument(Int64Counter, + nameVersion, + "Build metadata for this Controller", + "{unused}", + WithAsBuiltIn(), + ) + if err != nil { + return err + } + + version := argo.GetVersion() + m.AddInt(ctx, nameVersion, 1, InstAttribs{ + {Name: AttribBuildVersion, Value: version.Version}, + {Name: AttribBuildPlatform, Value: version.Platform}, + {Name: AttribBuildGoVersion, Value: version.GoVersion}, + {Name: AttribBuildDate, Value: version.BuildDate}, + {Name: AttribBuildCompiler, Value: version.Compiler}, + {Name: AttribBuildGitCommit, Value: version.GitCommit}, + {Name: AttribBuildGitTreeState, Value: version.GitTreeState}, + {Name: AttribBuildGitTag, Value: version.GitTag}, + }) + return nil +} diff --git a/util/telemetry/version_test.go b/util/telemetry/version_test.go new file mode 100644 index 000000000000..1337c1520b21 --- /dev/null +++ b/util/telemetry/version_test.go @@ -0,0 +1,31 @@ +package telemetry + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/attribute" + + "github.com/argoproj/argo-workflows/v3" +) + +func TestVersion(t *testing.T) { + _, te, err := createDefaultTestMetrics() + require.NoError(t, err) + assert.NotNil(t, te) + version := argo.GetVersion() + attribs := attribute.NewSet( + attribute.String(AttribBuildVersion, version.Version), + attribute.String(AttribBuildPlatform, version.Platform), + attribute.String(AttribBuildGoVersion, version.GoVersion), + attribute.String(AttribBuildDate, version.BuildDate), + attribute.String(AttribBuildCompiler, version.Compiler), + attribute.String(AttribBuildGitCommit, version.GitCommit), + attribute.String(AttribBuildGitTreeState, version.GitTreeState), + attribute.String(AttribBuildGitTag, version.GitTag), + ) + val, err := te.GetInt64CounterValue(`version`, &attribs) + require.NoError(t, err) + assert.Equal(t, int64(1), val) +} diff --git a/workflow/artifacts/http/http_test.go b/workflow/artifacts/http/http_test.go index 2bf1ebd2bc37..66a913748eba 100644 --- a/workflow/artifacts/http/http_test.go +++ b/workflow/artifacts/http/http_test.go @@ -48,9 +48,8 @@ func TestHTTPArtifactDriver_Load(t *testing.T) { }, "/tmp/not-found") require.Error(t, err) argoError, ok := err.(errors.ArgoError) - if assert.True(t, ok) { - assert.Equal(t, errors.CodeNotFound, argoError.Code()) - } + require.True(t, ok) + assert.Equal(t, errors.CodeNotFound, argoError.Code()) }) } @@ -64,9 +63,8 @@ func TestArtifactoryArtifactDriver_Load(t *testing.T) { }, "/tmp/not-found") require.Error(t, err) argoError, ok := err.(errors.ArgoError) - if assert.True(t, ok) { - assert.Equal(t, errors.CodeNotFound, argoError.Code()) - } + require.True(t, ok) + assert.Equal(t, errors.CodeNotFound, argoError.Code()) }) t.Run("Found", func(t *testing.T) { err := driver.Load(&wfv1.Artifact{ diff --git a/workflow/common/convert_test.go b/workflow/common/convert_test.go index 7930749cb529..95a9bf4f9eb2 100644 --- a/workflow/common/convert_test.go +++ b/workflow/common/convert_test.go @@ -109,9 +109,8 @@ spec: err = yaml.Unmarshal([]byte(cronWfInstanceIdString), &cronWf) require.NoError(t, err) wf = ConvertCronWorkflowToWorkflow(&cronWf) - if assert.Contains(t, wf.GetLabels(), LabelKeyControllerInstanceID) { - assert.Equal(t, "test-controller", wf.GetLabels()[LabelKeyControllerInstanceID]) - } + require.Contains(t, wf.GetLabels(), LabelKeyControllerInstanceID) + assert.Equal(t, "test-controller", wf.GetLabels()[LabelKeyControllerInstanceID]) err = yaml.Unmarshal([]byte(cronWfInstanceIdString), &cronWf) require.NoError(t, err) diff --git a/workflow/controller/cache_test.go b/workflow/controller/cache_test.go index f8dbd105a520..4a4aaf224417 100644 --- a/workflow/controller/cache_test.go +++ b/workflow/controller/cache_test.go @@ -63,10 +63,9 @@ func TestConfigMapCacheLoadHit(t *testing.T) { outputs := entry.Outputs require.NoError(t, err) - if assert.Len(t, outputs.Parameters, 1) { - assert.Equal(t, "hello", outputs.Parameters[0].Name) - assert.Equal(t, "foobar", outputs.Parameters[0].Value.String()) - } + require.Len(t, outputs.Parameters, 1) + assert.Equal(t, "hello", outputs.Parameters[0].Name) + assert.Equal(t, "foobar", outputs.Parameters[0].Value.String()) } func TestConfigMapCacheLoadMiss(t *testing.T) { diff --git a/workflow/controller/container_set_template_test.go b/workflow/controller/container_set_template_test.go index ea8c4d9919cd..470546b1e6b3 100644 --- a/workflow/controller/container_set_template_test.go +++ b/workflow/controller/container_set_template_test.go @@ -118,14 +118,13 @@ spec: {Name: "input-artifacts", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}}, }, pod.Spec.Volumes) - if assert.Len(t, pod.Spec.InitContainers, 1) { - c := pod.Spec.InitContainers[0] - assert.ElementsMatch(t, []corev1.VolumeMount{ - {Name: "input-artifacts", MountPath: "/argo/inputs/artifacts"}, - {Name: "workspace", MountPath: "/mainctrfs/workspace"}, - {Name: "var-run-argo", MountPath: common.VarRunArgoPath}, - }, c.VolumeMounts) - } + require.Len(t, pod.Spec.InitContainers, 1) + c := pod.Spec.InitContainers[0] + assert.ElementsMatch(t, []corev1.VolumeMount{ + {Name: "input-artifacts", MountPath: "/argo/inputs/artifacts"}, + {Name: "workspace", MountPath: "/mainctrfs/workspace"}, + {Name: "var-run-argo", MountPath: common.VarRunArgoPath}, + }, c.VolumeMounts) assert.Len(t, pod.Spec.Containers, 2) for _, c := range pod.Spec.Containers { diff --git a/workflow/controller/controller.go b/workflow/controller/controller.go index 50d222dcb55a..dc3ca0fd4c47 100644 --- a/workflow/controller/controller.go +++ b/workflow/controller/controller.go @@ -52,6 +52,7 @@ import ( "github.com/argoproj/argo-workflows/v3/util/diff" "github.com/argoproj/argo-workflows/v3/util/env" errorsutil "github.com/argoproj/argo-workflows/v3/util/errors" + "github.com/argoproj/argo-workflows/v3/util/telemetry" "github.com/argoproj/argo-workflows/v3/workflow/artifactrepositories" "github.com/argoproj/argo-workflows/v3/workflow/common" controllercache "github.com/argoproj/argo-workflows/v3/workflow/controller/cache" @@ -221,6 +222,7 @@ func NewWorkflowController(ctx context.Context, restConfig *rest.Config, kubecli wfc.maxStackDepth = wfc.getMaxStackDepth() wfc.metrics, err = metrics.New(ctx, `workflows-controller`, + `argo_workflows`, wfc.getMetricsServerConfig(), metrics.Callbacks{ PodPhase: wfc.getPodPhaseMetrics, @@ -1391,18 +1393,18 @@ func (wfc *WorkflowController) getMaxStackDepth() int { return maxAllowedStackDepth } -func (wfc *WorkflowController) getMetricsServerConfig() *metrics.Config { +func (wfc *WorkflowController) getMetricsServerConfig() *telemetry.Config { // Metrics config - modifiers := make(map[string]metrics.Modifier) + modifiers := make(map[string]telemetry.Modifier) for name, modifier := range wfc.Config.MetricsConfig.Modifiers { - modifiers[name] = metrics.Modifier{ + modifiers[name] = telemetry.Modifier{ Disabled: modifier.Disabled, DisabledAttributes: modifier.DisabledAttributes, HistogramBuckets: modifier.HistogramBuckets, } } - metricsConfig := metrics.Config{ + metricsConfig := telemetry.Config{ Enabled: wfc.Config.MetricsConfig.Enabled == nil || *wfc.Config.MetricsConfig.Enabled, Path: wfc.Config.MetricsConfig.Path, Port: wfc.Config.MetricsConfig.Port, diff --git a/workflow/controller/controller_test.go b/workflow/controller/controller_test.go index c1b2c52b59fa..e7c018d92857 100644 --- a/workflow/controller/controller_test.go +++ b/workflow/controller/controller_test.go @@ -32,6 +32,7 @@ import ( "github.com/argoproj/argo-workflows/v3/pkg/client/clientset/versioned/scheme" wfextv "github.com/argoproj/argo-workflows/v3/pkg/client/informers/externalversions" envutil "github.com/argoproj/argo-workflows/v3/util/env" + "github.com/argoproj/argo-workflows/v3/util/telemetry" armocks "github.com/argoproj/argo-workflows/v3/workflow/artifactrepositories/mocks" "github.com/argoproj/argo-workflows/v3/workflow/common" controllercache "github.com/argoproj/argo-workflows/v3/workflow/controller/cache" @@ -248,7 +249,7 @@ var defaultServiceAccount = &apiv1.ServiceAccount{ } // test exporter extract metric values from the metrics subsystem -var testExporter *metrics.TestExporter +var testExporter *telemetry.TestMetricsExporter func newController(options ...interface{}) (context.CancelFunc, *WorkflowController) { // get all the objects and add to the fake @@ -716,20 +717,17 @@ spec: assert.True(t, controller.processNextItem(ctx)) expectWorkflow(ctx, controller, "my-wf-0", func(wf *wfv1.Workflow) { - if assert.NotNil(t, wf) { - assert.Equal(t, wfv1.WorkflowRunning, wf.Status.Phase) - } + require.NotNil(t, wf) + assert.Equal(t, wfv1.WorkflowRunning, wf.Status.Phase) }) expectWorkflow(ctx, controller, "my-wf-1", func(wf *wfv1.Workflow) { - if assert.NotNil(t, wf) { - assert.Equal(t, wfv1.WorkflowPending, wf.Status.Phase) - assert.Equal(t, "Workflow processing has been postponed because too many workflows are already running", wf.Status.Message) - } + require.NotNil(t, wf) + assert.Equal(t, wfv1.WorkflowPending, wf.Status.Phase) + assert.Equal(t, "Workflow processing has been postponed because too many workflows are already running", wf.Status.Message) }) expectWorkflow(ctx, controller, "my-wf-2", func(wf *wfv1.Workflow) { - if assert.NotNil(t, wf) { - assert.Equal(t, wfv1.WorkflowFailed, wf.Status.Phase) - } + require.NotNil(t, wf) + assert.Equal(t, wfv1.WorkflowFailed, wf.Status.Phase) }) }) } @@ -995,19 +993,17 @@ status: // process my-wf-0; update status to Pending assert.True(t, controller.processNextItem(ctx)) expectWorkflow(ctx, controller, "my-wf-0", func(wf *wfv1.Workflow) { - if assert.NotNil(t, wf) { - assert.Equal(t, wfv1.WorkflowPending, wf.Status.Phase) - assert.Equal(t, "Workflow processing has been postponed because too many workflows are already running", wf.Status.Message) - } + require.NotNil(t, wf) + assert.Equal(t, wfv1.WorkflowPending, wf.Status.Phase) + assert.Equal(t, "Workflow processing has been postponed because too many workflows are already running", wf.Status.Message) }) // process my-wf-1; update status to Pending assert.True(t, controller.processNextItem(ctx)) expectWorkflow(ctx, controller, "my-wf-1", func(wf *wfv1.Workflow) { - if assert.NotNil(t, wf) { - assert.Equal(t, wfv1.WorkflowPending, wf.Status.Phase) - assert.Equal(t, "Workflow processing has been postponed because too many workflows are already running", wf.Status.Message) - } + require.NotNil(t, wf) + assert.Equal(t, wfv1.WorkflowPending, wf.Status.Phase) + assert.Equal(t, "Workflow processing has been postponed because too many workflows are already running", wf.Status.Message) }) }) } @@ -1088,23 +1084,21 @@ status: assert.True(t, controller.processNextItem(ctx)) if !ns0PendingWfTested { expectNamespacedWorkflow(ctx, controller, "ns-0", "my-ns-0-wf-0", func(wf *wfv1.Workflow) { - if assert.NotNil(t, wf) { - if wf.Status.Phase != "" { - assert.Equal(t, wfv1.WorkflowPending, wf.Status.Phase) - assert.Equal(t, "Workflow processing has been postponed because too many workflows are already running", wf.Status.Message) - ns0PendingWfTested = true - } + require.NotNil(t, wf) + if wf.Status.Phase != "" { + assert.Equal(t, wfv1.WorkflowPending, wf.Status.Phase) + assert.Equal(t, "Workflow processing has been postponed because too many workflows are already running", wf.Status.Message) + ns0PendingWfTested = true } }) } if !ns1PendingWfTested { expectNamespacedWorkflow(ctx, controller, "ns-1", "my-ns-1-wf-0", func(wf *wfv1.Workflow) { - if assert.NotNil(t, wf) { - if wf.Status.Phase != "" { - assert.Equal(t, wfv1.WorkflowPending, wf.Status.Phase) - assert.Equal(t, "Workflow processing has been postponed because too many workflows are already running", wf.Status.Message) - ns1PendingWfTested = true - } + require.NotNil(t, wf) + if wf.Status.Phase != "" { + assert.Equal(t, wfv1.WorkflowPending, wf.Status.Phase) + assert.Equal(t, "Workflow processing has been postponed because too many workflows are already running", wf.Status.Message) + ns1PendingWfTested = true } }) } diff --git a/workflow/controller/dag_test.go b/workflow/controller/dag_test.go index da87e12d6e85..dee2ef413f64 100644 --- a/workflow/controller/dag_test.go +++ b/workflow/controller/dag_test.go @@ -3365,14 +3365,11 @@ func TestDAGReferTaskAggregatedOutputs(t *testing.T) { woc.operate(ctx) dagNode := woc.wf.Status.Nodes.FindByDisplayName("parameter-aggregation-dag-h8b82") - if assert.NotNil(t, dagNode) { - if assert.NotNil(t, dagNode.Outputs) { - if assert.Len(t, dagNode.Outputs.Parameters, 2) { - assert.Equal(t, `["1","2"]`, dagNode.Outputs.Parameters[0].Value.String()) - assert.Equal(t, `["odd","even"]`, dagNode.Outputs.Parameters[1].Value.String()) - } - } - } + require.NotNil(t, dagNode) + require.NotNil(t, dagNode.Outputs) + require.Len(t, dagNode.Outputs.Parameters, 2) + assert.Equal(t, `["1","2"]`, dagNode.Outputs.Parameters[0].Value.String()) + assert.Equal(t, `["odd","even"]`, dagNode.Outputs.Parameters[1].Value.String()) } var dagHttpChildrenAssigned = `apiVersion: argoproj.io/v1alpha1 @@ -3450,11 +3447,9 @@ func TestDagHttpChildrenAssigned(t *testing.T) { assert.NotNil(t, dagNode) dagNode = woc.wf.Status.Nodes.FindByDisplayName("good1") - if assert.NotNil(t, dagNode) { - if assert.Len(t, dagNode.Children, 1) { - assert.Equal(t, "http-template-nv52d-495103493", dagNode.Children[0]) - } - } + require.NotNil(t, dagNode) + require.Len(t, dagNode.Children, 1) + assert.Equal(t, "http-template-nv52d-495103493", dagNode.Children[0]) } var retryTypeDagTaskRunExitNodeAfterCompleted = ` diff --git a/workflow/controller/estimation/estimator_factory_test.go b/workflow/controller/estimation/estimator_factory_test.go index ffb64242ed38..9848a8e16494 100644 --- a/workflow/controller/estimation/estimator_factory_test.go +++ b/workflow/controller/estimation/estimator_factory_test.go @@ -60,57 +60,52 @@ metadata: t.Run("None", func(t *testing.T) { p, err := f.NewEstimator(&wfv1.Workflow{}) require.NoError(t, err) - if assert.NotNil(t, p) { - e := p.(*estimator) - assert.Nil(t, e.baselineWF) - } + require.NotNil(t, p) + e := p.(*estimator) + assert.Nil(t, e.baselineWF) }) t.Run("WorkflowTemplate", func(t *testing.T) { p, err := f.NewEstimator(&wfv1.Workflow{ ObjectMeta: metav1.ObjectMeta{Namespace: "my-ns", Labels: map[string]string{common.LabelKeyWorkflowTemplate: "my-wftmpl"}}, }) require.NoError(t, err) - if assert.NotNil(t, p) { - e := p.(*estimator) - if assert.NotNil(t, e) && assert.NotNil(t, e.baselineWF) { - assert.Equal(t, "my-wftmpl-baseline", e.baselineWF.Name) - } - } + require.NotNil(t, p) + e := p.(*estimator) + require.NotNil(t, e) + require.NotNil(t, e.baselineWF) + assert.Equal(t, "my-wftmpl-baseline", e.baselineWF.Name) }) t.Run("ClusterWorkflowTemplate", func(t *testing.T) { p, err := f.NewEstimator(&wfv1.Workflow{ ObjectMeta: metav1.ObjectMeta{Namespace: "my-ns", Labels: map[string]string{common.LabelKeyClusterWorkflowTemplate: "my-cwft"}}, }) require.NoError(t, err) - if assert.NotNil(t, p) { - e := p.(*estimator) - if assert.NotNil(t, e) && assert.NotNil(t, e.baselineWF) { - assert.Equal(t, "my-cwft-baseline", e.baselineWF.Name) - } - } + require.NotNil(t, p) + e := p.(*estimator) + require.NotNil(t, e) + require.NotNil(t, e.baselineWF) + assert.Equal(t, "my-cwft-baseline", e.baselineWF.Name) }) t.Run("CronWorkflowTemplate", func(t *testing.T) { p, err := f.NewEstimator(&wfv1.Workflow{ ObjectMeta: metav1.ObjectMeta{Namespace: "my-ns", Labels: map[string]string{common.LabelKeyCronWorkflow: "my-cwf"}}, }) require.NoError(t, err) - if assert.NotNil(t, p) { - e := p.(*estimator) - if assert.NotNil(t, e) && assert.NotNil(t, e.baselineWF) { - assert.Equal(t, "my-cwf-baseline", e.baselineWF.Name) - } - } + require.NotNil(t, p) + e := p.(*estimator) + require.NotNil(t, e) + require.NotNil(t, e.baselineWF) + assert.Equal(t, "my-cwf-baseline", e.baselineWF.Name) }) t.Run("WorkflowArchive", func(t *testing.T) { p, err := f.NewEstimator(&wfv1.Workflow{ ObjectMeta: metav1.ObjectMeta{Namespace: "my-ns", Labels: map[string]string{common.LabelKeyWorkflowTemplate: "my-archived-wftmpl"}}, }) require.NoError(t, err) - if assert.NotNil(t, p) { - e := p.(*estimator) - if assert.NotNil(t, e) && assert.NotNil(t, e.baselineWF) { - assert.Equal(t, "my-archived-wftmpl-baseline", e.baselineWF.Name) - } - } + require.NotNil(t, p) + e := p.(*estimator) + require.NotNil(t, e) + require.NotNil(t, e.baselineWF) + assert.Equal(t, "my-archived-wftmpl-baseline", e.baselineWF.Name) }) } diff --git a/workflow/controller/exit_handler_test.go b/workflow/controller/exit_handler_test.go index 7b37d759ce61..b55923c95d3d 100644 --- a/workflow/controller/exit_handler_test.go +++ b/workflow/controller/exit_handler_test.go @@ -1062,11 +1062,9 @@ spec: hookNode := woc.wf.Status.Nodes.FindByDisplayName(exitNodeName) - if assert.NotNil(t, hookNode) { - assert.NotNil(t, hookNode.Inputs) - if assert.Len(t, hookNode.Inputs.Parameters, 1) { - assert.NotNil(t, hookNode.Inputs.Parameters[0].Value) - assert.Equal(t, hookNode.Inputs.Parameters[0].Value.String(), string(apiv1.PodFailed)) - } - } + require.NotNil(t, hookNode) + assert.NotNil(t, hookNode.Inputs) + require.Len(t, hookNode.Inputs.Parameters, 1) + assert.NotNil(t, hookNode.Inputs.Parameters[0].Value) + assert.Equal(t, hookNode.Inputs.Parameters[0].Value.String(), string(apiv1.PodFailed)) } diff --git a/workflow/controller/informer/cluster_workflow_template_convert_test.go b/workflow/controller/informer/cluster_workflow_template_convert_test.go index fd72fce03d63..45e541ba043a 100644 --- a/workflow/controller/informer/cluster_workflow_template_convert_test.go +++ b/workflow/controller/informer/cluster_workflow_template_convert_test.go @@ -24,9 +24,8 @@ func Test_objectToClusterWorkflowTemplate(t *testing.T) { "spec": "ops", }}) require.EqualError(t, err, "malformed cluster workflow template \"my-name\": cannot restore struct from: string") - if assert.NotNil(t, v) { - assert.Equal(t, "my-name", v.Name) - } + require.NotNil(t, v) + assert.Equal(t, "my-name", v.Name) }) t.Run("ClusterWorkflowTemplate", func(t *testing.T) { v, err := objectToClusterWorkflowTemplate(&unstructured.Unstructured{}) diff --git a/workflow/controller/informer/workflow_template_convert_test.go b/workflow/controller/informer/workflow_template_convert_test.go index 48d65d0b4d94..341e89bc06ae 100644 --- a/workflow/controller/informer/workflow_template_convert_test.go +++ b/workflow/controller/informer/workflow_template_convert_test.go @@ -24,10 +24,9 @@ func Test_objectToWorkflowTemplate(t *testing.T) { "spec": "ops", }}) require.EqualError(t, err, "malformed workflow template \"my-ns/my-name\": cannot restore struct from: string") - if assert.NotNil(t, v) { - assert.Equal(t, "my-ns", v.Namespace) - assert.Equal(t, "my-name", v.Name) - } + require.NotNil(t, v) + assert.Equal(t, "my-ns", v.Namespace) + assert.Equal(t, "my-name", v.Name) }) t.Run("WorkflowTemplate", func(t *testing.T) { v, err := objectToWorkflowTemplate(&unstructured.Unstructured{}) diff --git a/workflow/controller/operator.go b/workflow/controller/operator.go index fb7955cbe099..2ca55ca833a3 100644 --- a/workflow/controller/operator.go +++ b/workflow/controller/operator.go @@ -234,10 +234,7 @@ func (woc *wfOperationCtx) operate(ctx context.Context) { woc.addArtifactGCFinalizer() // Reconciliation of Outputs (Artifacts). See ReportOutputs() of executor.go. - err = woc.taskResultReconciliation() - if err != nil { - woc.markWorkflowError(ctx, fmt.Errorf("failed to reconcile: %v", err)) - } + woc.taskResultReconciliation() // Do artifact GC if task result reconciliation is complete. if woc.wf.Status.Fulfilled() { @@ -999,7 +996,7 @@ func (woc *wfOperationCtx) processNodeRetries(node *wfv1.NodeStatus, retryStrate maxDurationDeadline := time.Time{} // Process max duration limit if retryStrategy.Backoff.MaxDuration != "" && len(childNodeIds) > 0 { - maxDuration, err := parseStringToDuration(retryStrategy.Backoff.MaxDuration) + maxDuration, err := wfv1.ParseStringToDuration(retryStrategy.Backoff.MaxDuration) if err != nil { return nil, false, err } @@ -1019,7 +1016,7 @@ func (woc *wfOperationCtx) processNodeRetries(node *wfv1.NodeStatus, retryStrate return nil, false, fmt.Errorf("no base duration specified for retryStrategy") } - baseDuration, err := parseStringToDuration(retryStrategy.Backoff.Duration) + baseDuration, err := wfv1.ParseStringToDuration(retryStrategy.Backoff.Duration) if err != nil { return nil, false, err } @@ -1227,7 +1224,7 @@ func (woc *wfOperationCtx) podReconciliation(ctx context.Context) (error, bool) } if recentlyStarted { - // If the pod was deleted, then we it is possible that the controller never get another informer message about it. + // If the pod was deleted, then it is possible that the controller never get another informer message about it. // In this case, the workflow will only be requeued after the resync period (20m). This means // workflow will not update for 20m. Requeuing here prevents that happening. woc.requeue() @@ -1317,19 +1314,6 @@ func (woc *wfOperationCtx) getAllWorkflowPods() ([]*apiv1.Pod, error) { return pods, nil } -func (woc *wfOperationCtx) getAllWorkflowPodsMap() (map[string]*apiv1.Pod, error) { - podList, err := woc.getAllWorkflowPods() - if err != nil { - return nil, err - } - podMap := make(map[string]*apiv1.Pod) - for _, pod := range podList { - nodeID := woc.nodeID(pod) - podMap[nodeID] = pod - } - return podMap, nil -} - func printPodSpecLog(pod *apiv1.Pod, wfName string) { podSpecByte, err := json.Marshal(pod) log := log.WithField("workflow", wfName). @@ -1496,14 +1480,34 @@ func (woc *wfOperationCtx) assessNodeStatus(ctx context.Context, pod *apiv1.Pod, } } + waitContainerCleanedUp := true // We cannot fail the node if the wait container is still running because it may be busy saving outputs, and these // would not get captured successfully. for _, c := range pod.Status.ContainerStatuses { - if c.Name == common.WaitContainerName && c.State.Running != nil && new.Phase.Completed() { - woc.log.WithField("new.phase", new.Phase).Info("leaving phase un-changed: wait container is not yet terminated ") - new.Phase = old.Phase + if c.Name == common.WaitContainerName { + waitContainerCleanedUp = false + switch { + case c.State.Running != nil && new.Phase.Completed(): + woc.log.WithField("new.phase", new.Phase).Info("leaving phase un-changed: wait container is not yet terminated ") + new.Phase = old.Phase + case c.State.Terminated != nil && c.State.Terminated.ExitCode != 0: + // Mark its taskResult as completed directly since wait container did not exit normally, + // and it will never have a chance to report taskResult correctly. + nodeID := woc.nodeID(pod) + woc.log.WithFields(log.Fields{"nodeID": nodeID, "exitCode": c.State.Terminated.ExitCode, "reason": c.State.Terminated.Reason}). + Warn("marking its taskResult as completed since wait container did not exit normally") + woc.wf.Status.MarkTaskResultComplete(nodeID) + } } } + if pod.Status.Phase == apiv1.PodFailed && pod.Status.Reason == "Evicted" && waitContainerCleanedUp { + // Mark its taskResult as completed directly since wait container has been cleaned up because of pod evicted, + // and it will never have a chance to report taskResult correctly. + nodeID := woc.nodeID(pod) + woc.log.WithFields(log.Fields{"nodeID": nodeID}). + Warn("marking its taskResult as completed since wait container has been cleaned up.") + woc.wf.Status.MarkTaskResultComplete(nodeID) + } // if we are transitioning from Pending to a different state, clear out unchanged message if old.Phase == wfv1.NodePending && new.Phase != wfv1.NodePending && old.Message == new.Message { @@ -3265,15 +3269,66 @@ func (woc *wfOperationCtx) processAggregateNodeOutputs(scope *wfScope, prefix st // Adding per-output aggregated value placeholders for outputName, valueList := range outputParamValueLists { key = fmt.Sprintf("%s.outputs.parameters.%s", prefix, outputName) - valueListJSON, err := json.Marshal(valueList) + valueListJson, err := aggregatedJsonValueList(valueList) if err != nil { return err } - scope.addParamToScope(key, string(valueListJSON)) + scope.addParamToScope(key, valueListJson) } return nil } +// tryJsonUnmarshal unmarshals each item in the list assuming it is +// JSON and NOT a plain JSON value. +// If returns success only if all items can be unmarshalled and are either +// maps or lists +func tryJsonUnmarshal(valueList []string) ([]interface{}, bool) { + success := true + var list []interface{} + for _, value := range valueList { + var unmarshalledValue interface{} + err := json.Unmarshal([]byte(value), &unmarshalledValue) + if err != nil { + success = false + break // Unmarshal failed, fall back to strings + } + switch unmarshalledValue.(type) { + case []interface{}: + case map[string]interface{}: + // Keep these types + default: + // Drop anything else + success = false + } + if !success { + break + } + list = append(list, unmarshalledValue) + } + return list, success +} + +// aggregatedJsonValueList returns a string containing a JSON list, holding +// all of the values from the valueList. +// It tries to understand what's wanted from inner JSON using tryJsonUnmarshall +func aggregatedJsonValueList(valueList []string) (string, error) { + unmarshalledList, success := tryJsonUnmarshal(valueList) + var valueListJSON []byte + var err error + if success { + valueListJSON, err = json.Marshal(unmarshalledList) + if err != nil { + return "", err + } + } else { + valueListJSON, err = json.Marshal(valueList) + if err != nil { + return "", err + } + } + return string(valueListJSON), nil +} + // addParamToGlobalScope exports any desired node outputs to the global scope, and adds it to the global outputs. func (woc *wfOperationCtx) addParamToGlobalScope(param wfv1.Parameter) { if param.GlobalName == "" { @@ -3421,7 +3476,7 @@ func (woc *wfOperationCtx) executeSuspend(nodeName string, templateScope string, if err != nil { return nil, err } - suspendDuration, err := parseStringToDuration(tmpl.Suspend.Duration) + suspendDuration, err := wfv1.ParseStringToDuration(tmpl.Suspend.Duration) if err != nil { return node, err } @@ -3494,19 +3549,6 @@ func addRawOutputFields(node *wfv1.NodeStatus, tmpl *wfv1.Template) *wfv1.NodeSt return node } -func parseStringToDuration(durationString string) (time.Duration, error) { - var suspendDuration time.Duration - // If no units are attached, treat as seconds - if val, err := strconv.Atoi(durationString); err == nil { - suspendDuration = time.Duration(val) * time.Second - } else if duration, err := time.ParseDuration(durationString); err == nil { - suspendDuration = duration - } else { - return 0, fmt.Errorf("unable to parse %s as a duration: %w", durationString, err) - } - return suspendDuration, nil -} - func processItem(tmpl template.Template, name string, index int, item wfv1.Item, obj interface{}, whenCondition string) (string, error) { replaceMap := make(map[string]string) var newName string diff --git a/workflow/controller/operator_aggregation_test.go b/workflow/controller/operator_aggregation_test.go new file mode 100644 index 000000000000..b966c448a6d3 --- /dev/null +++ b/workflow/controller/operator_aggregation_test.go @@ -0,0 +1,64 @@ +package controller + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTryJsonUnmarshal(t *testing.T) { + for _, testcase := range []struct { + input []string + success bool + expected []interface{} + }{ + {[]string{"1"}, false, nil}, + {[]string{"1", "2"}, false, nil}, + {[]string{"foo"}, false, nil}, + {[]string{"foo", "bar"}, false, nil}, + {[]string{`["1"]`, "2"}, false, nil}, // Fails on second element + {[]string{`{"foo":"1"}`, "2"}, false, nil}, // Fails on second element + {[]string{`["1"]`, `["2"]`}, true, []interface{}{[]interface{}{"1"}, []interface{}{"2"}}}, + {[]string{`["1"]`, `["2"]`}, true, []interface{}{[]interface{}{"1"}, []interface{}{"2"}}}, + {[]string{"\n[\"1\"] \n", "\t[\"2\"]\t"}, true, []interface{}{[]interface{}{"1"}, []interface{}{"2"}}}, + {[]string{`{"number":"1"}`, `{"number":"2"}`}, true, []interface{}{map[string]interface{}{"number": "1"}, map[string]interface{}{"number": "2"}}}, + {[]string{`[{"foo":"apple", "bar":"pear"}]`, `{"foo":"banana"}`}, true, []interface{}{[]interface{}{map[string]interface{}{"bar": "pear", "foo": "apple"}}, map[string]interface{}{"foo": "banana"}}}, + } { + t.Run(fmt.Sprintf("Unmarshal %v", testcase.input), + func(t *testing.T) { + list, success := tryJsonUnmarshal(testcase.input) + require.Equal(t, testcase.success, success) + if success { + assert.Equal(t, testcase.expected, list) + } + }) + } +} + +func TestAggregatedJsonValueList(t *testing.T) { + for _, testcase := range []struct { + input []string + expected string + }{ + {[]string{"1"}, `["1"]`}, + {[]string{"1", "2"}, `["1","2"]`}, + {[]string{"foo"}, `["foo"]`}, + {[]string{"foo", "bar"}, `["foo","bar"]`}, + {[]string{`["1"]`, "2"}, `["[\"1\"]","2"]`}, // This is expected, but not really useful + {[]string{`{"foo":"1"}`, "2"}, `["{\"foo\":\"1\"}","2"]`}, // This is expected, but not really useful + {[]string{`["1"]`, `["2"]`}, `[["1"],["2"]]`}, + {[]string{` ["1"]`, `["2"] `}, `[["1"],["2"]]`}, + {[]string{"\n[\"1\"] \n", "\t[\"2\"]\t"}, `[["1"],["2"]]`}, + {[]string{`{"number":"1"}`, `{"number":"2"}`}, `[{"number":"1"},{"number":"2"}]`}, + {[]string{`[{"foo":"apple", "bar":"pear"}]`}, `[[{"bar":"pear","foo":"apple"}]]`}, // Sorted map keys here may make this a fragile test, can be dropped + } { + t.Run(fmt.Sprintf("Aggregate %v", testcase.input), + func(t *testing.T) { + result, err := aggregatedJsonValueList(testcase.input) + require.NoError(t, err) + assert.Equal(t, testcase.expected, result) + }) + } +} diff --git a/workflow/controller/operator_data_test.go b/workflow/controller/operator_data_test.go index 6ae315daac48..019a65704543 100644 --- a/workflow/controller/operator_data_test.go +++ b/workflow/controller/operator_data_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" wfv1 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" ) @@ -236,7 +237,6 @@ func TestDataTemplateCreatesPod(t *testing.T) { woc.operate(ctx) node := woc.wf.Status.Nodes.FindByDisplayName("collect-artifact") - if assert.NotNil(t, node) { - assert.Equal(t, wfv1.NodePending, node.Phase) - } + require.NotNil(t, node) + assert.Equal(t, wfv1.NodePending, node.Phase) } diff --git a/workflow/controller/operator_template_scope_test.go b/workflow/controller/operator_template_scope_test.go index 340582d53713..65088519b1fa 100644 --- a/workflow/controller/operator_template_scope_test.go +++ b/workflow/controller/operator_template_scope_test.go @@ -87,40 +87,34 @@ func TestTemplateScope(t *testing.T) { wf = woc.wf node := findNodeByName(wf.Status.Nodes, "test-template-scope[0].step") - if assert.NotNil(t, node, "Node %s not found", "test-templte-scope") { - assert.Equal(t, wfv1.NodeTypeSteps, node.Type) - assert.Equal(t, "local/test-template-scope", node.TemplateScope) - } + require.NotNil(t, node, "Node %s not found", "test-templte-scope") + assert.Equal(t, wfv1.NodeTypeSteps, node.Type) + assert.Equal(t, "local/test-template-scope", node.TemplateScope) node = findNodeByName(wf.Status.Nodes, "test-template-scope[0].step[0]") - if assert.NotNil(t, node, "Node %s not found", "test-templte-scope[0]") { - assert.Equal(t, wfv1.NodeTypeStepGroup, node.Type) - assert.Equal(t, "namespaced/test-template-scope-1", node.TemplateScope) - } + require.NotNil(t, node, "Node %s not found", "test-templte-scope[0]") + assert.Equal(t, wfv1.NodeTypeStepGroup, node.Type) + assert.Equal(t, "namespaced/test-template-scope-1", node.TemplateScope) node = findNodeByName(wf.Status.Nodes, "test-template-scope[0].step[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, "namespaced/test-template-scope-1", node.TemplateScope) - } + require.NotNil(t, node, "Node %s not found", "test-templte-scope[0].hello") + assert.Equal(t, wfv1.NodeTypePod, node.Type) + assert.Equal(t, "namespaced/test-template-scope-1", node.TemplateScope) node = findNodeByName(wf.Status.Nodes, "test-template-scope[0].step[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, "namespaced/test-template-scope-1", node.TemplateScope) - } + require.NotNil(t, node, "Node %s not found", "test-template-scope[0].other-wftmpl") + assert.Equal(t, wfv1.NodeTypeSteps, node.Type) + assert.Equal(t, "namespaced/test-template-scope-1", node.TemplateScope) node = findNodeByName(wf.Status.Nodes, "test-template-scope[0].step[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, "namespaced/test-template-scope-2", node.TemplateScope) - } + require.NotNil(t, node, "Node %s not found", "test-template-scope[0].other-wftmpl[0]") + assert.Equal(t, wfv1.NodeTypeStepGroup, node.Type) + assert.Equal(t, "namespaced/test-template-scope-2", node.TemplateScope) node = findNodeByName(wf.Status.Nodes, "test-template-scope[0].step[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, "namespaced/test-template-scope-2", node.TemplateScope) - } + require.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, "namespaced/test-template-scope-2", node.TemplateScope) } var testTemplateScopeWithParamWorkflowYaml = ` @@ -183,34 +177,29 @@ func TestTemplateScopeWithParam(t *testing.T) { require.NoError(t, err) node := findNodeByName(wf.Status.Nodes, "test-template-scope-with-param[0].step") - if assert.NotNil(t, node, "Node %s not found", "test-template-scope-with-param") { - assert.Equal(t, wfv1.NodeTypeSteps, node.Type) - assert.Equal(t, "local/test-template-scope-with-param", node.TemplateScope) - } + require.NotNil(t, node, "Node %s not found", "test-template-scope-with-param") + assert.Equal(t, wfv1.NodeTypeSteps, node.Type) + assert.Equal(t, "local/test-template-scope-with-param", node.TemplateScope) node = findNodeByName(wf.Status.Nodes, "test-template-scope-with-param[0].step[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, "namespaced/test-template-scope-with-param-1", node.TemplateScope) - } + require.NotNil(t, node, "Node %s not found", "test-template-scope-with-param[0]") + assert.Equal(t, wfv1.NodeTypeStepGroup, node.Type) + assert.Equal(t, "namespaced/test-template-scope-with-param-1", node.TemplateScope) node = findNodeByName(wf.Status.Nodes, "test-template-scope-with-param[0].step[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, "namespaced/test-template-scope-with-param-1", node.TemplateScope) - } + require.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, "namespaced/test-template-scope-with-param-1", node.TemplateScope) node = findNodeByName(wf.Status.Nodes, "test-template-scope-with-param[0].step[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, "namespaced/test-template-scope-with-param-1", node.TemplateScope) - } + require.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, "namespaced/test-template-scope-with-param-1", node.TemplateScope) node = findNodeByName(wf.Status.Nodes, "test-template-scope-with-param[0].step[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, "namespaced/test-template-scope-with-param-1", node.TemplateScope) - } + require.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, "namespaced/test-template-scope-with-param-1", node.TemplateScope) } var testTemplateScopeNestedStepsWithParamsWorkflowYaml = ` @@ -277,46 +266,39 @@ func TestTemplateScopeNestedStepsWithParams(t *testing.T) { require.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, "local/test-template-scope-nested-steps-with-params", node.TemplateScope) - } + require.NotNil(t, node, "Node %s not found", "test-template-scope-with-param") + assert.Equal(t, wfv1.NodeTypeSteps, node.Type) + assert.Equal(t, "local/test-template-scope-nested-steps-with-params", node.TemplateScope) node = findNodeByName(wf.Status.Nodes, "test-template-scope-nested-steps-with-params[0].step[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, "namespaced/test-template-scope-nested-steps-with-params-1", node.TemplateScope) - } + require.NotNil(t, node, "Node %s not found", "test-template-scope-with-param[0]") + assert.Equal(t, wfv1.NodeTypeStepGroup, node.Type) + assert.Equal(t, "namespaced/test-template-scope-nested-steps-with-params-1", node.TemplateScope) node = findNodeByName(wf.Status.Nodes, "test-template-scope-nested-steps-with-params[0].step[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, "namespaced/test-template-scope-nested-steps-with-params-1", node.TemplateScope) - } + require.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, "namespaced/test-template-scope-nested-steps-with-params-1", node.TemplateScope) node = findNodeByName(wf.Status.Nodes, "test-template-scope-nested-steps-with-params[0].step[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, "namespaced/test-template-scope-nested-steps-with-params-1", node.TemplateScope) - } + require.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, "namespaced/test-template-scope-nested-steps-with-params-1", node.TemplateScope) node = findNodeByName(wf.Status.Nodes, "test-template-scope-nested-steps-with-params[0].step[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, "namespaced/test-template-scope-nested-steps-with-params-1", node.TemplateScope) - } + require.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, "namespaced/test-template-scope-nested-steps-with-params-1", node.TemplateScope) node = findNodeByName(wf.Status.Nodes, "test-template-scope-nested-steps-with-params[0].step[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, "namespaced/test-template-scope-nested-steps-with-params-1", node.TemplateScope) - } + require.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, "namespaced/test-template-scope-nested-steps-with-params-1", node.TemplateScope) node = findNodeByName(wf.Status.Nodes, "test-template-scope-nested-steps-with-params[0].step[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, "namespaced/test-template-scope-nested-steps-with-params-1", node.TemplateScope) - } + require.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, "namespaced/test-template-scope-nested-steps-with-params-1", node.TemplateScope) } var testTemplateScopeDAGWorkflowYaml = ` @@ -386,40 +368,34 @@ func TestTemplateScopeDAG(t *testing.T) { require.NoError(t, err) node := findNodeByName(wf.Status.Nodes, "test-template-scope-dag[0].step") - if assert.NotNil(t, node, "Node %s not found", "test-template-scope-dag") { - assert.Equal(t, wfv1.NodeTypeDAG, node.Type) - assert.Equal(t, "local/test-template-scope-dag", node.TemplateScope) - } + require.NotNil(t, node, "Node %s not found", "test-template-scope-dag") + assert.Equal(t, wfv1.NodeTypeDAG, node.Type) + assert.Equal(t, "local/test-template-scope-dag", node.TemplateScope) node = findNodeByName(wf.Status.Nodes, "test-template-scope-dag[0].step.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, "namespaced/test-template-scope-dag-1", node.TemplateScope) - } + require.NotNil(t, node, "Node %s not found", "test-template-scope-dag.A") + assert.Equal(t, wfv1.NodeTypePod, node.Type) + assert.Equal(t, "namespaced/test-template-scope-dag-1", node.TemplateScope) node = findNodeByName(wf.Status.Nodes, "test-template-scope-dag[0].step.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, "namespaced/test-template-scope-dag-1", node.TemplateScope) - } + require.NotNil(t, node, "Node %s not found", "test-template-scope-dag.B") + assert.Equal(t, wfv1.NodeTypeTaskGroup, node.Type) + assert.Equal(t, "namespaced/test-template-scope-dag-1", node.TemplateScope) node = findNodeByName(wf.Status.Nodes, "test-template-scope-dag[0].step.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, "namespaced/test-template-scope-dag-1", node.TemplateScope) - } + require.NotNil(t, node, "Node %s not found", "test-template-scope-dag.B(0:x") + assert.Equal(t, wfv1.NodeTypePod, node.Type) + assert.Equal(t, "namespaced/test-template-scope-dag-1", node.TemplateScope) node = findNodeByName(wf.Status.Nodes, "test-template-scope-dag[0].step.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, "namespaced/test-template-scope-dag-1", node.TemplateScope) - } + require.NotNil(t, node, "Node %s not found", "test-template-scope-dag.B(0:x") + assert.Equal(t, wfv1.NodeTypePod, node.Type) + assert.Equal(t, "namespaced/test-template-scope-dag-1", node.TemplateScope) node = findNodeByName(wf.Status.Nodes, "test-template-scope-dag[0].step.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, "namespaced/test-template-scope-dag-1", node.TemplateScope) - } + require.NotNil(t, node, "Node %s not found", "test-template-scope-dag.B(0:x") + assert.Equal(t, wfv1.NodeTypePod, node.Type) + assert.Equal(t, "namespaced/test-template-scope-dag-1", node.TemplateScope) } func findNodeByName(nodes map[string]wfv1.NodeStatus, name string) *wfv1.NodeStatus { @@ -488,38 +464,32 @@ func TestTemplateClusterScope(t *testing.T) { require.NoError(t, err) node := findNodeByName(wf.Status.Nodes, "test-template-scope[0].step") - if assert.NotNil(t, node, "Node %s not found", "test-templte-scope") { - assert.Equal(t, wfv1.NodeTypeSteps, node.Type) - assert.Equal(t, "local/test-template-scope", node.TemplateScope) - } + require.NotNil(t, node, "Node %s not found", "test-templte-scope") + assert.Equal(t, wfv1.NodeTypeSteps, node.Type) + assert.Equal(t, "local/test-template-scope", node.TemplateScope) node = findNodeByName(wf.Status.Nodes, "test-template-scope[0].step[0]") - if assert.NotNil(t, node, "Node %s not found", "test-templte-scope[0]") { - assert.Equal(t, wfv1.NodeTypeStepGroup, node.Type) - assert.Equal(t, "cluster/test-template-scope-1", node.TemplateScope) - } + require.NotNil(t, node, "Node %s not found", "test-templte-scope[0]") + assert.Equal(t, wfv1.NodeTypeStepGroup, node.Type) + assert.Equal(t, "cluster/test-template-scope-1", node.TemplateScope) node = findNodeByName(wf.Status.Nodes, "test-template-scope[0].step[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, "cluster/test-template-scope-1", node.TemplateScope) - } + require.NotNil(t, node, "Node %s not found", "test-templte-scope[0].hello") + assert.Equal(t, wfv1.NodeTypePod, node.Type) + assert.Equal(t, "cluster/test-template-scope-1", node.TemplateScope) node = findNodeByName(wf.Status.Nodes, "test-template-scope[0].step[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, "cluster/test-template-scope-1", node.TemplateScope) - } + require.NotNil(t, node, "Node %s not found", "test-template-scope[0].other-wftmpl") + assert.Equal(t, wfv1.NodeTypeSteps, node.Type) + assert.Equal(t, "cluster/test-template-scope-1", node.TemplateScope) node = findNodeByName(wf.Status.Nodes, "test-template-scope[0].step[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, "namespaced/test-template-scope-2", node.TemplateScope) - } + require.NotNil(t, node, "Node %s not found", "test-template-scope[0].other-wftmpl[0]") + assert.Equal(t, wfv1.NodeTypeStepGroup, node.Type) + assert.Equal(t, "namespaced/test-template-scope-2", node.TemplateScope) node = findNodeByName(wf.Status.Nodes, "test-template-scope[0].step[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, "namespaced/test-template-scope-2", node.TemplateScope) - } + require.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, "namespaced/test-template-scope-2", node.TemplateScope) } diff --git a/workflow/controller/operator_test.go b/workflow/controller/operator_test.go index 2788b594ab6e..7ac9e7809112 100644 --- a/workflow/controller/operator_test.go +++ b/workflow/controller/operator_test.go @@ -81,13 +81,11 @@ func Test_wfOperationCtx_reapplyUpdate(t *testing.T) { // now force a re-apply update updatedWf, err := woc.reapplyUpdate(ctx, controller.wfclientset.ArgoprojV1alpha1().Workflows(""), nodes) require.NoError(t, err) - if assert.NotNil(t, updatedWf) { - assert.True(t, woc.controller.hydrator.IsHydrated(updatedWf)) - if assert.Contains(t, updatedWf.Status.Nodes, "foo") { - assert.Equal(t, "my-foo", updatedWf.Status.Nodes["foo"].Name) - assert.Equal(t, wfv1.NodeSucceeded, updatedWf.Status.Nodes["foo"].Phase, "phase is merged") - } - } + require.NotNil(t, updatedWf) + assert.True(t, woc.controller.hydrator.IsHydrated(updatedWf)) + require.Contains(t, updatedWf.Status.Nodes, "foo") + assert.Equal(t, "my-foo", updatedWf.Status.Nodes["foo"].Name) + assert.Equal(t, wfv1.NodeSucceeded, updatedWf.Status.Nodes["foo"].Phase, "phase is merged") }) t.Run("ErrUpdatingCompletedWorkflow", func(t *testing.T) { wf := &wfv1.Workflow{ @@ -369,9 +367,8 @@ func TestGlobalParams(t *testing.T) { ctx := context.Background() woc := newWorkflowOperationCtx(wf, controller) woc.operate(ctx) - if assert.Contains(t, woc.globalParams, "workflow.creationTimestamp") { - assert.NotContains(t, woc.globalParams["workflow.creationTimestamp"], "UTC") - } + require.Contains(t, woc.globalParams, "workflow.creationTimestamp") + assert.NotContains(t, woc.globalParams["workflow.creationTimestamp"], "UTC") for char := range strftime.FormatChars { assert.Contains(t, woc.globalParams, fmt.Sprintf("%s.%s", "workflow.creationTimestamp", string(char))) } @@ -1987,13 +1984,12 @@ func TestWorkflowStepRetry(t *testing.T) { woc.operate(ctx) pods, err = listPods(woc) require.NoError(t, err) - if assert.Len(t, pods.Items, 3) { - assert.Equal(t, "cowsay success", pods.Items[0].Spec.Containers[1].Args[0]) - assert.Equal(t, "cowsay failure", pods.Items[1].Spec.Containers[1].Args[0]) + require.Len(t, pods.Items, 3) + assert.Equal(t, "cowsay success", pods.Items[0].Spec.Containers[1].Args[0]) + assert.Equal(t, "cowsay failure", pods.Items[1].Spec.Containers[1].Args[0]) - // verify that after the cowsay failure pod failed, we are retrying cowsay success - assert.Equal(t, "cowsay success", pods.Items[2].Spec.Containers[1].Args[0]) - } + // verify that after the cowsay failure pod failed, we are retrying cowsay success + assert.Equal(t, "cowsay success", pods.Items[2].Spec.Containers[1].Args[0]) } var workflowParallelismLimit = ` @@ -2277,10 +2273,10 @@ func TestSidecarResourceLimits(t *testing.T) { break } } - if assert.NotNil(t, waitCtr) && assert.NotNil(t, waitCtr.Resources) { - assert.Len(t, waitCtr.Resources.Limits, 2) - assert.Len(t, waitCtr.Resources.Requests, 2) - } + require.NotNil(t, waitCtr) + require.NotNil(t, waitCtr.Resources) + assert.Len(t, waitCtr.Resources.Limits, 2) + assert.Len(t, waitCtr.Resources.Requests, 2) } // TestSuspendResume tests the suspend and resume feature @@ -3690,16 +3686,15 @@ func TestResourceWithOwnerReferenceTemplate(t *testing.T) { require.NoError(t, err) objectMetas[cm.Name] = cm.ObjectMeta } - if assert.Len(t, objectMetas["resource-cm-1"].OwnerReferences, 1) { - assert.Equal(t, "manual-ref-name", objectMetas["resource-cm-1"].OwnerReferences[0].Name) - } - if assert.Len(t, objectMetas["resource-cm-2"].OwnerReferences, 1) { - assert.Equal(t, "resource-with-ownerreference-template", objectMetas["resource-cm-2"].OwnerReferences[0].Name) - } - if assert.Len(t, objectMetas["resource-cm-3"].OwnerReferences, 2) { - assert.Equal(t, "manual-ref-name", objectMetas["resource-cm-3"].OwnerReferences[0].Name) - assert.Equal(t, "resource-with-ownerreference-template", objectMetas["resource-cm-3"].OwnerReferences[1].Name) - } + require.Len(t, objectMetas["resource-cm-1"].OwnerReferences, 1) + assert.Equal(t, "manual-ref-name", objectMetas["resource-cm-1"].OwnerReferences[0].Name) + + require.Len(t, objectMetas["resource-cm-2"].OwnerReferences, 1) + assert.Equal(t, "resource-with-ownerreference-template", objectMetas["resource-cm-2"].OwnerReferences[0].Name) + + require.Len(t, objectMetas["resource-cm-3"].OwnerReferences, 2) + assert.Equal(t, "manual-ref-name", objectMetas["resource-cm-3"].OwnerReferences[0].Name) + assert.Equal(t, "resource-with-ownerreference-template", objectMetas["resource-cm-3"].OwnerReferences[1].Name) } var stepScriptTmpl = ` @@ -4832,11 +4827,12 @@ func TestNestedStepGroupGlobalParams(t *testing.T) { woc.operate(ctx) node := woc.wf.Status.Nodes.FindByDisplayName("generate") - if assert.NotNil(t, node) && assert.NotNil(t, node.Outputs) && assert.Len(t, node.Outputs.Parameters, 1) { - assert.Equal(t, "hello-param", node.Outputs.Parameters[0].Name) - assert.Equal(t, "global-param", node.Outputs.Parameters[0].GlobalName) - assert.Equal(t, "hello world", node.Outputs.Parameters[0].Value.String()) - } + require.NotNil(t, node) + require.NotNil(t, node.Outputs) + require.Len(t, node.Outputs.Parameters, 1) + assert.Equal(t, "hello-param", node.Outputs.Parameters[0].Name) + assert.Equal(t, "global-param", node.Outputs.Parameters[0].GlobalName) + assert.Equal(t, "hello world", node.Outputs.Parameters[0].Value.String()) assert.Equal(t, "hello world", woc.wf.Status.Outputs.Parameters[0].Value.String()) assert.Equal(t, "global-param", woc.wf.Status.Outputs.Parameters[0].Name) @@ -5612,13 +5608,12 @@ func TestConfigMapCacheLoadOperate(t *testing.T) { woc := newWorkflowOperationCtx(wf, controller) woc.operate(ctx) - if assert.Len(t, woc.wf.Status.Nodes, 1) { - for _, node := range woc.wf.Status.Nodes { - assert.NotNil(t, node.Outputs) - assert.Equal(t, "hello", node.Outputs.Parameters[0].Name) - assert.Equal(t, "foobar", node.Outputs.Parameters[0].Value.String()) - assert.Equal(t, wfv1.NodeSucceeded, node.Phase) - } + require.Len(t, woc.wf.Status.Nodes, 1) + for _, node := range woc.wf.Status.Nodes { + assert.NotNil(t, node.Outputs) + assert.Equal(t, "hello", node.Outputs.Parameters[0].Name) + assert.Equal(t, "foobar", node.Outputs.Parameters[0].Value.String()) + assert.Equal(t, wfv1.NodeSucceeded, node.Phase) } } @@ -5685,11 +5680,10 @@ func TestConfigMapCacheLoadOperateNoOutputs(t *testing.T) { woc := newWorkflowOperationCtx(wf, controller) woc.operate(ctx) - if assert.Len(t, woc.wf.Status.Nodes, 1) { - for _, node := range woc.wf.Status.Nodes { - assert.Nil(t, node.Outputs) - assert.Equal(t, wfv1.NodeSucceeded, node.Phase) - } + require.Len(t, woc.wf.Status.Nodes, 1) + for _, node := range woc.wf.Status.Nodes { + assert.Nil(t, node.Outputs) + assert.Equal(t, wfv1.NodeSucceeded, node.Phase) } } @@ -5931,13 +5925,12 @@ func TestConfigMapCacheLoadOperateMaxAge(t *testing.T) { woc := newWorkflowOperationCtx(wf, controller) woc.operate(ctx) - if assert.Len(t, woc.wf.Status.Nodes, 1) { - for _, node := range woc.wf.Status.Nodes { - assert.NotNil(t, node.Outputs) - assert.Equal(t, "hello", node.Outputs.Parameters[0].Name) - assert.Equal(t, "foobar", node.Outputs.Parameters[0].Value.String()) - assert.Equal(t, wfv1.NodeSucceeded, node.Phase) - } + require.Len(t, woc.wf.Status.Nodes, 1) + for _, node := range woc.wf.Status.Nodes { + assert.NotNil(t, node.Outputs) + assert.Equal(t, "hello", node.Outputs.Parameters[0].Name) + assert.Equal(t, "foobar", node.Outputs.Parameters[0].Value.String()) + assert.Equal(t, wfv1.NodeSucceeded, node.Phase) } cancel() @@ -5951,11 +5944,10 @@ func TestConfigMapCacheLoadOperateMaxAge(t *testing.T) { woc = newWorkflowOperationCtx(wf, controller) woc.operate(ctx) - if assert.Len(t, woc.wf.Status.Nodes, 1) { - for _, node := range woc.wf.Status.Nodes { - assert.Nil(t, node.Outputs) - assert.Equal(t, wfv1.NodePending, node.Phase) - } + require.Len(t, woc.wf.Status.Nodes, 1) + for _, node := range woc.wf.Status.Nodes { + assert.Nil(t, node.Outputs) + assert.Equal(t, wfv1.NodePending, node.Phase) } } @@ -6199,11 +6191,10 @@ func TestConfigMapCacheLoadNoLabels(t *testing.T) { assert.NotPanics(t, fn) assert.Equal(t, wfv1.WorkflowError, woc.wf.Status.Phase) - if assert.Len(t, woc.wf.Status.Nodes, 1) { - for _, node := range woc.wf.Status.Nodes { - assert.Nil(t, node.Outputs) - assert.Equal(t, wfv1.NodeError, node.Phase) - } + require.Len(t, woc.wf.Status.Nodes, 1) + for _, node := range woc.wf.Status.Nodes { + assert.Nil(t, node.Outputs) + assert.Equal(t, wfv1.NodeError, node.Phase) } } @@ -6240,12 +6231,11 @@ func TestConfigMapCacheLoadNilOutputs(t *testing.T) { } assert.NotPanics(t, fn) - if assert.Len(t, woc.wf.Status.Nodes, 1) { - for _, node := range woc.wf.Status.Nodes { - assert.NotNil(t, node.Outputs) - assert.False(t, node.Outputs.HasOutputs()) - assert.Equal(t, wfv1.NodeSucceeded, node.Phase) - } + require.Len(t, woc.wf.Status.Nodes, 1) + for _, node := range woc.wf.Status.Nodes { + assert.NotNil(t, node.Outputs) + assert.False(t, node.Outputs.HasOutputs()) + assert.Equal(t, wfv1.NodeSucceeded, node.Phase) } } @@ -6277,9 +6267,8 @@ func TestConfigMapCacheSaveOperate(t *testing.T) { var entry cache.Entry wfv1.MustUnmarshal(rawEntry, &entry) - if assert.NotNil(t, entry.Outputs) { - assert.Equal(t, sampleOutputs, *entry.Outputs) - } + require.NotNil(t, entry.Outputs) + assert.Equal(t, sampleOutputs, *entry.Outputs) } var propagate = ` @@ -6705,9 +6694,10 @@ func TestGlobalVarsOnExit(t *testing.T) { woc.operate(ctx) node := woc.wf.Status.Nodes["hello-world-6gphm-8n22g-3224262006"] - if assert.NotNil(t, node) && assert.NotNil(t, node.Inputs) && assert.NotEmpty(t, node.Inputs.Parameters) { - assert.Equal(t, "nononono", node.Inputs.Parameters[0].Value.String()) - } + require.NotNil(t, node) + require.NotNil(t, node.Inputs) + require.NotEmpty(t, node.Inputs.Parameters) + assert.Equal(t, "nononono", node.Inputs.Parameters[0].Value.String()) } var deadlineWf = ` @@ -7291,13 +7281,12 @@ func TestWFWithRetryAndWithParam(t *testing.T) { pods, err := listPods(woc) require.NoError(t, err) assert.NotEmpty(t, pods.Items) - if assert.Len(t, pods.Items, 3) { - ctrs := pods.Items[0].Spec.Containers - assert.Len(t, ctrs, 2) - envs := ctrs[1].Env - assert.Len(t, envs, 8) - assert.Equal(t, apiv1.EnvVar{Name: "ARGO_INCLUDE_SCRIPT_OUTPUT", Value: "true"}, envs[3]) - } + require.Len(t, pods.Items, 3) + ctrs := pods.Items[0].Spec.Containers + assert.Len(t, ctrs, 2) + envs := ctrs[1].Env + assert.Len(t, envs, 8) + assert.Equal(t, apiv1.EnvVar{Name: "ARGO_INCLUDE_SCRIPT_OUTPUT", Value: "true"}, envs[3]) }) } @@ -7492,18 +7481,14 @@ func TestParamAggregation(t *testing.T) { woc.operate(ctx) evenNode := woc.wf.Status.Nodes.FindByDisplayName("print-evenness") - if assert.NotNil(t, evenNode) { - if assert.Len(t, evenNode.Inputs.Parameters, 1) { - assert.Equal(t, `["odd","even"]`, evenNode.Inputs.Parameters[0].Value.String()) - } - } + require.NotNil(t, evenNode) + require.Len(t, evenNode.Inputs.Parameters, 1) + assert.Equal(t, `["odd","even"]`, evenNode.Inputs.Parameters[0].Value.String()) numNode := woc.wf.Status.Nodes.FindByDisplayName("print-nums") - if assert.NotNil(t, numNode) { - if assert.Len(t, numNode.Inputs.Parameters, 1) { - assert.Equal(t, `["1","2"]`, numNode.Inputs.Parameters[0].Value.String()) - } - } + require.NotNil(t, numNode) + require.Len(t, numNode.Inputs.Parameters, 1) + assert.Equal(t, `["1","2"]`, numNode.Inputs.Parameters[0].Value.String()) } func TestPodHasContainerNeedingTermination(t *testing.T) { @@ -7700,10 +7685,9 @@ func TestRetryOnNodeAntiAffinity(t *testing.T) { woc.operate(ctx) node := woc.wf.Status.Nodes.FindByDisplayName("retry-fail(0)") - if assert.NotNil(t, node) { - assert.Equal(t, wfv1.NodeFailed, node.Phase) - assert.Equal(t, "node0", node.HostNodeName) - } + require.NotNil(t, node) + assert.Equal(t, wfv1.NodeFailed, node.Phase) + assert.Equal(t, "node0", node.HostNodeName) pods, err = listPods(woc) require.NoError(t, err) @@ -7733,10 +7717,9 @@ func TestRetryOnNodeAntiAffinity(t *testing.T) { woc.operate(ctx) node1 := woc.wf.Status.Nodes.FindByDisplayName("retry-fail(1)") - if assert.NotNil(t, node) { - assert.Equal(t, wfv1.NodeFailed, node1.Phase) - assert.Equal(t, "node1", node1.HostNodeName) - } + require.NotNil(t, node) + assert.Equal(t, wfv1.NodeFailed, node1.Phase) + assert.Equal(t, "node1", node1.HostNodeName) pods, err = listPods(woc) require.NoError(t, err) @@ -7784,10 +7767,9 @@ func TestNoPodsWhenShutdown(t *testing.T) { woc.operate(ctx) node := woc.wf.Status.Nodes.FindByDisplayName("hello-world") - if assert.NotNil(t, node) { - assert.Equal(t, wfv1.NodeFailed, node.Phase) - assert.Contains(t, node.Message, "workflow shutdown with strategy: Stop") - } + require.NotNil(t, node) + assert.Equal(t, wfv1.NodeFailed, node.Phase) + assert.Contains(t, node.Message, "workflow shutdown with strategy: Stop") } var wfscheVariable = ` @@ -7934,10 +7916,9 @@ spec: woc1.operate(ctx) node := woc1.wf.Status.Nodes.FindByDisplayName("whalesay") - if assert.NotNil(t, node) { - assert.Contains(t, node.Message, "workflow shutdown with strategy") - assert.Contains(t, node.Message, "Stop") - } + require.NotNil(t, node) + assert.Contains(t, node.Message, "workflow shutdown with strategy") + assert.Contains(t, node.Message, "Stop") }) t.Run("TerminateStrategy", func(t *testing.T) { @@ -7956,10 +7937,9 @@ spec: woc1 := newWorkflowOperationCtx(wfOut, controller) woc1.operate(ctx) for _, node := range woc1.wf.Status.Nodes { - if assert.NotNil(t, node) { - assert.Contains(t, node.Message, "workflow shutdown with strategy") - assert.Contains(t, node.Message, "Terminate") - } + require.NotNil(t, node) + assert.Contains(t, node.Message, "workflow shutdown with strategy") + assert.Contains(t, node.Message, "Terminate") } }) } @@ -8201,13 +8181,12 @@ func TestStepsFailFast(t *testing.T) { assert.Equal(t, wfv1.WorkflowFailed, woc.wf.Status.Phase) node := woc.wf.Status.Nodes.FindByDisplayName("iteration(0:a)") - if assert.NotNil(t, node) { - assert.Equal(t, wfv1.NodeFailed, node.Phase) - } + require.NotNil(t, node) + assert.Equal(t, wfv1.NodeFailed, node.Phase) + node = woc.wf.Status.Nodes.FindByDisplayName("seq-loop-pz4hh") - if assert.NotNil(t, node) { - assert.Equal(t, wfv1.NodeFailed, node.Phase) - } + require.NotNil(t, node) + assert.Equal(t, wfv1.NodeFailed, node.Phase) } func TestGetStepOrDAGTaskName(t *testing.T) { @@ -10527,10 +10506,9 @@ func TestMaxDepth(t *testing.T) { assert.Equal(t, wfv1.WorkflowError, woc.wf.Status.Phase) node := woc.wf.Status.Nodes["hello-world-713168755"] - if assert.NotNil(t, node) { - assert.Equal(t, wfv1.NodeError, node.Phase) - assert.Contains(t, node.Message, "Maximum recursion depth exceeded") - } + require.NotNil(t, node) + assert.Equal(t, wfv1.NodeError, node.Phase) + assert.Contains(t, node.Message, "Maximum recursion depth exceeded") // Max depth is enabled, but not too small, no error expected controller.maxStackDepth = 3 @@ -10540,9 +10518,8 @@ func TestMaxDepth(t *testing.T) { assert.Equal(t, wfv1.WorkflowRunning, woc.wf.Status.Phase) node = woc.wf.Status.Nodes["hello-world-713168755"] - if assert.NotNil(t, node) { - assert.Equal(t, wfv1.NodePending, node.Phase) - } + require.NotNil(t, node) + assert.Equal(t, wfv1.NodePending, node.Phase) makePodsPhase(ctx, woc, apiv1.PodSucceeded) woc.operate(ctx) @@ -10568,9 +10545,8 @@ func TestMaxDepthEnvVariable(t *testing.T) { assert.Equal(t, wfv1.WorkflowRunning, woc.wf.Status.Phase) node := woc.wf.Status.Nodes["hello-world-713168755"] - if assert.NotNil(t, node) { - assert.Equal(t, wfv1.NodePending, node.Phase) - } + require.NotNil(t, node) + assert.Equal(t, wfv1.NodePending, node.Phase) makePodsPhase(ctx, woc, apiv1.PodSucceeded) woc.operate(ctx) @@ -10902,10 +10878,9 @@ func TestWorkflowNeedReconcile(t *testing.T) { woc.operate(ctx) pods, err = listPods(woc) require.NoError(t, err) - if assert.Len(t, pods.Items, 2) { - assert.Equal(t, "hello1", pods.Items[0].Spec.Containers[1].Env[0].Value) - assert.Equal(t, "steps-need-reconcile", pods.Items[1].Spec.Containers[1].Env[0].Value) - } + require.Len(t, pods.Items, 2) + assert.Equal(t, "hello1", pods.Items[0].Spec.Containers[1].Env[0].Value) + assert.Equal(t, "steps-need-reconcile", pods.Items[1].Spec.Containers[1].Env[0].Value) } func TestWorkflowRunningButLabelCompleted(t *testing.T) { diff --git a/workflow/controller/operator_workflow_template_ref_test.go b/workflow/controller/operator_workflow_template_ref_test.go index bff5e18720bc..51e9fbf96596 100644 --- a/workflow/controller/operator_workflow_template_ref_test.go +++ b/workflow/controller/operator_workflow_template_ref_test.go @@ -341,10 +341,9 @@ func TestWorkflowTemplateRefWithShutdownAndSuspend(t *testing.T) { assert.NotEmpty(t, woc1.wf.Status.StoredWorkflowSpec.Shutdown) assert.Equal(t, wfv1.ShutdownStrategyTerminate, woc1.wf.Status.StoredWorkflowSpec.Shutdown) for _, node := range woc1.wf.Status.Nodes { - if assert.NotNil(t, node) { - assert.Contains(t, node.Message, "workflow shutdown with strategy") - assert.Contains(t, node.Message, "Terminate") - } + require.NotNil(t, node) + assert.Contains(t, node.Message, "workflow shutdown with strategy") + assert.Contains(t, node.Message, "Terminate") } }) t.Run("WorkflowTemplateRefWithShutdownStop", func(t *testing.T) { @@ -364,10 +363,9 @@ func TestWorkflowTemplateRefWithShutdownAndSuspend(t *testing.T) { assert.NotEmpty(t, woc1.wf.Status.StoredWorkflowSpec.Shutdown) assert.Equal(t, wfv1.ShutdownStrategyStop, woc1.wf.Status.StoredWorkflowSpec.Shutdown) for _, node := range woc1.wf.Status.Nodes { - if assert.NotNil(t, node) { - assert.Contains(t, node.Message, "workflow shutdown with strategy") - assert.Contains(t, node.Message, "Stop") - } + require.NotNil(t, node) + assert.Contains(t, node.Message, "workflow shutdown with strategy") + assert.Contains(t, node.Message, "Stop") } }) } diff --git a/workflow/controller/pod_cleanup.go b/workflow/controller/pod_cleanup.go index 55823c70d654..12787ae77d22 100644 --- a/workflow/controller/pod_cleanup.go +++ b/workflow/controller/pod_cleanup.go @@ -13,8 +13,11 @@ import ( func (woc *wfOperationCtx) queuePodsForCleanup() { delay := woc.controller.Config.GetPodGCDeleteDelayDuration() podGC := woc.execWf.Spec.PodGC - if podGC != nil && podGC.DeleteDelayDuration != nil { - delay = podGC.DeleteDelayDuration.Duration + podGCDelay, err := podGC.GetDeleteDelayDuration() + if err != nil { + woc.log.WithError(err).Warn("failed to parse podGC.deleteDelayDuration") + } else if podGCDelay >= 0 { + delay = podGCDelay } strategy := podGC.GetStrategy() selector, _ := podGC.GetLabelSelector() diff --git a/workflow/controller/scope.go b/workflow/controller/scope.go index 30eb60a54230..f709708dce98 100644 --- a/workflow/controller/scope.go +++ b/workflow/controller/scope.go @@ -144,7 +144,11 @@ func (s *wfScope) resolveArtifact(art *wfv1.Artifact) (*wfv1.Artifact, error) { return copyArt, errors.New(errors.CodeBadRequest, "failed to unmarshal artifact subpath for templating") } - return copyArt, copyArt.AppendToKey(resolvedSubPath) + err = copyArt.AppendToKey(resolvedSubPath) + if err != nil && copyArt.Optional { //Ignore error when artifact optional + return copyArt, nil + } + return copyArt, err } return &valArt, nil diff --git a/workflow/controller/steps_test.go b/workflow/controller/steps_test.go index c03c42e24e78..dae2fa623f2f 100644 --- a/workflow/controller/steps_test.go +++ b/workflow/controller/steps_test.go @@ -315,3 +315,133 @@ func TestOptionalArgumentAndParameter(t *testing.T) { woc.operate(ctx) assert.Equal(t, wfv1.WorkflowRunning, woc.wf.Status.Phase) } + +var artifactResolutionWhenOptionalAndSubpath = ` +apiVersion: argoproj.io/v1alpha1 +kind: Workflow +metadata: + name: artifact-passing-subpath-rx7f4 +spec: + entrypoint: artifact-example + templates: + - name: artifact-example + steps: + - - name: hello-world-to-file + template: hello-world-to-file + - - name: hello-world-to-file2 + template: hello-world-to-file2 + arguments: + artifacts: + - name: bar + from: "{{steps.hello-world-to-file.outputs.artifacts.foo}}" + optional: true + subpath: bar.txt + withParam: "[0, 1]" + + - name: hello-world-to-file + container: + image: busybox:latest + imagePullPolicy: IfNotPresent + command: [sh, -c] + args: ["sleep 1; echo hello world"] + outputs: + artifacts: + - name: foo + path: /tmp/foo + optional: true + archive: + none: {} + + - name: hello-world-to-file2 + inputs: + artifacts: + - name: bar + path: /tmp/bar.txt + optional: true + archive: + none: {} + container: + image: busybox:latest + imagePullPolicy: IfNotPresent + command: [sh, -c] + args: ["sleep 1; echo hello world"] +status: + nodes: + artifact-passing-subpath-rx7f4: + children: + - artifact-passing-subpath-rx7f4-1763046061 + displayName: artifact-passing-subpath-rx7f4 + id: artifact-passing-subpath-rx7f4 + name: artifact-passing-subpath-rx7f4 + phase: Running + progress: 1/1 + resourcesDuration: + cpu: 0 + memory: 5 + startedAt: "2024-09-06T04:53:32Z" + templateName: artifact-example + templateScope: local/artifact-passing-subpath-rx7f4 + type: Steps + artifact-passing-subpath-rx7f4-511855021: + boundaryID: artifact-passing-subpath-rx7f4 + children: + - artifact-passing-subpath-rx7f4-1696082680 + displayName: hello-world-to-file + finishedAt: "2024-09-06T04:53:39Z" + id: artifact-passing-subpath-rx7f4-511855021 + name: artifact-passing-subpath-rx7f4[0].hello-world-to-file + outputs: + artifacts: + - archive: + none: {} + name: foo + optional: true + path: /tmp/foo + - name: main-logs + s3: + key: artifact-passing-subpath-rx7f4/artifact-passing-subpath-rx7f4-hello-world-to-file-511855021/main.log + exitCode: "0" + phase: Succeeded + progress: 1/1 + resourcesDuration: + cpu: 0 + memory: 5 + startedAt: "2024-09-06T04:53:32Z" + templateName: hello-world-to-file + templateScope: local/artifact-passing-subpath-rx7f4 + type: Pod + artifact-passing-subpath-rx7f4-1763046061: + boundaryID: artifact-passing-subpath-rx7f4 + children: + - artifact-passing-subpath-rx7f4-511855021 + displayName: '[0]' + finishedAt: "2024-09-06T04:53:41Z" + id: artifact-passing-subpath-rx7f4-1763046061 + name: artifact-passing-subpath-rx7f4[0] + nodeFlag: {} + phase: Succeeded + progress: 1/1 + resourcesDuration: + cpu: 0 + memory: 5 + startedAt: "2024-09-06T04:53:32Z" + templateScope: local/artifact-passing-subpath-rx7f4 + type: StepGroup + phase: Running + taskResultsCompletionStatus: + artifact-passing-subpath-rx7f4-511855021: true` + +func TestOptionalArgumentUseSubPathInLoop(t *testing.T) { + cancel, controller := newController() + defer cancel() + wfcset := controller.wfclientset.ArgoprojV1alpha1().Workflows("") + + ctx := context.Background() + wf := wfv1.MustUnmarshalWorkflow(artifactResolutionWhenOptionalAndSubpath) + wf, err := wfcset.Create(ctx, wf, metav1.CreateOptions{}) + require.NoError(t, err) + woc := newWorkflowOperationCtx(wf, controller) + + woc.operate(ctx) + assert.Equal(t, wfv1.WorkflowRunning, woc.wf.Status.Phase) +} diff --git a/workflow/controller/taskresult.go b/workflow/controller/taskresult.go index ef08d33386ed..8118084dd360 100644 --- a/workflow/controller/taskresult.go +++ b/workflow/controller/taskresult.go @@ -54,19 +54,14 @@ func (wfc *WorkflowController) newWorkflowTaskResultInformer() cache.SharedIndex return informer } -func podAbsentTimeout(node *wfv1.NodeStatus) bool { - return time.Since(node.StartedAt.Time) <= envutil.LookupEnvDurationOr("POD_ABSENT_TIMEOUT", 2*time.Minute) +func recentlyDeleted(node *wfv1.NodeStatus) bool { + return time.Since(node.FinishedAt.Time) <= envutil.LookupEnvDurationOr("RECENTLY_DELETED_POD_DURATION", 10*time.Second) } -func (woc *wfOperationCtx) taskResultReconciliation() error { - +func (woc *wfOperationCtx) taskResultReconciliation() { objs, _ := woc.controller.taskResultInformer.GetIndexer().ByIndex(indexes.WorkflowIndex, woc.wf.Namespace+"/"+woc.wf.Name) woc.log.WithField("numObjs", len(objs)).Info("Task-result reconciliation") - podMap, err := woc.getAllWorkflowPodsMap() - if err != nil { - return err - } for _, obj := range objs { result := obj.(*wfv1.WorkflowTaskResult) resultName := result.GetName() @@ -75,7 +70,6 @@ func (woc *wfOperationCtx) taskResultReconciliation() error { woc.log.Debugf("task result name:\n%+v", resultName) label := result.Labels[common.LabelKeyReportOutputsCompleted] - // If the task result is completed, set the state to true. if label == "true" { woc.log.Debugf("Marking task result complete %s", resultName) @@ -85,33 +79,25 @@ func (woc *wfOperationCtx) taskResultReconciliation() error { woc.wf.Status.MarkTaskResultIncomplete(resultName) } - _, foundPod := podMap[result.Name] - node, err := woc.wf.Status.Nodes.Get(result.Name) + nodeID := result.Name + old, err := woc.wf.Status.Nodes.Get(nodeID) if err != nil { - if foundPod { - // how does this path make any sense? - // pod created but informer not yet updated - woc.log.Errorf("couldn't obtain node for %s, but found pod, this is not expected, doing nothing", result.Name) - } continue } - - if !foundPod && !node.Completed() { - if podAbsentTimeout(node) { - woc.log.Infof("Determined controller should timeout for %s", result.Name) - woc.wf.Status.MarkTaskResultComplete(resultName) - - woc.markNodePhase(node.Name, wfv1.NodeFailed, "pod was absent") + // Mark task result as completed if it has no chance to be completed. + if label == "false" && old.IsPodDeleted() { + if recentlyDeleted(old) { + woc.log.WithField("nodeID", nodeID).Debug("Wait for marking task result as completed because pod is recently deleted.") + // If the pod was deleted, then it is possible that the controller never get another informer message about it. + // In this case, the workflow will only be requeued after the resync period (20m). This means + // workflow will not update for 20m. Requeuing here prevents that happening. + woc.requeue() + continue } else { - woc.log.Debugf("Determined controller shouldn't timeout %s", result.Name) + woc.log.WithField("nodeID", nodeID).Info("Marking task result as completed because pod has been deleted for a while.") + woc.wf.Status.MarkTaskResultComplete(nodeID) } } - - nodeID := result.Name - old, err := woc.wf.Status.Nodes.Get(nodeID) - if err != nil { - continue - } newNode := old.DeepCopy() if result.Outputs.HasOutputs() { if newNode.Outputs == nil { @@ -133,5 +119,4 @@ func (woc *wfOperationCtx) taskResultReconciliation() error { woc.updated = true } } - return nil } diff --git a/workflow/controller/workflowpod_test.go b/workflow/controller/workflowpod_test.go index 3680bff28247..99ceefab4f0e 100644 --- a/workflow/controller/workflowpod_test.go +++ b/workflow/controller/workflowpod_test.go @@ -730,34 +730,30 @@ func TestVolumeAndVolumeMounts(t *testing.T) { require.NoError(t, err) assert.Len(t, pods.Items, 1) pod := pods.Items[0] - if assert.Len(t, pod.Spec.Volumes, 3) { - assert.Equal(t, "var-run-argo", pod.Spec.Volumes[0].Name) - assert.Equal(t, "tmp-dir-argo", pod.Spec.Volumes[1].Name) - assert.Equal(t, "volume-name", pod.Spec.Volumes[2].Name) - } - if assert.Len(t, pod.Spec.InitContainers, 1) { - init := pod.Spec.InitContainers[0] - if assert.Len(t, init.VolumeMounts, 1) { - assert.Equal(t, "var-run-argo", init.VolumeMounts[0].Name) - } - } + require.Len(t, pod.Spec.Volumes, 3) + assert.Equal(t, "var-run-argo", pod.Spec.Volumes[0].Name) + assert.Equal(t, "tmp-dir-argo", pod.Spec.Volumes[1].Name) + assert.Equal(t, "volume-name", pod.Spec.Volumes[2].Name) + + require.Len(t, pod.Spec.InitContainers, 1) + init := pod.Spec.InitContainers[0] + require.Len(t, init.VolumeMounts, 1) + assert.Equal(t, "var-run-argo", init.VolumeMounts[0].Name) + containers := pod.Spec.Containers - if assert.Len(t, containers, 2) { - wait := containers[0] - if assert.Len(t, wait.VolumeMounts, 3) { - assert.Equal(t, "volume-name", wait.VolumeMounts[0].Name) - assert.Equal(t, "tmp-dir-argo", wait.VolumeMounts[1].Name) - assert.Equal(t, "var-run-argo", wait.VolumeMounts[2].Name) - } - main := containers[1] - assert.Equal(t, []string{"/var/run/argo/argoexec", "emissary", - "--loglevel", getExecutorLogLevel(), "--log-format", woc.controller.cliExecutorLogFormat, - "--", "cowsay"}, main.Command) - if assert.Len(t, main.VolumeMounts, 2) { - assert.Equal(t, "volume-name", main.VolumeMounts[0].Name) - assert.Equal(t, "var-run-argo", main.VolumeMounts[1].Name) - } - } + require.Len(t, containers, 2) + wait := containers[0] + require.Len(t, wait.VolumeMounts, 3) + assert.Equal(t, "volume-name", wait.VolumeMounts[0].Name) + assert.Equal(t, "tmp-dir-argo", wait.VolumeMounts[1].Name) + assert.Equal(t, "var-run-argo", wait.VolumeMounts[2].Name) + main := containers[1] + assert.Equal(t, []string{"/var/run/argo/argoexec", "emissary", + "--loglevel", getExecutorLogLevel(), "--log-format", woc.controller.cliExecutorLogFormat, + "--", "cowsay"}, main.Command) + require.Len(t, main.VolumeMounts, 2) + assert.Equal(t, "volume-name", main.VolumeMounts[0].Name) + assert.Equal(t, "var-run-argo", main.VolumeMounts[1].Name) }) } @@ -1478,9 +1474,8 @@ func TestMainContainerCustomization(t *testing.T) { require.NoError(t, err) ctr := pod.Spec.Containers[1] assert.NotNil(t, ctr.SecurityContext) - if assert.NotNil(t, pod.Spec.Containers[1].Resources) { - assert.Equal(t, "0.200", pod.Spec.Containers[1].Resources.Limits.Cpu().AsDec().String()) - } + require.NotNil(t, pod.Spec.Containers[1].Resources) + assert.Equal(t, "0.200", pod.Spec.Containers[1].Resources.Limits.Cpu().AsDec().String()) }) // Workflow spec's main container takes precedence over config in controller diff --git a/workflow/creator/creator_test.go b/workflow/creator/creator_test.go index 0871eb36cc56..05e1ce5fbba0 100644 --- a/workflow/creator/creator_test.go +++ b/workflow/creator/creator_test.go @@ -7,6 +7,7 @@ import ( "github.com/go-jose/go-jose/v3/jwt" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" wfv1 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" @@ -24,34 +25,30 @@ func TestLabel(t *testing.T) { t.Run("NotEmpty", func(t *testing.T) { wf := &wfv1.Workflow{} Label(context.WithValue(context.TODO(), auth.ClaimsKey, &types.Claims{Claims: jwt.Claims{Subject: strings.Repeat("x", 63) + "y"}, Email: "my@email", PreferredUsername: "username"}), wf) - if assert.NotEmpty(t, wf.Labels) { - assert.Equal(t, strings.Repeat("x", 62)+"y", wf.Labels[common.LabelKeyCreator], "creator is truncated") - assert.Equal(t, "my.at.email", wf.Labels[common.LabelKeyCreatorEmail], "'@' is replaced by '.at.'") - assert.Equal(t, "username", wf.Labels[common.LabelKeyCreatorPreferredUsername], "username is matching") - } + require.NotEmpty(t, wf.Labels) + assert.Equal(t, strings.Repeat("x", 62)+"y", wf.Labels[common.LabelKeyCreator], "creator is truncated") + assert.Equal(t, "my.at.email", wf.Labels[common.LabelKeyCreatorEmail], "'@' is replaced by '.at.'") + assert.Equal(t, "username", wf.Labels[common.LabelKeyCreatorPreferredUsername], "username is matching") }) t.Run("TooLongHyphen", func(t *testing.T) { wf := &wfv1.Workflow{} Label(context.WithValue(context.TODO(), auth.ClaimsKey, &types.Claims{Claims: jwt.Claims{Subject: strings.Repeat("-", 63) + "y"}}), wf) - if assert.NotEmpty(t, wf.Labels) { - assert.Equal(t, "y", wf.Labels[common.LabelKeyCreator]) - } + require.NotEmpty(t, wf.Labels) + assert.Equal(t, "y", wf.Labels[common.LabelKeyCreator]) }) t.Run("InvalidDNSNames", func(t *testing.T) { wf := &wfv1.Workflow{} Label(context.WithValue(context.TODO(), auth.ClaimsKey, &types.Claims{Claims: jwt.Claims{Subject: "!@#$%^&*()--__" + strings.Repeat("y", 35) + "__--!@#$%^&*()"}, PreferredUsername: "us#er@name#"}), wf) - if assert.NotEmpty(t, wf.Labels) { - assert.Equal(t, strings.Repeat("y", 35), wf.Labels[common.LabelKeyCreator]) - assert.Equal(t, "us-er-name", wf.Labels[common.LabelKeyCreatorPreferredUsername], "username is truncated") - } + require.NotEmpty(t, wf.Labels) + assert.Equal(t, strings.Repeat("y", 35), wf.Labels[common.LabelKeyCreator]) + assert.Equal(t, "us-er-name", wf.Labels[common.LabelKeyCreatorPreferredUsername], "username is truncated") }) t.Run("InvalidDNSNamesWithMidDashes", func(t *testing.T) { wf := &wfv1.Workflow{} sub := strings.Repeat("x", 20) + strings.Repeat("-", 70) + strings.Repeat("x", 20) Label(context.WithValue(context.TODO(), auth.ClaimsKey, &types.Claims{Claims: jwt.Claims{Subject: sub}}), wf) - if assert.NotEmpty(t, wf.Labels) { - assert.Equal(t, strings.Repeat("x", 20), wf.Labels[common.LabelKeyCreator]) - } + require.NotEmpty(t, wf.Labels) + assert.Equal(t, strings.Repeat("x", 20), wf.Labels[common.LabelKeyCreator]) }) t.Run("DifferentUsersFromCreatorLabels", func(t *testing.T) { type input struct { diff --git a/workflow/cron/operator_test.go b/workflow/cron/operator_test.go index eb10bce8b55c..0c1aa82312bd 100644 --- a/workflow/cron/operator_test.go +++ b/workflow/cron/operator_test.go @@ -13,6 +13,7 @@ import ( "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" "github.com/argoproj/argo-workflows/v3/pkg/client/clientset/versioned/fake" + "github.com/argoproj/argo-workflows/v3/util/telemetry" "github.com/argoproj/argo-workflows/v3/workflow/common" "github.com/argoproj/argo-workflows/v3/workflow/metrics" "github.com/argoproj/argo-workflows/v3/workflow/util" @@ -181,7 +182,7 @@ func TestCronWorkflowConditionSubmissionError(t *testing.T) { v1alpha1.MustUnmarshal([]byte(invalidWf), &cronWf) cs := fake.NewSimpleClientset() - testMetrics, err := metrics.New(context.Background(), metrics.TestScopeName, &metrics.Config{}, metrics.Callbacks{}) + testMetrics, err := metrics.New(context.Background(), telemetry.TestScopeName, telemetry.TestScopeName, &telemetry.Config{}, metrics.Callbacks{}) require.NoError(t, err) woc := &cronWfOperationCtx{ wfClientset: cs, @@ -237,7 +238,7 @@ func TestSpecError(t *testing.T) { cs := fake.NewSimpleClientset() ctx := context.Background() - testMetrics, err := metrics.New(ctx, metrics.TestScopeName, &metrics.Config{}, metrics.Callbacks{}) + testMetrics, err := metrics.New(ctx, telemetry.TestScopeName, telemetry.TestScopeName, &telemetry.Config{}, metrics.Callbacks{}) require.NoError(t, err) woc := &cronWfOperationCtx{ wfClientset: cs, @@ -262,7 +263,7 @@ func TestScheduleTimeParam(t *testing.T) { v1alpha1.MustUnmarshal([]byte(scheduledWf), &cronWf) cs := fake.NewSimpleClientset() - testMetrics, _ := metrics.New(context.Background(), metrics.TestScopeName, &metrics.Config{}, metrics.Callbacks{}) + testMetrics, _ := metrics.New(context.Background(), telemetry.TestScopeName, telemetry.TestScopeName, &telemetry.Config{}, metrics.Callbacks{}) woc := &cronWfOperationCtx{ wfClientset: cs, wfClient: cs.ArgoprojV1alpha1().Workflows(""), @@ -312,7 +313,7 @@ func TestLastUsedSchedule(t *testing.T) { v1alpha1.MustUnmarshal([]byte(lastUsedSchedule), &cronWf) cs := fake.NewSimpleClientset() - testMetrics, err := metrics.New(context.Background(), metrics.TestScopeName, &metrics.Config{}, metrics.Callbacks{}) + testMetrics, err := metrics.New(context.Background(), telemetry.TestScopeName, telemetry.TestScopeName, &telemetry.Config{}, metrics.Callbacks{}) require.NoError(t, err) woc := &cronWfOperationCtx{ wfClientset: cs, @@ -330,9 +331,8 @@ func TestLastUsedSchedule(t *testing.T) { woc.cronWf.SetSchedule(woc.cronWf.Spec.GetScheduleString()) - if assert.NotNil(t, woc.cronWf.Annotations) { - assert.Equal(t, woc.cronWf.Spec.GetScheduleString(), woc.cronWf.GetLatestSchedule()) - } + require.NotNil(t, woc.cronWf.Annotations) + assert.Equal(t, woc.cronWf.Spec.GetScheduleString(), woc.cronWf.GetLatestSchedule()) } var forbidMissedSchedule = `apiVersion: argoproj.io/v1alpha1 @@ -441,7 +441,7 @@ func TestMultipleSchedules(t *testing.T) { v1alpha1.MustUnmarshal([]byte(multipleSchedulesWf), &cronWf) cs := fake.NewSimpleClientset() - testMetrics, err := metrics.New(context.Background(), metrics.TestScopeName, &metrics.Config{}, metrics.Callbacks{}) + testMetrics, err := metrics.New(context.Background(), telemetry.TestScopeName, telemetry.TestScopeName, &telemetry.Config{}, metrics.Callbacks{}) require.NoError(t, err) woc := &cronWfOperationCtx{ wfClientset: cs, @@ -504,7 +504,7 @@ func TestSpecErrorWithScheduleAndSchedules(t *testing.T) { cs := fake.NewSimpleClientset() ctx := context.Background() - testMetrics, err := metrics.New(ctx, metrics.TestScopeName, &metrics.Config{}, metrics.Callbacks{}) + testMetrics, err := metrics.New(ctx, telemetry.TestScopeName, telemetry.TestScopeName, &telemetry.Config{}, metrics.Callbacks{}) require.NoError(t, err) woc := &cronWfOperationCtx{ wfClientset: cs, @@ -565,7 +565,7 @@ func TestSpecErrorWithValidAndInvalidSchedules(t *testing.T) { cs := fake.NewSimpleClientset() ctx := context.Background() - testMetrics, err := metrics.New(ctx, metrics.TestScopeName, &metrics.Config{}, metrics.Callbacks{}) + testMetrics, err := metrics.New(ctx, telemetry.TestScopeName, telemetry.TestScopeName, &telemetry.Config{}, metrics.Callbacks{}) require.NoError(t, err) woc := &cronWfOperationCtx{ wfClientset: cs, diff --git a/workflow/executor/executor.go b/workflow/executor/executor.go index 716f8fa8f30e..4f0b52875ac9 100644 --- a/workflow/executor/executor.go +++ b/workflow/executor/executor.go @@ -267,11 +267,13 @@ func (we *WorkflowExecutor) LoadArtifacts(ctx context.Context) error { func (we *WorkflowExecutor) StageFiles() error { var filePath string var body []byte + mode := os.FileMode(0o644) switch we.Template.GetType() { case wfv1.TemplateTypeScript: log.Infof("Loading script source to %s", common.ExecutorScriptSourcePath) filePath = common.ExecutorScriptSourcePath body = []byte(we.Template.Script.Source) + mode = os.FileMode(0o755) case wfv1.TemplateTypeResource: if we.Template.Resource.ManifestFrom != nil && we.Template.Resource.ManifestFrom.Artifact != nil { log.Infof("manifest %s already staged", we.Template.Resource.ManifestFrom.Artifact.Name) @@ -283,7 +285,7 @@ func (we *WorkflowExecutor) StageFiles() error { default: return nil } - err := os.WriteFile(filePath, body, 0o644) + err := os.WriteFile(filePath, body, mode) if err != nil { return argoerrs.InternalWrapError(err) } diff --git a/workflow/gccontroller/gc_controller_test.go b/workflow/gccontroller/gc_controller_test.go index 5ac263ecba85..e1e77cdd4f09 100644 --- a/workflow/gccontroller/gc_controller_test.go +++ b/workflow/gccontroller/gc_controller_test.go @@ -15,6 +15,7 @@ import ( wfv1 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" fakewfclientset "github.com/argoproj/argo-workflows/v3/pkg/client/clientset/versioned/fake" + "github.com/argoproj/argo-workflows/v3/util/telemetry" "github.com/argoproj/argo-workflows/v3/workflow/metrics" "github.com/argoproj/argo-workflows/v3/workflow/util" ) @@ -345,7 +346,7 @@ func newTTLController(t *testing.T) *Controller { clock := testingclock.NewFakeClock(time.Now()) wfclientset := fakewfclientset.NewSimpleClientset() wfInformer := cache.NewSharedIndexInformer(nil, nil, 0, nil) - gcMetrics, err := metrics.New(context.Background(), metrics.TestScopeName, &metrics.Config{}, metrics.Callbacks{}) + gcMetrics, err := metrics.New(context.Background(), telemetry.TestScopeName, telemetry.TestScopeName, &telemetry.Config{}, metrics.Callbacks{}) require.NoError(t, err) return &Controller{ wfclientset: wfclientset, diff --git a/workflow/metrics/counter_cronworkflow_trigger.go b/workflow/metrics/counter_cronworkflow_trigger.go index 2f1950e4331e..f77c488b6b36 100644 --- a/workflow/metrics/counter_cronworkflow_trigger.go +++ b/workflow/metrics/counter_cronworkflow_trigger.go @@ -2,6 +2,8 @@ package metrics import ( "context" + + "github.com/argoproj/argo-workflows/v3/util/telemetry" ) const ( @@ -9,17 +11,17 @@ const ( ) func addCronWfTriggerCounter(_ context.Context, m *Metrics) error { - return m.createInstrument(int64Counter, + return m.CreateInstrument(telemetry.Int64Counter, nameCronTriggered, "Total number of cron workflows triggered", "{cronworkflow}", - withAsBuiltIn(), + telemetry.WithAsBuiltIn(), ) } func (m *Metrics) CronWfTrigger(ctx context.Context, name, namespace string) { - m.addInt(ctx, nameCronTriggered, 1, instAttribs{ - {name: labelCronWFName, value: name}, - {name: labelWorkflowNamespace, value: namespace}, + m.AddInt(ctx, nameCronTriggered, 1, telemetry.InstAttribs{ + {Name: telemetry.AttribCronWFName, Value: name}, + {Name: telemetry.AttribWorkflowNamespace, Value: namespace}, }) } diff --git a/workflow/metrics/counter_error.go b/workflow/metrics/counter_error.go index f53a14f44c13..f71f7e6d8315 100644 --- a/workflow/metrics/counter_error.go +++ b/workflow/metrics/counter_error.go @@ -2,6 +2,8 @@ package metrics import ( "context" + + "github.com/argoproj/argo-workflows/v3/util/telemetry" ) type ErrorCause string @@ -14,30 +16,30 @@ const ( ) func addErrorCounter(ctx context.Context, m *Metrics) error { - err := m.createInstrument(int64Counter, + err := m.CreateInstrument(telemetry.Int64Counter, nameErrorCount, "Number of errors encountered by the controller by cause", "{error}", - withAsBuiltIn(), + telemetry.WithAsBuiltIn(), ) if err != nil { return err } // Initialise all values to zero for _, cause := range []ErrorCause{ErrorCauseOperationPanic, ErrorCauseCronWorkflowSubmissionError, ErrorCauseCronWorkflowSpecError} { - m.addInt(ctx, nameErrorCount, 0, instAttribs{{name: labelErrorCause, value: string(cause)}}) + m.AddInt(ctx, nameErrorCount, 0, telemetry.InstAttribs{{Name: telemetry.AttribErrorCause, Value: string(cause)}}) } return nil } func (m *Metrics) OperationPanic(ctx context.Context) { - m.addInt(ctx, nameErrorCount, 1, instAttribs{{name: labelErrorCause, value: string(ErrorCauseOperationPanic)}}) + m.AddInt(ctx, nameErrorCount, 1, telemetry.InstAttribs{{Name: telemetry.AttribErrorCause, Value: string(ErrorCauseOperationPanic)}}) } func (m *Metrics) CronWorkflowSubmissionError(ctx context.Context) { - m.addInt(ctx, nameErrorCount, 1, instAttribs{{name: labelErrorCause, value: string(ErrorCauseCronWorkflowSubmissionError)}}) + m.AddInt(ctx, nameErrorCount, 1, telemetry.InstAttribs{{Name: telemetry.AttribErrorCause, Value: string(ErrorCauseCronWorkflowSubmissionError)}}) } func (m *Metrics) CronWorkflowSpecError(ctx context.Context) { - m.addInt(ctx, nameErrorCount, 1, instAttribs{{name: labelErrorCause, value: string(ErrorCauseCronWorkflowSpecError)}}) + m.AddInt(ctx, nameErrorCount, 1, telemetry.InstAttribs{{Name: telemetry.AttribErrorCause, Value: string(ErrorCauseCronWorkflowSpecError)}}) } diff --git a/workflow/metrics/counter_log.go b/workflow/metrics/counter_log.go index 96ed960943ab..b9cea55952ab 100644 --- a/workflow/metrics/counter_log.go +++ b/workflow/metrics/counter_log.go @@ -3,28 +3,30 @@ package metrics import ( "context" + "github.com/argoproj/argo-workflows/v3/util/telemetry" + log "github.com/sirupsen/logrus" ) type logMetric struct { - counter *instrument + counter *telemetry.Instrument } func addLogCounter(ctx context.Context, m *Metrics) error { const nameLogMessages = `log_messages` - err := m.createInstrument(int64Counter, + err := m.CreateInstrument(telemetry.Int64Counter, nameLogMessages, "Total number of log messages.", "{message}", - withAsBuiltIn(), + telemetry.WithAsBuiltIn(), ) lm := logMetric{ - counter: m.allInstruments[nameLogMessages], + counter: m.AllInstruments[nameLogMessages], } log.AddHook(lm) for _, level := range lm.Levels() { - m.addInt(ctx, nameLogMessages, 0, instAttribs{ - {name: labelLogLevel, value: level.String()}, + m.AddInt(ctx, nameLogMessages, 0, telemetry.InstAttribs{ + {Name: telemetry.AttribLogLevel, Value: level.String()}, }) } @@ -36,8 +38,8 @@ func (m logMetric) Levels() []log.Level { } func (m logMetric) Fire(entry *log.Entry) error { - (*m.counter).addInt(entry.Context, 1, instAttribs{ - {name: labelLogLevel, value: entry.Level.String()}, + (*m.counter).AddInt(entry.Context, 1, telemetry.InstAttribs{ + {Name: telemetry.AttribLogLevel, Value: entry.Level.String()}, }) return nil } diff --git a/workflow/metrics/counter_pod_missing.go b/workflow/metrics/counter_pod_missing.go index 991a2184796b..8f3b227d8ef0 100644 --- a/workflow/metrics/counter_pod_missing.go +++ b/workflow/metrics/counter_pod_missing.go @@ -2,6 +2,8 @@ package metrics import ( "context" + + "github.com/argoproj/argo-workflows/v3/util/telemetry" ) const ( @@ -9,18 +11,18 @@ const ( ) func addPodMissingCounter(_ context.Context, m *Metrics) error { - return m.createInstrument(int64Counter, + return m.CreateInstrument(telemetry.Int64Counter, namePodMissing, "Incidents of pod missing.", "{pod}", - withAsBuiltIn(), + telemetry.WithAsBuiltIn(), ) } func (m *Metrics) incPodMissing(ctx context.Context, val int64, recentlyStarted bool, phase string) { - m.addInt(ctx, namePodMissing, val, instAttribs{ - {name: labelRecentlyStarted, value: recentlyStarted}, - {name: labelNodePhase, value: phase}, + m.AddInt(ctx, namePodMissing, val, telemetry.InstAttribs{ + {Name: telemetry.AttribRecentlyStarted, Value: recentlyStarted}, + {Name: telemetry.AttribNodePhase, Value: phase}, }) } diff --git a/workflow/metrics/counter_pod_pending.go b/workflow/metrics/counter_pod_pending.go index 4c47fbb4a22f..5138df86abea 100644 --- a/workflow/metrics/counter_pod_pending.go +++ b/workflow/metrics/counter_pod_pending.go @@ -3,6 +3,8 @@ package metrics import ( "context" "strings" + + "github.com/argoproj/argo-workflows/v3/util/telemetry" ) const ( @@ -10,11 +12,11 @@ const ( ) func addPodPendingCounter(_ context.Context, m *Metrics) error { - return m.createInstrument(int64Counter, + return m.CreateInstrument(telemetry.Int64Counter, namePodPending, "Total number of pods that started pending by reason", "{pod}", - withAsBuiltIn(), + telemetry.WithAsBuiltIn(), ) } @@ -28,9 +30,9 @@ func (m *Metrics) ChangePodPending(ctx context.Context, reason, namespace string // the pod_phase metric can cope with this being visible return default: - m.addInt(ctx, namePodPending, 1, instAttribs{ - {name: labelPodPendingReason, value: splitReason[0]}, - {name: labelPodNamespace, value: namespace}, + m.AddInt(ctx, namePodPending, 1, telemetry.InstAttribs{ + {Name: telemetry.AttribPodPendingReason, Value: splitReason[0]}, + {Name: telemetry.AttribPodNamespace, Value: namespace}, }) } } diff --git a/workflow/metrics/counter_pod_phase.go b/workflow/metrics/counter_pod_phase.go index 530be09ad5ce..37686c5fc4c3 100644 --- a/workflow/metrics/counter_pod_phase.go +++ b/workflow/metrics/counter_pod_phase.go @@ -2,6 +2,8 @@ package metrics import ( "context" + + "github.com/argoproj/argo-workflows/v3/util/telemetry" ) const ( @@ -9,17 +11,17 @@ const ( ) func addPodPhaseCounter(_ context.Context, m *Metrics) error { - return m.createInstrument(int64Counter, + return m.CreateInstrument(telemetry.Int64Counter, namePodPhase, "Total number of Pods that have entered each phase", "{pod}", - withAsBuiltIn(), + telemetry.WithAsBuiltIn(), ) } func (m *Metrics) ChangePodPhase(ctx context.Context, phase, namespace string) { - m.addInt(ctx, namePodPhase, 1, instAttribs{ - {name: labelPodPhase, value: phase}, - {name: labelPodNamespace, value: namespace}, + m.AddInt(ctx, namePodPhase, 1, telemetry.InstAttribs{ + {Name: telemetry.AttribPodPhase, Value: phase}, + {Name: telemetry.AttribPodNamespace, Value: namespace}, }) } diff --git a/workflow/metrics/counter_template.go b/workflow/metrics/counter_template.go index 85861179cf42..f1cb3500cab3 100644 --- a/workflow/metrics/counter_template.go +++ b/workflow/metrics/counter_template.go @@ -2,6 +2,8 @@ package metrics import ( "context" + + "github.com/argoproj/argo-workflows/v3/util/telemetry" ) const ( @@ -9,25 +11,25 @@ const ( ) func addWorkflowTemplateCounter(_ context.Context, m *Metrics) error { - return m.createInstrument(int64Counter, + return m.CreateInstrument(telemetry.Int64Counter, nameWFTemplateTriggered, "Total number of workflow templates triggered by workflowTemplateRef", "{workflow_template}", - withAsBuiltIn(), + telemetry.WithAsBuiltIn(), ) } -func templateLabels(name, namespace string, cluster bool) instAttribs { - return instAttribs{ - {name: labelTemplateName, value: name}, - {name: labelTemplateNamespace, value: namespace}, - {name: labelTemplateCluster, value: cluster}, +func templateAttribs(name, namespace string, cluster bool) telemetry.InstAttribs { + return telemetry.InstAttribs{ + {Name: telemetry.AttribTemplateName, Value: name}, + {Name: telemetry.AttribTemplateNamespace, Value: namespace}, + {Name: telemetry.AttribTemplateCluster, Value: cluster}, } } func (m *Metrics) CountWorkflowTemplate(ctx context.Context, phase MetricWorkflowPhase, name, namespace string, cluster bool) { - labels := templateLabels(name, namespace, cluster) - labels = append(labels, instAttrib{name: labelWorkflowPhase, value: string(phase)}) + attribs := templateAttribs(name, namespace, cluster) + attribs = append(attribs, telemetry.InstAttrib{Name: telemetry.AttribWorkflowPhase, Value: string(phase)}) - m.addInt(ctx, nameWFTemplateTriggered, 1, labels) + m.AddInt(ctx, nameWFTemplateTriggered, 1, attribs) } diff --git a/workflow/metrics/counter_workflow_phase.go b/workflow/metrics/counter_workflow_phase.go index 693dcae12cf4..91c56eef4853 100644 --- a/workflow/metrics/counter_workflow_phase.go +++ b/workflow/metrics/counter_workflow_phase.go @@ -4,6 +4,7 @@ import ( "context" wfv1 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" + "github.com/argoproj/argo-workflows/v3/util/telemetry" ) const ( @@ -40,17 +41,17 @@ func ConvertWorkflowPhase(inPhase wfv1.WorkflowPhase) MetricWorkflowPhase { } func addWorkflowPhaseCounter(_ context.Context, m *Metrics) error { - return m.createInstrument(int64Counter, + return m.CreateInstrument(telemetry.Int64Counter, nameWorkflowPhaseCounter, "Total number of workflows that have entered each phase", "{workflow}", - withAsBuiltIn(), + telemetry.WithAsBuiltIn(), ) } func (m *Metrics) ChangeWorkflowPhase(ctx context.Context, phase MetricWorkflowPhase, namespace string) { - m.addInt(ctx, nameWorkflowPhaseCounter, 1, instAttribs{ - {name: labelWorkflowPhase, value: string(phase)}, - {name: labelWorkflowNamespace, value: namespace}, + m.AddInt(ctx, nameWorkflowPhaseCounter, 1, telemetry.InstAttribs{ + {Name: telemetry.AttribWorkflowPhase, Value: string(phase)}, + {Name: telemetry.AttribWorkflowNamespace, Value: namespace}, }) } diff --git a/workflow/metrics/gauge_pod_phase.go b/workflow/metrics/gauge_pod_phase.go index d9514ee90c79..b93e9c33729b 100644 --- a/workflow/metrics/gauge_pod_phase.go +++ b/workflow/metrics/gauge_pod_phase.go @@ -3,6 +3,8 @@ package metrics import ( "context" + "github.com/argoproj/argo-workflows/v3/util/telemetry" + "go.opentelemetry.io/otel/metric" ) @@ -11,16 +13,16 @@ type PodPhaseCallback func() map[string]int64 type podPhaseGauge struct { callback PodPhaseCallback - gauge *instrument + gauge *telemetry.Instrument } func addPodPhaseGauge(ctx context.Context, m *Metrics) error { const namePodsPhase = `pods_gauge` - err := m.createInstrument(int64ObservableGauge, + err := m.CreateInstrument(telemetry.Int64ObservableGauge, namePodsPhase, "Number of Pods from Workflows currently accessible by the controller by status.", "{pod}", - withAsBuiltIn(), + telemetry.WithAsBuiltIn(), ) if err != nil { return err @@ -29,9 +31,9 @@ func addPodPhaseGauge(ctx context.Context, m *Metrics) error { if m.callbacks.PodPhase != nil { ppGauge := podPhaseGauge{ callback: m.callbacks.PodPhase, - gauge: m.allInstruments[namePodsPhase], + gauge: m.AllInstruments[namePodsPhase], } - return m.allInstruments[namePodsPhase].registerCallback(m, ppGauge.update) + return m.AllInstruments[namePodsPhase].RegisterCallback(m.Metrics, ppGauge.update) } return nil } @@ -39,7 +41,7 @@ func addPodPhaseGauge(ctx context.Context, m *Metrics) error { func (p *podPhaseGauge) update(_ context.Context, o metric.Observer) error { phases := p.callback() for phase, val := range phases { - p.gauge.observeInt(o, val, instAttribs{{name: labelPodPhase, value: phase}}) + p.gauge.ObserveInt(o, val, telemetry.InstAttribs{{Name: telemetry.AttribPodPhase, Value: phase}}) } return nil } diff --git a/workflow/metrics/gauge_workflow_condition.go b/workflow/metrics/gauge_workflow_condition.go index 19e01917643f..3709cd07d9d1 100644 --- a/workflow/metrics/gauge_workflow_condition.go +++ b/workflow/metrics/gauge_workflow_condition.go @@ -6,6 +6,7 @@ import ( "go.opentelemetry.io/otel/metric" wfv1 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" + "github.com/argoproj/argo-workflows/v3/util/telemetry" ) // WorkflowConditionCallback is the function prototype to provide this gauge with the condition of the workflows @@ -13,16 +14,16 @@ type WorkflowConditionCallback func() map[wfv1.Condition]int64 type workflowConditionGauge struct { callback WorkflowConditionCallback - gauge *instrument + gauge *telemetry.Instrument } func addWorkflowConditionGauge(_ context.Context, m *Metrics) error { const nameWorkflowCondition = `workflow_condition` - err := m.createInstrument(int64ObservableGauge, + err := m.CreateInstrument(telemetry.Int64ObservableGauge, nameWorkflowCondition, "Workflow condition.", "{unit}", - withAsBuiltIn(), + telemetry.WithAsBuiltIn(), ) if err != nil { return err @@ -31,9 +32,9 @@ func addWorkflowConditionGauge(_ context.Context, m *Metrics) error { if m.callbacks.WorkflowCondition != nil { wfcGauge := workflowConditionGauge{ callback: m.callbacks.WorkflowCondition, - gauge: m.allInstruments[nameWorkflowCondition], + gauge: m.AllInstruments[nameWorkflowCondition], } - return m.allInstruments[nameWorkflowCondition].registerCallback(m, wfcGauge.update) + return m.AllInstruments[nameWorkflowCondition].RegisterCallback(m.Metrics, wfcGauge.update) } return nil // TODO init all phases? @@ -42,9 +43,9 @@ func addWorkflowConditionGauge(_ context.Context, m *Metrics) error { func (c *workflowConditionGauge) update(_ context.Context, o metric.Observer) error { conditions := c.callback() for condition, val := range conditions { - c.gauge.observeInt(o, val, instAttribs{ - {name: labelWorkflowType, value: string(condition.Type)}, - {name: labelWorkflowStatus, value: string(condition.Status)}, + c.gauge.ObserveInt(o, val, telemetry.InstAttribs{ + {Name: telemetry.AttribWorkflowType, Value: string(condition.Type)}, + {Name: telemetry.AttribWorkflowStatus, Value: string(condition.Status)}, }) } return nil diff --git a/workflow/metrics/gauge_workflow_phase.go b/workflow/metrics/gauge_workflow_phase.go index 357feaf19863..59a6e670e413 100644 --- a/workflow/metrics/gauge_workflow_phase.go +++ b/workflow/metrics/gauge_workflow_phase.go @@ -3,6 +3,8 @@ package metrics import ( "context" + "github.com/argoproj/argo-workflows/v3/util/telemetry" + "go.opentelemetry.io/otel/metric" ) @@ -11,16 +13,16 @@ type WorkflowPhaseCallback func() map[string]int64 type workflowPhaseGauge struct { callback WorkflowPhaseCallback - gauge *instrument + gauge *telemetry.Instrument } func addWorkflowPhaseGauge(_ context.Context, m *Metrics) error { const nameWorkflowPhaseGauge = `gauge` - err := m.createInstrument(int64ObservableGauge, + err := m.CreateInstrument(telemetry.Int64ObservableGauge, nameWorkflowPhaseGauge, "number of Workflows currently accessible by the controller by status", "{workflow}", - withAsBuiltIn(), + telemetry.WithAsBuiltIn(), ) if err != nil { return err @@ -29,9 +31,9 @@ func addWorkflowPhaseGauge(_ context.Context, m *Metrics) error { if m.callbacks.WorkflowPhase != nil { wfpGauge := workflowPhaseGauge{ callback: m.callbacks.WorkflowPhase, - gauge: m.allInstruments[nameWorkflowPhaseGauge], + gauge: m.AllInstruments[nameWorkflowPhaseGauge], } - return m.allInstruments[nameWorkflowPhaseGauge].registerCallback(m, wfpGauge.update) + return m.AllInstruments[nameWorkflowPhaseGauge].RegisterCallback(m.Metrics, wfpGauge.update) } return nil // TODO init all phases? @@ -40,7 +42,7 @@ func addWorkflowPhaseGauge(_ context.Context, m *Metrics) error { func (p *workflowPhaseGauge) update(_ context.Context, o metric.Observer) error { phases := p.callback() for phase, val := range phases { - p.gauge.observeInt(o, val, instAttribs{{name: labelWorkflowStatus, value: phase}}) + p.gauge.ObserveInt(o, val, telemetry.InstAttribs{{Name: telemetry.AttribWorkflowStatus, Value: phase}}) } return nil } diff --git a/workflow/metrics/histogram_durations.go b/workflow/metrics/histogram_durations.go index 88c00ad28bbb..6b6038e31ff2 100644 --- a/workflow/metrics/histogram_durations.go +++ b/workflow/metrics/histogram_durations.go @@ -4,6 +4,8 @@ import ( "context" "time" + "github.com/argoproj/argo-workflows/v3/util/telemetry" + "github.com/prometheus/client_golang/prometheus" log "github.com/sirupsen/logrus" @@ -24,15 +26,15 @@ func addOperationDurationHistogram(_ context.Context, m *Metrics) error { } bucketWidth := maxOperationTimeSeconds / float64(operationDurationMetricBucketCount) // The buckets here are only the 'defaults' and can be overridden with configmap defaults - return m.createInstrument(float64Histogram, + return m.CreateInstrument(telemetry.Float64Histogram, nameOperationDuration, "Histogram of durations of operations", "s", - withDefaultBuckets(prometheus.LinearBuckets(bucketWidth, bucketWidth, operationDurationMetricBucketCount)), - withAsBuiltIn(), + telemetry.WithDefaultBuckets(prometheus.LinearBuckets(bucketWidth, bucketWidth, operationDurationMetricBucketCount)), + telemetry.WithAsBuiltIn(), ) } func (m *Metrics) OperationCompleted(ctx context.Context, durationSeconds float64) { - m.record(ctx, nameOperationDuration, durationSeconds, instAttribs{}) + m.Record(ctx, nameOperationDuration, durationSeconds, telemetry.InstAttribs{}) } diff --git a/workflow/metrics/histogram_template.go b/workflow/metrics/histogram_template.go index 7fe50895b5d8..a466b9112fa3 100644 --- a/workflow/metrics/histogram_template.go +++ b/workflow/metrics/histogram_template.go @@ -3,6 +3,8 @@ package metrics import ( "context" "time" + + "github.com/argoproj/argo-workflows/v3/util/telemetry" ) const ( @@ -10,14 +12,14 @@ const ( ) func addWorkflowTemplateHistogram(_ context.Context, m *Metrics) error { - return m.createInstrument(float64Histogram, + return m.CreateInstrument(telemetry.Float64Histogram, nameWorkflowTemplateRuntime, "Duration of workflow template runs run through workflowTemplateRefs", "s", - withAsBuiltIn(), + telemetry.WithAsBuiltIn(), ) } func (m *Metrics) RecordWorkflowTemplateTime(ctx context.Context, duration time.Duration, name, namespace string, cluster bool) { - m.record(ctx, nameWorkflowTemplateRuntime, duration.Seconds(), templateLabels(name, namespace, cluster)) + m.Record(ctx, nameWorkflowTemplateRuntime, duration.Seconds(), templateAttribs(name, namespace, cluster)) } diff --git a/workflow/metrics/labels.go b/workflow/metrics/labels.go deleted file mode 100644 index 47de721689be..000000000000 --- a/workflow/metrics/labels.go +++ /dev/null @@ -1,43 +0,0 @@ -package metrics - -const ( - labelBuildVersion string = `version` - labelBuildPlatform string = `platform` - labelBuildGoVersion string = `go_version` - labelBuildDate string = `build_date` - labelBuildCompiler string = `compiler` - labelBuildGitCommit string = `git_commit` - labelBuildGitTreeState string = `git_treestate` - labelBuildGitTag string = `git_tag` - - labelCronWFName string = `name` - - labelErrorCause string = "cause" - - labelLogLevel string = `level` - - labelNodePhase string = `node_phase` - - labelPodPhase string = `phase` - labelPodNamespace string = `namespace` - labelPodPendingReason string = `reason` - - labelQueueName string = `queue_name` - - labelRecentlyStarted string = `recently_started` - - labelRequestKind = `kind` - labelRequestVerb = `verb` - labelRequestCode = `status_code` - - labelTemplateName string = `name` - labelTemplateNamespace string = `namespace` - labelTemplateCluster string = `cluster_scope` - - labelWorkerType string = `worker_type` - - labelWorkflowNamespace string = `namespace` - labelWorkflowPhase string = `phase` - labelWorkflowStatus = `status` - labelWorkflowType = `type` -) diff --git a/workflow/metrics/leader.go b/workflow/metrics/leader.go index ec58dbc1e774..ff3562b66b52 100644 --- a/workflow/metrics/leader.go +++ b/workflow/metrics/leader.go @@ -3,6 +3,8 @@ package metrics import ( "context" + "github.com/argoproj/argo-workflows/v3/util/telemetry" + "go.opentelemetry.io/otel/metric" ) @@ -10,16 +12,16 @@ type IsLeaderCallback func() bool type leaderGauge struct { callback IsLeaderCallback - gauge *instrument + gauge *telemetry.Instrument } func addIsLeader(ctx context.Context, m *Metrics) error { const nameLeader = `is_leader` - err := m.createInstrument(int64ObservableGauge, + err := m.CreateInstrument(telemetry.Int64ObservableGauge, nameLeader, "Emits 1 if leader, 0 otherwise. Always 1 if leader election is disabled.", "{leader}", - withAsBuiltIn(), + telemetry.WithAsBuiltIn(), ) if err != nil { return err @@ -29,9 +31,9 @@ func addIsLeader(ctx context.Context, m *Metrics) error { } lGauge := leaderGauge{ callback: m.callbacks.IsLeader, - gauge: m.allInstruments[nameLeader], + gauge: m.AllInstruments[nameLeader], } - return m.allInstruments[nameLeader].registerCallback(m, lGauge.update) + return m.AllInstruments[nameLeader].RegisterCallback(m.Metrics, lGauge.update) } func (l *leaderGauge) update(_ context.Context, o metric.Observer) error { @@ -39,6 +41,6 @@ func (l *leaderGauge) update(_ context.Context, o metric.Observer) error { if l.callback() { val = 1 } - l.gauge.observeInt(o, val, instAttribs{}) + l.gauge.ObserveInt(o, val, telemetry.InstAttribs{}) return nil } diff --git a/workflow/metrics/leader_test.go b/workflow/metrics/leader_test.go index 623c5931464d..76514ab4fa51 100644 --- a/workflow/metrics/leader_test.go +++ b/workflow/metrics/leader_test.go @@ -6,11 +6,13 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/attribute" + + "github.com/argoproj/argo-workflows/v3/util/telemetry" ) func TestIsLeader(t *testing.T) { _, te, err := createTestMetrics( - &Config{}, + &telemetry.Config{}, Callbacks{ IsLeader: func() bool { return true @@ -27,7 +29,7 @@ func TestIsLeader(t *testing.T) { func TestNotLeader(t *testing.T) { _, te, err := createTestMetrics( - &Config{}, + &telemetry.Config{}, Callbacks{ IsLeader: func() bool { return false diff --git a/workflow/metrics/metrics.go b/workflow/metrics/metrics.go index f2377333aa40..622b3147a9c4 100644 --- a/workflow/metrics/metrics.go +++ b/workflow/metrics/metrics.go @@ -2,96 +2,38 @@ package metrics import ( "context" - "os" - "sync" - "time" - log "github.com/sirupsen/logrus" - "go.opentelemetry.io/otel" + "github.com/argoproj/argo-workflows/v3/util/telemetry" - wfconfig "github.com/argoproj/argo-workflows/v3/config" - - "go.opentelemetry.io/contrib/instrumentation/runtime" - "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" - "go.opentelemetry.io/otel/metric" metricsdk "go.opentelemetry.io/otel/sdk/metric" - "go.opentelemetry.io/otel/sdk/metric/metricdata" - "go.opentelemetry.io/otel/sdk/resource" - semconv "go.opentelemetry.io/otel/semconv/v1.24.0" ) -type Config struct { - Enabled bool - Path string - Port int - TTL time.Duration - IgnoreErrors bool - Secure bool - Modifiers map[string]Modifier - Temporality wfconfig.MetricsTemporality -} - type Metrics struct { - // Ensures mutual exclusion in workflows map - mutex sync.RWMutex + *telemetry.Metrics - // Evil context for compatibility with legacy context free interfaces - ctx context.Context - otelMeter *metric.Meter - callbacks Callbacks - config *Config - - allInstruments map[string]*instrument + callbacks Callbacks realtimeWorkflows map[string][]realtimeTracker } -func New(ctx context.Context, serviceName string, config *Config, callbacks Callbacks, extraOpts ...metricsdk.Option) (*Metrics, error) { - res := resource.NewWithAttributes( - semconv.SchemaURL, - semconv.ServiceName(serviceName), - ) - - options := make([]metricsdk.Option, 0) - options = append(options, metricsdk.WithResource(res)) - _, otlpEnabled := os.LookupEnv(`OTEL_EXPORTER_OTLP_ENDPOINT`) - _, otlpMetricsEnabled := os.LookupEnv(`OTEL_EXPORTER_OTLP_METRICS_ENDPOINT`) - if otlpEnabled || otlpMetricsEnabled { - log.Info("Starting OTLP metrics exporter") - otelExporter, err := otlpmetricgrpc.New(ctx, otlpmetricgrpc.WithTemporalitySelector(getTemporality(config))) - if err != nil { - return nil, err - } - options = append(options, metricsdk.WithReader(metricsdk.NewPeriodicReader(otelExporter))) - } - - if config.Enabled { - log.Info("Starting Prometheus metrics exporter") - promExporter, err := config.prometheusMetricsExporter(`argo_workflows`) - if err != nil { - return nil, err - } - options = append(options, metricsdk.WithReader(promExporter)) +func New(ctx context.Context, serviceName, prometheusName string, config *telemetry.Config, callbacks Callbacks, extraOpts ...metricsdk.Option) (*Metrics, error) { + m, err := telemetry.NewMetrics(ctx, serviceName, prometheusName, config, extraOpts...) + if err != nil { + return nil, err } - options = append(options, extraOpts...) - options = append(options, view(config)) - - provider := metricsdk.NewMeterProvider(options...) - otel.SetMeterProvider(provider) - // Add runtime metrics - err := runtime.Start(runtime.WithMinimumReadMemStatsInterval(time.Second)) + err = m.Populate(ctx, + telemetry.AddVersion, + ) if err != nil { return nil, err } - meter := provider.Meter(serviceName) metrics := &Metrics{ - ctx: ctx, - otelMeter: &meter, + Metrics: m, callbacks: callbacks, - config: config, realtimeWorkflows: make(map[string][]realtimeTracker), } + err = metrics.populate(ctx, addIsLeader, addPodPhaseGauge, @@ -107,7 +49,6 @@ func New(ctx context.Context, serviceName string, config *Config, callbacks Call addErrorCounter, addLogCounter, addK8sRequests, - addVersion, addWorkflowConditionGauge, addWorkQueueMetrics, ) @@ -123,7 +64,6 @@ func New(ctx context.Context, serviceName string, config *Config, callbacks Call type addMetric func(context.Context, *Metrics) error func (m *Metrics) populate(ctx context.Context, adders ...addMetric) error { - m.allInstruments = make(map[string]*instrument) for _, adder := range adders { if err := adder(ctx, m); err != nil { return err @@ -131,18 +71,3 @@ func (m *Metrics) populate(ctx context.Context, adders ...addMetric) error { } return nil } - -func getTemporality(config *Config) metricsdk.TemporalitySelector { - switch config.Temporality { - case wfconfig.MetricsTemporalityCumulative: - return func(metricsdk.InstrumentKind) metricdata.Temporality { - return metricdata.CumulativeTemporality - } - case wfconfig.MetricsTemporalityDelta: - return func(metricsdk.InstrumentKind) metricdata.Temporality { - return metricdata.DeltaTemporality - } - default: - return metricsdk.DefaultTemporalitySelector - } -} diff --git a/workflow/metrics/metrics_custom.go b/workflow/metrics/metrics_custom.go index 3b6957e69576..f4057540408d 100644 --- a/workflow/metrics/metrics_custom.go +++ b/workflow/metrics/metrics_custom.go @@ -10,6 +10,7 @@ import ( "go.opentelemetry.io/otel/metric" wfv1 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" + "github.com/argoproj/argo-workflows/v3/util/telemetry" ) type RealTimeValueFunc func() float64 @@ -28,52 +29,56 @@ type customMetricValue struct { } type realtimeTracker struct { - inst *instrument + inst *telemetry.Instrument key string } -func (cmv *customMetricValue) getLabels() instAttribs { - labels := make(instAttribs, len(cmv.labels)) +func (cmv *customMetricValue) getLabels() telemetry.InstAttribs { + labels := make(telemetry.InstAttribs, len(cmv.labels)) for i := range cmv.labels { - labels[i] = instAttrib{name: cmv.labels[i].Key, value: cmv.labels[i].Value} + labels[i] = telemetry.InstAttrib{Name: cmv.labels[i].Key, Value: cmv.labels[i].Value} } return labels } -func (i *instrument) customUserdata(requireSuccess bool) map[string]*customMetricValue { - switch val := i.userdata.(type) { +func customUserdata(i *telemetry.Instrument, requireSuccess bool) map[string]*customMetricValue { + switch val := i.GetUserdata().(type) { case map[string]*customMetricValue: return val default: if requireSuccess { - panic(fmt.Errorf("internal error: unexpected userdata on custom metric %s", i.name)) + panic(fmt.Errorf("internal error: unexpected userdata on custom metric %s", i.GetName())) } return make(map[string]*customMetricValue) } } -func (i *instrument) getOrCreateValue(key string, labels []*wfv1.MetricLabel) *customMetricValue { - if value, ok := i.customUserdata(true)[key]; ok { +func getOrCreateValue(i *telemetry.Instrument, key string, labels []*wfv1.MetricLabel) *customMetricValue { + if value, ok := customUserdata(i, true)[key]; ok { return value } newValue := customMetricValue{ key: key, labels: labels, } - i.customUserdata(true)[key] = &newValue + customUserdata(i, true)[key] = &newValue return &newValue } +type customInstrument struct { + *telemetry.Instrument +} + // Common callback for realtime and gauges // For realtime this acts as a thunk to the calling convention // For non-realtime we have to fake observability as prometheus provides // up/down and set on the same gauge type, which otel forbids. -func (i *instrument) customCallback(_ context.Context, o metric.Observer) error { - for _, value := range i.customUserdata(true) { +func (i *customInstrument) customCallback(_ context.Context, o metric.Observer) error { + for _, value := range customUserdata(i.Instrument, true) { if value.rtValueFunc != nil { - i.observeFloat(o, value.rtValueFunc(), value.getLabels()) + i.ObserveFloat(o, value.rtValueFunc(), value.getLabels()) } else { - i.observeFloat(o, value.prometheusValue, value.getLabels()) + i.ObserveFloat(o, value.prometheusValue, value.getLabels()) } } return nil @@ -86,33 +91,33 @@ func (i *instrument) customCallback(_ context.Context, o metric.Observer) error // GetCustomMetric returns a custom (or any) metric from it's key // This is exported for legacy testing only -func (m *Metrics) GetCustomMetric(key string) *instrument { - m.mutex.RLock() - defer m.mutex.RUnlock() +func (m *Metrics) GetCustomMetric(key string) *telemetry.Instrument { + m.Mutex.RLock() + defer m.Mutex.RUnlock() // It's okay to return nil metrics in this function - return m.allInstruments[key] + return m.AllInstruments[key] } // CustomMetricExists returns if metric exists from its key // This is exported for testing only func (m *Metrics) CustomMetricExists(key string) bool { - m.mutex.RLock() - defer m.mutex.RUnlock() + m.Mutex.RLock() + defer m.Mutex.RUnlock() // It's okay to return nil metrics in this function - return m.allInstruments[key] != nil + return m.AllInstruments[key] != nil } // TODO labels on custom metrics -func (m *Metrics) matchExistingMetric(metricSpec *wfv1.Prometheus) (*instrument, error) { +func (m *Metrics) matchExistingMetric(metricSpec *wfv1.Prometheus) (*telemetry.Instrument, error) { key := metricSpec.Name - if inst, ok := m.allInstruments[key]; ok { - if inst.description != metricSpec.Help { - return nil, fmt.Errorf("Help for metric %s is already set to %s, it cannot be changed", metricSpec.Name, inst.description) + if inst, ok := m.AllInstruments[key]; ok { + if inst.GetDescription() != metricSpec.Help { + return nil, fmt.Errorf("Help for metric %s is already set to %s, it cannot be changed", metricSpec.Name, inst.GetDescription()) } wantedType := metricSpec.GetMetricType() - switch inst.otel.(type) { + switch inst.GetOtel().(type) { case *metric.Float64ObservableGauge: if wantedType != wfv1.MetricTypeGauge && !metricSpec.IsRealtime() { return nil, fmt.Errorf("Found existing gauge for custom metric %s of type %s", metricSpec.Name, wantedType) @@ -126,14 +131,14 @@ func (m *Metrics) matchExistingMetric(metricSpec *wfv1.Prometheus) (*instrument, return nil, fmt.Errorf("Found existing histogram for custom metric %s of type %s", metricSpec.Name, wantedType) } default: - return nil, fmt.Errorf("Found unwanted type %s for custom metric %s of type %s", reflect.TypeOf(inst.otel), metricSpec.Name, wantedType) + return nil, fmt.Errorf("Found unwanted type %s for custom metric %s of type %s", reflect.TypeOf(inst.GetOtel()), metricSpec.Name, wantedType) } return inst, nil } return nil, nil } -func (m *Metrics) ensureBaseMetric(metricSpec *wfv1.Prometheus, ownerKey string) (*instrument, error) { +func (m *Metrics) ensureBaseMetric(metricSpec *wfv1.Prometheus, ownerKey string) (*telemetry.Instrument, error) { metric, err := m.matchExistingMetric(metricSpec) if err != nil { return nil, err @@ -147,11 +152,11 @@ func (m *Metrics) ensureBaseMetric(metricSpec *wfv1.Prometheus, ownerKey string) return nil, err } m.attachCustomMetricToWorkflow(metricSpec, ownerKey) - inst := m.allInstruments[metricSpec.Name] + inst := m.AllInstruments[metricSpec.Name] if inst == nil { return nil, fmt.Errorf("Failed to create new metric %s", metricSpec.Name) } - inst.userdata = make(map[string]*customMetricValue) + inst.SetUserdata(make(map[string]*customMetricValue)) return inst, nil } @@ -163,7 +168,7 @@ func (m *Metrics) UpsertCustomMetric(ctx context.Context, metricSpec *wfv1.Prome if err != nil { return err } - metricValue := baseMetric.getOrCreateValue(metricSpec.GetKey(), metricSpec.Labels) + metricValue := getOrCreateValue(baseMetric, metricSpec.GetKey(), metricSpec.Labels) metricValue.lastUpdated = time.Now() metricType := metricSpec.GetMetricType() @@ -191,7 +196,7 @@ func (m *Metrics) UpsertCustomMetric(ctx context.Context, metricSpec *wfv1.Prome if err != nil { return err } - baseMetric.record(ctx, val, metricValue.getLabels()) + baseMetric.Record(ctx, val, metricValue.getLabels()) case metricType == wfv1.MetricTypeCounter: val, err := strconv.ParseFloat(metricSpec.Counter.Value, 64) if err != nil { @@ -213,7 +218,7 @@ func (m *Metrics) attachCustomMetricToWorkflow(metricSpec *wfv1.Prometheus, owne } } m.realtimeWorkflows[ownerKey] = append(m.realtimeWorkflows[ownerKey], realtimeTracker{ - inst: m.allInstruments[metricSpec.Name], + inst: m.AllInstruments[metricSpec.Name], key: metricSpec.GetKey(), }) } @@ -231,33 +236,35 @@ func (m *Metrics) createCustomMetric(metricSpec *wfv1.Prometheus) error { case metricType == wfv1.MetricTypeGauge: return m.createCustomGauge(metricSpec) case metricType == wfv1.MetricTypeHistogram: - return m.createInstrument(float64Histogram, metricSpec.Name, metricSpec.Help, "{item}", withDefaultBuckets(metricSpec.Histogram.GetBuckets())) + return m.CreateInstrument(telemetry.Float64Histogram, metricSpec.Name, metricSpec.Help, "{item}", telemetry.WithDefaultBuckets(metricSpec.Histogram.GetBuckets())) case metricType == wfv1.MetricTypeCounter: - err := m.createInstrument(float64ObservableUpDownCounter, metricSpec.Name, metricSpec.Help, "{item}") + err := m.CreateInstrument(telemetry.Float64ObservableUpDownCounter, metricSpec.Name, metricSpec.Help, "{item}") if err != nil { return err } - inst := m.allInstruments[metricSpec.Name] - return inst.registerCallback(m, inst.customCallback) + inst := m.AllInstruments[metricSpec.Name] + customInst := customInstrument{Instrument: inst} + return inst.RegisterCallback(m.Metrics, customInst.customCallback) default: return fmt.Errorf("invalid metric spec") } } func (m *Metrics) createCustomGauge(metricSpec *wfv1.Prometheus) error { - err := m.createInstrument(float64ObservableGauge, metricSpec.Name, metricSpec.Help, "{item}") + err := m.CreateInstrument(telemetry.Float64ObservableGauge, metricSpec.Name, metricSpec.Help, "{item}") if err != nil { return err } - inst := m.allInstruments[metricSpec.Name] - return inst.registerCallback(m, inst.customCallback) + inst := m.AllInstruments[metricSpec.Name] + customInst := customInstrument{Instrument: inst} + return inst.RegisterCallback(m.Metrics, customInst.customCallback) } func (m *Metrics) runCustomGC(ttl time.Duration) { - m.mutex.Lock() - defer m.mutex.Unlock() - for _, baseMetric := range m.allInstruments { - custom := baseMetric.customUserdata(false) + m.Mutex.Lock() + defer m.Mutex.Unlock() + for _, baseMetric := range m.AllInstruments { + custom := customUserdata(baseMetric, false) for key, value := range custom { if time.Since(value.lastUpdated) > ttl { delete(custom, key) @@ -284,8 +291,8 @@ func (m *Metrics) customMetricsGC(ctx context.Context, ttl time.Duration) { } func (m *Metrics) StopRealtimeMetricsForWfUID(key string) { - m.mutex.Lock() - defer m.mutex.Unlock() + m.Mutex.Lock() + defer m.Mutex.Unlock() if _, exists := m.realtimeWorkflows[key]; !exists { return @@ -293,7 +300,7 @@ func (m *Metrics) StopRealtimeMetricsForWfUID(key string) { realtimeMetrics := m.realtimeWorkflows[key] for _, metric := range realtimeMetrics { - delete(metric.inst.customUserdata(true), metric.key) + delete(customUserdata(metric.inst, true), metric.key) } delete(m.realtimeWorkflows, key) diff --git a/workflow/metrics/metrics_k8s_request.go b/workflow/metrics/metrics_k8s_request.go index b361baeae848..70fc46c29110 100644 --- a/workflow/metrics/metrics_k8s_request.go +++ b/workflow/metrics/metrics_k8s_request.go @@ -8,6 +8,7 @@ import ( "k8s.io/client-go/rest" "github.com/argoproj/argo-workflows/v3/util/k8s" + "github.com/argoproj/argo-workflows/v3/util/telemetry" ) const ( @@ -16,21 +17,21 @@ const ( ) func addK8sRequests(_ context.Context, m *Metrics) error { - err := m.createInstrument(int64Counter, + err := m.CreateInstrument(telemetry.Int64Counter, nameK8sRequestTotal, "Number of kubernetes requests executed.", "{request}", - withAsBuiltIn(), + telemetry.WithAsBuiltIn(), ) if err != nil { return err } - err = m.createInstrument(float64Histogram, + err = m.CreateInstrument(telemetry.Float64Histogram, nameK8sRequestDuration, "Duration of kubernetes requests executed.", "s", - withDefaultBuckets([]float64{0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0, 20.0, 60.0, 180.0}), - withAsBuiltIn(), + telemetry.WithDefaultBuckets([]float64{0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0, 20.0, 60.0, 180.0}), + telemetry.WithAsBuiltIn(), ) // Register this metrics with the global k8sMetrics.metrics = m @@ -53,13 +54,13 @@ func (m metricsRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) duration := time.Since(startTime) if x != nil && m.metrics != nil { verb, kind := k8s.ParseRequest(r) - attribs := instAttribs{ - {name: labelRequestKind, value: kind}, - {name: labelRequestVerb, value: verb}, - {name: labelRequestCode, value: x.StatusCode}, + attribs := telemetry.InstAttribs{ + {Name: telemetry.AttribRequestKind, Value: kind}, + {Name: telemetry.AttribRequestVerb, Value: verb}, + {Name: telemetry.AttribRequestCode, Value: x.StatusCode}, } - (*m.metrics).addInt(m.ctx, nameK8sRequestTotal, 1, attribs) - (*m.metrics).record(m.ctx, nameK8sRequestDuration, duration.Seconds(), attribs) + (*m.metrics).AddInt(m.ctx, nameK8sRequestTotal, 1, attribs) + (*m.metrics).Record(m.ctx, nameK8sRequestDuration, duration.Seconds(), attribs) } return x, err } diff --git a/workflow/metrics/metrics_test.go b/workflow/metrics/metrics_test.go index 113fa4d44cb6..de2b224f9bc3 100644 --- a/workflow/metrics/metrics_test.go +++ b/workflow/metrics/metrics_test.go @@ -12,13 +12,14 @@ import ( "k8s.io/utils/pointer" wfv1 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" + "github.com/argoproj/argo-workflows/v3/util/telemetry" ) func TestMetrics(t *testing.T) { m, te, err := CreateDefaultTestMetrics() require.NoError(t, err) // Default buckets: {5, 10, 15, 20, 25, 30} - m.OperationCompleted(m.ctx, 5) + m.OperationCompleted(m.Ctx, 5) assert.NotNil(t, te) attribs := attribute.NewSet() val, err := te.GetFloat64HistogramData(nameOperationDuration, &attribs) @@ -33,12 +34,12 @@ func TestErrors(t *testing.T) { assert.Nil(t, m.GetCustomMetric("does-not-exist")) require.NoError(t, err) - err = m.UpsertCustomMetric(m.ctx, &wfv1.Prometheus{ + err = m.UpsertCustomMetric(m.Ctx, &wfv1.Prometheus{ Name: "invalid.name", }, "owner", func() float64 { return 0.0 }) require.Error(t, err) - err = m.UpsertCustomMetric(m.ctx, &wfv1.Prometheus{ + err = m.UpsertCustomMetric(m.Ctx, &wfv1.Prometheus{ Name: "name", Labels: []*wfv1.MetricLabel{{ Key: "invalid-key", @@ -49,10 +50,10 @@ func TestErrors(t *testing.T) { } func TestMetricGC(t *testing.T) { - config := Config{ + config := telemetry.Config{ Enabled: true, - Path: defaultPrometheusServerPath, - Port: defaultPrometheusServerPort, + Path: telemetry.DefaultPrometheusServerPath, + Port: telemetry.DefaultPrometheusServerPort, TTL: 1 * time.Second, } @@ -63,7 +64,7 @@ func TestMetricGC(t *testing.T) { labels := []*wfv1.MetricLabel{ {Key: "foo", Value: "bar"}, } - err = m.UpsertCustomMetric(m.ctx, &wfv1.Prometheus{ + err = m.UpsertCustomMetric(m.Ctx, &wfv1.Prometheus{ Name: key, Labels: labels, Help: "none", @@ -73,7 +74,7 @@ func TestMetricGC(t *testing.T) { baseCm := m.GetCustomMetric(key) assert.NotNil(t, baseCm) - cm := baseCm.customUserdata(true) + cm := customUserdata(baseCm, true) assert.Len(t, cm, 1) // Ensure we get at least one TTL run @@ -92,15 +93,15 @@ func TestMetricGC(t *testing.T) { } func TestRealtimeMetricGC(t *testing.T) { - config := Config{ + config := telemetry.Config{ Enabled: true, - Path: defaultPrometheusServerPath, - Port: defaultPrometheusServerPort, + Path: telemetry.DefaultPrometheusServerPath, + Port: telemetry.DefaultPrometheusServerPort, TTL: 1 * time.Second, } ctx, cancel := context.WithCancel(context.Background()) defer cancel() - m, err := New(ctx, TestScopeName, &config, Callbacks{}) + m, err := New(ctx, telemetry.TestScopeName, telemetry.TestScopeName, &config, Callbacks{}) require.NoError(t, err) labels := []*wfv1.MetricLabel{ @@ -108,7 +109,7 @@ func TestRealtimeMetricGC(t *testing.T) { } name := "realtime_metric" wfKey := "workflow-uid" - err = m.UpsertCustomMetric(m.ctx, &wfv1.Prometheus{ + err = m.UpsertCustomMetric(m.Ctx, &wfv1.Prometheus{ Name: name, Labels: labels, Help: "None", @@ -146,31 +147,31 @@ func TestRealtimeMetricGC(t *testing.T) { func TestWorkflowQueueMetrics(t *testing.T) { m, te, err := getSharedMetrics() require.NoError(t, err) - attribs := attribute.NewSet(attribute.String(labelQueueName, "workflow_queue")) - wfQueue := m.RateLimiterWithBusyWorkers(m.ctx, workqueue.DefaultControllerRateLimiter(), "workflow_queue") + attribs := attribute.NewSet(attribute.String(telemetry.AttribQueueName, "workflow_queue")) + wfQueue := m.RateLimiterWithBusyWorkers(m.Ctx, workqueue.DefaultControllerRateLimiter(), "workflow_queue") defer wfQueue.ShutDown() - assert.NotNil(t, m.allInstruments[nameWorkersQueueDepth]) - assert.NotNil(t, m.allInstruments[nameWorkersQueueLatency]) + assert.NotNil(t, m.AllInstruments[nameWorkersQueueDepth]) + assert.NotNil(t, m.AllInstruments[nameWorkersQueueLatency]) wfQueue.Add("hello") - require.NotNil(t, m.allInstruments[nameWorkersQueueAdds]) + require.NotNil(t, m.AllInstruments[nameWorkersQueueAdds]) val, err := te.GetInt64CounterValue(nameWorkersQueueAdds, &attribs) require.NoError(t, err) assert.Equal(t, int64(1), val) } func TestRealTimeMetricDeletion(t *testing.T) { - config := Config{ + config := telemetry.Config{ Enabled: true, - Path: defaultPrometheusServerPath, - Port: defaultPrometheusServerPort, + Path: telemetry.DefaultPrometheusServerPath, + Port: telemetry.DefaultPrometheusServerPort, TTL: 1 * time.Second, } ctx, cancel := context.WithCancel(context.Background()) defer cancel() - m, err := New(ctx, TestScopeName, &config, Callbacks{}) + m, err := New(ctx, telemetry.TestScopeName, telemetry.TestScopeName, &config, Callbacks{}) require.NoError(t, err) // We've not yet fed a metric in for 123 @@ -200,7 +201,7 @@ func TestRealTimeMetricDeletion(t *testing.T) { m.StopRealtimeMetricsForWfUID("456") assert.Empty(t, m.realtimeWorkflows["456"]) - cm := baseCm.customUserdata(true) + cm := customUserdata(baseCm, true) assert.Len(t, cm, 1) assert.Len(t, m.realtimeWorkflows["123"], 1) diff --git a/workflow/metrics/test_helpers.go b/workflow/metrics/test_helpers.go new file mode 100644 index 000000000000..944d46a1cff4 --- /dev/null +++ b/workflow/metrics/test_helpers.go @@ -0,0 +1,53 @@ +package metrics + +import ( + "context" + "time" + + "go.opentelemetry.io/otel/sdk/metric" + "k8s.io/client-go/util/workqueue" + + "github.com/argoproj/argo-workflows/v3/util/telemetry" +) + +var sharedMetrics *Metrics = nil +var sharedTE *telemetry.TestMetricsExporter = nil + +// getSharedMetrics returns a singleton metrics with test exporter +// This is necessary because only the first call to workqueue.SetProvider +// takes effect within a single binary +// This can be fixed when we update to client-go 0.27 or later and we can +// create workqueues with https://godocs.io/k8s.io/client-go/util/workqueue#NewRateLimitingQueueWithConfig +func getSharedMetrics() (*Metrics, *telemetry.TestMetricsExporter, error) { + if sharedMetrics == nil { + config := telemetry.Config{ + Enabled: true, + TTL: 1 * time.Second, + } + var err error + sharedMetrics, sharedTE, err = createTestMetrics(&config, Callbacks{}) + if err != nil { + return nil, nil, err + } + + workqueue.SetProvider(sharedMetrics) + } + return sharedMetrics, sharedTE, nil +} + +// CreateDefaultTestMetrics creates a boring testExporter enabled +// metrics, suitable for many tests +func CreateDefaultTestMetrics() (*Metrics, *telemetry.TestMetricsExporter, error) { + config := telemetry.Config{ + Enabled: true, + } + return createTestMetrics(&config, Callbacks{}) +} + +func createTestMetrics(config *telemetry.Config, callbacks Callbacks) (*Metrics, *telemetry.TestMetricsExporter, error) { + ctx /* with cancel*/ := context.Background() + te := telemetry.NewTestMetricsExporter() + + m, err := New(ctx, telemetry.TestScopeName, telemetry.TestScopeName, config, callbacks, metric.WithReader(te)) + return m, te, err +} diff --git a/workflow/metrics/version.go b/workflow/metrics/version.go deleted file mode 100644 index afc2b1c0a3d8..000000000000 --- a/workflow/metrics/version.go +++ /dev/null @@ -1,33 +0,0 @@ -package metrics - -import ( - "context" - - "github.com/argoproj/argo-workflows/v3" -) - -func addVersion(ctx context.Context, m *Metrics) error { - const nameVersion = `version` - err := m.createInstrument(int64Counter, - nameVersion, - "Build metadata for this Controller", - "{unused}", - withAsBuiltIn(), - ) - if err != nil { - return err - } - - version := argo.GetVersion() - m.addInt(ctx, nameVersion, 1, instAttribs{ - {name: labelBuildVersion, value: version.Version}, - {name: labelBuildPlatform, value: version.Platform}, - {name: labelBuildGoVersion, value: version.GoVersion}, - {name: labelBuildDate, value: version.BuildDate}, - {name: labelBuildCompiler, value: version.Compiler}, - {name: labelBuildGitCommit, value: version.GitCommit}, - {name: labelBuildGitTreeState, value: version.GitTreeState}, - {name: labelBuildGitTag, value: version.GitTag}, - }) - return nil -} diff --git a/workflow/metrics/version_test.go b/workflow/metrics/version_test.go deleted file mode 100644 index 5d2c22d8165d..000000000000 --- a/workflow/metrics/version_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package metrics - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.opentelemetry.io/otel/attribute" - - "github.com/argoproj/argo-workflows/v3" -) - -func TestVersion(t *testing.T) { - _, te, err := CreateDefaultTestMetrics() - require.NoError(t, err) - assert.NotNil(t, te) - version := argo.GetVersion() - attribs := attribute.NewSet( - attribute.String(labelBuildVersion, version.Version), - attribute.String(labelBuildPlatform, version.Platform), - attribute.String(labelBuildGoVersion, version.GoVersion), - attribute.String(labelBuildDate, version.BuildDate), - attribute.String(labelBuildCompiler, version.Compiler), - attribute.String(labelBuildGitCommit, version.GitCommit), - attribute.String(labelBuildGitTreeState, version.GitTreeState), - attribute.String(labelBuildGitTag, version.GitTag), - ) - val, err := te.GetInt64CounterValue(`version`, &attribs) - require.NoError(t, err) - assert.Equal(t, int64(1), val) -} diff --git a/workflow/metrics/work_queue.go b/workflow/metrics/work_queue.go index acca059b73b3..366188a63f95 100644 --- a/workflow/metrics/work_queue.go +++ b/workflow/metrics/work_queue.go @@ -3,6 +3,8 @@ package metrics import ( "context" + "github.com/argoproj/argo-workflows/v3/util/telemetry" + log "github.com/sirupsen/logrus" "go.opentelemetry.io/otel/metric" "k8s.io/client-go/util/workqueue" @@ -26,100 +28,100 @@ var _ workqueue.MetricsProvider = &Metrics{} type workersBusyRateLimiterWorkQueue struct { workqueue.RateLimitingInterface workerType string - busyGauge *instrument + busyGauge *telemetry.Instrument // Evil storage of context for compatibility with legacy interface to workqueue ctx context.Context } func addWorkQueueMetrics(_ context.Context, m *Metrics) error { - err := m.createInstrument(int64UpDownCounter, + err := m.CreateInstrument(telemetry.Int64UpDownCounter, nameWorkersBusy, "Number of workers currently busy", "{worker}", - withAsBuiltIn(), + telemetry.WithAsBuiltIn(), ) if err != nil { return err } - err = m.createInstrument(int64UpDownCounter, + err = m.CreateInstrument(telemetry.Int64UpDownCounter, nameWorkersQueueDepth, "Depth of the queue", "{item}", - withAsBuiltIn(), + telemetry.WithAsBuiltIn(), ) if err != nil { return err } - err = m.createInstrument(int64Counter, + err = m.CreateInstrument(telemetry.Int64Counter, nameWorkersQueueAdds, "Adds to the queue", "{item}", - withAsBuiltIn(), + telemetry.WithAsBuiltIn(), ) if err != nil { return err } - err = m.createInstrument(float64Histogram, + err = m.CreateInstrument(telemetry.Float64Histogram, nameWorkersQueueLatency, "Time objects spend waiting in the queue", "s", - withDefaultBuckets([]float64{1.0, 5.0, 20.0, 60.0, 180.0}), - withAsBuiltIn(), + telemetry.WithDefaultBuckets([]float64{1.0, 5.0, 20.0, 60.0, 180.0}), + telemetry.WithAsBuiltIn(), ) if err != nil { return err } - err = m.createInstrument(float64Histogram, + err = m.CreateInstrument(telemetry.Float64Histogram, nameWorkersQueueDuration, "Time objects spend being processed from the queue", "s", - withDefaultBuckets([]float64{0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0, 20.0, 60.0, 180.0}), - withAsBuiltIn(), + telemetry.WithDefaultBuckets([]float64{0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0, 20.0, 60.0, 180.0}), + telemetry.WithAsBuiltIn(), ) if err != nil { return err } - err = m.createInstrument(int64Counter, + err = m.CreateInstrument(telemetry.Int64Counter, nameWorkersRetries, "Retries in the queues", "{item}", - withAsBuiltIn(), + telemetry.WithAsBuiltIn(), ) if err != nil { return err } - err = m.createInstrument(float64ObservableGauge, + err = m.CreateInstrument(telemetry.Float64ObservableGauge, nameWorkersUnfinishedWork, "Unfinished work time", "s", - withAsBuiltIn(), + telemetry.WithAsBuiltIn(), ) if err != nil { return err } unfinishedCallback := queueUserdata{ - gauge: m.allInstruments[nameWorkersUnfinishedWork], + gauge: m.AllInstruments[nameWorkersUnfinishedWork], } - m.allInstruments[nameWorkersUnfinishedWork].userdata = &unfinishedCallback - err = m.allInstruments[nameWorkersUnfinishedWork].registerCallback(m, unfinishedCallback.update) + m.AllInstruments[nameWorkersUnfinishedWork].SetUserdata(&unfinishedCallback) + err = m.AllInstruments[nameWorkersUnfinishedWork].RegisterCallback(m.Metrics, unfinishedCallback.update) if err != nil { return err } - err = m.createInstrument(float64ObservableGauge, + err = m.CreateInstrument(telemetry.Float64ObservableGauge, nameWorkersLongestRunning, "Longest running worker", "s", - withAsBuiltIn(), + telemetry.WithAsBuiltIn(), ) if err != nil { return err } longestRunningCallback := queueUserdata{ - gauge: m.allInstruments[nameWorkersLongestRunning], + gauge: m.AllInstruments[nameWorkersLongestRunning], } - m.allInstruments[nameWorkersLongestRunning].userdata = &longestRunningCallback - err = m.allInstruments[nameWorkersLongestRunning].registerCallback(m, longestRunningCallback.update) + m.AllInstruments[nameWorkersLongestRunning].SetUserdata(&longestRunningCallback) + err = m.AllInstruments[nameWorkersLongestRunning].RegisterCallback(m.Metrics, longestRunningCallback.update) if err != nil { return err } @@ -130,27 +132,27 @@ func (m *Metrics) RateLimiterWithBusyWorkers(ctx context.Context, workQueue work queue := workersBusyRateLimiterWorkQueue{ RateLimitingInterface: workqueue.NewNamedRateLimitingQueue(workQueue, queueName), workerType: queueName, - busyGauge: m.allInstruments[nameWorkersBusy], + busyGauge: m.AllInstruments[nameWorkersBusy], ctx: ctx, } queue.newWorker(ctx) return queue } -func (w *workersBusyRateLimiterWorkQueue) attributes() instAttribs { - return instAttribs{{name: labelWorkerType, value: w.workerType}} +func (w *workersBusyRateLimiterWorkQueue) attributes() telemetry.InstAttribs { + return telemetry.InstAttribs{{Name: telemetry.AttribWorkerType, Value: w.workerType}} } func (w *workersBusyRateLimiterWorkQueue) newWorker(ctx context.Context) { - w.busyGauge.addInt(ctx, 0, w.attributes()) + w.busyGauge.AddInt(ctx, 0, w.attributes()) } func (w *workersBusyRateLimiterWorkQueue) workerBusy(ctx context.Context) { - w.busyGauge.addInt(ctx, 1, w.attributes()) + w.busyGauge.AddInt(ctx, 1, w.attributes()) } func (w *workersBusyRateLimiterWorkQueue) workerFree(ctx context.Context) { - w.busyGauge.addInt(ctx, -1, w.attributes()) + w.busyGauge.AddInt(ctx, -1, w.attributes()) } func (w workersBusyRateLimiterWorkQueue) Get() (interface{}, bool) { @@ -168,29 +170,29 @@ func (w workersBusyRateLimiterWorkQueue) Done(item interface{}) { type queueMetric struct { ctx context.Context name string - inst *instrument + inst *telemetry.Instrument value *float64 } type queueUserdata struct { - gauge *instrument + gauge *telemetry.Instrument metrics []queueMetric } -func (q *queueMetric) attributes() instAttribs { - return instAttribs{{name: labelQueueName, value: q.name}} +func (q *queueMetric) attributes() telemetry.InstAttribs { + return telemetry.InstAttribs{{Name: telemetry.AttribQueueName, Value: q.name}} } func (q queueMetric) Inc() { - q.inst.addInt(q.ctx, 1, q.attributes()) + q.inst.AddInt(q.ctx, 1, q.attributes()) } func (q queueMetric) Dec() { - q.inst.addInt(q.ctx, -1, q.attributes()) + q.inst.AddInt(q.ctx, -1, q.attributes()) } func (q queueMetric) Observe(val float64) { - q.inst.record(q.ctx, val, q.attributes()) + q.inst.Record(q.ctx, val, q.attributes()) } // Observable gauge stores in the shim @@ -198,83 +200,83 @@ func (q queueMetric) Set(val float64) { *(q.value) = val } -func (i *instrument) queueUserdata() *queueUserdata { - switch val := i.userdata.(type) { +func getQueueUserdata(i *telemetry.Instrument) *queueUserdata { + switch val := i.GetUserdata().(type) { case *queueUserdata: return val default: - log.Errorf("internal error: unexpected userdata on queue metric %s", i.name) + log.Errorf("internal error: unexpected userdata on queue metric %s", i.GetName()) return &queueUserdata{} } } func (q *queueUserdata) update(_ context.Context, o metric.Observer) error { for _, metric := range q.metrics { - q.gauge.observeFloat(o, *metric.value, metric.attributes()) + q.gauge.ObserveFloat(o, *metric.value, metric.attributes()) } return nil } func (m *Metrics) NewDepthMetric(name string) workqueue.GaugeMetric { return queueMetric{ - ctx: m.ctx, + ctx: m.Ctx, name: name, - inst: m.allInstruments[nameWorkersQueueDepth], + inst: m.AllInstruments[nameWorkersQueueDepth], } } func (m *Metrics) NewAddsMetric(name string) workqueue.CounterMetric { return queueMetric{ - ctx: m.ctx, + ctx: m.Ctx, name: name, - inst: m.allInstruments[nameWorkersQueueAdds], + inst: m.AllInstruments[nameWorkersQueueAdds], } } func (m *Metrics) NewLatencyMetric(name string) workqueue.HistogramMetric { return queueMetric{ - ctx: m.ctx, + ctx: m.Ctx, name: name, - inst: m.allInstruments[nameWorkersQueueLatency], + inst: m.AllInstruments[nameWorkersQueueLatency], } } func (m *Metrics) NewWorkDurationMetric(name string) workqueue.HistogramMetric { return queueMetric{ - ctx: m.ctx, + ctx: m.Ctx, name: name, - inst: m.allInstruments[nameWorkersQueueDuration], + inst: m.AllInstruments[nameWorkersQueueDuration], } } func (m *Metrics) NewRetriesMetric(name string) workqueue.CounterMetric { return queueMetric{ - ctx: m.ctx, + ctx: m.Ctx, name: name, - inst: m.allInstruments[nameWorkersRetries], + inst: m.AllInstruments[nameWorkersRetries], } } func (m *Metrics) NewUnfinishedWorkSecondsMetric(name string) workqueue.SettableGaugeMetric { metric := queueMetric{ - ctx: m.ctx, + ctx: m.Ctx, name: name, - inst: m.allInstruments[nameWorkersUnfinishedWork], + inst: m.AllInstruments[nameWorkersUnfinishedWork], value: pointer.Float64(0.0), } - ud := metric.inst.queueUserdata() + ud := getQueueUserdata(metric.inst) ud.metrics = append(ud.metrics, metric) return metric } func (m *Metrics) NewLongestRunningProcessorSecondsMetric(name string) workqueue.SettableGaugeMetric { metric := queueMetric{ - ctx: m.ctx, + ctx: m.Ctx, name: name, - inst: m.allInstruments[nameWorkersLongestRunning], + inst: m.AllInstruments[nameWorkersLongestRunning], value: pointer.Float64(0.0), } - ud := metric.inst.queueUserdata() + ud := getQueueUserdata(metric.inst) ud.metrics = append(ud.metrics, metric) return metric } diff --git a/workflow/metrics/work_queue_test.go b/workflow/metrics/work_queue_test.go index f1a998ba7255..9c7f9766936b 100644 --- a/workflow/metrics/work_queue_test.go +++ b/workflow/metrics/work_queue_test.go @@ -7,21 +7,23 @@ import ( "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/attribute" "k8s.io/client-go/util/workqueue" + + "github.com/argoproj/argo-workflows/v3/util/telemetry" ) func TestMetricsWorkQueue(t *testing.T) { m, te, err := getSharedMetrics() require.NoError(t, err) - attribsWT := attribute.NewSet(attribute.String(labelWorkerType, "test")) + attribsWT := attribute.NewSet(attribute.String(telemetry.AttribWorkerType, "test")) - queue := m.RateLimiterWithBusyWorkers(m.ctx, workqueue.DefaultControllerRateLimiter(), "test") + queue := m.RateLimiterWithBusyWorkers(m.Ctx, workqueue.DefaultControllerRateLimiter(), "test") defer queue.ShutDown() val, err := te.GetInt64CounterValue(nameWorkersBusy, &attribsWT) require.NoError(t, err) assert.Equal(t, int64(0), val) - attribsQN := attribute.NewSet(attribute.String(labelQueueName, "test")) + attribsQN := attribute.NewSet(attribute.String(telemetry.AttribQueueName, "test")) queue.Add("A") val, err = te.GetInt64CounterValue(nameWorkersBusy, &attribsWT) require.NoError(t, err) diff --git a/workflow/templateresolution/context_test.go b/workflow/templateresolution/context_test.go index 5f136a3ef125..70abade41c04 100644 --- a/workflow/templateresolution/context_test.go +++ b/workflow/templateresolution/context_test.go @@ -191,9 +191,7 @@ func TestGetCurrentTemplateBase(t *testing.T) { // Get the template base of existing template name. tmplBase := ctx.GetCurrentTemplateBase() wftmpl, ok := tmplBase.(*wfv1.WorkflowTemplate) - if !assert.True(t, ok) { - t.Fatal("tmplBase is not a WorkflowTemplate") - } + require.True(t, ok, "tmplBase is not a WorkflowTemplate") assert.Equal(t, "base-workflow-template", wftmpl.Name) } @@ -216,9 +214,7 @@ func TestWithTemplateHolder(t *testing.T) { newCtx, err := ctx.WithTemplateHolder(&tmplHolder) require.NoError(t, err) tmplGetter, ok := newCtx.GetCurrentTemplateBase().(*wfv1.WorkflowTemplate) - if !assert.True(t, ok) { - t.Fatal("tmplBase is not a WorkflowTemplate") - } + require.True(t, ok, "tmplBase is not a WorkflowTemplate") assert.Equal(t, "base-workflow-template", tmplGetter.GetName()) // Get the template base of unexisting template name. @@ -226,9 +222,7 @@ func TestWithTemplateHolder(t *testing.T) { newCtx, err = ctx.WithTemplateHolder(&tmplHolder) require.NoError(t, err) tmplGetter, ok = newCtx.GetCurrentTemplateBase().(*wfv1.WorkflowTemplate) - if !assert.True(t, ok) { - t.Fatal("tmplBase is not a WorkflowTemplate") - } + require.True(t, ok, "tmplBase is not a WorkflowTemplate") assert.Equal(t, "base-workflow-template", tmplGetter.GetName()) // Get the template base of existing template reference. @@ -236,9 +230,7 @@ func TestWithTemplateHolder(t *testing.T) { newCtx, err = ctx.WithTemplateHolder(&tmplHolder) require.NoError(t, err) tmplGetter, ok = newCtx.GetCurrentTemplateBase().(*wfv1.WorkflowTemplate) - if !assert.True(t, ok) { - t.Fatal("tmplBase is not a WorkflowTemplate") - } + require.True(t, ok, "tmplBase is not a WorkflowTemplate") assert.Equal(t, "some-workflow-template", tmplGetter.GetName()) // Get the template base of unexisting template reference. @@ -263,9 +255,7 @@ func TestResolveTemplate(t *testing.T) { ctx, tmpl, _, err := ctx.ResolveTemplate(&tmplHolder) require.NoError(t, err) wftmpl, ok := ctx.tmplBase.(*wfv1.WorkflowTemplate) - if !assert.True(t, ok) { - t.Fatal("tmplBase is not a WorkflowTemplate") - } + require.True(t, ok, "tmplBase is not a WorkflowTemplate") assert.Equal(t, "base-workflow-template", wftmpl.Name) assert.Equal(t, "whalesay", tmpl.Name) @@ -276,9 +266,7 @@ func TestResolveTemplate(t *testing.T) { require.NoError(t, err) tmplGetter, ok = ctx.tmplBase.(*wfv1.WorkflowTemplate) - if !assert.True(t, ok) { - t.Fatal("tmplBase is not a WorkflowTemplate") - } + require.True(t, ok, "tmplBase is not a WorkflowTemplate") assert.Equal(t, "some-workflow-template", tmplGetter.GetName()) assert.Equal(t, "whalesay", tmpl.Name) assert.NotNil(t, tmpl.Container) @@ -289,9 +277,7 @@ func TestResolveTemplate(t *testing.T) { require.NoError(t, err) tmplGetter, ok = ctx.tmplBase.(*wfv1.WorkflowTemplate) - if !assert.True(t, ok) { - t.Fatal("tmplBase is not a WorkflowTemplate") - } + require.True(t, ok, "tmplBase is not a WorkflowTemplate") assert.Equal(t, "some-workflow-template", tmplGetter.GetName()) assert.Equal(t, "local-whalesay", tmpl.Name) assert.NotNil(t, tmpl.Steps) @@ -302,9 +288,7 @@ func TestResolveTemplate(t *testing.T) { require.NoError(t, err) tmplGetter, ok = ctx.tmplBase.(*wfv1.WorkflowTemplate) - if !assert.True(t, ok) { - t.Fatal("tmplBase is not a WorkflowTemplate") - } + require.True(t, ok, "tmplBase is not a WorkflowTemplate") assert.Equal(t, "some-workflow-template", tmplGetter.GetName()) assert.Equal(t, "another-whalesay", tmpl.Name) assert.NotNil(t, tmpl.Steps) @@ -317,9 +301,7 @@ func TestResolveTemplate(t *testing.T) { require.NoError(t, err) tmplGetter, ok = ctx.tmplBase.(*wfv1.WorkflowTemplate) - if !assert.True(t, ok) { - t.Fatal("tmplBase is not a WorkflowTemplate") - } + require.True(t, ok, "tmplBase is not a WorkflowTemplate") assert.Equal(t, "some-workflow-template", tmplGetter.GetName()) assert.Equal(t, "whalesay-with-arguments", tmpl.Name) @@ -331,9 +313,7 @@ func TestResolveTemplate(t *testing.T) { require.NoError(t, err) tmplGetter, ok = ctx.tmplBase.(*wfv1.WorkflowTemplate) - if !assert.True(t, ok) { - t.Fatal("tmplBase is not a WorkflowTemplate") - } + require.True(t, ok, "tmplBase is not a WorkflowTemplate") assert.Equal(t, "some-workflow-template", tmplGetter.GetName()) assert.Equal(t, "nested-whalesay-with-arguments", tmpl.Name) } @@ -348,9 +328,7 @@ func TestWithTemplateBase(t *testing.T) { // Get the template base of existing template name. newCtx := ctx.WithTemplateBase(anotherWftmpl) wftmpl, ok := newCtx.tmplBase.(*wfv1.WorkflowTemplate) - if !assert.True(t, ok) { - t.Fatal("tmplBase is not a WorkflowTemplate") - } + require.True(t, ok, "tmplBase is not a WorkflowTemplate") assert.Equal(t, "another-workflow-template", wftmpl.Name) } diff --git a/workflow/util/util_test.go b/workflow/util/util_test.go index 3e70d27b9dcd..67b1b2e84374 100644 --- a/workflow/util/util_test.go +++ b/workflow/util/util_test.go @@ -582,10 +582,9 @@ func TestApplySubmitOpts(t *testing.T) { wf := &wfv1.Workflow{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"a": "0", "b": "0"}}} err := ApplySubmitOpts(wf, &wfv1.SubmitOpts{Labels: "a=1"}) require.NoError(t, err) - if assert.Len(t, wf.GetLabels(), 2) { - assert.Equal(t, "1", wf.GetLabels()["a"]) - assert.Equal(t, "0", wf.GetLabels()["b"]) - } + require.Len(t, wf.GetLabels(), 2) + assert.Equal(t, "1", wf.GetLabels()["a"]) + assert.Equal(t, "0", wf.GetLabels()["b"]) }) t.Run("InvalidParameters", func(t *testing.T) { require.Error(t, ApplySubmitOpts(&wfv1.Workflow{}, &wfv1.SubmitOpts{Parameters: []string{"a"}})) @@ -601,10 +600,9 @@ func TestApplySubmitOpts(t *testing.T) { err := ApplySubmitOpts(wf, &wfv1.SubmitOpts{Parameters: []string{"a=81861780812"}}) require.NoError(t, err) parameters := wf.Spec.Arguments.Parameters - if assert.Len(t, parameters, 1) { - assert.Equal(t, "a", parameters[0].Name) - assert.Equal(t, "81861780812", parameters[0].Value.String()) - } + require.Len(t, parameters, 1) + assert.Equal(t, "a", parameters[0].Name) + assert.Equal(t, "81861780812", parameters[0].Value.String()) }) t.Run("PodPriorityClassName", func(t *testing.T) { wf := &wfv1.Workflow{} @@ -624,9 +622,8 @@ func TestReadParametersFile(t *testing.T) { err = ReadParametersFile(file.Name(), opts) require.NoError(t, err) parameters := opts.Parameters - if assert.Len(t, parameters, 1) { - assert.Equal(t, "a=81861780812", parameters[0]) - } + require.Len(t, parameters, 1) + assert.Equal(t, "a=81861780812", parameters[0]) } func TestFormulateResubmitWorkflow(t *testing.T) { @@ -1090,9 +1087,8 @@ func TestFormulateRetryWorkflow(t *testing.T) { require.NoError(t, err) wf, _, err = FormulateRetryWorkflow(ctx, wf, false, "", nil) require.NoError(t, err) - if assert.Len(t, wf.Status.Nodes, 1) { - assert.Equal(t, wfv1.NodeRunning, wf.Status.Nodes[""].Phase) - } + require.Len(t, wf.Status.Nodes, 1) + assert.Equal(t, wfv1.NodeRunning, wf.Status.Nodes[""].Phase) }) t.Run("Skipped and Suspended Nodes", func(t *testing.T) { wf := &wfv1.Workflow{ @@ -1123,16 +1119,15 @@ func TestFormulateRetryWorkflow(t *testing.T) { require.NoError(t, err) wf, _, err = FormulateRetryWorkflow(ctx, wf, true, "id=suspended", nil) require.NoError(t, err) - if assert.Len(t, wf.Status.Nodes, 3) { - assert.Equal(t, wfv1.NodeRunning, wf.Status.Nodes["entrypoint"].Phase) - assert.Equal(t, wfv1.NodeRunning, wf.Status.Nodes["suspended"].Phase) - assert.Equal(t, wfv1.Parameter{ - Name: "param-1", - Value: nil, - ValueFrom: &wfv1.ValueFrom{Supplied: &wfv1.SuppliedValueFrom{}}, - }, wf.Status.Nodes["suspended"].Outputs.Parameters[0]) - assert.Equal(t, wfv1.NodeSkipped, wf.Status.Nodes["skipped"].Phase) - } + require.Len(t, wf.Status.Nodes, 3) + assert.Equal(t, wfv1.NodeRunning, wf.Status.Nodes["entrypoint"].Phase) + assert.Equal(t, wfv1.NodeRunning, wf.Status.Nodes["suspended"].Phase) + assert.Equal(t, wfv1.Parameter{ + Name: "param-1", + Value: nil, + ValueFrom: &wfv1.ValueFrom{Supplied: &wfv1.SuppliedValueFrom{}}, + }, wf.Status.Nodes["suspended"].Outputs.Parameters[0]) + assert.Equal(t, wfv1.NodeSkipped, wf.Status.Nodes["skipped"].Phase) }) t.Run("Nested DAG with Non-group Node Selected", func(t *testing.T) { wf := &wfv1.Workflow{ @@ -1155,12 +1150,11 @@ func TestFormulateRetryWorkflow(t *testing.T) { wf, _, err = FormulateRetryWorkflow(ctx, wf, true, "id=3", nil) require.NoError(t, err) // Node #3, #4 are deleted and will be recreated so only 3 nodes left in wf.Status.Nodes - if assert.Len(t, wf.Status.Nodes, 3) { - assert.Equal(t, wfv1.NodeSucceeded, wf.Status.Nodes["my-nested-dag-1"].Phase) - // The parent group nodes should be running. - assert.Equal(t, wfv1.NodeRunning, wf.Status.Nodes["1"].Phase) - assert.Equal(t, wfv1.NodeRunning, wf.Status.Nodes["2"].Phase) - } + require.Len(t, wf.Status.Nodes, 3) + assert.Equal(t, wfv1.NodeSucceeded, wf.Status.Nodes["my-nested-dag-1"].Phase) + // The parent group nodes should be running. + assert.Equal(t, wfv1.NodeRunning, wf.Status.Nodes["1"].Phase) + assert.Equal(t, wfv1.NodeRunning, wf.Status.Nodes["2"].Phase) }) t.Run("Nested DAG without Node Selected", func(t *testing.T) { wf := &wfv1.Workflow{ @@ -1183,13 +1177,12 @@ func TestFormulateRetryWorkflow(t *testing.T) { wf, _, err = FormulateRetryWorkflow(ctx, wf, true, "", nil) require.NoError(t, err) // Node #2, #3, and #4 are deleted and will be recreated so only 2 nodes left in wf.Status.Nodes - if assert.Len(t, wf.Status.Nodes, 4) { - assert.Equal(t, wfv1.NodeSucceeded, wf.Status.Nodes["my-nested-dag-2"].Phase) - assert.Equal(t, wfv1.NodeSucceeded, wf.Status.Nodes["1"].Phase) - assert.Equal(t, wfv1.NodeSucceeded, wf.Status.Nodes["2"].Phase) - assert.Equal(t, wfv1.NodeSucceeded, wf.Status.Nodes["3"].Phase) - assert.Equal(t, "", string(wf.Status.Nodes["4"].Phase)) - } + require.Len(t, wf.Status.Nodes, 4) + assert.Equal(t, wfv1.NodeSucceeded, wf.Status.Nodes["my-nested-dag-2"].Phase) + assert.Equal(t, wfv1.NodeSucceeded, wf.Status.Nodes["1"].Phase) + assert.Equal(t, wfv1.NodeSucceeded, wf.Status.Nodes["2"].Phase) + assert.Equal(t, wfv1.NodeSucceeded, wf.Status.Nodes["3"].Phase) + assert.Equal(t, "", string(wf.Status.Nodes["4"].Phase)) }) t.Run("OverrideParams", func(t *testing.T) { @@ -1319,13 +1312,12 @@ func TestFormulateRetryWorkflow(t *testing.T) { wf, _, err = FormulateRetryWorkflow(ctx, wf, true, "id=4", nil) require.NoError(t, err) // Node #4 is deleted and will be recreated so only 4 nodes left in wf.Status.Nodes - if assert.Len(t, wf.Status.Nodes, 4) { - assert.Equal(t, wfv1.NodeSucceeded, wf.Status.Nodes["successful-workflow-2"].Phase) - // The parent group nodes should be running. - assert.Equal(t, wfv1.NodeRunning, wf.Status.Nodes["1"].Phase) - assert.Equal(t, wfv1.NodeSucceeded, wf.Status.Nodes["2"].Phase) - assert.Equal(t, wfv1.NodeSucceeded, wf.Status.Nodes["3"].Phase) - } + require.Len(t, wf.Status.Nodes, 4) + assert.Equal(t, wfv1.NodeSucceeded, wf.Status.Nodes["successful-workflow-2"].Phase) + // The parent group nodes should be running. + assert.Equal(t, wfv1.NodeRunning, wf.Status.Nodes["1"].Phase) + assert.Equal(t, wfv1.NodeSucceeded, wf.Status.Nodes["2"].Phase) + assert.Equal(t, wfv1.NodeSucceeded, wf.Status.Nodes["3"].Phase) }) t.Run("Retry continue on failed workflow", func(t *testing.T) { @@ -1349,11 +1341,10 @@ func TestFormulateRetryWorkflow(t *testing.T) { require.NoError(t, err) wf, podsToDelete, err := FormulateRetryWorkflow(ctx, wf, false, "", nil) require.NoError(t, err) - if assert.Len(t, wf.Status.Nodes, 4) { - assert.Equal(t, wfv1.NodeFailed, wf.Status.Nodes["2"].Phase) - assert.Equal(t, wfv1.NodeSucceeded, wf.Status.Nodes["3"].Phase) - assert.Len(t, podsToDelete, 2) - } + require.Len(t, wf.Status.Nodes, 4) + assert.Equal(t, wfv1.NodeFailed, wf.Status.Nodes["2"].Phase) + assert.Equal(t, wfv1.NodeSucceeded, wf.Status.Nodes["3"].Phase) + assert.Len(t, podsToDelete, 2) }) t.Run("Retry continue on failed workflow with restartSuccessful and nodeFieldSelector", func(t *testing.T) { @@ -1377,11 +1368,10 @@ func TestFormulateRetryWorkflow(t *testing.T) { require.NoError(t, err) wf, podsToDelete, err := FormulateRetryWorkflow(ctx, wf, true, "id=3", nil) require.NoError(t, err) - if assert.Len(t, wf.Status.Nodes, 2) { - assert.Equal(t, wfv1.NodeSucceeded, wf.Status.Nodes["1"].Phase) - assert.Equal(t, wfv1.NodeRunning, wf.Status.Nodes["continue-on-failed-workflow-2"].Phase) - assert.Len(t, podsToDelete, 4) - } + require.Len(t, wf.Status.Nodes, 2) + assert.Equal(t, wfv1.NodeSucceeded, wf.Status.Nodes["1"].Phase) + assert.Equal(t, wfv1.NodeRunning, wf.Status.Nodes["continue-on-failed-workflow-2"].Phase) + assert.Len(t, podsToDelete, 4) }) }