Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit a7dcee7

Browse files
committedSep 3, 2020
initial import of "hat"
1 parent 8a1602d commit a7dcee7

21 files changed

+5642
-0
lines changed
 

‎.github/workflows/hat.yaml

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: Hono Admin Tool
2+
3+
on:
4+
push:
5+
branches: ["master"]
6+
paths:
7+
- 'hat/**'
8+
pull_request:
9+
branches: ["master"]
10+
paths:
11+
- 'hat/**'
12+
13+
env:
14+
CARGO_TERM_COLOR: always
15+
16+
jobs:
17+
18+
build:
19+
20+
runs-on: ${{ matrix.os }}
21+
22+
strategy:
23+
matrix:
24+
os: [ubuntu-latest, windows-latest, macos-latest]
25+
26+
steps:
27+
28+
- uses: actions/checkout@v2
29+
30+
- uses: actions/cache@v2
31+
with:
32+
path: |
33+
~/.cargo/registry
34+
~/.cargo/git
35+
target
36+
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
37+
38+
- name: Build
39+
run: cargo build --release --verbose
40+
41+
- name: Run tests
42+
run: cargo test --release --verbose
43+
44+
- uses: actions/upload-artifact@v2
45+
with:
46+
name: hat-${{ matrix.os }}
47+
path: |
48+
target/release/hat
49+
target/release/hat.exe

‎hat/.gitignore

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Rust
2+
3+
target/
4+
5+
# Eclipse
6+
7+
.project
8+
.settings/
9+
10+
# IntelliJ
11+
12+
*.iml
13+
.idea/*
14+

‎hat/Cargo.lock

+2,202
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎hat/Cargo.toml

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
[package]
2+
name = "hat"
3+
version = "0.7.2"
4+
authors = ["Jens Reimann <jreimann@redhat.com>"]
5+
edition = "2018"
6+
7+
[dependencies]
8+
clap = { version = "2.33", features = ["suggestions", "wrap_help", "color"] }
9+
log = "0.4"
10+
simplelog = "0.5"
11+
12+
serde = { version = "1.0", features = ["derive"] }
13+
serde_yaml = "0.8"
14+
serde_json = "1.0"
15+
16+
failure_derive = "0.1"
17+
failure = "0.1"
18+
19+
url = "2.1.1"
20+
percent-encoding = "2.1"
21+
22+
dirs = "2"
23+
24+
tokio = { version = "0.2", features = [ "macros", "rt-threaded" ] }
25+
reqwest = { version = "0.10.4", features = [ "gzip", "json" ] }
26+
27+
http = "0.2"
28+
29+
base64 = "0.10"
30+
31+
rand = "0.6"
32+
33+
sha2 = "0.8"
34+
bcrypt = "0.5"
35+
36+
colored_json = "2.1"
37+
ansi_term = "0.12"
38+
39+
futures = "0.3.4"
40+
41+
[dependencies.kube]
42+
version = "0.28.1"
43+
default-features = false
44+
features = [ "rustls-tls" ]
45+
46+
[dev-dependencies.k8s-openapi]
47+
version = "0.7.1"
48+
default-features = false
49+
features = ["v1_17"]

‎hat/README.md

+167
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
# HAT – Hono Admin Tool [![GitHub release](https://img.shields.io/github/release/ctron/hat.svg)](https://github.com/ctron/hat/releases)
2+
3+
Getting help:
4+
5+
hat --help
6+
7+
## Global switches
8+
9+
Temporarily use a different context (with `-c` or `--context`):
10+
11+
hat -c context2 device create …
12+
13+
Or override a tenant (with `-t` or `--tenant`):
14+
15+
hat -t my.tenant device get …
16+
17+
## Managing contexts
18+
19+
Create a new context:
20+
21+
hat context create foo https://device-registry.hono.my
22+
23+
Create a new context with credentials:
24+
25+
hat context create foo https://device-registry.hono.my --username <username> --password <password>
26+
27+
Create a new context, using local Kubernetes token:
28+
29+
hat context create foo https://device-registry.hono.my --use-kubernetes
30+
31+
Update an existing context:
32+
33+
hat context update foo --url https://device-registry.hono.my
34+
hat context update foo --username <username> --password <password>
35+
hat context update foo --use-kubernetes
36+
37+
Delete an existing context:
38+
39+
hat context delete foo
40+
41+
Switch to an existing context:
42+
43+
hat context switch foo
44+
45+
List existing contexts:
46+
47+
hat context list
48+
49+
### Default tenant
50+
51+
It is possible to set a *default tenant*, which is used on all calls when using
52+
this context.
53+
54+
Set a default tenant when creating a context:
55+
56+
hat context create foo https://device-registry.hono.my --tenant <tenant>
57+
58+
Or update later:
59+
60+
hat context update foo https://device-registry.hono.my --tenant <tenant>
61+
62+
It is possible to override the default tenant with `-t` or `--tenant`:
63+
64+
hat device create -t my-tenant 4711
65+
66+
Or by setting the environment variable `HAT_TENANT`:
67+
68+
HAT_TENANT=foo hat device create 4711
69+
70+
## Tenants
71+
72+
Creating a new tenant:
73+
74+
hat tenant create my-tenant
75+
76+
Creating a new tenant with payload:
77+
78+
hat tenant create my-tenant '{"enabled": false}'
79+
80+
Getting a tenant:
81+
82+
hat tenant get my-tenant
83+
84+
Deleting a tenant:
85+
86+
hat tenant delete my-tenant
87+
88+
Enable/Disable a tenant:
89+
90+
hat tenant enable my-tenant
91+
hat tenant disable my-tenant
92+
93+
## Device registrations
94+
95+
Register a new device:
96+
97+
hat device create 4711
98+
99+
Register a new device with payload:
100+
101+
hat device create 4711 '{…}'
102+
103+
Inspect the device:
104+
105+
hat device get 4711
106+
107+
Enable/Disable a device:
108+
109+
hat device enable 4711
110+
hat device disable 4711
111+
112+
### Set via
113+
114+
You can also set the "via" attribute directly:
115+
116+
hat device set-via 4711 my-gw
117+
118+
### Set defaults entry
119+
120+
Set a defaults entry using:
121+
122+
hat device set-defaults 4711 key value
123+
124+
The value will be converted into a JSON value. If it cannot
125+
be parsed, it will be stored as a string (depending on the
126+
shell you are using, you might need different quotation marks):
127+
128+
hat device set-defaults 4711 key true # Booolean: true
129+
hat device set-defaults 4711 key '"true"' # String: true
130+
hat device set-defaults 4711 key 123 # Number: 123
131+
hat device set-defaults 4711 key '"123"' # String: 123
132+
hat device set-defaults 4711 key foobar # String: foobar
133+
hat device set-defaults 4711 key '{"foo":123}' # Object: {"foo":123}
134+
135+
Delete an entry by omitting the value:
136+
137+
hat device set-defaults 4711 key value
138+
139+
## Credentials
140+
141+
Replace credentials:
142+
143+
hat creds set device1 '[]'
144+
145+
Clear all credentials:
146+
147+
hat creds set device1
148+
149+
Add a password:
150+
151+
hat creds add-password device1 sensor1 password
152+
153+
Set password as only password:
154+
155+
hat creds set-password device1 sensor1 password
156+
157+
Set password with pre-hashed password:
158+
159+
hat creds set-password device1 sensor1 password --hash sha-512
160+
161+
Set PSK:
162+
163+
hat creds set-psk device1 sensor1 PSK
164+
165+
Enable X509:
166+
167+
hat creds enable-x509 device1 sensor1

‎hat/hat.wxi

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<Include>
3+
4+
<?define ProductName = "Eclipse Hono™ Admin Tool" ?>
5+
6+
<?define Win64 = "yes" ?>
7+
<?define PlatformProgramFilesFolder = "ProgramFiles64Folder" ?>
8+
<?define ProductUpgradeCode = "1b4094a4-2add-11e9-b676-c85b762e5a2c" ?>
9+
10+
</Include>

‎hat/hat.wxs

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
3+
<?include $(sys.CURRENTDIR)\hat.wxi?>
4+
5+
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
6+
<Product Name='$(var.ProductName)'
7+
Manufacturer='Jens Reimann'
8+
Id='*'
9+
UpgradeCode='fe1d0bba-2adb-11e9-aca7-c85b762e5a2c'
10+
Language='1033' Codepage='1252'
11+
Version='$(var.Version)'>
12+
13+
<Package Id='*'
14+
Keywords='Installer'
15+
Description="Eclipse Hono™ Admin Tool Installer"
16+
Manufacturer='Jens Reimann'
17+
InstallerVersion='300'
18+
Languages='1033' SummaryCodepage='1252'
19+
Compressed='yes' />
20+
21+
<Media Id="1" Cabinet="contents.cab" EmbedCab="yes" CompressionLevel="high"/>
22+
23+
<MajorUpgrade
24+
DowngradeErrorMessage="A later version of [ProductName] is already installed. Setup will now exit."/>
25+
26+
<Directory Id="TARGETDIR" Name="SourceDir">
27+
<Directory Id="$(var.PlatformProgramFilesFolder)" Name="PFiles">
28+
<Directory Id="INSTALLDIR" Name="$(var.ProductName)">
29+
<Component Id="MainComponent" Guid="78d3b722-2add-11e9-a113-c85b762e5a2c" DiskId="1">
30+
<File Id="MainExe" Name="hat.exe" DiskId="1" Source="target/release/hat.exe" KeyPath="yes"/>
31+
<Environment Id="PATH" Name="PATH" Value="[INSTALLDIR]" Permanent="yes" Part="last" Action="set" System="yes" />
32+
</Component>
33+
</Directory>
34+
</Directory>
35+
</Directory>
36+
37+
<DirectoryRef Id="TARGETDIR">
38+
<Merge Id="VCRedist" SourceFile="c:\Program Files (x86)\Common Files\Merge Modules\Microsoft_VC140_CRT_x64.msm" DiskId="1" Language="0"/>
39+
</DirectoryRef>
40+
41+
<Feature Id="Complete"
42+
Title="$(var.ProductName)"
43+
Description="The $(var.ProductName)"
44+
Level="1">
45+
<ComponentRef Id="MainComponent"/>
46+
</Feature>
47+
<Feature Id="VCRedist" Title="Visual C++ Runtime" AllowAdvertise="no" Display="hidden" Level="1">
48+
<MergeRef Id="VCRedist"/>
49+
</Feature>
50+
51+
</Product>
52+
</Wix>

‎hat/src/args.rs

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2019, 2020 Red Hat Inc
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information regarding copyright ownership.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*******************************************************************************/
13+
14+
use crate::utils::Either;
15+
16+
pub fn flag_arg(name: &str, matches: &clap::ArgMatches) -> Option<bool> {
17+
matches.is_present(name).either(
18+
Some(matches.value_of(name).map_or(true, map_switch_value)),
19+
None,
20+
)
21+
}
22+
23+
pub fn map_switch_value(value: &str) -> bool {
24+
match value {
25+
"true" | "yes" | "on" => true,
26+
_ => false,
27+
}
28+
}
29+
30+
#[cfg(test)]
31+
mod tests {
32+
use super::*;
33+
34+
fn setup<'a, 'b>() -> clap::App<'a, 'b> {
35+
clap::App::new("test").arg(
36+
clap::Arg::with_name("k")
37+
.short("k")
38+
.min_values(0)
39+
.max_values(1)
40+
.takes_value(true),
41+
)
42+
}
43+
44+
#[test]
45+
fn test_flag_arg_1() {
46+
let app = setup();
47+
let m = app.get_matches_from(vec!["test"]);
48+
assert_eq!(flag_arg("k", &m), None);
49+
}
50+
51+
#[test]
52+
fn test_flag_arg_2() {
53+
let app = setup();
54+
let m = app.get_matches_from(vec!["test", "-k"]);
55+
assert_eq!(flag_arg("k", &m), Some(true));
56+
}
57+
58+
#[test]
59+
fn test_flag_arg_3() {
60+
let app = setup();
61+
let m = app.get_matches_from(vec!["test", "-k=false"]);
62+
assert_eq!(flag_arg("k", &m), Some(false));
63+
}
64+
65+
#[test]
66+
fn test_flag_arg_4() {
67+
let app = setup();
68+
69+
let m = app.get_matches_from(vec!["test", "-k=true"]);
70+
assert_eq!(flag_arg("k", &m), Some(true));
71+
}
72+
}

‎hat/src/client.rs

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2020 Red Hat Inc
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information regarding copyright ownership.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*******************************************************************************/
13+
14+
use crate::context::Context;
15+
use crate::error;
16+
use crate::overrides::Overrides;
17+
18+
type Result<T> = std::result::Result<T, error::Error>;
19+
20+
pub struct Client {
21+
pub client: reqwest::Client,
22+
}
23+
24+
impl Client {
25+
pub async fn new(context: &Context, overrides: &Overrides) -> Result<Self> {
26+
let client = context.create_client(overrides).await?;
27+
28+
Ok(Client { client })
29+
}
30+
}

‎hat/src/context.rs

+594
Large diffs are not rendered by default.

‎hat/src/credentials.rs

+494
Large diffs are not rendered by default.

‎hat/src/devices.rs

+383
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,383 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2018, 2019 Red Hat Inc
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information regarding copyright ownership.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*******************************************************************************/
13+
14+
use clap::{App, ArgMatches};
15+
16+
use crate::context::Context;
17+
use crate::help::help;
18+
19+
use serde_json::value::Value::Object;
20+
use serde_json::value::{Map, Value};
21+
22+
use http::header::CONTENT_TYPE;
23+
use http::method::Method;
24+
use http::status::StatusCode;
25+
26+
use crate::error;
27+
use crate::error::ErrorKind::*;
28+
29+
use crate::utils::Either;
30+
31+
use crate::client::Client;
32+
use crate::overrides::Overrides;
33+
use crate::resource::Tracer;
34+
use crate::resource::{
35+
resource_append_path, resource_delete, resource_err_bad_request, resource_get,
36+
resource_id_from_location, resource_modify, resource_url, AuthExt,
37+
};
38+
use futures::executor::block_on;
39+
40+
type Result<T> = std::result::Result<T, error::Error>;
41+
const RESOURCE_NAME: &str = "devices";
42+
const RESOURCE_LABEL: &str = "Device";
43+
const PROP_ENABLED: &str = "enabled";
44+
const PROP_VIA: &str = "via";
45+
const PROP_DEFAULTS: &str = "defaults";
46+
47+
pub async fn registration(
48+
app: &mut App<'_, '_>,
49+
matches: &ArgMatches<'_>,
50+
overrides: &Overrides,
51+
context: &Context,
52+
) -> Result<()> {
53+
let client = Client::new(context, overrides).await?;
54+
55+
match matches.subcommand() {
56+
("create", Some(cmd_matches)) => {
57+
registration_create(
58+
context,
59+
overrides,
60+
cmd_matches.value_of("device"),
61+
cmd_matches.value_of("payload"),
62+
)
63+
.await?
64+
}
65+
("update", Some(cmd_matches)) => {
66+
registration_update(
67+
context,
68+
overrides,
69+
cmd_matches.value_of("device").unwrap(),
70+
cmd_matches.value_of("payload"),
71+
)
72+
.await?
73+
}
74+
("get", Some(cmd_matches)) => {
75+
registration_get(context, overrides, cmd_matches.value_of("device").unwrap()).await?
76+
}
77+
("delete", Some(cmd_matches)) => {
78+
registration_delete(context, overrides, cmd_matches.value_of("device").unwrap()).await?
79+
}
80+
("enable", Some(cmd_matches)) => {
81+
registration_enable(
82+
context,
83+
overrides,
84+
cmd_matches.value_of("device").unwrap(),
85+
true,
86+
)
87+
.await?
88+
}
89+
("disable", Some(cmd_matches)) => {
90+
registration_enable(
91+
context,
92+
overrides,
93+
cmd_matches.value_of("device").unwrap(),
94+
false,
95+
)
96+
.await?
97+
}
98+
("set-via", Some(cmd_matches)) => {
99+
registration_via(
100+
&client,
101+
context,
102+
overrides,
103+
cmd_matches.value_of("device").unwrap(),
104+
cmd_matches.values_of("via"),
105+
)
106+
.await?
107+
}
108+
("set-default", Some(cmd_matches)) => {
109+
registration_set_default(
110+
&client,
111+
context,
112+
overrides,
113+
cmd_matches.value_of("device").unwrap(),
114+
cmd_matches.value_of("defaults-name").unwrap(),
115+
cmd_matches.value_of("defaults-value"),
116+
)
117+
.await?
118+
}
119+
_ => help(app)?,
120+
};
121+
122+
Ok(())
123+
}
124+
125+
async fn registration_create(
126+
context: &Context,
127+
overrides: &Overrides,
128+
device: Option<&str>,
129+
payload: Option<&str>,
130+
) -> Result<()> {
131+
let tenant = context.make_tenant(overrides)?;
132+
let url = resource_url(context, overrides, RESOURCE_NAME, &[&tenant])?;
133+
134+
let url = resource_append_path(url, device)?;
135+
136+
let payload = match payload {
137+
Some(_) => serde_json::from_str(payload.unwrap())?,
138+
_ => serde_json::value::Map::new(),
139+
};
140+
141+
let client = context.create_client(overrides).await?;
142+
143+
let device = client
144+
.request(Method::POST, url)
145+
.apply_auth(context)?
146+
.header(CONTENT_TYPE, "application/json")
147+
.json(&payload)
148+
.trace()
149+
.send()
150+
.await
151+
.trace()
152+
.map_err(error::Error::from)
153+
.and_then(|response| match response.status() {
154+
StatusCode::CREATED => Ok(response),
155+
StatusCode::CONFLICT => Err(AlreadyExists(device.unwrap().to_string()).into()),
156+
StatusCode::BAD_REQUEST => block_on(resource_err_bad_request(response)),
157+
_ => Err(UnexpectedResult(response.status()).into()),
158+
})
159+
.and_then(resource_id_from_location)?;
160+
161+
println!("Registered device {} for tenant {}", device, tenant);
162+
163+
Ok(())
164+
}
165+
166+
async fn registration_update(
167+
context: &Context,
168+
overrides: &Overrides,
169+
device: &str,
170+
payload: Option<&str>,
171+
) -> Result<()> {
172+
let tenant = context.make_tenant(overrides)?;
173+
let url = resource_url(
174+
context,
175+
overrides,
176+
RESOURCE_NAME,
177+
&[&tenant, &device.into()],
178+
)?;
179+
180+
let payload = match payload {
181+
Some(_) => serde_json::from_str(payload.unwrap())?,
182+
_ => serde_json::value::Map::new(),
183+
};
184+
185+
let client = context.create_client(overrides).await?;
186+
187+
client
188+
.request(Method::PUT, url)
189+
.apply_auth(context)?
190+
.header(CONTENT_TYPE, "application/json")
191+
.json(&payload)
192+
.trace()
193+
.send()
194+
.await
195+
.trace()
196+
.map_err(error::Error::from)
197+
.and_then(|response| match response.status() {
198+
StatusCode::NO_CONTENT => Ok(response),
199+
StatusCode::NOT_FOUND => Err(NotFound(device.to_string()).into()),
200+
StatusCode::BAD_REQUEST => block_on(resource_err_bad_request(response)),
201+
_ => Err(UnexpectedResult(response.status()).into()),
202+
})?;
203+
204+
println!("Updated device device {} for tenant {}", device, tenant);
205+
206+
Ok(())
207+
}
208+
209+
async fn registration_delete(context: &Context, overrides: &Overrides, device: &str) -> Result<()> {
210+
let url = resource_url(
211+
context,
212+
overrides,
213+
RESOURCE_NAME,
214+
&[&context.make_tenant(overrides)?, &device.into()],
215+
)?;
216+
resource_delete(&context, overrides, &url, RESOURCE_LABEL, &device).await
217+
}
218+
219+
async fn registration_get(context: &Context, overrides: &Overrides, device: &str) -> Result<()> {
220+
let url = resource_url(
221+
context,
222+
overrides,
223+
RESOURCE_NAME,
224+
&[&context.make_tenant(overrides)?, &device.into()],
225+
)?;
226+
resource_get(&context, overrides, &url, RESOURCE_LABEL).await
227+
}
228+
229+
async fn registration_enable(
230+
context: &Context,
231+
overrides: &Overrides,
232+
device: &str,
233+
status: bool,
234+
) -> Result<()> {
235+
let client = Client::new(context, overrides).await?;
236+
237+
let url = resource_url(
238+
context,
239+
overrides,
240+
RESOURCE_NAME,
241+
&[&context.make_tenant(overrides)?, &device.into()],
242+
)?;
243+
244+
resource_modify(
245+
&client,
246+
&context,
247+
&url,
248+
&url,
249+
RESOURCE_LABEL,
250+
|reg: &mut Map<String, Value>| {
251+
reg.insert(PROP_ENABLED.into(), serde_json::value::Value::Bool(status));
252+
Ok(())
253+
},
254+
)
255+
.await?;
256+
257+
println!(
258+
"Registration for device {} {}",
259+
device,
260+
status.either("enabled", "disabled")
261+
);
262+
263+
Ok(())
264+
}
265+
266+
fn registration_url<S>(context: &Context, overrides: &Overrides, device: S) -> Result<url::Url>
267+
where
268+
S: Into<String>,
269+
{
270+
resource_url(
271+
context,
272+
overrides,
273+
RESOURCE_NAME,
274+
&[&context.make_tenant(overrides)?, &device.into()],
275+
)
276+
}
277+
278+
async fn registration_set_default(
279+
client: &Client,
280+
context: &Context,
281+
overrides: &Overrides,
282+
device: &str,
283+
name: &str,
284+
payload: Option<&str>,
285+
) -> Result<()> {
286+
let payload: Option<Value> = match payload {
287+
Some(p) => Some(serde_json::from_str(p).unwrap_or_else(|_| Value::String(p.into()))),
288+
_ => None,
289+
};
290+
291+
let url = registration_url(context, overrides, device)?;
292+
293+
resource_modify(
294+
client,
295+
&context,
296+
&url,
297+
&url,
298+
RESOURCE_LABEL,
299+
|reg: &mut Map<String, Value>| {
300+
match &payload {
301+
None => match reg.get_mut(PROP_DEFAULTS.into()) {
302+
Some(Object(ref mut defaults)) => {
303+
// remove from defaults map
304+
defaults.remove(name);
305+
}
306+
_ => {}
307+
},
308+
Some(payload) => match reg.get_mut(PROP_DEFAULTS.into()) {
309+
Some(Object(ref mut defaults)) => {
310+
// add to defaults map
311+
defaults.insert(name.into(), payload.clone());
312+
}
313+
_ => {
314+
// defaults is either not present, or not an object
315+
let mut defaults = Map::new();
316+
defaults.insert(name.into(), payload.clone());
317+
reg.insert(PROP_DEFAULTS.into(), Object(defaults));
318+
}
319+
},
320+
};
321+
322+
Ok(())
323+
},
324+
)
325+
.await?;
326+
327+
match payload {
328+
None => println!("Cleared default value {} for device {}", name, device),
329+
Some(ref v) => {
330+
println!(
331+
"Set default value {} for device {} to {:#?}",
332+
name, device, v
333+
);
334+
}
335+
}
336+
337+
Ok(())
338+
}
339+
340+
async fn registration_via(
341+
client: &Client,
342+
context: &Context,
343+
overrides: &Overrides,
344+
device: &str,
345+
via: Option<clap::Values<'_>>,
346+
) -> Result<()> {
347+
let url = registration_url(context, overrides, device)?;
348+
349+
resource_modify(
350+
client,
351+
&context,
352+
&url,
353+
&url,
354+
RESOURCE_LABEL,
355+
|reg: &mut Map<String, Value>| {
356+
match via {
357+
None => {
358+
reg.remove(PROP_VIA.into());
359+
}
360+
Some(ref v) => {
361+
let json = serde_json::value::to_value::<Vec<&str>>(v.clone().collect())?;
362+
reg.insert(PROP_VIA.into(), json);
363+
}
364+
};
365+
366+
Ok(())
367+
},
368+
)
369+
.await?;
370+
371+
match via {
372+
None => println!("Gateways cleared for device {}", device),
373+
Some(v) => {
374+
println!(
375+
"Gateway(s) set for device {} {:#?}",
376+
device,
377+
v.collect::<Vec<&str>>()
378+
);
379+
}
380+
}
381+
382+
Ok(())
383+
}

‎hat/src/error.rs

+187
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2018 Red Hat Inc
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information regarding copyright ownership.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*******************************************************************************/
13+
14+
use failure::{Backtrace, Context, Fail};
15+
use std::fmt::{self, Display};
16+
17+
#[derive(Debug)]
18+
pub struct Error {
19+
inner: Context<ErrorKind>,
20+
}
21+
22+
#[derive(Clone, Debug, Fail)]
23+
pub enum ErrorKind {
24+
#[fail(display = "{}", _0)]
25+
GenericError(String),
26+
27+
#[fail(display = "I/O error: {:?}", _0)]
28+
Io(::std::io::ErrorKind),
29+
30+
#[fail(display = "Command Line Error: {:?}", _0)]
31+
CommandLine(::clap::ErrorKind),
32+
33+
#[fail(display = "Request error: {}", _0)]
34+
Request(String),
35+
36+
#[fail(display = "Response error: {}", _0)]
37+
Response(String),
38+
39+
#[fail(display = "URL format error")]
40+
UrlError,
41+
42+
#[fail(display = "JSON format error: {:?}", _0)]
43+
JsonError(::serde_json::error::Category),
44+
45+
#[fail(display = "YAML format error")]
46+
YamlError,
47+
48+
#[fail(display = "Invalid UTF-8 string")]
49+
Utf8Error,
50+
51+
#[fail(display = "Kubernetes client error")]
52+
KubeError,
53+
54+
// context errors
55+
#[fail(display = "Context '{}' already exists", _0)]
56+
ContextExistsError(String),
57+
#[fail(display = "Unknown context '{}'", _0)]
58+
ContextUnknownError(String),
59+
#[fail(display = "Invalid context name: {}", _0)]
60+
ContextNameError(String),
61+
62+
// remote errors
63+
#[fail(display = "Resource not found: {}", _0)]
64+
NotFound(String),
65+
66+
#[fail(display = "Resource already exists: {}", _0)]
67+
AlreadyExists(String),
68+
69+
#[fail(display = "Malformed request: {}", _0)]
70+
MalformedRequest(String),
71+
72+
#[fail(display = "Unexpected return code: {}", _0)]
73+
UnexpectedResult(http::StatusCode),
74+
}
75+
76+
impl Fail for Error {
77+
fn cause(&self) -> Option<&dyn Fail> {
78+
self.inner.cause()
79+
}
80+
81+
fn backtrace(&self) -> Option<&Backtrace> {
82+
self.inner.backtrace()
83+
}
84+
}
85+
86+
impl Display for Error {
87+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
88+
Display::fmt(&self.inner, f)
89+
}
90+
}
91+
92+
#[allow(dead_code)]
93+
impl Error {
94+
pub fn kind(&self) -> ErrorKind {
95+
self.inner.get_context().clone()
96+
}
97+
}
98+
99+
impl From<ErrorKind> for Error {
100+
fn from(kind: ErrorKind) -> Error {
101+
Error {
102+
inner: Context::new(kind),
103+
}
104+
}
105+
}
106+
107+
impl From<Context<ErrorKind>> for Error {
108+
fn from(inner: Context<ErrorKind>) -> Error {
109+
Error { inner }
110+
}
111+
}
112+
113+
impl From<reqwest::Error> for Error {
114+
fn from(err: reqwest::Error) -> Error {
115+
let msg = format!("{}", err);
116+
err.context(ErrorKind::Request(msg)).into()
117+
}
118+
}
119+
120+
impl From<http::header::ToStrError> for Error {
121+
fn from(err: http::header::ToStrError) -> Error {
122+
let msg = err.to_string();
123+
err.context(ErrorKind::Request(msg)).into()
124+
}
125+
}
126+
127+
impl From<url::ParseError> for Error {
128+
fn from(_err: url::ParseError) -> Error {
129+
ErrorKind::UrlError.into()
130+
}
131+
}
132+
133+
impl From<serde_json::Error> for Error {
134+
fn from(err: serde_json::Error) -> Error {
135+
let cat = err.classify();
136+
err.context(ErrorKind::JsonError(cat)).into()
137+
}
138+
}
139+
140+
impl From<serde_yaml::Error> for Error {
141+
fn from(err: serde_yaml::Error) -> Error {
142+
err.context(ErrorKind::YamlError).into()
143+
}
144+
}
145+
146+
impl From<std::io::Error> for Error {
147+
fn from(err: std::io::Error) -> Error {
148+
let kind = err.kind();
149+
err.context(ErrorKind::Io(kind)).into()
150+
}
151+
}
152+
153+
impl From<clap::Error> for Error {
154+
fn from(err: clap::Error) -> Error {
155+
let kind = err.kind;
156+
err.context(ErrorKind::CommandLine(kind)).into()
157+
}
158+
}
159+
160+
impl From<std::str::Utf8Error> for Error {
161+
fn from(err: std::str::Utf8Error) -> Error {
162+
err.context(ErrorKind::Utf8Error).into()
163+
}
164+
}
165+
166+
impl From<std::string::FromUtf8Error> for Error {
167+
fn from(err: std::string::FromUtf8Error) -> Error {
168+
err.context(ErrorKind::Utf8Error).into()
169+
}
170+
}
171+
172+
impl From<bcrypt::BcryptError> for Error {
173+
fn from(err: bcrypt::BcryptError) -> Error {
174+
err.context(ErrorKind::GenericError(
175+
"Failed to generate BCrypt hash".into(),
176+
))
177+
.into()
178+
}
179+
}
180+
181+
impl From<kube::Error> for Error {
182+
fn from(err: kube::Error) -> Error {
183+
Error {
184+
inner: err.context(ErrorKind::KubeError),
185+
}
186+
}
187+
}

‎hat/src/hash.rs

+149
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2018 Red Hat Inc
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information regarding copyright ownership.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*******************************************************************************/
13+
14+
use sha2::Digest;
15+
use sha2::{Sha256, Sha512};
16+
17+
use rand::rngs::EntropyRng;
18+
use rand::RngCore;
19+
20+
use std::fmt;
21+
22+
use serde_json::value::{Map, Value};
23+
24+
pub enum HashFunction {
25+
Plain,
26+
Sha256,
27+
Sha512,
28+
Bcrypt(u8),
29+
}
30+
31+
use crate::error;
32+
type Result<T> = std::result::Result<T, error::Error>;
33+
34+
impl std::str::FromStr for HashFunction {
35+
type Err = &'static str;
36+
37+
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
38+
match s {
39+
"plain" => Ok(HashFunction::Plain),
40+
"sha-256" => Ok(HashFunction::Sha256),
41+
"sha-512" => Ok(HashFunction::Sha512),
42+
"bcrypt" => Ok(HashFunction::Bcrypt(10)),
43+
_ => HashFunction::from_bcrypt(s).unwrap_or_else(|| Err("Unknown hash function")),
44+
}
45+
}
46+
}
47+
48+
impl fmt::Display for HashFunction {
49+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
50+
match self {
51+
HashFunction::Plain => write!(f, "plain"),
52+
HashFunction::Sha256 => write!(f, "sha-256"),
53+
HashFunction::Sha512 => write!(f, "sha-512"),
54+
HashFunction::Bcrypt(i) => write!(f, "bcrypt:{}", i),
55+
}
56+
}
57+
}
58+
59+
fn do_hash<D: Digest + Default>(salt: &[u8], password: &str) -> (String, Option<String>) {
60+
let mut md = D::default();
61+
md.input(salt);
62+
md.input(password);
63+
64+
let dig = md.result();
65+
66+
(base64::encode(&dig), Some(base64::encode(&salt)))
67+
}
68+
69+
fn do_bcrypt(password: &str, iterations: u8) -> Result<(String, Option<String>)> {
70+
let mut hash = bcrypt::hash(password, u32::from(iterations))?;
71+
72+
hash.replace_range(1..3, "2a");
73+
74+
Ok((hash, None))
75+
}
76+
77+
fn gen_salt(size: usize) -> Vec<u8> {
78+
let mut rnd = EntropyRng::new();
79+
let mut salt = vec![0; size];
80+
81+
rnd.fill_bytes(&mut salt);
82+
salt
83+
}
84+
85+
impl HashFunction {
86+
pub fn name(&self) -> &str {
87+
match self {
88+
HashFunction::Plain => "plain",
89+
HashFunction::Sha256 => "sha-256",
90+
HashFunction::Sha512 => "sha-512",
91+
HashFunction::Bcrypt(_) => "bcrypt", // we omit the iterations here
92+
}
93+
}
94+
95+
fn from_bcrypt(s: &str) -> Option<std::result::Result<HashFunction, &'static str>> {
96+
let v: Vec<&str> = s.splitn(2, ':').collect();
97+
98+
match (v.get(0), v.get(1)) {
99+
(Some(t), None) if *t == "bcrypt" => Some(Ok(HashFunction::Bcrypt(10))),
100+
(Some(t), Some(i)) if *t == "bcrypt" => {
101+
let iter = i.parse::<u8>();
102+
103+
Some(
104+
iter.map(HashFunction::Bcrypt)
105+
.map_err(|_| "Failed to parse number of iterations"),
106+
)
107+
}
108+
_ => None,
109+
}
110+
}
111+
112+
fn insert_hash<D: Digest + Default>(
113+
&self,
114+
new_pair: &mut Map<String, Value>,
115+
password: &str,
116+
) -> Result<()> {
117+
new_pair.insert("hash-function".into(), self.name().into());
118+
let r = do_hash::<D>(gen_salt(16).as_slice(), password);
119+
new_pair.insert("pwd-hash".into(), r.0.into());
120+
if let Some(salt) = r.1 {
121+
new_pair.insert("salt".into(), salt.into());
122+
}
123+
Ok(())
124+
}
125+
126+
fn insert_bcrypt(
127+
&self,
128+
new_pair: &mut Map<String, Value>,
129+
password: &str,
130+
i: u8,
131+
) -> Result<()> {
132+
new_pair.insert("hash-function".into(), self.name().into());
133+
let r = do_bcrypt(password, i)?;
134+
new_pair.insert("pwd-hash".into(), r.0.into());
135+
Ok(())
136+
}
137+
138+
pub fn insert(&self, new_pair: &mut Map<String, Value>, password: &str) -> Result<()> {
139+
match self {
140+
HashFunction::Plain => {
141+
new_pair.insert("pwd-plain".into(), password.into());
142+
Ok(())
143+
}
144+
HashFunction::Sha256 => self.insert_hash::<Sha256>(new_pair, password),
145+
HashFunction::Sha512 => self.insert_hash::<Sha512>(new_pair, password),
146+
HashFunction::Bcrypt(i) => self.insert_bcrypt(new_pair, password, *i),
147+
}
148+
}
149+
}

‎hat/src/help.rs

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2018 Red Hat Inc
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information regarding copyright ownership.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*******************************************************************************/
13+
14+
use crate::error;
15+
use clap::App;
16+
17+
pub fn help(app: &mut App) -> Result<(), error::Error> {
18+
app.print_help()?;
19+
println!();
20+
Ok(())
21+
}

‎hat/src/main.rs

+474
Large diffs are not rendered by default.

‎hat/src/output.rs

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2018 Red Hat Inc
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information regarding copyright ownership.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*******************************************************************************/
13+
14+
use colored_json::write_colored_json;
15+
16+
use serde_json::value::Value;
17+
18+
use crate::error;
19+
use std::io::stdout;
20+
use std::io::Write;
21+
22+
pub fn display_json_value(value: &Value) -> std::result::Result<(), error::Error> {
23+
let mut out = stdout();
24+
25+
{
26+
let mut out = out.lock();
27+
write_colored_json(value, &mut out)?;
28+
out.write_all("\n".as_bytes())?;
29+
}
30+
31+
out.flush()?;
32+
33+
Ok(())
34+
}

‎hat/src/overrides.rs

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2018 Red Hat Inc
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information regarding copyright ownership.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*******************************************************************************/
13+
14+
use crate::args::flag_arg;
15+
16+
pub struct Overrides {
17+
context: Option<String>,
18+
url: Option<String>,
19+
tenant: Option<String>,
20+
use_kubernetes: Option<bool>,
21+
kubernetes_context: Option<String>,
22+
kubernetes_cluster: Option<String>,
23+
insecure: Option<bool>,
24+
}
25+
26+
impl Overrides {
27+
pub fn context(&self) -> Option<String> {
28+
self.context.clone()
29+
}
30+
pub fn url(&self) -> Option<&String> {
31+
self.url.as_ref()
32+
}
33+
pub fn tenant(&self) -> Option<String> {
34+
self.tenant.clone()
35+
}
36+
pub fn use_kubernetes(&self) -> Option<bool> {
37+
self.use_kubernetes
38+
}
39+
pub fn kubernetes_cluster(&self) -> Option<&String> {
40+
self.kubernetes_cluster.as_ref()
41+
}
42+
pub fn kubernetes_context(&self) -> Option<&String> {
43+
self.kubernetes_context.as_ref()
44+
}
45+
pub fn insecure(&self) -> Option<bool> {
46+
self.insecure
47+
}
48+
}
49+
50+
impl<'a> From<&'a clap::ArgMatches<'a>> for Overrides {
51+
fn from(matches: &'a clap::ArgMatches) -> Self {
52+
Overrides {
53+
context: matches.value_of("context").map(ToString::to_string),
54+
url: matches.value_of("url").map(ToString::to_string),
55+
tenant: matches.value_of("tenant").map(ToString::to_string),
56+
use_kubernetes: flag_arg("use-kubernetes", matches),
57+
kubernetes_cluster: matches
58+
.value_of("kubernetes-cluster")
59+
.map(ToString::to_string),
60+
kubernetes_context: matches
61+
.value_of("kubernetes-context")
62+
.map(ToString::to_string),
63+
insecure: flag_arg("insecure", matches),
64+
}
65+
}
66+
}

‎hat/src/resource.rs

+318
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2018, 2019 Red Hat Inc
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information regarding copyright ownership.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*******************************************************************************/
13+
14+
use log::info;
15+
16+
use url;
17+
use url::Url;
18+
19+
use std::collections::HashMap;
20+
21+
use http::header::{CONTENT_TYPE, ETAG, IF_MATCH, LOCATION};
22+
use http::Method;
23+
use http::StatusCode;
24+
25+
use crate::error::ErrorKind::{MalformedRequest, NotFound, Response, UnexpectedResult};
26+
27+
use crate::context::Context;
28+
use crate::error;
29+
30+
use crate::client::Client;
31+
use crate::output::display_json_value;
32+
use crate::overrides::Overrides;
33+
use futures::executor::block_on;
34+
use serde::de::DeserializeOwned;
35+
use serde::Serialize;
36+
37+
type Result<T> = std::result::Result<T, error::Error>;
38+
39+
pub trait AuthExt
40+
where
41+
Self: Sized,
42+
{
43+
fn apply_auth(self, context: &Context) -> Result<Self>;
44+
}
45+
46+
impl AuthExt for reqwest::RequestBuilder {
47+
fn apply_auth(self, context: &Context) -> Result<Self> {
48+
if context.use_kubernetes() {
49+
// we already got configured, do nothing in addition
50+
Ok(self)
51+
} else if let Some(token) = context.token() {
52+
Ok(self.bearer_auth(token))
53+
} else if let Some(user) = context.username() {
54+
Ok(self.basic_auth(user, context.password().clone()))
55+
} else {
56+
Ok(self)
57+
}
58+
}
59+
}
60+
61+
pub trait IfMatch {
62+
fn if_match(self, value: Option<&http::header::HeaderValue>) -> Self;
63+
}
64+
65+
impl IfMatch for reqwest::RequestBuilder {
66+
fn if_match(self, value: Option<&http::header::HeaderValue>) -> Self {
67+
if let Some(etag) = value {
68+
self.header(IF_MATCH, etag.clone())
69+
} else {
70+
self
71+
}
72+
}
73+
}
74+
75+
pub trait Tracer {
76+
fn trace(self) -> Self;
77+
}
78+
79+
impl Tracer for reqwest::RequestBuilder {
80+
fn trace(self) -> Self {
81+
info!("{:#?}", self);
82+
self
83+
}
84+
}
85+
86+
impl Tracer for reqwest::Client {
87+
fn trace(self) -> Self {
88+
info!("{:#?}", self);
89+
self
90+
}
91+
}
92+
93+
impl Tracer for std::result::Result<reqwest::Response, reqwest::Error> {
94+
fn trace(self) -> Self {
95+
info!("{:#?}", self);
96+
self
97+
}
98+
}
99+
100+
pub fn resource_url<S>(
101+
context: &Context,
102+
overrides: &Overrides,
103+
resource: &str,
104+
segments: S,
105+
) -> Result<url::Url>
106+
where
107+
S: IntoIterator,
108+
S::Item: AsRef<str>,
109+
{
110+
resource_url_query(context, overrides, resource, segments, None)
111+
}
112+
113+
pub fn resource_append_path<S>(url: url::Url, segments: S) -> Result<url::Url>
114+
where
115+
S: IntoIterator,
116+
S::Item: AsRef<str>,
117+
{
118+
let mut url = url.clone();
119+
{
120+
let mut path = url
121+
.path_segments_mut()
122+
.map_err(|_| error::ErrorKind::UrlError)?;
123+
124+
path.extend(segments);
125+
}
126+
127+
Ok(url)
128+
}
129+
130+
pub fn resource_url_query<S>(
131+
context: &Context,
132+
overrides: &Overrides,
133+
resource: &str,
134+
segments: S,
135+
query: Option<&HashMap<String, String>>,
136+
) -> Result<url::Url>
137+
where
138+
S: IntoIterator,
139+
S::Item: AsRef<str>,
140+
{
141+
let url = context.to_url(overrides)?;
142+
let url = resource_append_path(url, Some(resource))?;
143+
let mut url = resource_append_path(url, segments)?;
144+
145+
if let Some(q) = query {
146+
let mut query = url.query_pairs_mut();
147+
for (name, value) in q {
148+
query.append_pair(name, value);
149+
}
150+
}
151+
152+
Ok(url)
153+
}
154+
155+
pub async fn resource_delete(
156+
context: &Context,
157+
overrides: &Overrides,
158+
url: &url::Url,
159+
resource_type: &str,
160+
resource_name: &str,
161+
) -> Result<()> {
162+
let client = context.create_client(overrides).await?;
163+
164+
client
165+
.request(Method::DELETE, url.clone())
166+
.apply_auth(context)?
167+
.trace()
168+
.send()
169+
.await
170+
.trace()
171+
.map_err(error::Error::from)
172+
.and_then(|response| match response.status() {
173+
StatusCode::NO_CONTENT => Ok(response),
174+
StatusCode::NOT_FOUND => Ok(response),
175+
_ => Err(UnexpectedResult(response.status()).into()),
176+
})?;
177+
178+
println!("{} deleted: {}", resource_type, resource_name);
179+
180+
Ok(())
181+
}
182+
183+
pub async fn resource_get(
184+
context: &Context,
185+
overrides: &Overrides,
186+
url: &url::Url,
187+
resource_type: &str,
188+
) -> Result<()> {
189+
let client = context.create_client(overrides).await?;
190+
191+
let result: serde_json::value::Value = client
192+
.request(Method::GET, url.clone())
193+
.apply_auth(context)?
194+
.trace()
195+
.send()
196+
.await
197+
.trace()
198+
.map_err(error::Error::from)
199+
.and_then(|response| match response.status() {
200+
StatusCode::OK => Ok(response),
201+
StatusCode::NOT_FOUND => Err(NotFound(resource_type.to_string()).into()),
202+
_ => Err(UnexpectedResult(response.status()).into()),
203+
})?
204+
.json()
205+
.await?;
206+
207+
display_json_value(&result)?;
208+
209+
Ok(())
210+
}
211+
212+
pub async fn resource_modify_with_create<C, F, T>(
213+
client: &Client,
214+
context: &Context,
215+
read_url: &Url,
216+
update_url: &Url,
217+
resource_name: &str,
218+
creator: C,
219+
mut modifier: F,
220+
) -> Result<reqwest::Response>
221+
where
222+
F: FnMut(&mut T) -> Result<()>,
223+
C: Fn() -> Result<T>,
224+
T: Serialize + DeserializeOwned + std::fmt::Debug,
225+
{
226+
// get
227+
228+
let response = client
229+
.client
230+
.request(Method::GET, read_url.clone())
231+
.apply_auth(context)?
232+
.trace()
233+
.send()
234+
.await
235+
.trace()
236+
.map_err(error::Error::from)?;
237+
238+
// retrieve ETag header
239+
let etag = &response.headers().get(ETAG).map(|o| o.clone());
240+
241+
let mut payload: T = match response.status() {
242+
StatusCode::OK => response.json().await.map_err(error::Error::from),
243+
StatusCode::NOT_FOUND => creator(),
244+
_ => Err(UnexpectedResult(response.status()).into()),
245+
}?;
246+
247+
info!("GET Payload: {:#?}", payload);
248+
249+
// call consumer
250+
251+
modifier(&mut payload)?;
252+
253+
info!("PUT Payload: {:#?}", payload);
254+
255+
// update
256+
257+
client
258+
.client
259+
.request(Method::PUT, update_url.clone())
260+
.apply_auth(context)?
261+
.header(CONTENT_TYPE, "application/json")
262+
.if_match(etag.as_ref())
263+
.json(&payload)
264+
.trace()
265+
.send()
266+
.await
267+
.trace()
268+
.map_err(error::Error::from)
269+
.and_then(|response| match response.status() {
270+
StatusCode::NO_CONTENT => Ok(response),
271+
StatusCode::NOT_FOUND => Err(NotFound(resource_name.into()).into()),
272+
StatusCode::BAD_REQUEST => block_on(resource_err_bad_request(response)),
273+
_ => Err(UnexpectedResult(response.status()).into()),
274+
})
275+
}
276+
277+
pub async fn resource_err_bad_request<T>(response: reqwest::Response) -> Result<T> {
278+
Err(MalformedRequest(response.text().await.unwrap_or_else(|_| "<unknown>".into())).into())
279+
}
280+
281+
pub async fn resource_modify<F, T>(
282+
client: &Client,
283+
context: &Context,
284+
read_url: &Url,
285+
update_url: &Url,
286+
resource_name: &str,
287+
modifier: F,
288+
) -> Result<reqwest::Response>
289+
where
290+
F: FnMut(&mut T) -> Result<()>,
291+
T: Serialize + DeserializeOwned + std::fmt::Debug,
292+
{
293+
resource_modify_with_create(
294+
client,
295+
context,
296+
read_url,
297+
update_url,
298+
resource_name,
299+
|| Err(NotFound(resource_name.into()).into()),
300+
modifier,
301+
)
302+
.await
303+
}
304+
305+
pub fn resource_id_from_location(response: reqwest::Response) -> Result<String> {
306+
let loc = response.headers().get(LOCATION);
307+
308+
if let Some(s) = loc {
309+
let id: String = s.to_str()?.into();
310+
311+
let s = id.split('/').last();
312+
313+
s.map(|s| s.into())
314+
.ok_or_else(|| Response(String::from("Missing ID element in 'Location' header")).into())
315+
} else {
316+
Err(Response(String::from("Missing 'Location' header in response")).into())
317+
}
318+
}

‎hat/src/tenant.rs

+249
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2018 Red Hat Inc
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information regarding copyright ownership.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*******************************************************************************/
13+
14+
use clap::{App, ArgMatches};
15+
16+
use crate::context::Context;
17+
use crate::help::help;
18+
19+
use http::header::*;
20+
use http::method::Method;
21+
use http::status::StatusCode;
22+
23+
use serde_json::value::*;
24+
25+
use crate::error;
26+
use crate::error::ErrorKind::*;
27+
28+
use crate::resource::{
29+
resource_delete, resource_err_bad_request, resource_get, resource_id_from_location,
30+
resource_modify, resource_url, AuthExt,
31+
};
32+
33+
use crate::client::Client;
34+
use crate::overrides::Overrides;
35+
use crate::resource::Tracer;
36+
use futures::executor::block_on;
37+
38+
type Result<T> = std::result::Result<T, error::Error>;
39+
40+
static KEY_ENABLED: &str = "enabled";
41+
static RESOURCE_NAME: &str = "devices";
42+
43+
pub async fn tenant(
44+
app: &mut App<'_, '_>,
45+
matches: &ArgMatches<'_>,
46+
overrides: &Overrides,
47+
context: &Context,
48+
) -> Result<()> {
49+
let client = Client::new(context, overrides).await?;
50+
51+
match matches.subcommand() {
52+
("create", Some(cmd_matches)) => {
53+
tenant_create(
54+
context,
55+
overrides,
56+
cmd_matches.value_of("tenant_name"),
57+
cmd_matches.value_of("payload"),
58+
)
59+
.await?
60+
}
61+
("update", Some(cmd_matches)) => {
62+
tenant_update(
63+
context,
64+
overrides,
65+
cmd_matches.value_of("tenant_name").unwrap(),
66+
cmd_matches.value_of("payload"),
67+
)
68+
.await?
69+
}
70+
("get", Some(cmd_matches)) => {
71+
tenant_get(
72+
context,
73+
overrides,
74+
cmd_matches.value_of("tenant_name").unwrap(),
75+
)
76+
.await?
77+
}
78+
("delete", Some(cmd_matches)) => {
79+
tenant_delete(
80+
context,
81+
overrides,
82+
cmd_matches.value_of("tenant_name").unwrap(),
83+
)
84+
.await?
85+
}
86+
("enable", Some(cmd_matches)) => {
87+
tenant_enable(
88+
&client,
89+
context,
90+
overrides,
91+
cmd_matches.value_of("tenant_name").unwrap(),
92+
)
93+
.await?
94+
}
95+
("disable", Some(cmd_matches)) => {
96+
tenant_disable(
97+
&client,
98+
context,
99+
overrides,
100+
cmd_matches.value_of("tenant_name").unwrap(),
101+
)
102+
.await?
103+
}
104+
_ => help(app)?,
105+
};
106+
107+
Ok(())
108+
}
109+
110+
async fn tenant_create(
111+
context: &Context,
112+
overrides: &Overrides,
113+
tenant: Option<&str>,
114+
payload: Option<&str>,
115+
) -> Result<()> {
116+
let url = resource_url(context, overrides, RESOURCE_NAME, tenant)?;
117+
118+
let payload = match payload {
119+
Some(_) => serde_json::from_str(payload.unwrap())?,
120+
_ => serde_json::value::Map::new(),
121+
};
122+
123+
let client = context.create_client(overrides).await?;
124+
125+
let tenant = client
126+
.request(Method::POST, url)
127+
.apply_auth(context)?
128+
.header(CONTENT_TYPE, "application/json")
129+
.json(&payload)
130+
.trace()
131+
.send()
132+
.await
133+
.trace()
134+
.map_err(error::Error::from)
135+
.and_then(|response| match response.status() {
136+
StatusCode::CREATED => Ok(response),
137+
StatusCode::CONFLICT => Err(AlreadyExists(tenant.unwrap().to_string()).into()),
138+
StatusCode::BAD_REQUEST => block_on(resource_err_bad_request(response)),
139+
_ => Err(UnexpectedResult(response.status()).into()),
140+
})
141+
.and_then(resource_id_from_location)?;
142+
143+
println!("Created tenant: {}", tenant);
144+
145+
Ok(())
146+
}
147+
148+
async fn tenant_update(
149+
context: &Context,
150+
overrides: &Overrides,
151+
tenant: &str,
152+
payload: Option<&str>,
153+
) -> Result<()> {
154+
let url = resource_url(context, overrides, RESOURCE_NAME, Some(tenant))?;
155+
156+
let mut payload = match payload {
157+
Some(_) => serde_json::from_str(payload.unwrap())?,
158+
_ => serde_json::value::Map::new(),
159+
};
160+
161+
payload.insert(
162+
"tenant-id".to_string(),
163+
serde_json::value::to_value(tenant)?,
164+
);
165+
166+
let client = context.create_client(overrides).await?;
167+
168+
client
169+
.request(Method::PUT, url)
170+
.apply_auth(context)?
171+
.header(CONTENT_TYPE, "application/json")
172+
.json(&payload)
173+
.trace()
174+
.send()
175+
.await
176+
.map_err(error::Error::from)
177+
.and_then(|response| match response.status() {
178+
StatusCode::NO_CONTENT => Ok(response),
179+
StatusCode::NOT_FOUND => Err(NotFound(tenant.to_string()).into()),
180+
StatusCode::BAD_REQUEST => block_on(resource_err_bad_request(response)),
181+
_ => Err(UnexpectedResult(response.status()).into()),
182+
})?;
183+
184+
println!("Updated tenant: {}", tenant);
185+
186+
Ok(())
187+
}
188+
189+
async fn tenant_delete(context: &Context, overrides: &Overrides, tenant: &str) -> Result<()> {
190+
let url = resource_url(context, overrides, RESOURCE_NAME, Some(tenant))?;
191+
resource_delete(&context, overrides, &url, "Tenant", tenant).await
192+
}
193+
194+
async fn tenant_enable(
195+
client: &Client,
196+
context: &Context,
197+
overrides: &Overrides,
198+
tenant: &str,
199+
) -> Result<()> {
200+
let url = resource_url(context, overrides, RESOURCE_NAME, Some(tenant))?;
201+
202+
resource_modify(
203+
client,
204+
&context,
205+
&url,
206+
&url,
207+
tenant,
208+
|payload: &mut Map<String, Value>| {
209+
payload.insert(KEY_ENABLED.into(), Value::Bool(true));
210+
Ok(())
211+
},
212+
)
213+
.await?;
214+
215+
println!("Tenant {} enabled", tenant);
216+
217+
Ok(())
218+
}
219+
220+
async fn tenant_disable(
221+
client: &Client,
222+
context: &Context,
223+
overrides: &Overrides,
224+
tenant: &str,
225+
) -> Result<()> {
226+
let url = resource_url(context, overrides, RESOURCE_NAME, Some(tenant))?;
227+
228+
resource_modify(
229+
client,
230+
&context,
231+
&url,
232+
&url,
233+
tenant,
234+
|payload: &mut Map<String, Value>| {
235+
payload.insert(KEY_ENABLED.into(), Value::Bool(false));
236+
Ok(())
237+
},
238+
)
239+
.await?;
240+
241+
println!("Tenant {} disabled", tenant);
242+
243+
Ok(())
244+
}
245+
246+
async fn tenant_get(context: &Context, overrides: &Overrides, tenant: &str) -> Result<()> {
247+
let url = resource_url(context, overrides, RESOURCE_NAME, Some(tenant))?;
248+
resource_get(&context, overrides, &url, "Tenant").await
249+
}

‎hat/src/utils.rs

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2018 Red Hat Inc
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information regarding copyright ownership.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*******************************************************************************/
13+
14+
pub trait Either {
15+
fn either<T>(&self, this: T, that: T) -> T;
16+
}
17+
18+
impl Either for bool {
19+
/// transforms the bool in a value of either the first (when true) or the second (when false)
20+
/// parameter.
21+
fn either<T>(&self, when_true: T, when_false: T) -> T {
22+
if *self {
23+
when_true
24+
} else {
25+
when_false
26+
}
27+
}
28+
}

0 commit comments

Comments
 (0)
Please sign in to comment.