Skip to content
This repository was archived by the owner on Oct 31, 2023. It is now read-only.

Commit 36f5cab

Browse files
Initial Implementation of OCI push and pull commands (#20)
* implemented pull and push oci commands * renamed http_server short flag due to conflict * moved username, password and insecure to top level reigstry struct * point oci-distribution to git repo
1 parent c9878f1 commit 36f5cab

File tree

4 files changed

+347
-2
lines changed

4 files changed

+347
-2
lines changed

Cargo.toml

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "wash-cli"
3-
version = "0.1.7"
3+
version = "0.1.8"
44
authors = ["Brooks Townsend <[email protected]>"]
55
edition = "2018"
66
repository = "https://github.com/wascc/wash"
@@ -19,12 +19,15 @@ env_logger = "0.7.1"
1919
serde_json = "1.0.59"
2020
serde = { version = "1.0.117", features = ["derive"] }
2121
crossbeam = "0.7.3"
22+
tokio = { version = "0.2.0" }
23+
spinners = "1.2.0"
2224

2325
nkeys = "0.0.11"
2426
latticeclient = "0.4.0"
2527
wascap = "0.5.1"
2628
term-table = "1.3.0"
2729
provider-archive = "0.3.0"
30+
oci-distribution = { git = "https://github.com/brooksmtownsend/krustlet", branch = "oci-push" }
2831

2932
[[bin]]
3033
name = "wash"

src/claims.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ struct ActorMetadata {
238238
#[structopt(short = "g", long = "msg")]
239239
msg_broker: bool,
240240
/// Enable the HTTP server standard capability
241-
#[structopt(short = "s", long = "http_server")]
241+
#[structopt(short = "q", long = "http_server")]
242242
http_server: bool,
243243
/// Enable the HTTP client standard capability
244244
#[structopt(short = "h", long = "http_client")]

src/main.rs

+6
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ mod keys;
99
use keys::KeysCli;
1010
mod par;
1111
use par::ParCli;
12+
mod reg;
13+
use reg::RegCli;
1214

1315
/// This renders appropriately with escape characters
1416
const ASCII: &str = "
@@ -44,6 +46,9 @@ enum CliCommand {
4446
/// Utilities for creating, inspecting, and modifying capability provider archive files
4547
#[structopt(name = "par")]
4648
Par(ParCli),
49+
/// Utilities for interacting with OCI compliant registries
50+
#[structopt(name = "reg")]
51+
Reg(RegCli),
4752
}
4853

4954
fn main() {
@@ -55,6 +60,7 @@ fn main() {
5560
CliCommand::Lattice(latticecli) => lattice::handle_command(latticecli),
5661
CliCommand::Claims(claimscli) => claims::handle_command(claimscli),
5762
CliCommand::Par(parcli) => par::handle_command(parcli),
63+
CliCommand::Reg(regcli) => reg::handle_command(regcli),
5864
};
5965

6066
match res {

src/reg.rs

+336
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
extern crate oci_distribution;
2+
use oci_distribution::client::*;
3+
use oci_distribution::secrets::RegistryAuth;
4+
use oci_distribution::Reference;
5+
use provider_archive::ProviderArchive;
6+
use spinners::{Spinner, Spinners};
7+
use std::fs::File;
8+
use std::io::prelude::*;
9+
use structopt::clap::AppSettings;
10+
use structopt::StructOpt;
11+
use tokio::runtime::*;
12+
13+
const PROVIDER_ARCHIVE_MEDIA_TYPE: &str = "application/vnd.wascc.provider.archive.layer.v1+par";
14+
const PROVIDER_ARCHIVE_CONFIG_MEDIA_TYPE: &str = "application/vnd.wascc.provider.archive.config";
15+
const PROVIDER_ARCHIVE_FILE_EXTENSION: &str = ".par.gz";
16+
const WASM_MEDIA_TYPE: &str = "application/vnd.module.wasm.content.layer.v1+wasm";
17+
const WASM_CONFIG_MEDIA_TYPE: &str = "application/vnd.wascc.actor.archive.config";
18+
const WASM_FILE_EXTENSION: &str = ".wasm";
19+
20+
const SHOWER_EMOJI: &str = "\u{1F6BF}";
21+
22+
enum SupportedArtifacts {
23+
Par,
24+
Wasm,
25+
}
26+
27+
#[derive(Debug, StructOpt, Clone)]
28+
#[structopt(
29+
global_settings(&[AppSettings::ColoredHelp, AppSettings::VersionlessSubcommands]),
30+
name = "reg")]
31+
pub struct RegCli {
32+
#[structopt(flatten)]
33+
command: RegCliCommand,
34+
35+
/// OCI username, if omitted anonymous authentication will be used
36+
#[structopt(
37+
short = "u",
38+
long = "user",
39+
env = "WASH_REG_USER",
40+
hide_env_values = true
41+
)]
42+
user: Option<String>,
43+
44+
/// OCI password, if omitted anonymous authentication will be used
45+
#[structopt(
46+
short = "p",
47+
long = "password",
48+
env = "WASH_REG_PASSWORD",
49+
hide_env_values = true
50+
)]
51+
password: Option<String>,
52+
53+
/// Allow insecure (HTTP) registry connections
54+
#[structopt(long = "insecure")]
55+
insecure: bool,
56+
}
57+
58+
#[derive(Debug, Clone, StructOpt)]
59+
enum RegCliCommand {
60+
/// Pull an artifact from an OCI compliant registry
61+
#[structopt(name = "pull")]
62+
Pull(PullCommand),
63+
/// Push an artifact to an OCI compliant registry
64+
#[structopt(name = "push")]
65+
Push(PushCommand),
66+
}
67+
68+
#[derive(StructOpt, Debug, Clone)]
69+
struct PullCommand {
70+
/// URL of artifact
71+
#[structopt(name = "url")]
72+
url: String,
73+
74+
/// Path to output
75+
#[structopt(short = "o", long = "output")]
76+
output: Option<String>,
77+
78+
/// Digest to verify artifact against
79+
#[structopt(short = "d", long = "digest")]
80+
digest: Option<String>,
81+
82+
/// Allow latest artifact tags
83+
#[structopt(long = "allow-latest")]
84+
allow_latest: bool,
85+
}
86+
87+
#[derive(StructOpt, Debug, Clone)]
88+
struct PushCommand {
89+
/// URL to push artifact to
90+
#[structopt(name = "url")]
91+
url: String,
92+
93+
/// Path to artifact to push
94+
#[structopt(name = "artifact")]
95+
artifact: String,
96+
97+
/// Path to config file, if omitted will default to a blank configuration
98+
#[structopt(short = "c", long = "config")]
99+
config: Option<String>,
100+
101+
/// Allow latest artifact tags
102+
#[structopt(long = "allow-latest")]
103+
allow_latest: bool,
104+
}
105+
106+
pub fn handle_command(cli: RegCli) -> Result<(), Box<dyn ::std::error::Error>> {
107+
match cli.command {
108+
RegCliCommand::Pull(cmd) => handle_pull(cmd, cli.user, cli.password, cli.insecure),
109+
RegCliCommand::Push(cmd) => handle_push(cmd, cli.user, cli.password, cli.insecure),
110+
}
111+
}
112+
113+
fn handle_pull(
114+
cmd: PullCommand,
115+
user: Option<String>,
116+
password: Option<String>,
117+
insecure: bool,
118+
) -> Result<(), Box<dyn ::std::error::Error>> {
119+
let image: Reference = cmd.url.parse().unwrap();
120+
121+
if image.tag().unwrap_or("latest") == "latest" && !cmd.allow_latest {
122+
return Err(
123+
"Pulling artifacts with tag 'latest' is prohibited. This can be overriden with a flag"
124+
.into(),
125+
);
126+
};
127+
128+
let mut client = Client::new(ClientConfig {
129+
protocol: if insecure {
130+
ClientProtocol::Http
131+
} else {
132+
ClientProtocol::Https
133+
},
134+
});
135+
136+
let auth = match (user, password) {
137+
(Some(user), Some(password)) => RegistryAuth::Basic(user, password),
138+
_ => RegistryAuth::Anonymous,
139+
};
140+
141+
let sp = Spinner::new(
142+
Spinners::Dots12,
143+
format!(" Downloading {} ...", image.whole()),
144+
);
145+
146+
// Asynchronous code from the oci-distribution crate must run on the tokio runtime
147+
let mut rt = Runtime::new()?;
148+
let image_data = rt.block_on(client.pull(
149+
&image,
150+
&auth,
151+
vec![PROVIDER_ARCHIVE_MEDIA_TYPE, WASM_MEDIA_TYPE],
152+
))?;
153+
154+
sp.message(format!(" Validating {} ...", image.whole()));
155+
156+
// Reformatting digest in case the sha256: prefix is left off
157+
let digest = match cmd.digest {
158+
Some(d) if d.starts_with("sha256:") => Some(d),
159+
Some(d) => Some(format!("sha256:{}", d)),
160+
None => None,
161+
};
162+
163+
match (digest, image_data.digest) {
164+
(Some(digest), Some(image_digest)) if digest != image_digest => {
165+
Err("Image digest did not match provided digest, aborting")
166+
}
167+
_ => Ok(()),
168+
}?;
169+
170+
let artifact = image_data
171+
.layers
172+
.iter()
173+
.map(|l| l.data.clone())
174+
.flatten()
175+
.collect::<Vec<_>>();
176+
177+
let file_extension = match validate_artifact(&artifact, image.repository())? {
178+
SupportedArtifacts::Par => PROVIDER_ARCHIVE_FILE_EXTENSION,
179+
SupportedArtifacts::Wasm => WASM_FILE_EXTENSION,
180+
};
181+
182+
// Output to provided file, or use artifact_name.file_extension
183+
let outfile = cmd.output.unwrap_or(format!(
184+
"{}{}",
185+
image
186+
.repository()
187+
.to_string()
188+
.split('/')
189+
.collect::<Vec<_>>()
190+
.pop()
191+
.unwrap()
192+
.to_string(),
193+
file_extension
194+
));
195+
let mut f = File::create(outfile.clone())?;
196+
f.write_all(&artifact)?;
197+
198+
sp.stop();
199+
println!(
200+
"\n{} Successfully pulled and validated {}",
201+
SHOWER_EMOJI, outfile
202+
);
203+
204+
Ok(())
205+
}
206+
207+
/// Helper function to determine artifact type and validate that it is
208+
/// a valid artifact of that type
209+
fn validate_artifact(
210+
artifact: &[u8],
211+
name: &str,
212+
) -> Result<SupportedArtifacts, Box<dyn ::std::error::Error>> {
213+
match validate_actor_module(artifact, name) {
214+
Ok(_) => Ok(SupportedArtifacts::Wasm),
215+
Err(_) => match validate_provider_archive(artifact, name) {
216+
Ok(_) => Ok(SupportedArtifacts::Par),
217+
Err(_) => Err("Unsupported artifact type".into()),
218+
},
219+
}
220+
}
221+
222+
/// Attempts to inspect the claims of an actor module
223+
/// Will fail without actor claims, or if the artifact is invalid
224+
fn validate_actor_module(
225+
artifact: &[u8],
226+
module: &str,
227+
) -> Result<(), Box<dyn ::std::error::Error>> {
228+
match wascap::wasm::extract_claims(&artifact) {
229+
Ok(Some(_token)) => Ok(()),
230+
Ok(None) => Err(format!("No capabilities discovered in actor module : {}", &module).into()),
231+
Err(e) => Err(Box::new(e)),
232+
}
233+
}
234+
235+
/// Attempts to unpack a provider archive
236+
/// Will fail without claims or if the archive is invalid
237+
fn validate_provider_archive(
238+
artifact: &[u8],
239+
archive: &str,
240+
) -> Result<(), Box<dyn ::std::error::Error>> {
241+
match ProviderArchive::try_load(artifact) {
242+
Ok(_par) => Ok(()),
243+
Err(_e) => Err(format!("Invalid provider archive : {}", archive).into()),
244+
}
245+
}
246+
247+
fn handle_push(
248+
cmd: PushCommand,
249+
user: Option<String>,
250+
password: Option<String>,
251+
insecure: bool,
252+
) -> Result<(), Box<dyn ::std::error::Error>> {
253+
let image: Reference = cmd.url.parse().unwrap();
254+
255+
if image.tag().unwrap() == "latest" && !cmd.allow_latest {
256+
return Err(
257+
"Pushing artifacts with tag 'latest' is prohibited. This can be overriden with a flag"
258+
.into(),
259+
);
260+
};
261+
262+
let sp = Spinner::new(Spinners::Dots12, format!(" Loading {} ...", cmd.artifact));
263+
let mut config_buf = vec![];
264+
match cmd.config {
265+
Some(config_file) => {
266+
let mut f = File::open(config_file)?;
267+
f.read_to_end(&mut config_buf)?;
268+
}
269+
None => {
270+
// If no config provided, send blank config
271+
config_buf = b"{}".to_vec();
272+
}
273+
};
274+
275+
let mut artifact_buf = vec![];
276+
let mut f = File::open(cmd.artifact.clone())?;
277+
f.read_to_end(&mut artifact_buf)?;
278+
279+
sp.message(format!(" Verifying {} ...", cmd.artifact));
280+
281+
let (artifact_media_type, config_media_type) =
282+
match validate_artifact(&artifact_buf, &cmd.artifact)? {
283+
SupportedArtifacts::Wasm => (WASM_MEDIA_TYPE, WASM_CONFIG_MEDIA_TYPE),
284+
SupportedArtifacts::Par => (
285+
PROVIDER_ARCHIVE_MEDIA_TYPE,
286+
PROVIDER_ARCHIVE_CONFIG_MEDIA_TYPE,
287+
),
288+
};
289+
290+
let image_data = ImageData {
291+
layers: vec![ImageLayer {
292+
data: artifact_buf,
293+
media_type: artifact_media_type.to_string(),
294+
}],
295+
digest: None,
296+
};
297+
298+
let mut client = Client::new(ClientConfig {
299+
protocol: if insecure {
300+
ClientProtocol::Http
301+
} else {
302+
ClientProtocol::Https
303+
},
304+
});
305+
306+
let auth = match (user, password) {
307+
(Some(user), Some(password)) => RegistryAuth::Basic(user, password),
308+
_ => RegistryAuth::Anonymous,
309+
};
310+
311+
sp.message(format!(
312+
" Pushing {} to {} ...",
313+
cmd.artifact,
314+
image.whole()
315+
));
316+
317+
// Asynchronous code from the oci-distribution crate must run on the tokio runtime
318+
let mut rt = Runtime::new()?;
319+
rt.block_on(client.push(
320+
&image,
321+
&image_data,
322+
&config_buf,
323+
config_media_type,
324+
&auth,
325+
None,
326+
))?;
327+
328+
sp.stop();
329+
println!(
330+
"\n{} Successfully validated and pushed to {}",
331+
SHOWER_EMOJI,
332+
image.whole()
333+
);
334+
335+
Ok(())
336+
}

0 commit comments

Comments
 (0)