├── .rustfmt.toml
├── .gitignore
├── .dockerignore
├── src
├── lib.rs
├── templates.rs
├── cli.rs
├── k8s.rs
├── server.rs
└── services
│ ├── slack.rs
│ └── mod.rs
├── docs
├── content
│ ├── assets
│ │ └── demo.gif
│ ├── compatibility.md
│ ├── services
│ │ ├── slack
│ │ │ ├── manifest.yaml
│ │ │ └── index.md
│ │ └── index.md
│ ├── quickstart.md
│ └── index.md
├── requirements.txt
├── overrides
│ └── main.html
├── Dockerfile
├── ext.py
└── Makefile
├── plugin.yaml
├── LICENSE
├── Cargo.toml
├── README.md
├── Dockerfile
├── .github
└── workflows
│ ├── ci.yml
│ └── cd.yml
├── Makefile
├── mkdocs.yaml
├── manifests
├── workflow.yaml
└── install.yaml
├── tests
└── integration.rs
└── Cargo.lock
/.rustfmt.toml:
--------------------------------------------------------------------------------
1 | edition = "2021"
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | __pycache__/
3 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | **/__pycache__/
2 | target/
3 |
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | pub mod k8s;
2 | pub mod server;
3 | pub mod services;
4 | pub mod templates;
5 |
--------------------------------------------------------------------------------
/docs/content/assets/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kjagiello/hermes/HEAD/docs/content/assets/demo.gif
--------------------------------------------------------------------------------
/docs/requirements.txt:
--------------------------------------------------------------------------------
1 | mike==1.1.2
2 | mkdocs-macros-plugin==0.6.3
3 | mkdocs-material-extensions==1.0.3
4 | mkdocs-material==8.1.3
5 | mkdocs==1.2.3
6 | toml==0.10.2
7 |
--------------------------------------------------------------------------------
/docs/content/compatibility.md:
--------------------------------------------------------------------------------
1 | !!! warning "Compatibilty warning"
2 |
3 | Hermes requires Argo Workflows 3.3+ which comes with support for [template
4 | executor plugins](https://argoproj.github.io/argo-workflows/executor_plugins/).
5 |
--------------------------------------------------------------------------------
/docs/content/services/slack/manifest.yaml:
--------------------------------------------------------------------------------
1 | _metadata:
2 | major_version: 1
3 | minor_version: 1
4 | display_information:
5 | name: Hermes
6 | features:
7 | bot_user:
8 | display_name: Hermes
9 | always_online: true
10 | oauth_config:
11 | scopes:
12 | bot:
13 | - chat:write
14 | - chat:write.customize
15 | settings:
16 | org_deploy_enabled: false
17 | socket_mode_enabled: false
18 | token_rotation_enabled: false
19 |
--------------------------------------------------------------------------------
/docs/overrides/main.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block outdated %}
4 | {% if config.extra.hermes_version == "dev" %}
5 | This document is for the development version of Hermes, which can be
6 | significantly different from previous releases. For older releases, use the
7 | version selector below.
8 | {% else %}
9 | You are viewing an outdated version of the documentation.
10 |
11 | Click here to go to latest.
12 |
13 | {% endif %}
14 | {% endblock %}
15 |
--------------------------------------------------------------------------------
/docs/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.9.9-slim-buster
2 |
3 | ARG HERMES_VERSION
4 | ENV HERMES_VERSION $HERMES_VERSION
5 |
6 | ENV PIP_PIP_VERSION 21.3.1
7 | ENV APT_GIT_VERSION 1:2.20.*
8 |
9 | RUN --mount=type=cache,target=/var/cache/apt --mount=type=cache,target=/var/lib/apt \
10 | set -x && apt-get update && apt-get install --no-install-recommends -y \
11 | git=$APT_GIT_VERSION
12 |
13 | WORKDIR /app
14 | ADD docs/requirements.txt .
15 | RUN --mount=type=cache,target=/root/.cache/pip \
16 | set -x && \
17 | pip install pip==$PIP_PIP_VERSION && \
18 | pip install -r requirements.txt
19 |
--------------------------------------------------------------------------------
/plugin.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: ConfigMap
3 | metadata:
4 | name: hermes
5 | labels:
6 | workflows.argoproj.io/configmap-type: ExecutorPlugin
7 | workflows.argoproj.io/version: '>= v3.3'
8 | data:
9 | sidecar.container: |
10 | name: hermes
11 | image: ghcr.io/kjagiello/hermes:0.1.0
12 | imagePullPolicy: IfNotPresent
13 | command: ['-p', '3030']
14 | ports:
15 | - containerPort: 3030
16 | resources:
17 | limits:
18 | cpu: 200m
19 | memory: 64Mi
20 | requests:
21 | cpu: 100m
22 | memory: 32Mi
23 | securityContext:
24 | runAsNonRoot: true
25 | runAsUser: 1000
26 |
--------------------------------------------------------------------------------
/docs/ext.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | from urllib.parse import quote_plus
3 |
4 |
5 | def define_env(env):
6 | docs_dir = env.variables.config["docs_dir"]
7 | env.variables["hermes_version"] = (
8 | "master"
9 | if (version := env.variables["extra"]["hermes_version"]) == "dev"
10 | else version
11 | )
12 |
13 | @env.macro
14 | def raw_github_url(path: str) -> str:
15 | tpl = env.variables["github_raw_url_tpl"]
16 | return tpl.format(version=env.variables["hermes_version"], path=path)
17 |
18 | @env.macro
19 | def import_file(path: str) -> str:
20 | with open(Path(docs_dir) / Path(path), "r") as f:
21 | return f.read()
22 |
23 | @env.filter
24 | def urlencode(val: str) -> str:
25 | "Reverse a string (and uppercase)"
26 | return quote_plus(val)
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Krzysztof Jagiello
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "argo-hermes"
3 | version = "0.1.0"
4 | description = "Notifications plugin for your Argo workflows"
5 | authors = ["Krzysztof Jagiello "]
6 | license = "MIT"
7 | readme = "README.md"
8 | repository = "https://github.com/kjagiello/hermes"
9 | categories = ["web-programming::http-server"]
10 | keywords = ["argo", "argo-workflows", "notifications", "slack"]
11 | edition = "2021"
12 |
13 | [[bin]]
14 | name = "hermes"
15 | path = "src/cli.rs"
16 | doc = false
17 |
18 | [profile.dev]
19 | split-debuginfo = "unpacked"
20 |
21 | [profile.release]
22 | lto = true
23 | codegen-units = 1
24 |
25 | [dependencies]
26 | k8s-openapi = { version = "0.13.1", default-features = false, features = ["v1_22"] }
27 | kube = { version = "0.65.0", features = ["client"] }
28 | reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls"] }
29 | tokio = { version = "1", features = ["full"] }
30 | warp = "0.3"
31 | serde = { version = "~1.0", features = ["derive"] }
32 | serde_json = "1.0"
33 | async-trait = "0.1.52"
34 | lazy_static = "1.4.0"
35 | handlebars = "4.1.6"
36 | parking_lot = "0.11.2"
37 | as-any = "0.2.1"
38 | base64 = "0.13.0"
39 | clap = "3.0.7"
40 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Hermes -- notifications for Argo Workflows
2 |
3 | Hermes aims to provide a streamlined way of sending notifications to various
4 | messaging services from your [Argo Workflows](https://argoproj.github.io/argo-workflows/)
5 | pipelines.
6 |
7 | 
8 |
9 | ## Features
10 |
11 | - **Easy to use** – Hermes is a [template executor
12 | plugin](https://github.com/argoproj/argo-workflows/pull/7256). Once
13 | installed, Argo Workflows will automatically provide a Hermes instance for you to
14 | interact with from your workflow.
15 | - **Template system** – keep a centralized set of reusable notification
16 | templates and use them freely in your workflows.
17 | - **In-place updates** – avoid clutter in your channels by updating existing
18 | messages and keep the history of changes in a thread under the notification
19 | message instead.
20 | - **Multiple recipient support** – do you need to send notifications to different
21 | channels or services from a single workflow? No problem.
22 |
23 | ## Supported services
24 |
25 | - Slack
26 |
27 | ## Documentation
28 |
29 | Visit the [documentation](https://kjagiello.github.io/hermes) to learn how to
30 | install and use Hermes.
31 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM rust:1.58-buster as builder
2 |
3 | RUN mkdir /hermes /volume
4 | WORKDIR /hermes
5 |
6 | # Rust lacks a straightforward way to only install dependencies, so we have to fake the existence
7 | # of the project in order for this to work. The idea is basically to build a separate layer with
8 | # only the dependencies, so that we don't have to reinstall them on every source code change.
9 | # Related issue: https://github.com/rust-lang/cargo/issues/2644
10 | RUN mkdir src && touch src/lib.rs && echo "fn main() {}" > src/cli.rs
11 | COPY ./Cargo.toml .
12 | COPY ./Cargo.lock .
13 | RUN --mount=type=cache,target=/usr/local/cargo/registry \
14 | --mount=type=cache,sharing=private,target=/hermes/target \
15 | cargo build --release
16 |
17 | # Build the source code without installing the dependencies. In order to make rust pick up changes
18 | # in the source files, we have to bump their "date modified".
19 | COPY ./src/ ./src/
20 | RUN --mount=type=cache,target=/usr/local/cargo/registry \
21 | --mount=type=cache,sharing=private,target=/hermes/target \
22 | find src/ -type f -exec touch {} + \
23 | && cargo build --release \
24 | && ls -la ./target/release \
25 | && cp ./target/release/hermes /volume
26 |
27 | FROM gcr.io/distroless/cc
28 | COPY --from=builder /volume/hermes /
29 | ENTRYPOINT ["/hermes"]
30 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | pull_request:
5 |
6 | jobs:
7 | check:
8 | name: Check
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Checkout sources
12 | uses: actions/checkout@v2
13 |
14 | - name: Install toolchain
15 | uses: actions-rs/toolchain@v1
16 | with:
17 | profile: minimal
18 | toolchain: nightly
19 | override: true
20 |
21 | - name: Run cargo check
22 | uses: actions-rs/cargo@v1
23 | with:
24 | command: check
25 |
26 | test:
27 | name: Test
28 | runs-on: ubuntu-latest
29 | steps:
30 | - name: Checkout sources
31 | uses: actions/checkout@v2
32 |
33 | - name: Install toolchain
34 | uses: actions-rs/toolchain@v1
35 | with:
36 | toolchain: nightly
37 | profile: minimal
38 | override: true
39 |
40 | - name: Run cargo test
41 | uses: actions-rs/cargo@v1
42 | with:
43 | command: test
44 |
45 | lint:
46 | name: Lint
47 | runs-on: ubuntu-latest
48 | steps:
49 | - name: Checkout sources
50 | uses: actions/checkout@v2
51 |
52 | - name: Install toolchain
53 | uses: actions-rs/toolchain@v1
54 | with:
55 | profile: minimal
56 | toolchain: nightly
57 | override: true
58 | components: rustfmt
59 |
60 | - name: Run cargo fmt
61 | uses: actions-rs/cargo@v1
62 | with:
63 | command: fmt
64 | args: -- --check
65 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | VERSION := latest
2 | IMAGE_NAMESPACE := ghcr.io/kjagiello
3 | IMAGE_NAME := hermes
4 |
5 | KUBECTX := $(shell [[ "`which kubectl`" != '' ]] && kubectl config current-context || echo none)
6 | K3D := $(shell [[ "$(KUBECTX)" == "k3d-"* ]] && echo true || echo false)
7 | K3D_CLUSTER_NAME ?= k3s-default
8 | SED := $(shell [[ "`which gsed`" != '' ]] && echo "gsed" || echo "sed")
9 |
10 | require_var = $(if $(value $(1)),,$(error $(1) not set))
11 |
12 | .PHONY: build-image
13 | build-image: PLATFORM=linux/amd64
14 | build-image:
15 | docker buildx install
16 | docker build \
17 | --load \
18 | --platform $(PLATFORM) \
19 | --cache-from "type=local,src=/tmp/.buildx-cache" \
20 | --cache-to "type=local,dest=/tmp/.buildx-cache" \
21 | -t $(IMAGE_NAMESPACE)/$(IMAGE_NAME):$(VERSION) \
22 | .
23 |
24 | .PHONY: install-image
25 | install-image: build-image
26 | if [ $(K3D) = true ]; then k3d image import -c $(K3D_CLUSTER_NAME) $(IMAGE_NAMESPACE)/$(IMAGE_NAME):$(VERSION); fi
27 |
28 | .PHONY: install
29 | install: SLACK_TOKEN=
30 | install: install-image
31 | $(call require_var,SLACK_TOKEN)
32 | envsubst < manifests/install.yaml | kubectl apply -f -
33 |
34 | .PHONY: update-version
35 | update-version: VERSION=
36 | update-version:
37 | $(call require_var,VERSION)
38 | @$(SED) -i -E "s/^version = \".+\"/version = \"$$VERSION\"/g" Cargo.toml
39 | @$(SED) -i -E "0,/version = \".+\"/s//version = \"$$VERSION\"/" Cargo.lock
40 | @$(SED) -i -E "s/ghcr\.io\/.+:[^\s]+/ghcr.io\/kjagiello\/hermes:$$VERSION/" plugin.yaml
41 | @echo "The version has updated to $$VERSION"
42 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | IMAGE_NAME := hermes-docs:latest
2 |
3 | # Extract the app version from Cargo.toml as default
4 | HERMES_VERSION ?= $(shell \
5 | cargo metadata \
6 | --format-version 1 \
7 | --no-deps \
8 | --manifest-path ../Cargo.toml \
9 | | jq -r '.packages[0].version' \
10 | )
11 |
12 | ifeq ($(HERMES_VERSION),)
13 | $(error Could not determine the version of Hermes)
14 | endif
15 |
16 | DOCKER_RUN_PORT := 8000
17 | DOCKER_RUN_MOUNTS := -v $(CURDIR):/app/docs \
18 | -v $(CURDIR)/../plugin.yaml:/app/plugin.yaml \
19 | -v $(CURDIR)/../mkdocs.yaml:/app/mkdocs.yaml
20 | DOCKER_RUN_OPTS := --rm \
21 | $(DOCKER_RUN_MOUNTS) \
22 | -p $(DOCKER_RUN_PORT):8000 \
23 | -e HERMES_VERSION=$(HERMES_VERSION)
24 |
25 | .PHONY: build
26 | build:
27 | docker buildx install
28 | docker build \
29 | --load \
30 | --cache-from "type=local,src=/tmp/.buildx-cache" \
31 | --cache-to "type=local,dest=/tmp/.buildx-cache" \
32 | --build-arg HERMES_VERSION=$(HERMES_VERSION) \
33 | -f ./Dockerfile \
34 | -t $(IMAGE_NAME) \
35 | ..
36 |
37 | .PHONY: serve
38 | serve: build
39 | docker run $(DOCKER_RUN_OPTS) $(IMAGE_NAME) mkdocs serve --dev-addr 0.0.0.0:8000
40 |
41 | .PHONY: deploy
42 | deploy: DOCKER_RUN_OPTS += -v $(CURDIR)/../.git:/app/.git
43 | deploy: build
44 | docker run $(DOCKER_RUN_OPTS) $(IMAGE_NAME) \
45 | mike deploy -F mkdocs.yaml \
46 | $(OPTS) $(VERSION) $(ALIAS)
47 | @if [ -n "$(SET_DEFAULT)" ]; then \
48 | docker run $(DOCKER_RUN_OPTS) $(IMAGE_NAME) \
49 | mike set-default -F mkdocs.yaml $(VERSION); \
50 | fi
51 |
52 | .PHONY: shell
53 | shell: build
54 | docker run -it $(DOCKER_RUN_OPTS) $(IMAGE_NAME) bash
55 |
--------------------------------------------------------------------------------
/mkdocs.yaml:
--------------------------------------------------------------------------------
1 | site_name: Hermes
2 | site_description: Notifications for your Argo Workflows
3 | site_url: https://kjagiello.github.io/hermes/
4 |
5 | repo_name: kjagiello/hermes
6 | repo_url: https://github.com/kjagiello/hermes
7 | edit_uri: ""
8 |
9 | docs_dir: docs/content
10 |
11 | theme:
12 | name: material
13 | palette:
14 | scheme: "default"
15 | primary: "black"
16 | accent: "indigo"
17 | icon:
18 | logo: material/send
19 | features:
20 | - content.code.annotate
21 | - navigation.tracking
22 | - navigation.tabs
23 | - navigation.tabs.sticky
24 | - navigation.instant
25 | - navigation.sections
26 | custom_dir: docs/overrides
27 |
28 | nav:
29 | - Home:
30 | - Introduction: "index.md"
31 | - Quickstart: "quickstart.md"
32 | - Services:
33 | - Introduction to services: "services/index.md"
34 | - Supported services:
35 | - Slack: "services/slack/index.md"
36 |
37 | extra:
38 | version:
39 | provider: mike
40 | social:
41 | - icon: fontawesome/brands/github
42 | link: https://github.com/kjagiello/hermes
43 | github_raw_url_tpl: https://raw.githubusercontent.com/kjagiello/hermes/{version}/{path}
44 | hermes_version: !ENV HERMES_VERSION
45 |
46 | markdown_extensions:
47 | - admonition
48 | - pymdownx.details
49 | - pymdownx.highlight
50 | - pymdownx.inlinehilite
51 | - pymdownx.snippets:
52 | check_paths: true
53 | - pymdownx.superfences
54 | - pymdownx.tabbed:
55 | alternate_style: true
56 | - pymdownx.emoji:
57 | emoji_index: !!python/name:materialx.emoji.twemoji
58 | emoji_generator: !!python/name:materialx.emoji.to_svg
59 | - attr_list
60 | - def_list
61 | - md_in_html
62 |
63 | plugins:
64 | - search
65 | - macros:
66 | module_name: docs/ext
67 | include_dir: docs/
68 |
--------------------------------------------------------------------------------
/manifests/workflow.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: argoproj.io/v1alpha1
2 | kind: Workflow
3 | metadata:
4 | generateName: notifications-test-
5 | spec:
6 | entrypoint: main
7 | templates:
8 | - name: main
9 | steps:
10 | - - name: setup-notifications
11 | template: hermes-setup
12 |
13 | - - name: pre-notification
14 | template: hermes-notify
15 | arguments:
16 | parameters:
17 | - name: message
18 | value: "Deployment started :hourglass_flowing_sand:"
19 |
20 | - - name: hello
21 | template: hello
22 |
23 | - - name: post-notification
24 | template: hermes-notify
25 | arguments:
26 | parameters:
27 | - name: message
28 | value: "Deployment succeeded :white_check_mark:"
29 |
30 | - name: hermes-setup
31 | plugin:
32 | hermes:
33 | setup:
34 | alias: default
35 | service: slack
36 | config:
37 | token: slack-token
38 | icon_emoji: ":rocket:"
39 |
40 | - name: hermes-notify
41 | inputs:
42 | parameters:
43 | - name: message
44 | plugin:
45 | hermes:
46 | notify:
47 | target: default
48 | template: hermes-template-slack-default
49 | config:
50 | channel: kjagiello-sandbox
51 | context:
52 | message: "{{inputs.parameters.message}}"
53 | app: hermes
54 | env: prod
55 | revision_sha: "deadbeef"
56 | revision_url: "https://google.com"
57 | log_url: "https://google.com"
58 |
59 | - name: hello
60 | container:
61 | image: docker/whalesay
62 | command: [cowsay]
63 | args: ["hello world"]
64 |
--------------------------------------------------------------------------------
/src/templates.rs:
--------------------------------------------------------------------------------
1 | use async_trait::async_trait;
2 | use std::collections::HashMap;
3 | use std::fmt;
4 | use std::sync::Arc;
5 |
6 | pub enum TemplateError {
7 | /// The template was not found
8 | NotFound,
9 | /// The template does not comply with the expected format of the template (a key -> template
10 | /// mapping)
11 | InvalidFormat(String),
12 | /// Any other error that might happen during the retrieval, i.e. failing to retrieve a
13 | /// ConfigMap from Kubernetes
14 | GenericError(String),
15 | }
16 |
17 | impl fmt::Display for TemplateError {
18 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
19 | match &*self {
20 | TemplateError::NotFound => write!(f, "Template not found"),
21 | TemplateError::InvalidFormat(s) => write!(f, "Invalid template format: {}", s),
22 | TemplateError::GenericError(s) => write!(f, "{}", s),
23 | }
24 | }
25 | }
26 |
27 | /// A notification template
28 | ///
29 | /// A simple template can consist of multiple sub-templates. This allows for the services to have
30 | /// more flexibility in how to handle the notifications. For example, the provided Slack service
31 | /// is expecting a "primary" and a "secondary" template, where the primary one is used to
32 | /// create/update a detailed channel message and the "secondary" to provide a less detailed message
33 | /// in the thread under the channel message.
34 | pub type Template = HashMap;
35 |
36 | /// Provides a way to fetch templates
37 | #[async_trait]
38 | pub trait TemplateRegistry: Sync + Send {
39 | /// Retrieves a template
40 | ///
41 | /// # Arguments
42 | ///
43 | /// * `name` - The name of the template to retrieve
44 | async fn get(&self, name: &str) -> Result, TemplateError>;
45 | }
46 |
47 | pub type TemplateRegistryRef = Arc;
48 |
--------------------------------------------------------------------------------
/docs/content/quickstart.md:
--------------------------------------------------------------------------------
1 | # Quick Start
2 |
3 | {% include "content/compatibility.md" %}
4 |
5 | ## Install Hermes
6 |
7 | Install Hermes by creating following the ConfigMap in your cluster:
8 |
9 | === "plugin.yaml"
10 |
11 | ```yaml
12 | --8<-- "plugin.yaml"
13 | ```
14 |
15 | === "kubectl"
16 |
17 | ```yaml
18 | kubectl apply -f \
19 | {{ raw_github_url("plugin.yaml") }}
20 | ```
21 |
22 | !!! hint
23 |
24 | Keep in mind that template executor plugins run as containers within a
25 | single pod, thus port collisions can occur. If your encounter this issue,
26 | you might have to adjust the port in the plugin manifest of Hermes.
27 |
28 | ## Service account
29 |
30 | Authentication tokens for the different services are passed to Hermes as
31 | secrets, which in turn requires that Hermes is able to fetch them using the
32 | Kubernetes API. Argo Workflows, by default, uses a service account with limited
33 | permissions, so in order to successfully run Hermes you will have to create a
34 | custom Role for your workflow that grants the `get` permission to the secrets
35 | needed by Hermes.
36 |
37 | See an example below:
38 |
39 | ```yaml
40 | ---
41 | # Role
42 | apiVersion: rbac.authorization.k8s.io/v1
43 | kind: Role
44 | metadata:
45 | name: workflow-role
46 | rules:
47 | # Pod get/watch is used to identify the container IDs of the current pod.
48 | # Pod patch is used to annotate the step's outputs back to controller (e.g. artifact location).
49 | - apiGroups:
50 | - ""
51 | verbs:
52 | - get
53 | - watch
54 | - patch
55 | resources:
56 | - pods
57 | # Logs get/watch are used to get the pods logs for script outputs, and for log archival
58 | - apiGroups:
59 | - ""
60 | verbs:
61 | - get
62 | - watch
63 | resources:
64 | - pods/log
65 | # Access to secrets
66 | - apiGroups:
67 | - ""
68 | verbs:
69 | - get
70 | resources:
71 | - secrets
72 | resourceNames:
73 | # List your secrets here
74 | - ...
75 |
76 | ---
77 | # RoleBinding
78 | apiVersion: rbac.authorization.k8s.io/v1
79 | kind: RoleBinding
80 | metadata:
81 | name: workflow-permissions
82 | roleRef:
83 | apiGroup: rbac.authorization.k8s.io
84 | kind: Role
85 | name: workflow-role
86 | subject:
87 | kind: ServiceAccount
88 | name: workflow-sa
89 |
90 | ---
91 | # ServiceAccount
92 | apiVersion: v1
93 | kind: ServiceAccount
94 | metadata:
95 | name: workflow-sa
96 | ```
97 |
98 | ## What's next?
99 |
100 | Now that Hermes is installed it is time to take a look on how to send some
101 | notifications. In order to do that, let's get yourself familiarized with
102 | [services](services/index.md).
103 |
--------------------------------------------------------------------------------
/src/cli.rs:
--------------------------------------------------------------------------------
1 | use argo_hermes::k8s::templates::K8sTemplateRegistry;
2 | use argo_hermes::server::filters;
3 | use argo_hermes::services::registries::DefaultServiceRegistry;
4 | use clap::{App, Arg};
5 | use std::io;
6 | use std::net::SocketAddr;
7 | use std::net::ToSocketAddrs;
8 | use std::process;
9 |
10 | const PKG_NAME: &str = env!("CARGO_PKG_NAME");
11 | const PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
12 | const PKG_AUTHORS: &str = env!("CARGO_PKG_AUTHORS");
13 |
14 | fn main() -> io::Result<()> {
15 | let matches = App::new(PKG_NAME)
16 | .bin_name(PKG_NAME)
17 | .version(PKG_VERSION)
18 | .author(PKG_AUTHORS)
19 | .about("Notifications for your Argo Workflows.")
20 | .arg(
21 | Arg::new("host")
22 | .short('h')
23 | .takes_value(true)
24 | .help("Host to bind Hermes to [default: 0.0.0.0]"),
25 | )
26 | .arg(
27 | Arg::new("port")
28 | .short('p')
29 | .takes_value(true)
30 | .help("Port to bind Hermes to [default: 3030]"),
31 | )
32 | .get_matches();
33 | let port: u16 = matches
34 | .value_of("port")
35 | .unwrap_or("3030")
36 | .parse()
37 | .unwrap_or_else(|_| {
38 | println!("Specified port is not in the valid range (1-65535)");
39 | process::exit(1);
40 | });
41 | let addr: SocketAddr = {
42 | let default_host = format!("0.0.0.0:{}", port);
43 | matches
44 | .value_of("host")
45 | .map(|host| format!("{}:{}", host, port))
46 | .unwrap_or(default_host)
47 | .to_socket_addrs()
48 | .unwrap_or_else(|err| {
49 | println!("Specified host is not valid: {}", err);
50 | process::exit(1);
51 | })
52 | .next()
53 | .unwrap_or_else(|| {
54 | println!("The given host was not resolvable");
55 | process::exit(1);
56 | })
57 | };
58 | serve(addr);
59 | Ok(())
60 | }
61 |
62 | #[tokio::main]
63 | async fn serve(addr: SocketAddr) {
64 | let service_registry = DefaultServiceRegistry::with_default_services();
65 | let template_registry = K8sTemplateRegistry::new()
66 | .await
67 | .expect("Failed to init k8s template registry");
68 |
69 | let api = filters::routes(service_registry, template_registry);
70 | let (addr, server) = warp::serve(api)
71 | .try_bind_with_graceful_shutdown(addr, async {
72 | tokio::signal::ctrl_c()
73 | .await
74 | .expect("http_server: Failed to listen for CRTL+c");
75 | println!("Shutting down the server");
76 | })
77 | .unwrap_or_else(|e| {
78 | println!("Failed to start the server: {}", e);
79 | std::process::exit(1);
80 | });
81 |
82 | println!("Starting the server at {:?}", addr);
83 | tokio::task::spawn(server).await.expect("hello ");
84 | }
85 |
--------------------------------------------------------------------------------
/manifests/install.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: ConfigMap
3 | metadata:
4 | name: hermes-template-slack-default
5 | data:
6 | primary: |
7 | {
8 | "text": "{{message}}",
9 | "blocks": [
10 | {
11 | "type": "section",
12 | "text": {
13 | "type": "mrkdwn",
14 | "text": "{{message}}"
15 | }
16 | },
17 | {
18 | "type": "section",
19 | "fields": [
20 | {
21 | "type": "mrkdwn",
22 | "text": "*Application*\n{{app}}"
23 | },
24 | {
25 | "type": "mrkdwn",
26 | "text": "*Environment*\n{{env}}"
27 | }
28 | ]
29 | },
30 | {
31 | "type": "section",
32 | "fields": [
33 | {
34 | "type": "mrkdwn",
35 | "text": "*Revision*\n<{{revision_url}}|{{revision_sha}}>"
36 | }
37 | ]
38 | },
39 | {
40 | "type": "context",
41 | "elements": [
42 | {
43 | "type": "mrkdwn",
44 | "text": "<{{log_url}}|View pipeline logs>"
45 | }
46 | ]
47 | }
48 | ]
49 | }
50 | secondary: |
51 | {"text": "{{message}}"}
52 | ---
53 | apiVersion: v1
54 | kind: Secret
55 | metadata:
56 | name: slack-token
57 | stringData:
58 | token: $SLACK_TOKEN
59 | ---
60 | kind: Role
61 | apiVersion: rbac.authorization.k8s.io/v1
62 | metadata:
63 | namespace: argo
64 | name: secret-access
65 | rules:
66 | - apiGroups: [""]
67 | resources: ["secrets"]
68 | verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
69 | ---
70 | kind: RoleBinding
71 | apiVersion: rbac.authorization.k8s.io/v1
72 | metadata:
73 | name: secret-access-binding
74 | namespace: argo
75 | subjects:
76 | - kind: ServiceAccount
77 | name: default
78 | namespace: argo
79 | roleRef:
80 | kind: Role
81 | name: secret-access
82 | apiGroup: rbac.authorization.k8s.io
83 | ---
84 | apiVersion: v1
85 | kind: ConfigMap
86 | metadata:
87 | name: hermes
88 | labels:
89 | workflows.argoproj.io/configmap-type: ExecutorPlugin
90 | annotations:
91 | workflows.argoproj.io/description: |
92 | This plugin sends a Slack message.
93 | You must create a secret:
94 | ```yaml
95 | apiVersion: v1
96 | kind: Secret
97 | metadata:
98 | name: slack-executor-plugin
99 | stringData:
100 | URL: https://hooks.slack.com/services/.../.../...
101 | ```
102 | Example:
103 | ```yaml
104 | apiVersion: argoproj.io/v1alpha1
105 | kind: Workflow
106 | metadata:
107 | generateName: slack-example-
108 | spec:
109 | entrypoint: main
110 | templates:
111 | - name: main
112 | plugin:
113 | slack:
114 | text: "{{workflow.name}} finished!"
115 | ```
116 | workflows.argoproj.io/version: '>= v3.3'
117 | data:
118 | sidecar.container: |
119 | name: hermes
120 | image: ghcr.io/kjagiello/hermes:latest
121 | imagePullPolicy: IfNotPresent
122 | ports:
123 | - containerPort: 3030
124 | resources:
125 | limits:
126 | cpu: 200m
127 | memory: 64Mi
128 | requests:
129 | cpu: 100m
130 | memory: 32Mi
131 | securityContext:
132 | runAsNonRoot: true
133 | runAsUser: 1000
134 |
--------------------------------------------------------------------------------
/docs/content/index.md:
--------------------------------------------------------------------------------
1 | # Hermes – notifications for Argo Workflows
2 |
3 | Hermes aims to provide a streamlined way of sending notifications to various
4 | messaging services from your [Argo Workflows](https://argoproj.github.io/argo-workflows/)
5 | pipelines.
6 |
7 |
8 | 
9 | An example of a Slack notification sent using Hermes
10 |
11 |
12 | ## Features
13 |
14 | - **Easy to use** – Hermes is a [template executor
15 | plugin](https://github.com/argoproj/argo-workflows/pull/7256). Once
16 | installed, Argo Workflows will automatically provide a Hermes instance for you to
17 | interact with from your workflow.
18 | - **Template system** – keep a centralized set of reusable notification
19 | templates and use them freely in your workflows.
20 | - **In-place updates** – avoid clutter in your channels by updating existing
21 | messages and keep the history of changes in a thread under the notification
22 | message instead.
23 | - **Multiple recipient support** – do you need to send notifications to different
24 | channels or services from a single workflow? No problem.
25 |
26 | ## Quickstart
27 |
28 | {% include "content/compatibility.md" %}
29 |
30 | Keen to take Hermes for a spin? Go ahead and visit the [quickstart guide](quickstart.md).
31 |
32 | ## Usage example
33 |
34 | In case you need some more convincing before you give Hermes a chance, take a
35 | look at an example workflow that sends the notifications shown in the demo
36 | above.
37 |
38 | ```yaml
39 | {% raw %}
40 | apiVersion: argoproj.io/v1alpha1
41 | kind: Workflow
42 | metadata:
43 | generateName: notifications-test-
44 | spec:
45 | entrypoint: main
46 | templates:
47 | - name: main
48 | steps:
49 | - - name: setup-notifications
50 | template: hermes-setup
51 |
52 | - - name: pre-notification
53 | template: hermes-notify
54 | arguments:
55 | parameters:
56 | - name: message
57 | value: "Deployment started :hourglass_flowing_sand:"
58 |
59 | - - name: hello
60 | template: hello
61 |
62 | - - name: post-notification
63 | template: hermes-notify
64 | arguments:
65 | parameters:
66 | - name: message
67 | value: "Deployment succeeded :white_check_mark:"
68 |
69 | - name: hermes-setup
70 | plugin:
71 | hermes:
72 | setup:
73 | alias: default
74 | service: slack
75 | config:
76 | token: slack-token
77 | icon_emoji: ":rocket:"
78 |
79 | - name: hermes-notify
80 | inputs:
81 | parameters:
82 | - name: message
83 | plugin:
84 | hermes:
85 | notify:
86 | target: default
87 | template: hermes-template-slack-default
88 | config:
89 | channel: sandbox
90 | context:
91 | message: "{{inputs.parameters.message}}"
92 | app: hermes
93 | env: prod
94 | revision_sha: "deadbeef"
95 | revision_url: "http://github.com/..."
96 | log_url: "http://github.com/..."
97 |
98 | - name: hello
99 | container:
100 | image: docker/whalesay
101 | command: [cowsay]
102 | args: ["hello world"]
103 | {% endraw %}
104 | ```
105 |
106 | If this managed to catch your interest, learn how to setup Hermes using the [quickstart guide](quickstart.md).
107 |
--------------------------------------------------------------------------------
/src/k8s.rs:
--------------------------------------------------------------------------------
1 | use kube::api::Api;
2 | use kube::Client;
3 |
4 | pub mod templates {
5 | use super::*;
6 | use crate::templates::{Template, TemplateError, TemplateRegistry};
7 | use async_trait::async_trait;
8 | use k8s_openapi::api::core::v1::ConfigMap;
9 | use parking_lot::Mutex;
10 | use std::collections::HashMap;
11 | use std::sync::Arc;
12 |
13 | /// A template registry backed by the Kubernetes ConfigMaps
14 | ///
15 | /// As there is no practical reason (AFAICS) for the templates to change during a workflow run,
16 | /// they are fetched only once and then cached for the lifetime of the process.
17 | pub struct K8sTemplateRegistry {
18 | templates: Arc>>>,
19 | client: Client,
20 | }
21 |
22 | impl K8sTemplateRegistry {
23 | pub async fn new() -> Result, String> {
24 | Ok(Arc::from(Self {
25 | templates: Arc::new(Mutex::new(HashMap::new())),
26 | client: Client::try_default()
27 | .await
28 | .map_err(|e| format!("Kubernetes client error: {:#?}", e))?,
29 | }))
30 | }
31 | }
32 |
33 | #[async_trait]
34 | impl TemplateRegistry for K8sTemplateRegistry {
35 | /// Retrieves a ConfigMap template
36 | ///
37 | /// # Arguments
38 | ///
39 | /// * `name` - The name of the ConfigMap to retrieve the template from
40 | async fn get(&self, name: &str) -> Result, TemplateError> {
41 | // Check for the template in the cache
42 | let cached_template = {
43 | let registry = self.templates.lock();
44 | registry.get(name).cloned()
45 | };
46 | let (cache, template) = match cached_template {
47 | Some(t) => (false, t),
48 | None => {
49 | // Template was not found in the cache. Retrieve the ConfigMap
50 | let client: Api = Api::default_namespaced(self.client.clone());
51 | let raw_template = client
52 | .get(name)
53 | .await
54 | .map_err(|e| {
55 | TemplateError::GenericError(format!(
56 | "Failed to retrieve ConfigMap: {}",
57 | e
58 | ))
59 | })
60 | .and_then(|cm| {
61 | cm.data.ok_or_else(|| {
62 | TemplateError::GenericError("ConfigMap missing data".into())
63 | })
64 | })?;
65 | let template: Arc = Arc::new(raw_template.into_iter().collect());
66 | (true, template)
67 | }
68 | };
69 |
70 | if cache {
71 | // Store it in the cache
72 | let mut registry = self.templates.lock();
73 | registry.insert(name.into(), template.clone());
74 | }
75 |
76 | Ok(template)
77 | }
78 | }
79 | }
80 |
81 | pub mod secrets {
82 | use super::*;
83 | use k8s_openapi::api::core::v1::Secret;
84 | use std::collections::BTreeMap;
85 |
86 | fn decode(secret: &Secret) -> BTreeMap {
87 | let mut res = BTreeMap::new();
88 | if let Some(data) = secret.data.clone() {
89 | for (k, v) in data {
90 | // Accept only data that cleanly converts to utf-8
91 | if let Ok(b) = std::str::from_utf8(&v.0) {
92 | res.insert(k, b.to_string());
93 | }
94 | }
95 | }
96 | res
97 | }
98 |
99 | pub async fn get_secret(name: &str) -> Result
100 | where
101 | T: serde::de::DeserializeOwned,
102 | {
103 | let client = Client::try_default()
104 | .await
105 | .map_err(|e| format!("Kubernetes client error: {:#?}", e))?;
106 | let client: Api = Api::default_namespaced(client);
107 | let secret = client
108 | .get(name)
109 | .await
110 | .map_err(|e| format!("Failed to retrieve Secret: {}", e))
111 | .as_ref()
112 | .map(decode)?;
113 | let value = serde_json::to_value(secret)
114 | .map_err(|e| format!("Could not parse the secret: {}", e))?;
115 | let concrete = serde_json::from_value(value)
116 | .map_err(|e| format!("Could not map the secret: {}", e))?;
117 | Ok(concrete)
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/.github/workflows/cd.yml:
--------------------------------------------------------------------------------
1 | name: CD
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | tags:
8 | - 'v*'
9 |
10 | jobs:
11 | validate-release:
12 | name: Validate release
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - name: Checkout sources
17 | uses: actions/checkout@v2
18 |
19 | - name: Install dependencies
20 | run: pip install yq
21 |
22 | - name: Extract version
23 | run: |
24 | version=$(echo ${{github.ref_name}} | cut -c2-)
25 | echo "VERSION=$version" >> $GITHUB_ENV
26 |
27 | - name: Validate the crate version in Cargo.toml
28 | run: |
29 | CRATE_VERSION=$(tomlq -r '.package.version' Cargo.toml)
30 | if [[ $CRATE_VERSION != $VERSION ]]; then
31 | echo "Crate version did not match the release tag"
32 | exit 1
33 | fi
34 | if: ${{ startsWith(github.ref, 'refs/tags/v') }}
35 |
36 | - name: Validate the crate version in Cargo.lock
37 | run: |
38 | CRATE_LOCK_VERSION=$(
39 | tomlq -r '.package[] | select(.name == "argo-hermes") | .version' Cargo.lock
40 | )
41 | if [[ $CRATE_LOCK_VERSION != $VERSION ]]; then
42 | echo "Crate lock version did not match the release tag"
43 | exit 1
44 | fi
45 | if: ${{ startsWith(github.ref, 'refs/tags/v') }}
46 |
47 | - name: Validate the image tag in plugin.yaml
48 | run: |
49 | IMAGE_TAG=$(
50 | yq -r '.data."sidecar.container"' plugin.yaml \
51 | | yq -r .image | cut -d':' -f2
52 | )
53 | if [[ $IMAGE_TAG != $VERSION ]]; then
54 | echo "Image tag did not match the release tag"
55 | exit 1
56 | fi
57 | if: ${{ startsWith(github.ref, 'refs/tags/v') }}
58 |
59 | release-image:
60 | name: Build and push image
61 | runs-on: ubuntu-latest
62 | needs: validate-release
63 | permissions:
64 | packages: write
65 | contents: read
66 |
67 | steps:
68 | - name: Checkout sources
69 | uses: actions/checkout@v2
70 |
71 | - name: Log in to GitHub Docker Registry
72 | uses: docker/login-action@v1
73 | with:
74 | registry: ghcr.io
75 | username: ${{ github.repository_owner }}
76 | password: ${{ secrets.GITHUB_TOKEN }}
77 |
78 | - name: Docker meta
79 | id: meta
80 | uses: docker/metadata-action@v3
81 | with:
82 | images: ghcr.io/${{ github.repository }}
83 | tags: |
84 | type=edge
85 | type=semver,pattern={{version}}
86 | type=semver,pattern={{major}}.{{minor}}
87 |
88 | - name: Set up Docker Buildx
89 | uses: docker/setup-buildx-action@v1
90 |
91 | - name: Build container image
92 | uses: docker/build-push-action@v2
93 | with:
94 | push: true
95 | cache-from: type=gha
96 | cache-to: type=gha,mode=max
97 | tags: ${{ steps.meta.outputs.tags }}
98 | labels: ${{ steps.meta.outputs.labels }}
99 |
100 | release-docs:
101 | name: Build and push docs
102 | needs: validate-release
103 | runs-on: ubuntu-latest
104 |
105 | steps:
106 | - name: Checkout sources
107 | uses: actions/checkout@v2
108 | with:
109 | persist-credentials: false
110 | fetch-depth: 0
111 |
112 | - name: Extract version
113 | id: raw_version
114 | run: |
115 | version=$(echo ${{github.ref_name}} | cut -c2-)
116 | echo "::set-output name=version::$version"
117 |
118 | - name: Split version code
119 | uses: xom9ikk/split@v1
120 | id: version
121 | with:
122 | string: ${{ steps.raw_version.outputs.version }}
123 | separator: .
124 |
125 | - name: Set up Docker Buildx
126 | uses: docker/setup-buildx-action@v1
127 |
128 | - name: Setup Git user
129 | run: |
130 | git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
131 | git config --local user.name "github-actions[bot]"
132 |
133 | - name: Build and commit the docs (on release)
134 | if: ${{ startsWith(github.ref, 'refs/tags/v') }}
135 | working-directory: docs/
136 | run: |
137 | make deploy \
138 | VERSION=${{ steps.version.outputs._0 }}.${{ steps.version.outputs._1 }} \
139 | ALIAS=latest \
140 | OPTS=--update-aliases \
141 | SET_DEFAULT=true
142 |
143 | - name: Build and commit the docs (dev)
144 | if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
145 | working-directory: docs/
146 | run: |
147 | make deploy VERSION=dev HERMES_VERSION=dev
148 |
149 | - name: Checkout the gh-pages branch
150 | run: git checkout gh-pages
151 |
152 | - name: Push changes
153 | uses: ad-m/github-push-action@master
154 | with:
155 | github_token: ${{ secrets.GITHUB_TOKEN }}
156 | branch: gh-pages
157 |
158 | create-release:
159 | name: Create a new release
160 | runs-on: ubuntu-latest
161 | if: ${{ startsWith(github.ref, 'refs/tags/v') }}
162 | needs:
163 | - release-image
164 | - release-docs
165 | permissions:
166 | contents: write
167 |
168 | steps:
169 | - uses: ncipollo/release-action@v1
170 | with:
171 | token: ${{ secrets.GITHUB_TOKEN }}
172 | generateReleaseNotes: true
173 |
--------------------------------------------------------------------------------
/src/server.rs:
--------------------------------------------------------------------------------
1 | pub mod filters {
2 | use super::handlers;
3 | use crate::services::ServiceRegistryRef;
4 | use crate::templates::TemplateRegistryRef;
5 | use warp::Filter;
6 |
7 | pub fn routes(
8 | service_registry: ServiceRegistryRef,
9 | template_registry: TemplateRegistryRef,
10 | ) -> impl warp::Filter + Clone {
11 | template_execute(service_registry, template_registry)
12 | }
13 |
14 | fn template_execute(
15 | service_registry: ServiceRegistryRef,
16 | template_registry: TemplateRegistryRef,
17 | ) -> impl warp::Filter + Clone {
18 | warp::path!("api" / "v1" / "template.execute")
19 | .and(warp::post())
20 | .and(warp::body::json())
21 | .and(with_service_registry(service_registry))
22 | .and(with_template_registry(template_registry))
23 | .and_then(handlers::dispatch)
24 | }
25 |
26 | fn with_service_registry(
27 | registry: ServiceRegistryRef,
28 | ) -> impl Filter + Clone
29 | {
30 | warp::any().map(move || registry.clone())
31 | }
32 |
33 | fn with_template_registry(
34 | registry: TemplateRegistryRef,
35 | ) -> impl Filter + Clone
36 | {
37 | warp::any().map(move || registry.clone())
38 | }
39 | }
40 |
41 | mod handlers {
42 | use super::models;
43 | use crate::services::{Notification, ServiceRegistryRef};
44 | use crate::templates::TemplateRegistryRef;
45 | use std::convert::Infallible;
46 |
47 | type CommandResult = Result;
48 |
49 | pub async fn dispatch(
50 | input: models::Input,
51 | service_registry: ServiceRegistryRef,
52 | template_registry: TemplateRegistryRef,
53 | ) -> Result {
54 | let result = match input.template.plugin.hermes {
55 | models::Command::Setup(models::CommandSetup { setup: config }) => {
56 | setup(config, service_registry).await
57 | }
58 | models::Command::Notify(models::CommandNotify { notify: config }) => {
59 | notify(config, service_registry, template_registry).await
60 | }
61 | };
62 | let response = result
63 | .map(|m| {
64 | warp::reply::json(&models::Response {
65 | node: models::Node {
66 | phase: "Succeeded".into(),
67 | message: m,
68 | },
69 | })
70 | })
71 | .unwrap_or_else(|m| {
72 | warp::reply::json(&models::Response {
73 | node: models::Node {
74 | phase: "Failed".into(),
75 | message: m,
76 | },
77 | })
78 | });
79 | Ok(response)
80 | }
81 |
82 | async fn setup(
83 | config: models::ServiceConfig,
84 | service_registry: ServiceRegistryRef,
85 | ) -> CommandResult {
86 | service_registry
87 | .setup(&config.alias, &config.service, config.config)
88 | .await
89 | .map(|()| "Service setup successful".into())
90 | .map_err(|e| e.to_string())
91 | }
92 |
93 | async fn notify(
94 | config: models::NotificationConfig,
95 | service_registry: ServiceRegistryRef,
96 | template_registry: TemplateRegistryRef,
97 | ) -> CommandResult {
98 | let template = template_registry
99 | .get(&config.template)
100 | .await
101 | .map_err(|e| format!("Template retrieval failed: {}", e))?;
102 | let service = service_registry
103 | .get(&config.target)
104 | .ok_or_else(|| format!("Service instance \"{}\" not found", config.target))?;
105 | service
106 | .notify(
107 | config.config,
108 | Notification {
109 | template,
110 | context: config.context,
111 | },
112 | )
113 | .await
114 | .map(|_| "Notification sent".into())
115 | .map_err(|e| e.to_string())
116 | }
117 | }
118 |
119 | mod models {
120 | use serde::{Deserialize, Serialize};
121 |
122 | #[derive(Debug, Serialize)]
123 | pub struct Response {
124 | pub node: Node,
125 | }
126 |
127 | #[derive(Debug, Serialize)]
128 | pub struct Node {
129 | pub phase: String,
130 | pub message: String,
131 | }
132 |
133 | #[derive(Debug, Deserialize)]
134 | pub struct Input {
135 | pub template: Template,
136 | }
137 |
138 | #[derive(Debug, Deserialize)]
139 | pub struct Template {
140 | pub plugin: Plugin,
141 | }
142 |
143 | #[derive(Debug, Deserialize)]
144 | pub struct Plugin {
145 | pub hermes: Command,
146 | }
147 |
148 | #[derive(Debug, Deserialize)]
149 | pub struct ServiceConfig {
150 | pub alias: String,
151 | pub service: String,
152 | pub config: serde_json::Value,
153 | }
154 |
155 | #[derive(Debug, Deserialize)]
156 | pub struct CommandSetup {
157 | pub setup: ServiceConfig,
158 | }
159 |
160 | #[derive(Debug, Deserialize)]
161 | pub struct NotificationConfig {
162 | pub target: String,
163 | pub template: String,
164 | pub context: serde_json::Value,
165 | pub config: serde_json::Value,
166 | }
167 |
168 | #[derive(Debug, Deserialize)]
169 | pub struct CommandNotify {
170 | pub notify: NotificationConfig,
171 | }
172 |
173 | #[derive(Debug, Deserialize)]
174 | #[serde(untagged)]
175 | pub enum Command {
176 | Setup(CommandSetup),
177 | Notify(CommandNotify),
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/docs/content/services/index.md:
--------------------------------------------------------------------------------
1 | A service, in context of Hermes, provides an integration with a messaging
2 | service, i.e. Slack, Teams, etc. Note that every service have different
3 | requirements as for configuration (authentication, templates, etc) and also
4 | provide different set of capabilities (Slack supports in-place updates of
5 | notifications, while IRC would not). For this reason, this guide will only show
6 | the service-agnostic parts of interacting with Hermes.
7 |
8 | ## Service configuration
9 |
10 | Let's start with a minimal workflow in which we call two templates that will be
11 | defined later on in this guide.
12 |
13 | ```yaml
14 | {% raw %}
15 | apiVersion: argoproj.io/v1alpha1
16 | kind: Workflow
17 | metadata:
18 | generateName: notifications-test-
19 | spec:
20 | entrypoint: main
21 | templates:
22 | - name: main
23 | steps:
24 | - - name: setup-service
25 | template: hermes-setup
26 |
27 | - - name: send-notification
28 | template: hermes-notify
29 | arguments:
30 | parameters:
31 | - name: message
32 | value: "Hello world!"
33 |
34 | - name: hermes-setup
35 | # To be defined...
36 |
37 | - name: hermes-notify
38 | # To be defined...
39 | {% endraw %}
40 | ```
41 |
42 | ### Setting up a service
43 |
44 | A service is setup by issuing a `setup` call to Hermes. It expects following
45 | parameters:
46 |
47 | - `alias` – an alias for the service that we will use to send
48 | notifications. This allows us to have multiple instances of the same service
49 | (imagine a scenario when you would like to send notifications to multiple
50 | Slack workspaces from the same workflow)
51 | - `service` - the name of the service that we want to setup, e.g. `slack`
52 | - `config` - service specific configuration, e.g. authenthication token, custom
53 | avatar, etc
54 |
55 | ```yaml title="hermes-setup"
56 | - name: hermes-setup
57 | plugin:
58 | hermes:
59 | setup:
60 | alias: default
61 | service: some-service
62 | config:
63 | # Service specific config
64 | ```
65 |
66 | ### Adding a template
67 |
68 | In order to send a notification, we have to setup a template that we will use
69 | to render the notification. A template at its core is just a
70 | [ConfigMap](https://kubernetes.io/docs/concepts/configuration/configmap/) that
71 | has following shape:
72 |
73 | ```yaml title="hermes-template"
74 | {% raw %}
75 | apiVersion: v1
76 | kind: ConfigMap
77 | metadata:
78 | name: hermes-template
79 | data:
80 | subtemplate1: |
81 | {"message": "{{message}}"}
82 | # subtemplate2: ...
83 | {% endraw %}
84 | ```
85 |
86 | The `{{message}}` you are seeing in the template above is a [Handlebar
87 | expression](https://handlebarsjs.com/guide/#simple-expressions). It will be
88 | populated with the context provided by you when sending a notification.
89 | Internally, Hermes uses
90 | [handlebar-rust](https://github.com/sunng87/handlebars-rust) as the template
91 | engine.
92 |
93 | Also, as you probably have noticed, a template consists of sub-templates.
94 | Sub-templates allow services to have multiple ways of presenting the same
95 | notification, i.e. in case of Slack a message might be sent to the channel
96 | using the `primary` sub-template and updates to the message will be posted in
97 | the message thread using the `secondary` sub-template. Every service defines
98 | its own set of required sub-templates.
99 |
100 | ### Sending a notification
101 |
102 | Now that we have both the service and template setup, it is time to see how
103 | sending notifications works. Notifications are sent using the `notify` call to
104 | Hermes. It expects following parameters:
105 |
106 | - `target` – the alias of the service that we want to use to send a notification
107 | - `config` - service specific configuration, e.g. name of a slack channel
108 | - `template` - the name of the template to use for the notification
109 | - `context` - the context to render the template with
110 |
111 | ```yaml title="hermes-notify"
112 | {% raw %}
113 | - name: hermes-notify
114 | inputs:
115 | parameters:
116 | - name: message
117 | plugin:
118 | hermes:
119 | notify:
120 | target: default
121 | config:
122 | # Service specific config
123 | template: hermes-template
124 | context:
125 | # Template context
126 | message: "{{inputs.parameters.message}}"
127 | {% endraw %}
128 | ```
129 |
130 | ### Complete workflow
131 |
132 | Putting all the puzzle pieces together we end up with the following workflow.
133 |
134 | ```yaml
135 | {% raw %}
136 | apiVersion: argoproj.io/v1alpha1
137 | kind: Workflow
138 | metadata:
139 | generateName: notifications-test-
140 | spec:
141 | entrypoint: main
142 | templates:
143 | - name: main
144 | steps:
145 | - - name: setup-notifications
146 | template: hermes-setup
147 |
148 | - - name: send-notification
149 | template: hermes-notify
150 | arguments:
151 | parameters:
152 | - name: message
153 | value: "Hello world!"
154 |
155 | - name: hermes-setup
156 | plugin:
157 | hermes:
158 | setup:
159 | alias: default
160 | service: some-service
161 | config:
162 | # Service specific config
163 |
164 | - name: hermes-notify
165 | inputs:
166 | parameters:
167 | - name: message
168 | plugin:
169 | hermes:
170 | notify:
171 | target: default
172 | template: hermes-template
173 | config:
174 | # Service specific config
175 | context:
176 | # Template context
177 | message: "{{inputs.parameters.message}}"
178 | {% endraw %}
179 | ```
180 |
181 | The workflow above is of course not entirely complete as the service-specific
182 | parts are still missing, but replacing the service-specific parts with actual
183 | config should result in you seeing a "Hello world!" message in the messagin
184 | service of your choosing.
185 |
186 | ## What's next?
187 |
188 | Now that you are familiar with the core concepts of Hermes, you will be able
189 | to use this information to setup an actual service. Choose one of the supported
190 | services below and start sending notifications:
191 |
192 | - [Slack](slack/index.md)
193 |
--------------------------------------------------------------------------------
/docs/content/services/slack/index.md:
--------------------------------------------------------------------------------
1 | ## Setup Slack service
2 |
3 | ### Create Slack application
4 |
5 | In order to send notifications to your Slack channels, first you need to setup
6 | a Slack application in your workspace. An application defines the permissions
7 | and appearance of the bot that will interact with your channels. The end goal
8 | of this step is to obtain a Slack authentication token that will be used by
9 | Hermes to send notifications.
10 |
11 | Choose one of the following two ways of creating the Slack application.
12 |
13 | #### Use the manifest button
14 |
15 | The button below will lead you to the app creation wizard in your Slack
16 | workspace where you will get to review the permissions it is requesting before
17 | adding it.
18 |
19 | [Install Manifest :fontawesome-brands-slack:](
20 | https://api.slack.com/apps?new_app=1&manifest_yaml={{ import_file("services/slack/manifest.yaml") | urlencode }}
21 | ){ target=_blank .md-button .md-button--primary }
22 |
23 | #### Manually create the application
24 |
25 | If you do not really trust the button above, you can instead perform this
26 | process manually.
27 |
28 | 1. Go to [Slack Applications](https://api.slack.com/apps)
29 | 2. Press the "Create New App" button
30 | 3. Choose the "From an app manifest" option
31 | 4. Select the workspace you wish to create the app in
32 | 5. Paste in the manifest below
33 |
34 | ```yaml title="manifest.yaml"
35 | --8<-- "docs/content/services/slack/manifest.yaml"
36 | ```
37 |
38 | ### Obtain the OAuth Token
39 |
40 | 1. Go to [Slack Applications](https://api.slack.com/apps)
41 | 2. Select the newly created Hermes app
42 | 3. Click on "OAuth & Permissions" in the menu on the left
43 | 4. Copy the "Bot User OAuth Token" and save it for later
44 |
45 | ## Example
46 |
47 | The provided example will generate following notification in your Slack channel.
48 |
49 |
50 | 
51 |
52 |
53 | ### Slack token
54 |
55 | Create a secret containing the Slack token that you obtained using the [setup
56 | guide](#obtain-the-oauth-token).
57 |
58 | !!! warning "Access to secrets"
59 |
60 | This example assumes that your workflow will be run using a service account
61 | that has access to this secret.
62 |
63 | ```yaml
64 | apiVersion: v1
65 | kind: Secret
66 | metadata:
67 | name: slack-token
68 | stringData:
69 | token: # Your Slack token goes here
70 | ```
71 |
72 | ### Template
73 |
74 | Add the following template:
75 |
76 | ```yaml
77 | {% raw %}
78 | apiVersion: v1
79 | kind: ConfigMap
80 | metadata:
81 | name: hermes-template-slack-default
82 | data:
83 | primary: |
84 | {
85 | "text": "{{message}}",
86 | "blocks": [
87 | {
88 | "type": "section",
89 | "text": {
90 | "type": "mrkdwn",
91 | "text": "{{message}}"
92 | }
93 | },
94 | {
95 | "type": "section",
96 | "fields": [
97 | {
98 | "type": "mrkdwn",
99 | "text": "*Application*\n{{app}}"
100 | },
101 | {
102 | "type": "mrkdwn",
103 | "text": "*Environment*\n{{env}}"
104 | }
105 | ]
106 | },
107 | {
108 | "type": "section",
109 | "fields": [
110 | {
111 | "type": "mrkdwn",
112 | "text": "*Revision*\n<{{revision_url}}|{{revision_sha}}>"
113 | }
114 | ]
115 | },
116 | {
117 | "type": "context",
118 | "elements": [
119 | {
120 | "type": "mrkdwn",
121 | "text": "<{{log_url}}|View pipeline logs>"
122 | }
123 | ]
124 | }
125 | ]
126 | }
127 | secondary: |
128 | {"text": "{{message}}"}
129 | {% endraw %}
130 | ```
131 |
132 | ### Send notification
133 |
134 | Fill in the channel name in the workflow below and submit it.
135 |
136 | ```yaml
137 | {% raw %}
138 | apiVersion: argoproj.io/v1alpha1
139 | kind: Workflow
140 | metadata:
141 | generateName: notifications-test-
142 | spec:
143 | entrypoint: main
144 | templates:
145 | - name: main
146 | steps:
147 | - - name: setup-notifications
148 | template: hermes-setup
149 |
150 | - - name: pre-notification
151 | template: hermes-notify
152 | arguments:
153 | parameters:
154 | - name: message
155 | value: "Deployment started :hourglass_flowing_sand:"
156 |
157 | - - name: hello
158 | template: hello
159 |
160 | - - name: post-notification
161 | template: hermes-notify
162 | arguments:
163 | parameters:
164 | - name: message
165 | value: "Deployment succeeded :white_check_mark:"
166 |
167 | - name: hermes-setup
168 | plugin:
169 | hermes:
170 | setup:
171 | alias: default
172 | service: slack
173 | config:
174 | token: slack-token
175 | icon_emoji: ":rocket:"
176 |
177 | - name: hermes-notify
178 | inputs:
179 | parameters:
180 | - name: message
181 | plugin:
182 | hermes:
183 | notify:
184 | target: default
185 | template: hermes-template-slack-default
186 | config:
187 | channel: # Your Slack channel name goes here
188 | context:
189 | message: "{{inputs.parameters.message}}"
190 | app: hermes
191 | env: prod
192 | revision_sha: "deadbeef"
193 | revision_url: "https://google.com"
194 | log_url: "https://google.com"
195 |
196 | - name: hello
197 | container:
198 | image: docker/whalesay
199 | command: [cowsay]
200 | args: ["hello world"]
201 | {% endraw %}
202 | ```
203 |
204 | ## Reference
205 |
206 | ### Setup config
207 |
208 |
209 | | Field | Required | Description |
210 | | - | - | - |
211 | | token | yes | The name of the secret containing the Slack OAuth token. The token has to be stored in the `token` field in the secret. |
212 | | icon_emoji | no | A shortcode for the emoji to use as the bot avatar, e.g. `:rocket`. |
213 |
214 |
215 | ### Notify config
216 |
217 | | Field | Required | Description |
218 | | - | - | - |
219 | | channel | yes | The name of the channel to send the notification to, .e.g `argo-alerts`. |
220 |
--------------------------------------------------------------------------------
/src/services/slack.rs:
--------------------------------------------------------------------------------
1 | use super::{CallError, FactoryError, Notification, Service, ServiceFactory};
2 | use crate::k8s::secrets::get_secret;
3 | use async_trait::async_trait;
4 | use parking_lot::Mutex;
5 | use reqwest::header;
6 | use serde::Deserialize;
7 | use std::collections::HashMap;
8 | use std::sync::Arc;
9 |
10 | #[derive(Deserialize)]
11 | struct ServiceConfig {
12 | token: String,
13 | icon_emoji: Option,
14 | }
15 |
16 | impl ServiceConfig {
17 | fn from_value(value: serde_json::Value) -> Result {
18 | serde_json::from_value(value).map_err(|e| FactoryError::ConfigError(e.to_string()))
19 | }
20 | }
21 |
22 | #[derive(Deserialize)]
23 | struct NotificationConfig {
24 | channel: String,
25 | }
26 |
27 | impl NotificationConfig {
28 | fn from_value(value: serde_json::Value) -> Result {
29 | serde_json::from_value(value).map_err(|e| CallError::ConfigError(e.to_string()))
30 | }
31 | }
32 |
33 | #[derive(Clone)]
34 | struct Channel {
35 | channel_id: String,
36 | thread_id: String,
37 | }
38 |
39 | #[derive(Deserialize)]
40 | struct TokenSecret {
41 | token: String,
42 | }
43 |
44 | pub struct SlackFactory;
45 |
46 | #[async_trait]
47 | impl ServiceFactory for SlackFactory {
48 | async fn from_config(config: serde_json::Value) -> Result, FactoryError> {
49 | let config = ServiceConfig::from_value(config)?;
50 | let token_secret: TokenSecret = get_secret(&config.token)
51 | .await
52 | .map_err(|e| FactoryError::ConfigError(format!("Invalid token secret: {}", e)))?;
53 | Ok(Arc::new(Slack {
54 | config: ServiceConfig {
55 | icon_emoji: config.icon_emoji,
56 | token: token_secret.token,
57 | },
58 | channels: Arc::new(Mutex::new(HashMap::new())),
59 | }))
60 | }
61 | }
62 |
63 | #[derive(Debug, Deserialize)]
64 | struct SlackSuccessResponse {
65 | ok: bool,
66 | channel: String,
67 | ts: String,
68 | }
69 |
70 | #[derive(Debug, Deserialize)]
71 | struct SlackErrorResponse {
72 | ok: bool,
73 | error: String,
74 | }
75 |
76 | #[derive(Debug, Deserialize)]
77 | #[serde(untagged)]
78 | enum SlackResponse {
79 | Success(SlackSuccessResponse),
80 | Error(SlackErrorResponse),
81 | }
82 |
83 | #[derive(Debug, Deserialize)]
84 | struct RenderedTemplate {
85 | text: Option,
86 | blocks: Option,
87 | }
88 |
89 | pub struct Slack {
90 | config: ServiceConfig,
91 | channels: Arc>>>,
92 | }
93 |
94 | impl Slack {
95 | fn get_channel(&self, name: &str) -> Option> {
96 | self.channels.lock().get(name).cloned()
97 | }
98 |
99 | fn update_channel(&self, name: &str, channel: Channel) {
100 | let mut channels = self.channels.lock();
101 | channels.insert(name.into(), Box::from(channel));
102 | }
103 |
104 | fn render(
105 | &self,
106 | notification: &Notification,
107 | subtemplate: &str,
108 | ) -> Result {
109 | let raw_template = notification
110 | .render(subtemplate)
111 | .map_err(|e| CallError::RenderError(e.to_string()))?;
112 | serde_json::from_str(raw_template.as_str())
113 | .map_err(|e| CallError::RenderError(e.to_string()))
114 | }
115 |
116 | async fn post(
117 | &self,
118 | call: &str,
119 | payload: &serde_json::Value,
120 | ) -> Result {
121 | let url = format!("https://slack.com/api/{}", call);
122 | let response: SlackResponse = reqwest::Client::new()
123 | .post(url)
124 | .header(
125 | header::AUTHORIZATION,
126 | format!("Bearer {}", self.config.token),
127 | )
128 | .json(&payload)
129 | .send()
130 | .await
131 | .map_err(|e| format!("Unexpected error: {}", e))?
132 | .json()
133 | .await
134 | .map_err(|e| format!("Slack response parsing error: {}", e))?;
135 |
136 | match response {
137 | SlackResponse::Success(r) if r.ok => Ok(r),
138 | SlackResponse::Error(r) if !r.ok => Err(format!("{} {}", r.error, self.config.token)),
139 | _ => unreachable!(),
140 | }
141 | }
142 | }
143 |
144 | #[async_trait]
145 | impl Service for Slack {
146 | async fn notify(
147 | &self,
148 | config: serde_json::Value,
149 | notification: Notification,
150 | ) -> Result<(), CallError> {
151 | // TODO: There is a potential fast-path here that could be taken for the case when the
152 | // channel ID and thread ID are known (for subsequent notifications in the same channel).
153 | // As they are known, we could issue the primary and secondary notifications in parallel.
154 | let notification_config = NotificationConfig::from_value(config)?;
155 |
156 | // Retrieve the cached data about the channel, if any
157 | let channel_data = self.get_channel(¬ification_config.channel);
158 | let channel = channel_data
159 | .as_ref()
160 | .map(|c| c.channel_id.clone())
161 | .unwrap_or_else(|| notification_config.channel.clone());
162 | let thread_id = channel_data.as_ref().map(|c| c.thread_id.clone());
163 |
164 | // Create new or update the existing primary notification
165 | let template = self.render(¬ification, "primary")?;
166 | let payload = serde_json::json!({
167 | "channel": channel,
168 | "ts": thread_id,
169 | "icon_emoji": self.config.icon_emoji,
170 | "text": template.text,
171 | "blocks": template.blocks,
172 | });
173 | let call = thread_id
174 | .and(Some("chat.update"))
175 | .unwrap_or("chat.postMessage");
176 | let SlackSuccessResponse {
177 | ts: thread_id,
178 | channel,
179 | ..
180 | } = self.post(call, &payload).await.map_err(CallError::Fail)?;
181 |
182 | // Create new secondary notification (a thread message)
183 | let template = self.render(¬ification, "secondary")?;
184 | let payload = serde_json::json!({
185 | "channel": channel,
186 | "thread_ts": thread_id,
187 | "icon_emoji": self.config.icon_emoji,
188 | "text": template.text,
189 | "blocks": template.blocks,
190 | });
191 | self.post("chat.postMessage", &payload)
192 | .await
193 | .map_err(CallError::Fail)?;
194 |
195 | // Update the cache if needed
196 | if channel_data.is_none() {
197 | self.update_channel(
198 | ¬ification_config.channel,
199 | Channel {
200 | channel_id: channel,
201 | thread_id,
202 | },
203 | )
204 | };
205 |
206 | Ok(())
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/src/services/mod.rs:
--------------------------------------------------------------------------------
1 | use as_any::AsAny;
2 | use async_trait::async_trait;
3 | use handlebars::Handlebars;
4 | use std::collections::HashMap;
5 | use std::fmt;
6 | use std::future::Future;
7 | use std::pin::Pin;
8 | use std::sync::Arc;
9 |
10 | mod slack;
11 |
12 | #[derive(Debug)]
13 | pub enum FactoryError {
14 | ServiceNotFound,
15 | ConfigError(String),
16 | }
17 |
18 | impl fmt::Display for FactoryError {
19 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
20 | match self {
21 | FactoryError::ServiceNotFound => write!(f, "Service not found"),
22 | FactoryError::ConfigError(s) => write!(f, "Invalid config: {}", s),
23 | }
24 | }
25 | }
26 |
27 | #[derive(Debug)]
28 | pub enum CallError {
29 | // TODO: less ambiguous name
30 | Fail(String),
31 | ConfigError(String),
32 | RenderError(String),
33 | }
34 |
35 | impl fmt::Display for CallError {
36 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
37 | match self {
38 | CallError::Fail(s) => write!(f, "Call failure: {}", s),
39 | CallError::ConfigError(s) => write!(f, "Invalid config: {}", s),
40 | CallError::RenderError(s) => write!(f, "Render error: {}", s),
41 | }
42 | }
43 | }
44 |
45 | pub struct Notification {
46 | /// A raw notification template
47 | pub template: Arc>,
48 | /// A context to render the template with
49 | pub context: serde_json::Value,
50 | }
51 |
52 | #[derive(Debug)]
53 | pub enum RenderError {
54 | SubTemplateNotFound,
55 | RenderError(String),
56 | }
57 |
58 | impl fmt::Display for RenderError {
59 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
60 | match self {
61 | RenderError::SubTemplateNotFound => write!(f, "Sub-template not found"),
62 | RenderError::RenderError(s) => write!(f, "{}", s),
63 | }
64 | }
65 | }
66 |
67 | impl Notification {
68 | /// Renders the notification using Handlebars
69 | ///
70 | /// # Arguments
71 | ///
72 | /// * `subtemplate` - Name of the sub-template to render
73 | pub fn render(&self, subtemplate: &str) -> Result {
74 | let template = self
75 | .template
76 | .get(subtemplate)
77 | .ok_or(RenderError::SubTemplateNotFound)?;
78 | let mut handlebars = Handlebars::new();
79 | handlebars.set_strict_mode(true);
80 | let rendered = handlebars
81 | .render_template(template, &self.context)
82 | .map_err(|e| RenderError::RenderError(format!("Failed to render: {}", e)))?;
83 | Ok(rendered)
84 | }
85 | }
86 |
87 | #[async_trait]
88 | pub trait ServiceFactory {
89 | /// Instantiates a notifier service from config
90 | ///
91 | /// # Arguments
92 | ///
93 | /// * `config` - A service specific configuration
94 | async fn from_config(config: serde_json::Value) -> Result, FactoryError>;
95 | }
96 |
97 | pub type ServiceFactoryFn =
98 | fn(
99 | serde_json::Value,
100 | ) -> Pin, FactoryError>> + Send>>;
101 |
102 | /// Defines a target that notifications can be sent to, e.g. Slack, Teams, etc
103 | #[async_trait]
104 | pub trait Service: Sync + Send + AsAny {
105 | /// Sends a notification with the given context
106 | ///
107 | /// # Arguments
108 | ///
109 | /// * `notification` - A notification to send
110 | async fn notify(
111 | &self,
112 | config: serde_json::Value,
113 | notification: Notification,
114 | ) -> Result<(), CallError>;
115 | }
116 |
117 | /// Registry of active service instances
118 | #[async_trait]
119 | pub trait ServiceRegistry: Sync + Send {
120 | /// Sets up a new service instance
121 | ///
122 | /// A single service can be instantiated multiple times with different configurations and given
123 | /// different aliases. This way you can for example target multiple Slack organizations from a
124 | /// single workflow.
125 | ///
126 | /// # Arguments
127 | ///
128 | /// * `alias` - Alias to assign to the service instance. This allows for having multiple
129 | /// instances of a single service
130 | /// * `service_name` - Name of the service to instantiate, i.e. slack, teams
131 | /// * `config` - Service config with a dynamic shape. To be validated by a service factory
132 | async fn setup(
133 | &self,
134 | alias: &str,
135 | service_name: &str,
136 | config: serde_json::Value,
137 | ) -> Result<(), FactoryError>;
138 |
139 | /// Retrieves a service instance
140 | ///
141 | /// # Arguments
142 | ///
143 | /// * `alias` - The alias of a service instance to retrieve
144 | fn get(&self, alias: &str) -> Option>;
145 | }
146 |
147 | pub type ServiceRegistryRef = Arc;
148 |
149 | pub mod registries {
150 | use super::{slack, FactoryError, Service, ServiceFactory, ServiceFactoryFn, ServiceRegistry};
151 | use async_trait::async_trait;
152 | use lazy_static::lazy_static;
153 | use parking_lot::Mutex;
154 | use std::collections::HashMap;
155 | use std::sync::Arc;
156 |
157 | /// A registry of all the active service instances
158 | pub struct DefaultServiceRegistry {
159 | services: HashMap>,
160 | instances: Arc>>>,
161 | }
162 |
163 | impl DefaultServiceRegistry {
164 | pub fn with_default_services() -> Arc {
165 | Self::with_services(SERVICES.clone())
166 | }
167 |
168 | pub fn with_services(services: HashMap>) -> Arc {
169 | Arc::from(Self {
170 | services,
171 | instances: Arc::new(Mutex::new(HashMap::new())),
172 | })
173 | }
174 | }
175 |
176 | #[async_trait]
177 | impl ServiceRegistry for DefaultServiceRegistry {
178 | async fn setup(
179 | &self,
180 | alias: &str,
181 | service_name: &str,
182 | config: serde_json::Value,
183 | ) -> Result<(), FactoryError> {
184 | let factory = self
185 | .services
186 | .get(service_name)
187 | .ok_or(FactoryError::ServiceNotFound)?;
188 | let service = factory(config).await?;
189 | let mut instances = self.instances.lock();
190 | instances.insert(alias.into(), service);
191 | Ok(())
192 | }
193 |
194 | fn get(&self, alias: &str) -> Option> {
195 | self.instances.lock().get(alias).cloned()
196 | }
197 | }
198 |
199 | lazy_static! {
200 | /// Holds a registry of all the available services
201 | static ref SERVICES: HashMap> = {
202 | let services = [
203 | ("slack", slack::SlackFactory::from_config),
204 | ];
205 | let mut factories: HashMap<_, Arc> = HashMap::new();
206 | for (name, factory) in services {
207 | factories.insert(name.into(), Arc::new(factory));
208 | }
209 | factories
210 | };
211 | }
212 | }
213 |
--------------------------------------------------------------------------------
/tests/integration.rs:
--------------------------------------------------------------------------------
1 | use argo_hermes::server;
2 | use argo_hermes::services::registries::DefaultServiceRegistry;
3 | use argo_hermes::services::ServiceRegistryRef;
4 | use std::sync::Arc;
5 | use warp::http::Response;
6 | use warp::http::StatusCode;
7 | use warp::test::request;
8 |
9 | mod mocks {
10 | use argo_hermes::services::{
11 | CallError, FactoryError, Notification, Service, ServiceFactory, ServiceFactoryFn,
12 | };
13 | use argo_hermes::templates::{Template, TemplateError, TemplateRegistry};
14 | use async_trait::async_trait;
15 | use lazy_static::lazy_static;
16 | use parking_lot::Mutex;
17 | use std::collections::HashMap;
18 | use std::sync::Arc;
19 |
20 | #[derive(Default)]
21 | pub struct MockTemplateRegistry;
22 |
23 | #[async_trait]
24 | impl TemplateRegistry for MockTemplateRegistry {
25 | async fn get(&self, name: &str) -> Result, TemplateError> {
26 | match name {
27 | "default" => {
28 | let mut subtemplates: HashMap = HashMap::new();
29 | subtemplates.insert("primary".into(), "Message: {{ message}}".into());
30 | Ok(Arc::new(subtemplates))
31 | }
32 | _ => Err(TemplateError::NotFound),
33 | }
34 | }
35 | }
36 |
37 | pub struct MockServiceFactory;
38 |
39 | #[async_trait]
40 | impl ServiceFactory for MockServiceFactory {
41 | async fn from_config(
42 | service_config: serde_json::Value,
43 | ) -> Result, FactoryError> {
44 | Ok(Arc::new(MockService {
45 | service_config,
46 | calls: Arc::new(Mutex::new(vec![])),
47 | }))
48 | }
49 | }
50 |
51 | pub struct NotificationCall {
52 | pub config: serde_json::Value,
53 | pub notification: Notification,
54 | }
55 |
56 | pub struct MockService {
57 | pub service_config: serde_json::Value,
58 | pub calls: Arc>>,
59 | }
60 |
61 | #[async_trait]
62 | impl Service for MockService {
63 | async fn notify(
64 | &self,
65 | config: serde_json::Value,
66 | notification: Notification,
67 | ) -> Result<(), CallError> {
68 | // TODO: simulate error
69 | let mut calls = self.calls.lock();
70 | calls.push(NotificationCall {
71 | config,
72 | notification,
73 | });
74 | Ok(())
75 | }
76 | }
77 |
78 | lazy_static! {
79 | /// Holds a registry of all the available services
80 | pub static ref SERVICES: HashMap> = {
81 | let services = [
82 | ("mock", MockServiceFactory::from_config),
83 | ];
84 | let mut factories: HashMap<_, Arc> = HashMap::new();
85 | for (name, factory) in services {
86 | factories.insert(name.into(), Arc::new(factory));
87 | }
88 | factories
89 | };
90 | }
91 | }
92 |
93 | fn deserialize(res: Response) -> serde_json::Result {
94 | let (_parts, body) = res.into_parts();
95 | let body = serde_json::from_slice(&body)?;
96 | Ok(body)
97 | }
98 |
99 | #[tokio::test]
100 | async fn test_setup_success() {
101 | let service_registry: ServiceRegistryRef =
102 | DefaultServiceRegistry::with_services(mocks::SERVICES.clone());
103 | let template_registry = Arc::new(mocks::MockTemplateRegistry::default());
104 | let api = server::filters::routes(service_registry.clone(), template_registry);
105 |
106 | let service_def = serde_json::json!({
107 | "alias": "default",
108 | "service": "mock",
109 | "config": {
110 | "token": "topsecret123",
111 | }
112 | });
113 | let res = request()
114 | .method("POST")
115 | .path("/api/v1/template.execute")
116 | .json(&serde_json::json!({
117 | "template": {
118 | "plugin": {
119 | "hermes": {
120 | "setup": service_def
121 | }
122 | }
123 | }
124 | }))
125 | .reply(&api)
126 | .await;
127 |
128 | assert_eq!(res.status(), StatusCode::OK);
129 | assert_eq!(
130 | deserialize(res).unwrap(),
131 | serde_json::json!({
132 | "node": {
133 | "phase": "Succeeded",
134 | "message": "Service setup successful",
135 | },
136 | })
137 | );
138 |
139 | let service_t = service_registry.get("default").unwrap();
140 | let service = service_t
141 | .as_any()
142 | .downcast_ref::()
143 | .expect("not found");
144 | assert_eq!(service.service_config, service_def["config"]);
145 | assert_eq!(service.calls.lock().len(), 0);
146 | }
147 |
148 | #[tokio::test]
149 | async fn test_setup_missing_service() {
150 | let service_registry: ServiceRegistryRef =
151 | DefaultServiceRegistry::with_services(mocks::SERVICES.clone());
152 | let template_registry = Arc::new(mocks::MockTemplateRegistry::default());
153 | let api = server::filters::routes(service_registry.clone(), template_registry);
154 |
155 | let res = request()
156 | .method("POST")
157 | .path("/api/v1/template.execute")
158 | .json(&serde_json::json!({
159 | "template": {
160 | "plugin": {
161 | "hermes": {
162 | "setup": {
163 | "alias": "default",
164 | "service": "blabla",
165 | "config": {
166 | "token": "topsecret123",
167 | }
168 | }
169 | }
170 | }
171 | }
172 | }))
173 | .reply(&api)
174 | .await;
175 |
176 | assert_eq!(res.status(), StatusCode::OK);
177 | assert_eq!(
178 | deserialize(res).unwrap(),
179 | serde_json::json!({
180 | "node": {
181 | "phase": "Failed",
182 | "message": "Service not found",
183 | },
184 | })
185 | );
186 | }
187 |
188 | #[tokio::test]
189 | async fn test_notify_success() {
190 | let service_registry: ServiceRegistryRef =
191 | DefaultServiceRegistry::with_services(mocks::SERVICES.clone());
192 | let template_registry = Arc::new(mocks::MockTemplateRegistry::default());
193 | let api = server::filters::routes(service_registry.clone(), template_registry);
194 |
195 | service_registry
196 | .setup("default", "mock", serde_json::json!({}))
197 | .await
198 | .expect("Setup failed");
199 |
200 | let context = serde_json::json!({
201 | "message": "Hello world",
202 | });
203 | let config = serde_json::json!({
204 | "channel": "sandbox",
205 | });
206 | let res = request()
207 | .method("POST")
208 | .path("/api/v1/template.execute")
209 | .json(&serde_json::json!({
210 | "template": {
211 | "plugin": {
212 | "hermes": {
213 | "notify": {
214 | "target": "default",
215 | "template": "default",
216 | "context": context,
217 | "config": config,
218 | }
219 | }
220 | }
221 | }
222 | }))
223 | .reply(&api)
224 | .await;
225 |
226 | assert_eq!(res.status(), StatusCode::OK);
227 | assert_eq!(
228 | deserialize(res).unwrap(),
229 | serde_json::json!({
230 | "node": {
231 | "phase": "Succeeded",
232 | "message": "Notification sent",
233 | },
234 | })
235 | );
236 |
237 | let service_t = service_registry.get("default").unwrap();
238 | let service = service_t
239 | .as_any()
240 | .downcast_ref::()
241 | .expect("not found");
242 | let calls = service.calls.lock();
243 | assert_eq!(calls.len(), 1);
244 | let call = &calls[0];
245 | assert_eq!(call.config, config);
246 | let rendered = call.notification.render("primary").unwrap();
247 | assert_eq!(rendered, "Message: Hello world");
248 | }
249 |
--------------------------------------------------------------------------------
/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 3
4 |
5 | [[package]]
6 | name = "argo-hermes"
7 | version = "0.1.0"
8 | dependencies = [
9 | "as-any",
10 | "async-trait",
11 | "base64",
12 | "clap",
13 | "handlebars",
14 | "k8s-openapi",
15 | "kube",
16 | "lazy_static",
17 | "parking_lot",
18 | "reqwest",
19 | "serde",
20 | "serde_json",
21 | "tokio",
22 | "warp",
23 | ]
24 |
25 | [[package]]
26 | name = "as-any"
27 | version = "0.2.1"
28 | source = "registry+https://github.com/rust-lang/crates.io-index"
29 | checksum = "088ccb346677e658e7ccd9627c62576fba881f4db7fab71fa9e21bf31c0aa4cb"
30 |
31 | [[package]]
32 | name = "async-trait"
33 | version = "0.1.52"
34 | source = "registry+https://github.com/rust-lang/crates.io-index"
35 | checksum = "061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3"
36 | dependencies = [
37 | "proc-macro2",
38 | "quote",
39 | "syn",
40 | ]
41 |
42 | [[package]]
43 | name = "atty"
44 | version = "0.2.14"
45 | source = "registry+https://github.com/rust-lang/crates.io-index"
46 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
47 | dependencies = [
48 | "hermit-abi",
49 | "libc",
50 | "winapi",
51 | ]
52 |
53 | [[package]]
54 | name = "autocfg"
55 | version = "1.0.1"
56 | source = "registry+https://github.com/rust-lang/crates.io-index"
57 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
58 |
59 | [[package]]
60 | name = "base64"
61 | version = "0.13.0"
62 | source = "registry+https://github.com/rust-lang/crates.io-index"
63 | checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
64 |
65 | [[package]]
66 | name = "bitflags"
67 | version = "1.2.1"
68 | source = "registry+https://github.com/rust-lang/crates.io-index"
69 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
70 |
71 | [[package]]
72 | name = "block-buffer"
73 | version = "0.7.3"
74 | source = "registry+https://github.com/rust-lang/crates.io-index"
75 | checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b"
76 | dependencies = [
77 | "block-padding",
78 | "byte-tools",
79 | "byteorder",
80 | "generic-array 0.12.4",
81 | ]
82 |
83 | [[package]]
84 | name = "block-buffer"
85 | version = "0.9.0"
86 | source = "registry+https://github.com/rust-lang/crates.io-index"
87 | checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
88 | dependencies = [
89 | "generic-array 0.14.4",
90 | ]
91 |
92 | [[package]]
93 | name = "block-padding"
94 | version = "0.1.5"
95 | source = "registry+https://github.com/rust-lang/crates.io-index"
96 | checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5"
97 | dependencies = [
98 | "byte-tools",
99 | ]
100 |
101 | [[package]]
102 | name = "buf_redux"
103 | version = "0.8.4"
104 | source = "registry+https://github.com/rust-lang/crates.io-index"
105 | checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f"
106 | dependencies = [
107 | "memchr",
108 | "safemem",
109 | ]
110 |
111 | [[package]]
112 | name = "bumpalo"
113 | version = "3.8.0"
114 | source = "registry+https://github.com/rust-lang/crates.io-index"
115 | checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c"
116 |
117 | [[package]]
118 | name = "byte-tools"
119 | version = "0.3.1"
120 | source = "registry+https://github.com/rust-lang/crates.io-index"
121 | checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
122 |
123 | [[package]]
124 | name = "byteorder"
125 | version = "1.4.3"
126 | source = "registry+https://github.com/rust-lang/crates.io-index"
127 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
128 |
129 | [[package]]
130 | name = "bytes"
131 | version = "1.1.0"
132 | source = "registry+https://github.com/rust-lang/crates.io-index"
133 | checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
134 |
135 | [[package]]
136 | name = "cc"
137 | version = "1.0.72"
138 | source = "registry+https://github.com/rust-lang/crates.io-index"
139 | checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee"
140 |
141 | [[package]]
142 | name = "cfg-if"
143 | version = "1.0.0"
144 | source = "registry+https://github.com/rust-lang/crates.io-index"
145 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
146 |
147 | [[package]]
148 | name = "chrono"
149 | version = "0.4.19"
150 | source = "registry+https://github.com/rust-lang/crates.io-index"
151 | checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
152 | dependencies = [
153 | "libc",
154 | "num-integer",
155 | "num-traits",
156 | "serde",
157 | "time",
158 | "winapi",
159 | ]
160 |
161 | [[package]]
162 | name = "clap"
163 | version = "3.0.7"
164 | source = "registry+https://github.com/rust-lang/crates.io-index"
165 | checksum = "12e8611f9ae4e068fa3e56931fded356ff745e70987ff76924a6e0ab1c8ef2e3"
166 | dependencies = [
167 | "atty",
168 | "bitflags",
169 | "indexmap",
170 | "os_str_bytes",
171 | "strsim",
172 | "termcolor",
173 | "textwrap",
174 | ]
175 |
176 | [[package]]
177 | name = "core-foundation"
178 | version = "0.9.2"
179 | source = "registry+https://github.com/rust-lang/crates.io-index"
180 | checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3"
181 | dependencies = [
182 | "core-foundation-sys",
183 | "libc",
184 | ]
185 |
186 | [[package]]
187 | name = "core-foundation-sys"
188 | version = "0.8.3"
189 | source = "registry+https://github.com/rust-lang/crates.io-index"
190 | checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
191 |
192 | [[package]]
193 | name = "cpufeatures"
194 | version = "0.2.1"
195 | source = "registry+https://github.com/rust-lang/crates.io-index"
196 | checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469"
197 | dependencies = [
198 | "libc",
199 | ]
200 |
201 | [[package]]
202 | name = "digest"
203 | version = "0.8.1"
204 | source = "registry+https://github.com/rust-lang/crates.io-index"
205 | checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5"
206 | dependencies = [
207 | "generic-array 0.12.4",
208 | ]
209 |
210 | [[package]]
211 | name = "digest"
212 | version = "0.9.0"
213 | source = "registry+https://github.com/rust-lang/crates.io-index"
214 | checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
215 | dependencies = [
216 | "generic-array 0.14.4",
217 | ]
218 |
219 | [[package]]
220 | name = "dirs-next"
221 | version = "2.0.0"
222 | source = "registry+https://github.com/rust-lang/crates.io-index"
223 | checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
224 | dependencies = [
225 | "cfg-if",
226 | "dirs-sys-next",
227 | ]
228 |
229 | [[package]]
230 | name = "dirs-sys-next"
231 | version = "0.1.2"
232 | source = "registry+https://github.com/rust-lang/crates.io-index"
233 | checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
234 | dependencies = [
235 | "libc",
236 | "redox_users",
237 | "winapi",
238 | ]
239 |
240 | [[package]]
241 | name = "either"
242 | version = "1.6.1"
243 | source = "registry+https://github.com/rust-lang/crates.io-index"
244 | checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
245 |
246 | [[package]]
247 | name = "encoding_rs"
248 | version = "0.8.30"
249 | source = "registry+https://github.com/rust-lang/crates.io-index"
250 | checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df"
251 | dependencies = [
252 | "cfg-if",
253 | ]
254 |
255 | [[package]]
256 | name = "fake-simd"
257 | version = "0.1.2"
258 | source = "registry+https://github.com/rust-lang/crates.io-index"
259 | checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
260 |
261 | [[package]]
262 | name = "fnv"
263 | version = "1.0.7"
264 | source = "registry+https://github.com/rust-lang/crates.io-index"
265 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
266 |
267 | [[package]]
268 | name = "foreign-types"
269 | version = "0.3.2"
270 | source = "registry+https://github.com/rust-lang/crates.io-index"
271 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
272 | dependencies = [
273 | "foreign-types-shared",
274 | ]
275 |
276 | [[package]]
277 | name = "foreign-types-shared"
278 | version = "0.1.1"
279 | source = "registry+https://github.com/rust-lang/crates.io-index"
280 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
281 |
282 | [[package]]
283 | name = "form_urlencoded"
284 | version = "1.0.1"
285 | source = "registry+https://github.com/rust-lang/crates.io-index"
286 | checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
287 | dependencies = [
288 | "matches",
289 | "percent-encoding",
290 | ]
291 |
292 | [[package]]
293 | name = "futures"
294 | version = "0.3.19"
295 | source = "registry+https://github.com/rust-lang/crates.io-index"
296 | checksum = "28560757fe2bb34e79f907794bb6b22ae8b0e5c669b638a1132f2592b19035b4"
297 | dependencies = [
298 | "futures-channel",
299 | "futures-core",
300 | "futures-executor",
301 | "futures-io",
302 | "futures-sink",
303 | "futures-task",
304 | "futures-util",
305 | ]
306 |
307 | [[package]]
308 | name = "futures-channel"
309 | version = "0.3.19"
310 | source = "registry+https://github.com/rust-lang/crates.io-index"
311 | checksum = "ba3dda0b6588335f360afc675d0564c17a77a2bda81ca178a4b6081bd86c7f0b"
312 | dependencies = [
313 | "futures-core",
314 | "futures-sink",
315 | ]
316 |
317 | [[package]]
318 | name = "futures-core"
319 | version = "0.3.19"
320 | source = "registry+https://github.com/rust-lang/crates.io-index"
321 | checksum = "d0c8ff0461b82559810cdccfde3215c3f373807f5e5232b71479bff7bb2583d7"
322 |
323 | [[package]]
324 | name = "futures-executor"
325 | version = "0.3.19"
326 | source = "registry+https://github.com/rust-lang/crates.io-index"
327 | checksum = "29d6d2ff5bb10fb95c85b8ce46538a2e5f5e7fdc755623a7d4529ab8a4ed9d2a"
328 | dependencies = [
329 | "futures-core",
330 | "futures-task",
331 | "futures-util",
332 | ]
333 |
334 | [[package]]
335 | name = "futures-io"
336 | version = "0.3.19"
337 | source = "registry+https://github.com/rust-lang/crates.io-index"
338 | checksum = "b1f9d34af5a1aac6fb380f735fe510746c38067c5bf16c7fd250280503c971b2"
339 |
340 | [[package]]
341 | name = "futures-macro"
342 | version = "0.3.19"
343 | source = "registry+https://github.com/rust-lang/crates.io-index"
344 | checksum = "6dbd947adfffb0efc70599b3ddcf7b5597bb5fa9e245eb99f62b3a5f7bb8bd3c"
345 | dependencies = [
346 | "proc-macro2",
347 | "quote",
348 | "syn",
349 | ]
350 |
351 | [[package]]
352 | name = "futures-sink"
353 | version = "0.3.19"
354 | source = "registry+https://github.com/rust-lang/crates.io-index"
355 | checksum = "e3055baccb68d74ff6480350f8d6eb8fcfa3aa11bdc1a1ae3afdd0514617d508"
356 |
357 | [[package]]
358 | name = "futures-task"
359 | version = "0.3.19"
360 | source = "registry+https://github.com/rust-lang/crates.io-index"
361 | checksum = "6ee7c6485c30167ce4dfb83ac568a849fe53274c831081476ee13e0dce1aad72"
362 |
363 | [[package]]
364 | name = "futures-util"
365 | version = "0.3.19"
366 | source = "registry+https://github.com/rust-lang/crates.io-index"
367 | checksum = "d9b5cf40b47a271f77a8b1bec03ca09044d99d2372c0de244e66430761127164"
368 | dependencies = [
369 | "futures-channel",
370 | "futures-core",
371 | "futures-io",
372 | "futures-macro",
373 | "futures-sink",
374 | "futures-task",
375 | "memchr",
376 | "pin-project-lite",
377 | "pin-utils",
378 | "slab",
379 | ]
380 |
381 | [[package]]
382 | name = "generic-array"
383 | version = "0.12.4"
384 | source = "registry+https://github.com/rust-lang/crates.io-index"
385 | checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd"
386 | dependencies = [
387 | "typenum",
388 | ]
389 |
390 | [[package]]
391 | name = "generic-array"
392 | version = "0.14.4"
393 | source = "registry+https://github.com/rust-lang/crates.io-index"
394 | checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817"
395 | dependencies = [
396 | "typenum",
397 | "version_check",
398 | ]
399 |
400 | [[package]]
401 | name = "getrandom"
402 | version = "0.2.3"
403 | source = "registry+https://github.com/rust-lang/crates.io-index"
404 | checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
405 | dependencies = [
406 | "cfg-if",
407 | "libc",
408 | "wasi",
409 | ]
410 |
411 | [[package]]
412 | name = "h2"
413 | version = "0.3.9"
414 | source = "registry+https://github.com/rust-lang/crates.io-index"
415 | checksum = "8f072413d126e57991455e0a922b31e4c8ba7c2ffbebf6b78b4f8521397d65cd"
416 | dependencies = [
417 | "bytes",
418 | "fnv",
419 | "futures-core",
420 | "futures-sink",
421 | "futures-util",
422 | "http",
423 | "indexmap",
424 | "slab",
425 | "tokio",
426 | "tokio-util",
427 | "tracing",
428 | ]
429 |
430 | [[package]]
431 | name = "handlebars"
432 | version = "4.1.6"
433 | source = "registry+https://github.com/rust-lang/crates.io-index"
434 | checksum = "167fa173496c9eadd8749cca6f8339ac88e248f3ad2442791d0b743318a94fc0"
435 | dependencies = [
436 | "log",
437 | "pest",
438 | "pest_derive",
439 | "quick-error 2.0.1",
440 | "serde",
441 | "serde_json",
442 | ]
443 |
444 | [[package]]
445 | name = "hashbrown"
446 | version = "0.11.2"
447 | source = "registry+https://github.com/rust-lang/crates.io-index"
448 | checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
449 |
450 | [[package]]
451 | name = "headers"
452 | version = "0.3.5"
453 | source = "registry+https://github.com/rust-lang/crates.io-index"
454 | checksum = "a4c4eb0471fcb85846d8b0690695ef354f9afb11cb03cac2e1d7c9253351afb0"
455 | dependencies = [
456 | "base64",
457 | "bitflags",
458 | "bytes",
459 | "headers-core",
460 | "http",
461 | "httpdate",
462 | "mime",
463 | "sha-1 0.9.8",
464 | ]
465 |
466 | [[package]]
467 | name = "headers-core"
468 | version = "0.2.0"
469 | source = "registry+https://github.com/rust-lang/crates.io-index"
470 | checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429"
471 | dependencies = [
472 | "http",
473 | ]
474 |
475 | [[package]]
476 | name = "hermit-abi"
477 | version = "0.1.19"
478 | source = "registry+https://github.com/rust-lang/crates.io-index"
479 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
480 | dependencies = [
481 | "libc",
482 | ]
483 |
484 | [[package]]
485 | name = "http"
486 | version = "0.2.5"
487 | source = "registry+https://github.com/rust-lang/crates.io-index"
488 | checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b"
489 | dependencies = [
490 | "bytes",
491 | "fnv",
492 | "itoa 0.4.8",
493 | ]
494 |
495 | [[package]]
496 | name = "http-body"
497 | version = "0.4.4"
498 | source = "registry+https://github.com/rust-lang/crates.io-index"
499 | checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6"
500 | dependencies = [
501 | "bytes",
502 | "http",
503 | "pin-project-lite",
504 | ]
505 |
506 | [[package]]
507 | name = "http-range-header"
508 | version = "0.3.0"
509 | source = "registry+https://github.com/rust-lang/crates.io-index"
510 | checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29"
511 |
512 | [[package]]
513 | name = "httparse"
514 | version = "1.5.1"
515 | source = "registry+https://github.com/rust-lang/crates.io-index"
516 | checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503"
517 |
518 | [[package]]
519 | name = "httpdate"
520 | version = "1.0.2"
521 | source = "registry+https://github.com/rust-lang/crates.io-index"
522 | checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
523 |
524 | [[package]]
525 | name = "hyper"
526 | version = "0.14.16"
527 | source = "registry+https://github.com/rust-lang/crates.io-index"
528 | checksum = "b7ec3e62bdc98a2f0393a5048e4c30ef659440ea6e0e572965103e72bd836f55"
529 | dependencies = [
530 | "bytes",
531 | "futures-channel",
532 | "futures-core",
533 | "futures-util",
534 | "h2",
535 | "http",
536 | "http-body",
537 | "httparse",
538 | "httpdate",
539 | "itoa 0.4.8",
540 | "pin-project-lite",
541 | "socket2",
542 | "tokio",
543 | "tower-service",
544 | "tracing",
545 | "want",
546 | ]
547 |
548 | [[package]]
549 | name = "hyper-rustls"
550 | version = "0.23.0"
551 | source = "registry+https://github.com/rust-lang/crates.io-index"
552 | checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac"
553 | dependencies = [
554 | "http",
555 | "hyper",
556 | "rustls",
557 | "tokio",
558 | "tokio-rustls",
559 | ]
560 |
561 | [[package]]
562 | name = "hyper-timeout"
563 | version = "0.4.1"
564 | source = "registry+https://github.com/rust-lang/crates.io-index"
565 | checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1"
566 | dependencies = [
567 | "hyper",
568 | "pin-project-lite",
569 | "tokio",
570 | "tokio-io-timeout",
571 | ]
572 |
573 | [[package]]
574 | name = "hyper-tls"
575 | version = "0.5.0"
576 | source = "registry+https://github.com/rust-lang/crates.io-index"
577 | checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
578 | dependencies = [
579 | "bytes",
580 | "hyper",
581 | "native-tls",
582 | "tokio",
583 | "tokio-native-tls",
584 | ]
585 |
586 | [[package]]
587 | name = "idna"
588 | version = "0.2.3"
589 | source = "registry+https://github.com/rust-lang/crates.io-index"
590 | checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
591 | dependencies = [
592 | "matches",
593 | "unicode-bidi",
594 | "unicode-normalization",
595 | ]
596 |
597 | [[package]]
598 | name = "indexmap"
599 | version = "1.7.0"
600 | source = "registry+https://github.com/rust-lang/crates.io-index"
601 | checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
602 | dependencies = [
603 | "autocfg",
604 | "hashbrown",
605 | ]
606 |
607 | [[package]]
608 | name = "instant"
609 | version = "0.1.12"
610 | source = "registry+https://github.com/rust-lang/crates.io-index"
611 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
612 | dependencies = [
613 | "cfg-if",
614 | ]
615 |
616 | [[package]]
617 | name = "ipnet"
618 | version = "2.3.1"
619 | source = "registry+https://github.com/rust-lang/crates.io-index"
620 | checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9"
621 |
622 | [[package]]
623 | name = "itoa"
624 | version = "0.4.8"
625 | source = "registry+https://github.com/rust-lang/crates.io-index"
626 | checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
627 |
628 | [[package]]
629 | name = "itoa"
630 | version = "1.0.1"
631 | source = "registry+https://github.com/rust-lang/crates.io-index"
632 | checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
633 |
634 | [[package]]
635 | name = "js-sys"
636 | version = "0.3.55"
637 | source = "registry+https://github.com/rust-lang/crates.io-index"
638 | checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84"
639 | dependencies = [
640 | "wasm-bindgen",
641 | ]
642 |
643 | [[package]]
644 | name = "jsonpath_lib"
645 | version = "0.3.0"
646 | source = "registry+https://github.com/rust-lang/crates.io-index"
647 | checksum = "eaa63191d68230cccb81c5aa23abd53ed64d83337cacbb25a7b8c7979523774f"
648 | dependencies = [
649 | "log",
650 | "serde",
651 | "serde_json",
652 | ]
653 |
654 | [[package]]
655 | name = "k8s-openapi"
656 | version = "0.13.1"
657 | source = "registry+https://github.com/rust-lang/crates.io-index"
658 | checksum = "4f8de9873b904e74b3533f77493731ee26742418077503683db44e1b3c54aa5c"
659 | dependencies = [
660 | "base64",
661 | "bytes",
662 | "chrono",
663 | "serde",
664 | "serde-value",
665 | "serde_json",
666 | ]
667 |
668 | [[package]]
669 | name = "kube"
670 | version = "0.65.0"
671 | source = "registry+https://github.com/rust-lang/crates.io-index"
672 | checksum = "9ec231e9ec9e84789f9eb414d1ac40ce6c90d0517fb272a335b4233f2e272b1e"
673 | dependencies = [
674 | "k8s-openapi",
675 | "kube-client",
676 | "kube-core",
677 | ]
678 |
679 | [[package]]
680 | name = "kube-client"
681 | version = "0.65.0"
682 | source = "registry+https://github.com/rust-lang/crates.io-index"
683 | checksum = "95dddb1fcced906d79cdae530ff39079c2d3772b2d623088fdbebe610bfa8217"
684 | dependencies = [
685 | "base64",
686 | "bytes",
687 | "chrono",
688 | "dirs-next",
689 | "either",
690 | "futures",
691 | "http",
692 | "http-body",
693 | "hyper",
694 | "hyper-timeout",
695 | "hyper-tls",
696 | "jsonpath_lib",
697 | "k8s-openapi",
698 | "kube-core",
699 | "openssl",
700 | "pem",
701 | "pin-project",
702 | "serde",
703 | "serde_json",
704 | "serde_yaml",
705 | "thiserror",
706 | "tokio",
707 | "tokio-native-tls",
708 | "tokio-util",
709 | "tower",
710 | "tower-http",
711 | "tracing",
712 | ]
713 |
714 | [[package]]
715 | name = "kube-core"
716 | version = "0.65.0"
717 | source = "registry+https://github.com/rust-lang/crates.io-index"
718 | checksum = "c52b6ab05d160691083430f6f431707a4e05b64903f2ffa0095ee5efde759117"
719 | dependencies = [
720 | "chrono",
721 | "form_urlencoded",
722 | "http",
723 | "k8s-openapi",
724 | "once_cell",
725 | "serde",
726 | "serde_json",
727 | "thiserror",
728 | ]
729 |
730 | [[package]]
731 | name = "lazy_static"
732 | version = "1.4.0"
733 | source = "registry+https://github.com/rust-lang/crates.io-index"
734 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
735 |
736 | [[package]]
737 | name = "libc"
738 | version = "0.2.112"
739 | source = "registry+https://github.com/rust-lang/crates.io-index"
740 | checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125"
741 |
742 | [[package]]
743 | name = "linked-hash-map"
744 | version = "0.5.4"
745 | source = "registry+https://github.com/rust-lang/crates.io-index"
746 | checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
747 |
748 | [[package]]
749 | name = "lock_api"
750 | version = "0.4.5"
751 | source = "registry+https://github.com/rust-lang/crates.io-index"
752 | checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109"
753 | dependencies = [
754 | "scopeguard",
755 | ]
756 |
757 | [[package]]
758 | name = "log"
759 | version = "0.4.14"
760 | source = "registry+https://github.com/rust-lang/crates.io-index"
761 | checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
762 | dependencies = [
763 | "cfg-if",
764 | ]
765 |
766 | [[package]]
767 | name = "maplit"
768 | version = "1.0.2"
769 | source = "registry+https://github.com/rust-lang/crates.io-index"
770 | checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
771 |
772 | [[package]]
773 | name = "matches"
774 | version = "0.1.9"
775 | source = "registry+https://github.com/rust-lang/crates.io-index"
776 | checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
777 |
778 | [[package]]
779 | name = "memchr"
780 | version = "2.4.1"
781 | source = "registry+https://github.com/rust-lang/crates.io-index"
782 | checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
783 |
784 | [[package]]
785 | name = "mime"
786 | version = "0.3.16"
787 | source = "registry+https://github.com/rust-lang/crates.io-index"
788 | checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
789 |
790 | [[package]]
791 | name = "mime_guess"
792 | version = "2.0.3"
793 | source = "registry+https://github.com/rust-lang/crates.io-index"
794 | checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212"
795 | dependencies = [
796 | "mime",
797 | "unicase",
798 | ]
799 |
800 | [[package]]
801 | name = "mio"
802 | version = "0.7.14"
803 | source = "registry+https://github.com/rust-lang/crates.io-index"
804 | checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc"
805 | dependencies = [
806 | "libc",
807 | "log",
808 | "miow",
809 | "ntapi",
810 | "winapi",
811 | ]
812 |
813 | [[package]]
814 | name = "miow"
815 | version = "0.3.7"
816 | source = "registry+https://github.com/rust-lang/crates.io-index"
817 | checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
818 | dependencies = [
819 | "winapi",
820 | ]
821 |
822 | [[package]]
823 | name = "multipart"
824 | version = "0.18.0"
825 | source = "registry+https://github.com/rust-lang/crates.io-index"
826 | checksum = "00dec633863867f29cb39df64a397cdf4a6354708ddd7759f70c7fb51c5f9182"
827 | dependencies = [
828 | "buf_redux",
829 | "httparse",
830 | "log",
831 | "mime",
832 | "mime_guess",
833 | "quick-error 1.2.3",
834 | "rand",
835 | "safemem",
836 | "tempfile",
837 | "twoway",
838 | ]
839 |
840 | [[package]]
841 | name = "native-tls"
842 | version = "0.2.8"
843 | source = "registry+https://github.com/rust-lang/crates.io-index"
844 | checksum = "48ba9f7719b5a0f42f338907614285fb5fd70e53858141f69898a1fb7203b24d"
845 | dependencies = [
846 | "lazy_static",
847 | "libc",
848 | "log",
849 | "openssl",
850 | "openssl-probe",
851 | "openssl-sys",
852 | "schannel",
853 | "security-framework",
854 | "security-framework-sys",
855 | "tempfile",
856 | ]
857 |
858 | [[package]]
859 | name = "ntapi"
860 | version = "0.3.6"
861 | source = "registry+https://github.com/rust-lang/crates.io-index"
862 | checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44"
863 | dependencies = [
864 | "winapi",
865 | ]
866 |
867 | [[package]]
868 | name = "num-integer"
869 | version = "0.1.44"
870 | source = "registry+https://github.com/rust-lang/crates.io-index"
871 | checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
872 | dependencies = [
873 | "autocfg",
874 | "num-traits",
875 | ]
876 |
877 | [[package]]
878 | name = "num-traits"
879 | version = "0.2.14"
880 | source = "registry+https://github.com/rust-lang/crates.io-index"
881 | checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
882 | dependencies = [
883 | "autocfg",
884 | ]
885 |
886 | [[package]]
887 | name = "num_cpus"
888 | version = "1.13.1"
889 | source = "registry+https://github.com/rust-lang/crates.io-index"
890 | checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
891 | dependencies = [
892 | "hermit-abi",
893 | "libc",
894 | ]
895 |
896 | [[package]]
897 | name = "once_cell"
898 | version = "1.9.0"
899 | source = "registry+https://github.com/rust-lang/crates.io-index"
900 | checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5"
901 |
902 | [[package]]
903 | name = "opaque-debug"
904 | version = "0.2.3"
905 | source = "registry+https://github.com/rust-lang/crates.io-index"
906 | checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c"
907 |
908 | [[package]]
909 | name = "opaque-debug"
910 | version = "0.3.0"
911 | source = "registry+https://github.com/rust-lang/crates.io-index"
912 | checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
913 |
914 | [[package]]
915 | name = "openssl"
916 | version = "0.10.38"
917 | source = "registry+https://github.com/rust-lang/crates.io-index"
918 | checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95"
919 | dependencies = [
920 | "bitflags",
921 | "cfg-if",
922 | "foreign-types",
923 | "libc",
924 | "once_cell",
925 | "openssl-sys",
926 | ]
927 |
928 | [[package]]
929 | name = "openssl-probe"
930 | version = "0.1.4"
931 | source = "registry+https://github.com/rust-lang/crates.io-index"
932 | checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a"
933 |
934 | [[package]]
935 | name = "openssl-sys"
936 | version = "0.9.72"
937 | source = "registry+https://github.com/rust-lang/crates.io-index"
938 | checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb"
939 | dependencies = [
940 | "autocfg",
941 | "cc",
942 | "libc",
943 | "pkg-config",
944 | "vcpkg",
945 | ]
946 |
947 | [[package]]
948 | name = "ordered-float"
949 | version = "2.8.0"
950 | source = "registry+https://github.com/rust-lang/crates.io-index"
951 | checksum = "97c9d06878b3a851e8026ef94bf7fef9ba93062cd412601da4d9cf369b1cc62d"
952 | dependencies = [
953 | "num-traits",
954 | ]
955 |
956 | [[package]]
957 | name = "os_str_bytes"
958 | version = "6.0.0"
959 | source = "registry+https://github.com/rust-lang/crates.io-index"
960 | checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64"
961 | dependencies = [
962 | "memchr",
963 | ]
964 |
965 | [[package]]
966 | name = "parking_lot"
967 | version = "0.11.2"
968 | source = "registry+https://github.com/rust-lang/crates.io-index"
969 | checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
970 | dependencies = [
971 | "instant",
972 | "lock_api",
973 | "parking_lot_core",
974 | ]
975 |
976 | [[package]]
977 | name = "parking_lot_core"
978 | version = "0.8.5"
979 | source = "registry+https://github.com/rust-lang/crates.io-index"
980 | checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216"
981 | dependencies = [
982 | "cfg-if",
983 | "instant",
984 | "libc",
985 | "redox_syscall",
986 | "smallvec",
987 | "winapi",
988 | ]
989 |
990 | [[package]]
991 | name = "pem"
992 | version = "1.0.1"
993 | source = "registry+https://github.com/rust-lang/crates.io-index"
994 | checksum = "06673860db84d02a63942fa69cd9543f2624a5df3aea7f33173048fa7ad5cf1a"
995 | dependencies = [
996 | "base64",
997 | "once_cell",
998 | "regex",
999 | ]
1000 |
1001 | [[package]]
1002 | name = "percent-encoding"
1003 | version = "2.1.0"
1004 | source = "registry+https://github.com/rust-lang/crates.io-index"
1005 | checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
1006 |
1007 | [[package]]
1008 | name = "pest"
1009 | version = "2.1.3"
1010 | source = "registry+https://github.com/rust-lang/crates.io-index"
1011 | checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53"
1012 | dependencies = [
1013 | "ucd-trie",
1014 | ]
1015 |
1016 | [[package]]
1017 | name = "pest_derive"
1018 | version = "2.1.0"
1019 | source = "registry+https://github.com/rust-lang/crates.io-index"
1020 | checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0"
1021 | dependencies = [
1022 | "pest",
1023 | "pest_generator",
1024 | ]
1025 |
1026 | [[package]]
1027 | name = "pest_generator"
1028 | version = "2.1.3"
1029 | source = "registry+https://github.com/rust-lang/crates.io-index"
1030 | checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55"
1031 | dependencies = [
1032 | "pest",
1033 | "pest_meta",
1034 | "proc-macro2",
1035 | "quote",
1036 | "syn",
1037 | ]
1038 |
1039 | [[package]]
1040 | name = "pest_meta"
1041 | version = "2.1.3"
1042 | source = "registry+https://github.com/rust-lang/crates.io-index"
1043 | checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d"
1044 | dependencies = [
1045 | "maplit",
1046 | "pest",
1047 | "sha-1 0.8.2",
1048 | ]
1049 |
1050 | [[package]]
1051 | name = "pin-project"
1052 | version = "1.0.8"
1053 | source = "registry+https://github.com/rust-lang/crates.io-index"
1054 | checksum = "576bc800220cc65dac09e99e97b08b358cfab6e17078de8dc5fee223bd2d0c08"
1055 | dependencies = [
1056 | "pin-project-internal",
1057 | ]
1058 |
1059 | [[package]]
1060 | name = "pin-project-internal"
1061 | version = "1.0.8"
1062 | source = "registry+https://github.com/rust-lang/crates.io-index"
1063 | checksum = "6e8fe8163d14ce7f0cdac2e040116f22eac817edabff0be91e8aff7e9accf389"
1064 | dependencies = [
1065 | "proc-macro2",
1066 | "quote",
1067 | "syn",
1068 | ]
1069 |
1070 | [[package]]
1071 | name = "pin-project-lite"
1072 | version = "0.2.7"
1073 | source = "registry+https://github.com/rust-lang/crates.io-index"
1074 | checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443"
1075 |
1076 | [[package]]
1077 | name = "pin-utils"
1078 | version = "0.1.0"
1079 | source = "registry+https://github.com/rust-lang/crates.io-index"
1080 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
1081 |
1082 | [[package]]
1083 | name = "pkg-config"
1084 | version = "0.3.24"
1085 | source = "registry+https://github.com/rust-lang/crates.io-index"
1086 | checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe"
1087 |
1088 | [[package]]
1089 | name = "ppv-lite86"
1090 | version = "0.2.15"
1091 | source = "registry+https://github.com/rust-lang/crates.io-index"
1092 | checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba"
1093 |
1094 | [[package]]
1095 | name = "proc-macro2"
1096 | version = "1.0.34"
1097 | source = "registry+https://github.com/rust-lang/crates.io-index"
1098 | checksum = "2f84e92c0f7c9d58328b85a78557813e4bd845130db68d7184635344399423b1"
1099 | dependencies = [
1100 | "unicode-xid",
1101 | ]
1102 |
1103 | [[package]]
1104 | name = "quick-error"
1105 | version = "1.2.3"
1106 | source = "registry+https://github.com/rust-lang/crates.io-index"
1107 | checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
1108 |
1109 | [[package]]
1110 | name = "quick-error"
1111 | version = "2.0.1"
1112 | source = "registry+https://github.com/rust-lang/crates.io-index"
1113 | checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
1114 |
1115 | [[package]]
1116 | name = "quote"
1117 | version = "1.0.10"
1118 | source = "registry+https://github.com/rust-lang/crates.io-index"
1119 | checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
1120 | dependencies = [
1121 | "proc-macro2",
1122 | ]
1123 |
1124 | [[package]]
1125 | name = "rand"
1126 | version = "0.8.4"
1127 | source = "registry+https://github.com/rust-lang/crates.io-index"
1128 | checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
1129 | dependencies = [
1130 | "libc",
1131 | "rand_chacha",
1132 | "rand_core",
1133 | "rand_hc",
1134 | ]
1135 |
1136 | [[package]]
1137 | name = "rand_chacha"
1138 | version = "0.3.1"
1139 | source = "registry+https://github.com/rust-lang/crates.io-index"
1140 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
1141 | dependencies = [
1142 | "ppv-lite86",
1143 | "rand_core",
1144 | ]
1145 |
1146 | [[package]]
1147 | name = "rand_core"
1148 | version = "0.6.3"
1149 | source = "registry+https://github.com/rust-lang/crates.io-index"
1150 | checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
1151 | dependencies = [
1152 | "getrandom",
1153 | ]
1154 |
1155 | [[package]]
1156 | name = "rand_hc"
1157 | version = "0.3.1"
1158 | source = "registry+https://github.com/rust-lang/crates.io-index"
1159 | checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7"
1160 | dependencies = [
1161 | "rand_core",
1162 | ]
1163 |
1164 | [[package]]
1165 | name = "redox_syscall"
1166 | version = "0.2.10"
1167 | source = "registry+https://github.com/rust-lang/crates.io-index"
1168 | checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff"
1169 | dependencies = [
1170 | "bitflags",
1171 | ]
1172 |
1173 | [[package]]
1174 | name = "redox_users"
1175 | version = "0.4.0"
1176 | source = "registry+https://github.com/rust-lang/crates.io-index"
1177 | checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64"
1178 | dependencies = [
1179 | "getrandom",
1180 | "redox_syscall",
1181 | ]
1182 |
1183 | [[package]]
1184 | name = "regex"
1185 | version = "1.5.4"
1186 | source = "registry+https://github.com/rust-lang/crates.io-index"
1187 | checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
1188 | dependencies = [
1189 | "regex-syntax",
1190 | ]
1191 |
1192 | [[package]]
1193 | name = "regex-syntax"
1194 | version = "0.6.25"
1195 | source = "registry+https://github.com/rust-lang/crates.io-index"
1196 | checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
1197 |
1198 | [[package]]
1199 | name = "remove_dir_all"
1200 | version = "0.5.3"
1201 | source = "registry+https://github.com/rust-lang/crates.io-index"
1202 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
1203 | dependencies = [
1204 | "winapi",
1205 | ]
1206 |
1207 | [[package]]
1208 | name = "reqwest"
1209 | version = "0.11.8"
1210 | source = "registry+https://github.com/rust-lang/crates.io-index"
1211 | checksum = "7c4e0a76dc12a116108933f6301b95e83634e0c47b0afbed6abbaa0601e99258"
1212 | dependencies = [
1213 | "base64",
1214 | "bytes",
1215 | "encoding_rs",
1216 | "futures-core",
1217 | "futures-util",
1218 | "http",
1219 | "http-body",
1220 | "hyper",
1221 | "hyper-rustls",
1222 | "ipnet",
1223 | "js-sys",
1224 | "lazy_static",
1225 | "log",
1226 | "mime",
1227 | "percent-encoding",
1228 | "pin-project-lite",
1229 | "rustls",
1230 | "rustls-pemfile",
1231 | "serde",
1232 | "serde_json",
1233 | "serde_urlencoded",
1234 | "tokio",
1235 | "tokio-rustls",
1236 | "url",
1237 | "wasm-bindgen",
1238 | "wasm-bindgen-futures",
1239 | "web-sys",
1240 | "webpki-roots",
1241 | "winreg",
1242 | ]
1243 |
1244 | [[package]]
1245 | name = "ring"
1246 | version = "0.16.20"
1247 | source = "registry+https://github.com/rust-lang/crates.io-index"
1248 | checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
1249 | dependencies = [
1250 | "cc",
1251 | "libc",
1252 | "once_cell",
1253 | "spin",
1254 | "untrusted",
1255 | "web-sys",
1256 | "winapi",
1257 | ]
1258 |
1259 | [[package]]
1260 | name = "rustls"
1261 | version = "0.20.2"
1262 | source = "registry+https://github.com/rust-lang/crates.io-index"
1263 | checksum = "d37e5e2290f3e040b594b1a9e04377c2c671f1a1cfd9bfdef82106ac1c113f84"
1264 | dependencies = [
1265 | "log",
1266 | "ring",
1267 | "sct",
1268 | "webpki",
1269 | ]
1270 |
1271 | [[package]]
1272 | name = "rustls-pemfile"
1273 | version = "0.2.1"
1274 | source = "registry+https://github.com/rust-lang/crates.io-index"
1275 | checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9"
1276 | dependencies = [
1277 | "base64",
1278 | ]
1279 |
1280 | [[package]]
1281 | name = "ryu"
1282 | version = "1.0.9"
1283 | source = "registry+https://github.com/rust-lang/crates.io-index"
1284 | checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f"
1285 |
1286 | [[package]]
1287 | name = "safemem"
1288 | version = "0.3.3"
1289 | source = "registry+https://github.com/rust-lang/crates.io-index"
1290 | checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
1291 |
1292 | [[package]]
1293 | name = "schannel"
1294 | version = "0.1.19"
1295 | source = "registry+https://github.com/rust-lang/crates.io-index"
1296 | checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75"
1297 | dependencies = [
1298 | "lazy_static",
1299 | "winapi",
1300 | ]
1301 |
1302 | [[package]]
1303 | name = "scoped-tls"
1304 | version = "1.0.0"
1305 | source = "registry+https://github.com/rust-lang/crates.io-index"
1306 | checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2"
1307 |
1308 | [[package]]
1309 | name = "scopeguard"
1310 | version = "1.1.0"
1311 | source = "registry+https://github.com/rust-lang/crates.io-index"
1312 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
1313 |
1314 | [[package]]
1315 | name = "sct"
1316 | version = "0.7.0"
1317 | source = "registry+https://github.com/rust-lang/crates.io-index"
1318 | checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4"
1319 | dependencies = [
1320 | "ring",
1321 | "untrusted",
1322 | ]
1323 |
1324 | [[package]]
1325 | name = "security-framework"
1326 | version = "2.3.1"
1327 | source = "registry+https://github.com/rust-lang/crates.io-index"
1328 | checksum = "23a2ac85147a3a11d77ecf1bc7166ec0b92febfa4461c37944e180f319ece467"
1329 | dependencies = [
1330 | "bitflags",
1331 | "core-foundation",
1332 | "core-foundation-sys",
1333 | "libc",
1334 | "security-framework-sys",
1335 | ]
1336 |
1337 | [[package]]
1338 | name = "security-framework-sys"
1339 | version = "2.4.2"
1340 | source = "registry+https://github.com/rust-lang/crates.io-index"
1341 | checksum = "a9dd14d83160b528b7bfd66439110573efcfbe281b17fc2ca9f39f550d619c7e"
1342 | dependencies = [
1343 | "core-foundation-sys",
1344 | "libc",
1345 | ]
1346 |
1347 | [[package]]
1348 | name = "serde"
1349 | version = "1.0.132"
1350 | source = "registry+https://github.com/rust-lang/crates.io-index"
1351 | checksum = "8b9875c23cf305cd1fd7eb77234cbb705f21ea6a72c637a5c6db5fe4b8e7f008"
1352 | dependencies = [
1353 | "serde_derive",
1354 | ]
1355 |
1356 | [[package]]
1357 | name = "serde-value"
1358 | version = "0.7.0"
1359 | source = "registry+https://github.com/rust-lang/crates.io-index"
1360 | checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c"
1361 | dependencies = [
1362 | "ordered-float",
1363 | "serde",
1364 | ]
1365 |
1366 | [[package]]
1367 | name = "serde_derive"
1368 | version = "1.0.132"
1369 | source = "registry+https://github.com/rust-lang/crates.io-index"
1370 | checksum = "ecc0db5cb2556c0e558887d9bbdcf6ac4471e83ff66cf696e5419024d1606276"
1371 | dependencies = [
1372 | "proc-macro2",
1373 | "quote",
1374 | "syn",
1375 | ]
1376 |
1377 | [[package]]
1378 | name = "serde_json"
1379 | version = "1.0.73"
1380 | source = "registry+https://github.com/rust-lang/crates.io-index"
1381 | checksum = "bcbd0344bc6533bc7ec56df11d42fb70f1b912351c0825ccb7211b59d8af7cf5"
1382 | dependencies = [
1383 | "indexmap",
1384 | "itoa 1.0.1",
1385 | "ryu",
1386 | "serde",
1387 | ]
1388 |
1389 | [[package]]
1390 | name = "serde_urlencoded"
1391 | version = "0.7.0"
1392 | source = "registry+https://github.com/rust-lang/crates.io-index"
1393 | checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9"
1394 | dependencies = [
1395 | "form_urlencoded",
1396 | "itoa 0.4.8",
1397 | "ryu",
1398 | "serde",
1399 | ]
1400 |
1401 | [[package]]
1402 | name = "serde_yaml"
1403 | version = "0.8.23"
1404 | source = "registry+https://github.com/rust-lang/crates.io-index"
1405 | checksum = "a4a521f2940385c165a24ee286aa8599633d162077a54bdcae2a6fd5a7bfa7a0"
1406 | dependencies = [
1407 | "indexmap",
1408 | "ryu",
1409 | "serde",
1410 | "yaml-rust",
1411 | ]
1412 |
1413 | [[package]]
1414 | name = "sha-1"
1415 | version = "0.8.2"
1416 | source = "registry+https://github.com/rust-lang/crates.io-index"
1417 | checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df"
1418 | dependencies = [
1419 | "block-buffer 0.7.3",
1420 | "digest 0.8.1",
1421 | "fake-simd",
1422 | "opaque-debug 0.2.3",
1423 | ]
1424 |
1425 | [[package]]
1426 | name = "sha-1"
1427 | version = "0.9.8"
1428 | source = "registry+https://github.com/rust-lang/crates.io-index"
1429 | checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6"
1430 | dependencies = [
1431 | "block-buffer 0.9.0",
1432 | "cfg-if",
1433 | "cpufeatures",
1434 | "digest 0.9.0",
1435 | "opaque-debug 0.3.0",
1436 | ]
1437 |
1438 | [[package]]
1439 | name = "signal-hook-registry"
1440 | version = "1.4.0"
1441 | source = "registry+https://github.com/rust-lang/crates.io-index"
1442 | checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
1443 | dependencies = [
1444 | "libc",
1445 | ]
1446 |
1447 | [[package]]
1448 | name = "slab"
1449 | version = "0.4.5"
1450 | source = "registry+https://github.com/rust-lang/crates.io-index"
1451 | checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5"
1452 |
1453 | [[package]]
1454 | name = "smallvec"
1455 | version = "1.7.0"
1456 | source = "registry+https://github.com/rust-lang/crates.io-index"
1457 | checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309"
1458 |
1459 | [[package]]
1460 | name = "socket2"
1461 | version = "0.4.2"
1462 | source = "registry+https://github.com/rust-lang/crates.io-index"
1463 | checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516"
1464 | dependencies = [
1465 | "libc",
1466 | "winapi",
1467 | ]
1468 |
1469 | [[package]]
1470 | name = "spin"
1471 | version = "0.5.2"
1472 | source = "registry+https://github.com/rust-lang/crates.io-index"
1473 | checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
1474 |
1475 | [[package]]
1476 | name = "strsim"
1477 | version = "0.10.0"
1478 | source = "registry+https://github.com/rust-lang/crates.io-index"
1479 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
1480 |
1481 | [[package]]
1482 | name = "syn"
1483 | version = "1.0.82"
1484 | source = "registry+https://github.com/rust-lang/crates.io-index"
1485 | checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59"
1486 | dependencies = [
1487 | "proc-macro2",
1488 | "quote",
1489 | "unicode-xid",
1490 | ]
1491 |
1492 | [[package]]
1493 | name = "tempfile"
1494 | version = "3.2.0"
1495 | source = "registry+https://github.com/rust-lang/crates.io-index"
1496 | checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22"
1497 | dependencies = [
1498 | "cfg-if",
1499 | "libc",
1500 | "rand",
1501 | "redox_syscall",
1502 | "remove_dir_all",
1503 | "winapi",
1504 | ]
1505 |
1506 | [[package]]
1507 | name = "termcolor"
1508 | version = "1.1.2"
1509 | source = "registry+https://github.com/rust-lang/crates.io-index"
1510 | checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
1511 | dependencies = [
1512 | "winapi-util",
1513 | ]
1514 |
1515 | [[package]]
1516 | name = "textwrap"
1517 | version = "0.14.2"
1518 | source = "registry+https://github.com/rust-lang/crates.io-index"
1519 | checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80"
1520 |
1521 | [[package]]
1522 | name = "thiserror"
1523 | version = "1.0.30"
1524 | source = "registry+https://github.com/rust-lang/crates.io-index"
1525 | checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
1526 | dependencies = [
1527 | "thiserror-impl",
1528 | ]
1529 |
1530 | [[package]]
1531 | name = "thiserror-impl"
1532 | version = "1.0.30"
1533 | source = "registry+https://github.com/rust-lang/crates.io-index"
1534 | checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
1535 | dependencies = [
1536 | "proc-macro2",
1537 | "quote",
1538 | "syn",
1539 | ]
1540 |
1541 | [[package]]
1542 | name = "time"
1543 | version = "0.1.44"
1544 | source = "registry+https://github.com/rust-lang/crates.io-index"
1545 | checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
1546 | dependencies = [
1547 | "libc",
1548 | "wasi",
1549 | "winapi",
1550 | ]
1551 |
1552 | [[package]]
1553 | name = "tinyvec"
1554 | version = "1.5.1"
1555 | source = "registry+https://github.com/rust-lang/crates.io-index"
1556 | checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2"
1557 | dependencies = [
1558 | "tinyvec_macros",
1559 | ]
1560 |
1561 | [[package]]
1562 | name = "tinyvec_macros"
1563 | version = "0.1.0"
1564 | source = "registry+https://github.com/rust-lang/crates.io-index"
1565 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
1566 |
1567 | [[package]]
1568 | name = "tokio"
1569 | version = "1.15.0"
1570 | source = "registry+https://github.com/rust-lang/crates.io-index"
1571 | checksum = "fbbf1c778ec206785635ce8ad57fe52b3009ae9e0c9f574a728f3049d3e55838"
1572 | dependencies = [
1573 | "bytes",
1574 | "libc",
1575 | "memchr",
1576 | "mio",
1577 | "num_cpus",
1578 | "once_cell",
1579 | "parking_lot",
1580 | "pin-project-lite",
1581 | "signal-hook-registry",
1582 | "tokio-macros",
1583 | "winapi",
1584 | ]
1585 |
1586 | [[package]]
1587 | name = "tokio-io-timeout"
1588 | version = "1.1.1"
1589 | source = "registry+https://github.com/rust-lang/crates.io-index"
1590 | checksum = "90c49f106be240de154571dd31fbe48acb10ba6c6dd6f6517ad603abffa42de9"
1591 | dependencies = [
1592 | "pin-project-lite",
1593 | "tokio",
1594 | ]
1595 |
1596 | [[package]]
1597 | name = "tokio-macros"
1598 | version = "1.7.0"
1599 | source = "registry+https://github.com/rust-lang/crates.io-index"
1600 | checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7"
1601 | dependencies = [
1602 | "proc-macro2",
1603 | "quote",
1604 | "syn",
1605 | ]
1606 |
1607 | [[package]]
1608 | name = "tokio-native-tls"
1609 | version = "0.3.0"
1610 | source = "registry+https://github.com/rust-lang/crates.io-index"
1611 | checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b"
1612 | dependencies = [
1613 | "native-tls",
1614 | "tokio",
1615 | ]
1616 |
1617 | [[package]]
1618 | name = "tokio-rustls"
1619 | version = "0.23.2"
1620 | source = "registry+https://github.com/rust-lang/crates.io-index"
1621 | checksum = "a27d5f2b839802bd8267fa19b0530f5a08b9c08cd417976be2a65d130fe1c11b"
1622 | dependencies = [
1623 | "rustls",
1624 | "tokio",
1625 | "webpki",
1626 | ]
1627 |
1628 | [[package]]
1629 | name = "tokio-stream"
1630 | version = "0.1.8"
1631 | source = "registry+https://github.com/rust-lang/crates.io-index"
1632 | checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3"
1633 | dependencies = [
1634 | "futures-core",
1635 | "pin-project-lite",
1636 | "tokio",
1637 | ]
1638 |
1639 | [[package]]
1640 | name = "tokio-tungstenite"
1641 | version = "0.15.0"
1642 | source = "registry+https://github.com/rust-lang/crates.io-index"
1643 | checksum = "511de3f85caf1c98983545490c3d09685fa8eb634e57eec22bb4db271f46cbd8"
1644 | dependencies = [
1645 | "futures-util",
1646 | "log",
1647 | "pin-project",
1648 | "tokio",
1649 | "tungstenite",
1650 | ]
1651 |
1652 | [[package]]
1653 | name = "tokio-util"
1654 | version = "0.6.9"
1655 | source = "registry+https://github.com/rust-lang/crates.io-index"
1656 | checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0"
1657 | dependencies = [
1658 | "bytes",
1659 | "futures-core",
1660 | "futures-sink",
1661 | "log",
1662 | "pin-project-lite",
1663 | "tokio",
1664 | ]
1665 |
1666 | [[package]]
1667 | name = "tower"
1668 | version = "0.4.11"
1669 | source = "registry+https://github.com/rust-lang/crates.io-index"
1670 | checksum = "5651b5f6860a99bd1adb59dbfe1db8beb433e73709d9032b413a77e2fb7c066a"
1671 | dependencies = [
1672 | "futures-core",
1673 | "futures-util",
1674 | "pin-project",
1675 | "pin-project-lite",
1676 | "tokio",
1677 | "tokio-util",
1678 | "tower-layer",
1679 | "tower-service",
1680 | "tracing",
1681 | ]
1682 |
1683 | [[package]]
1684 | name = "tower-http"
1685 | version = "0.2.0"
1686 | source = "registry+https://github.com/rust-lang/crates.io-index"
1687 | checksum = "39ee603d6e665ecc7e0f8d479eedb4626bd4726f0ee6119cee5b3a6bf184cac0"
1688 | dependencies = [
1689 | "base64",
1690 | "bitflags",
1691 | "bytes",
1692 | "futures-core",
1693 | "futures-util",
1694 | "http",
1695 | "http-body",
1696 | "http-range-header",
1697 | "pin-project-lite",
1698 | "tower-layer",
1699 | "tower-service",
1700 | "tracing",
1701 | ]
1702 |
1703 | [[package]]
1704 | name = "tower-layer"
1705 | version = "0.3.1"
1706 | source = "registry+https://github.com/rust-lang/crates.io-index"
1707 | checksum = "343bc9466d3fe6b0f960ef45960509f84480bf4fd96f92901afe7ff3df9d3a62"
1708 |
1709 | [[package]]
1710 | name = "tower-service"
1711 | version = "0.3.1"
1712 | source = "registry+https://github.com/rust-lang/crates.io-index"
1713 | checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6"
1714 |
1715 | [[package]]
1716 | name = "tracing"
1717 | version = "0.1.29"
1718 | source = "registry+https://github.com/rust-lang/crates.io-index"
1719 | checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105"
1720 | dependencies = [
1721 | "cfg-if",
1722 | "log",
1723 | "pin-project-lite",
1724 | "tracing-attributes",
1725 | "tracing-core",
1726 | ]
1727 |
1728 | [[package]]
1729 | name = "tracing-attributes"
1730 | version = "0.1.18"
1731 | source = "registry+https://github.com/rust-lang/crates.io-index"
1732 | checksum = "f4f480b8f81512e825f337ad51e94c1eb5d3bbdf2b363dcd01e2b19a9ffe3f8e"
1733 | dependencies = [
1734 | "proc-macro2",
1735 | "quote",
1736 | "syn",
1737 | ]
1738 |
1739 | [[package]]
1740 | name = "tracing-core"
1741 | version = "0.1.21"
1742 | source = "registry+https://github.com/rust-lang/crates.io-index"
1743 | checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4"
1744 | dependencies = [
1745 | "lazy_static",
1746 | ]
1747 |
1748 | [[package]]
1749 | name = "try-lock"
1750 | version = "0.2.3"
1751 | source = "registry+https://github.com/rust-lang/crates.io-index"
1752 | checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
1753 |
1754 | [[package]]
1755 | name = "tungstenite"
1756 | version = "0.14.0"
1757 | source = "registry+https://github.com/rust-lang/crates.io-index"
1758 | checksum = "a0b2d8558abd2e276b0a8df5c05a2ec762609344191e5fd23e292c910e9165b5"
1759 | dependencies = [
1760 | "base64",
1761 | "byteorder",
1762 | "bytes",
1763 | "http",
1764 | "httparse",
1765 | "log",
1766 | "rand",
1767 | "sha-1 0.9.8",
1768 | "thiserror",
1769 | "url",
1770 | "utf-8",
1771 | ]
1772 |
1773 | [[package]]
1774 | name = "twoway"
1775 | version = "0.1.8"
1776 | source = "registry+https://github.com/rust-lang/crates.io-index"
1777 | checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1"
1778 | dependencies = [
1779 | "memchr",
1780 | ]
1781 |
1782 | [[package]]
1783 | name = "typenum"
1784 | version = "1.14.0"
1785 | source = "registry+https://github.com/rust-lang/crates.io-index"
1786 | checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec"
1787 |
1788 | [[package]]
1789 | name = "ucd-trie"
1790 | version = "0.1.3"
1791 | source = "registry+https://github.com/rust-lang/crates.io-index"
1792 | checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
1793 |
1794 | [[package]]
1795 | name = "unicase"
1796 | version = "2.6.0"
1797 | source = "registry+https://github.com/rust-lang/crates.io-index"
1798 | checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
1799 | dependencies = [
1800 | "version_check",
1801 | ]
1802 |
1803 | [[package]]
1804 | name = "unicode-bidi"
1805 | version = "0.3.7"
1806 | source = "registry+https://github.com/rust-lang/crates.io-index"
1807 | checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
1808 |
1809 | [[package]]
1810 | name = "unicode-normalization"
1811 | version = "0.1.19"
1812 | source = "registry+https://github.com/rust-lang/crates.io-index"
1813 | checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
1814 | dependencies = [
1815 | "tinyvec",
1816 | ]
1817 |
1818 | [[package]]
1819 | name = "unicode-xid"
1820 | version = "0.2.2"
1821 | source = "registry+https://github.com/rust-lang/crates.io-index"
1822 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
1823 |
1824 | [[package]]
1825 | name = "untrusted"
1826 | version = "0.7.1"
1827 | source = "registry+https://github.com/rust-lang/crates.io-index"
1828 | checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
1829 |
1830 | [[package]]
1831 | name = "url"
1832 | version = "2.2.2"
1833 | source = "registry+https://github.com/rust-lang/crates.io-index"
1834 | checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
1835 | dependencies = [
1836 | "form_urlencoded",
1837 | "idna",
1838 | "matches",
1839 | "percent-encoding",
1840 | ]
1841 |
1842 | [[package]]
1843 | name = "utf-8"
1844 | version = "0.7.6"
1845 | source = "registry+https://github.com/rust-lang/crates.io-index"
1846 | checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
1847 |
1848 | [[package]]
1849 | name = "vcpkg"
1850 | version = "0.2.15"
1851 | source = "registry+https://github.com/rust-lang/crates.io-index"
1852 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
1853 |
1854 | [[package]]
1855 | name = "version_check"
1856 | version = "0.9.3"
1857 | source = "registry+https://github.com/rust-lang/crates.io-index"
1858 | checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
1859 |
1860 | [[package]]
1861 | name = "want"
1862 | version = "0.3.0"
1863 | source = "registry+https://github.com/rust-lang/crates.io-index"
1864 | checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0"
1865 | dependencies = [
1866 | "log",
1867 | "try-lock",
1868 | ]
1869 |
1870 | [[package]]
1871 | name = "warp"
1872 | version = "0.3.2"
1873 | source = "registry+https://github.com/rust-lang/crates.io-index"
1874 | checksum = "3cef4e1e9114a4b7f1ac799f16ce71c14de5778500c5450ec6b7b920c55b587e"
1875 | dependencies = [
1876 | "bytes",
1877 | "futures-channel",
1878 | "futures-util",
1879 | "headers",
1880 | "http",
1881 | "hyper",
1882 | "log",
1883 | "mime",
1884 | "mime_guess",
1885 | "multipart",
1886 | "percent-encoding",
1887 | "pin-project",
1888 | "scoped-tls",
1889 | "serde",
1890 | "serde_json",
1891 | "serde_urlencoded",
1892 | "tokio",
1893 | "tokio-stream",
1894 | "tokio-tungstenite",
1895 | "tokio-util",
1896 | "tower-service",
1897 | "tracing",
1898 | ]
1899 |
1900 | [[package]]
1901 | name = "wasi"
1902 | version = "0.10.0+wasi-snapshot-preview1"
1903 | source = "registry+https://github.com/rust-lang/crates.io-index"
1904 | checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
1905 |
1906 | [[package]]
1907 | name = "wasm-bindgen"
1908 | version = "0.2.78"
1909 | source = "registry+https://github.com/rust-lang/crates.io-index"
1910 | checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce"
1911 | dependencies = [
1912 | "cfg-if",
1913 | "wasm-bindgen-macro",
1914 | ]
1915 |
1916 | [[package]]
1917 | name = "wasm-bindgen-backend"
1918 | version = "0.2.78"
1919 | source = "registry+https://github.com/rust-lang/crates.io-index"
1920 | checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b"
1921 | dependencies = [
1922 | "bumpalo",
1923 | "lazy_static",
1924 | "log",
1925 | "proc-macro2",
1926 | "quote",
1927 | "syn",
1928 | "wasm-bindgen-shared",
1929 | ]
1930 |
1931 | [[package]]
1932 | name = "wasm-bindgen-futures"
1933 | version = "0.4.28"
1934 | source = "registry+https://github.com/rust-lang/crates.io-index"
1935 | checksum = "8e8d7523cb1f2a4c96c1317ca690031b714a51cc14e05f712446691f413f5d39"
1936 | dependencies = [
1937 | "cfg-if",
1938 | "js-sys",
1939 | "wasm-bindgen",
1940 | "web-sys",
1941 | ]
1942 |
1943 | [[package]]
1944 | name = "wasm-bindgen-macro"
1945 | version = "0.2.78"
1946 | source = "registry+https://github.com/rust-lang/crates.io-index"
1947 | checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9"
1948 | dependencies = [
1949 | "quote",
1950 | "wasm-bindgen-macro-support",
1951 | ]
1952 |
1953 | [[package]]
1954 | name = "wasm-bindgen-macro-support"
1955 | version = "0.2.78"
1956 | source = "registry+https://github.com/rust-lang/crates.io-index"
1957 | checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab"
1958 | dependencies = [
1959 | "proc-macro2",
1960 | "quote",
1961 | "syn",
1962 | "wasm-bindgen-backend",
1963 | "wasm-bindgen-shared",
1964 | ]
1965 |
1966 | [[package]]
1967 | name = "wasm-bindgen-shared"
1968 | version = "0.2.78"
1969 | source = "registry+https://github.com/rust-lang/crates.io-index"
1970 | checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc"
1971 |
1972 | [[package]]
1973 | name = "web-sys"
1974 | version = "0.3.55"
1975 | source = "registry+https://github.com/rust-lang/crates.io-index"
1976 | checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb"
1977 | dependencies = [
1978 | "js-sys",
1979 | "wasm-bindgen",
1980 | ]
1981 |
1982 | [[package]]
1983 | name = "webpki"
1984 | version = "0.22.0"
1985 | source = "registry+https://github.com/rust-lang/crates.io-index"
1986 | checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
1987 | dependencies = [
1988 | "ring",
1989 | "untrusted",
1990 | ]
1991 |
1992 | [[package]]
1993 | name = "webpki-roots"
1994 | version = "0.22.1"
1995 | source = "registry+https://github.com/rust-lang/crates.io-index"
1996 | checksum = "c475786c6f47219345717a043a37ec04cb4bc185e28853adcc4fa0a947eba630"
1997 | dependencies = [
1998 | "webpki",
1999 | ]
2000 |
2001 | [[package]]
2002 | name = "winapi"
2003 | version = "0.3.9"
2004 | source = "registry+https://github.com/rust-lang/crates.io-index"
2005 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
2006 | dependencies = [
2007 | "winapi-i686-pc-windows-gnu",
2008 | "winapi-x86_64-pc-windows-gnu",
2009 | ]
2010 |
2011 | [[package]]
2012 | name = "winapi-i686-pc-windows-gnu"
2013 | version = "0.4.0"
2014 | source = "registry+https://github.com/rust-lang/crates.io-index"
2015 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
2016 |
2017 | [[package]]
2018 | name = "winapi-util"
2019 | version = "0.1.5"
2020 | source = "registry+https://github.com/rust-lang/crates.io-index"
2021 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
2022 | dependencies = [
2023 | "winapi",
2024 | ]
2025 |
2026 | [[package]]
2027 | name = "winapi-x86_64-pc-windows-gnu"
2028 | version = "0.4.0"
2029 | source = "registry+https://github.com/rust-lang/crates.io-index"
2030 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
2031 |
2032 | [[package]]
2033 | name = "winreg"
2034 | version = "0.7.0"
2035 | source = "registry+https://github.com/rust-lang/crates.io-index"
2036 | checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69"
2037 | dependencies = [
2038 | "winapi",
2039 | ]
2040 |
2041 | [[package]]
2042 | name = "yaml-rust"
2043 | version = "0.4.5"
2044 | source = "registry+https://github.com/rust-lang/crates.io-index"
2045 | checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
2046 | dependencies = [
2047 | "linked-hash-map",
2048 | ]
2049 |
--------------------------------------------------------------------------------