concourse_resource/lib.rs
1#![deny(
2 warnings,
3 missing_debug_implementations,
4 missing_copy_implementations,
5 trivial_casts,
6 trivial_numeric_casts,
7 unsafe_code,
8 unstable_features,
9 unused_import_braces,
10 unused_qualifications,
11 missing_docs
12)]
13
14//! Helper to implement a [Concourse](https://concourse-ci.org/) resource in Rust
15//!
16//! [Concourse documentation](https://concourse-ci.org/implementing-resource-types.html)
17
18use serde::{de::DeserializeOwned, Deserialize, Serialize};
19use serde_json::{Map, Value};
20
21pub use concourse_resource_derive::*;
22
23pub mod internal;
24
25/// Output of the "in" step of the resource
26#[allow(missing_debug_implementations)]
27#[derive(Serialize)]
28pub struct InOutput<V, M> {
29 /// The fetched version.
30 pub version: V,
31 /// A list of key-value pairs. This data is intended for public consumption and will make
32 /// it upstream, intended to be shown on the build's page.
33 pub metadata: Option<M>,
34}
35
36/// Output of the "out" step of the resource
37#[allow(missing_debug_implementations)]
38#[derive(Serialize)]
39pub struct OutOutput<V, M> {
40 /// The resulting version.
41 pub version: V,
42 /// A list of key-value pairs. This data is intended for public consumption and will make
43 /// it upstream, intended to be shown on the build's page.
44 pub metadata: Option<M>,
45}
46
47/// Trait for Metadata to be usable as Concourse Metadata. This trait can be derived if the
48/// base struct implement `serde::Deserialize`
49pub trait IntoMetadataKV {
50 /// Turn `self` into a `Vec` of `internal::KV`
51 fn into_metadata_kv(self) -> Vec<internal::KV>;
52}
53
54/// Empty value that can be used as `InParams`, `InMetadata`, `OutParams` or `OutMetadata` for
55/// a `Resource`
56#[allow(missing_debug_implementations)]
57#[derive(Serialize, Deserialize, Copy, Clone, Default)]
58pub struct Empty;
59impl IntoMetadataKV for Empty {
60 fn into_metadata_kv(self) -> Vec<internal::KV> {
61 vec![]
62 }
63}
64
65/// When used in a "get" or "put" step, metadata about the running build is made available
66/// via environment variables.
67///
68/// If the build is a one-off, `name`, `job_name`, `pipeline_name`, and `pipeline_instance_vars`
69/// will be `None`. `pipeline_instance_vars` will also be `None` if the build's pipeline is not a
70/// pipeline instance (i.e. it is a regular pipeline).
71///
72/// [Concourse documentation](https://concourse-ci.org/implementing-resource-types.html#resource-metadata)
73#[derive(Debug)]
74pub struct BuildMetadata {
75 /// The internal identifier for the build. Right now this is numeric but it may become
76 /// a guid in the future. Treat it as an absolute reference to the build.
77 pub id: String,
78 /// The build number within the build's job.
79 pub name: Option<String>,
80 /// The name of the build's job.
81 pub job_name: Option<String>,
82 /// The pipeline that the build's job lives in.
83 pub pipeline_name: Option<String>,
84 /// The pipeline's instance vars, used to differentiate pipeline instances.
85 pub pipeline_instance_vars: Option<Map<String, Value>>,
86 /// The team that the build belongs to.
87 pub team_name: String,
88 /// The public URL for your ATC; useful for debugging.
89 pub atc_external_url: String,
90}
91
92/// The methods and associated types needed to implement a resource
93pub trait Resource {
94 /// A version of the resource
95 type Version: Serialize + DeserializeOwned;
96
97 /// Resource configuration, from the `source` field
98 type Source: DeserializeOwned;
99
100 /// Parameters for the "in" step, from the `params` field
101 type InParams: DeserializeOwned;
102 /// A list of key-value pairs for the "in" step. This data is intended for public
103 /// consumption and will make it upstream, intended to be shown on the build's page.
104 type InMetadata: Serialize + IntoMetadataKV;
105
106 /// Parameters for the "out" step, from the `params` field
107 type OutParams: DeserializeOwned;
108 /// A list of key-value pairs for the "out" step. This data is intended for public
109 /// consumption and will make it upstream, intended to be shown on the build's page.
110 type OutMetadata: Serialize + IntoMetadataKV;
111
112 /// A resource type's check method is invoked to detect new versions of the resource. It is
113 /// given the configured source and current version, and must return the array of new
114 /// versions, in chronological order, including the requested version if it's still valid.
115 ///
116 /// [Concourse documentation](https://concourse-ci.org/implementing-resource-types.html#resource-check)
117 fn resource_check(
118 source: Option<Self::Source>,
119 version: Option<Self::Version>,
120 ) -> Vec<Self::Version>;
121
122 /// The in method is passed the configured source, a precise version of the resource to fetch
123 /// and a destination directory. The method must fetch the resource and place it in the given
124 /// directory.
125 ///
126 /// If the desired resource version is unavailable (for example, if it was deleted), the
127 /// method must return an error.
128 ///
129 /// The method must return the fetched version, and may return metadata as a list of
130 /// key-value pairs. This data is intended for public consumption and will make it upstream,
131 /// intended to be shown on the build's page.
132 ///
133 /// [Concourse documentation](https://concourse-ci.org/implementing-resource-types.html#in)
134 fn resource_in(
135 source: Option<Self::Source>,
136 version: Self::Version,
137 params: Option<Self::InParams>,
138 output_path: &str,
139 ) -> Result<InOutput<Self::Version, Self::InMetadata>, Box<dyn std::error::Error>>;
140
141 /// The out method is called with the resource's source configuration, the configured params
142 /// and a path to the directory containing the build's full set of sources.
143 ///
144 /// The script must return the resulting version of the resource. Additionally, it may return
145 /// metadata as a list of key-value pairs. This data is intended for public consumption and
146 /// will make it upstream, intended to be shown on the build's page.
147 ///
148 /// [Concourse documentation](https://concourse-ci.org/implementing-resource-types.html#out)
149 fn resource_out(
150 source: Option<Self::Source>,
151 params: Option<Self::OutParams>,
152 input_path: &str,
153 ) -> OutOutput<Self::Version, Self::OutMetadata>;
154
155 /// When used in a "get" or "put" step, will return [metadata](struct.BuildMetadata.html) about the running build is
156 /// made available via environment variables.
157 ///
158 /// [Concourse documentation](https://concourse-ci.org/implementing-resource-types.html#resource-metadata)
159 fn build_metadata() -> BuildMetadata {
160 BuildMetadata {
161 id: std::env::var("BUILD_ID").expect("environment variable BUILD_ID should be present"),
162 name: std::env::var("BUILD_NAME").ok(),
163 job_name: std::env::var("BUILD_JOB_NAME").ok(),
164 pipeline_name: std::env::var("BUILD_PIPELINE_NAME").ok(),
165 pipeline_instance_vars: std::env::var("BUILD_PIPELINE_INSTANCE_VARS")
166 .ok()
167 .and_then(|instance_vars| serde_json::from_str(&instance_vars[..]).ok()),
168 team_name: std::env::var("BUILD_TEAM_NAME")
169 .expect("environment variable BUILD_TEAM_NAME should be present"),
170 atc_external_url: std::env::var("ATC_EXTERNAL_URL")
171 .expect("environment variable ATC_EXTERNAL_URL should be present"),
172 }
173 }
174}
175
176/// Macro that will build the `main` function from a struct implementing the `Resource` trait
177#[macro_export]
178macro_rules! create_resource {
179 ($resource:ty) => {
180 use std::io::Read;
181
182 use concourse_resource::internal::*;
183
184 fn main() {
185 let mut input_buffer = String::new();
186 let stdin = std::io::stdin();
187 let mut handle = stdin.lock();
188
189 handle.read_to_string(&mut input_buffer).unwrap();
190
191 let mut args = std::env::args();
192
193 match args.next().expect("should have a bin name").as_ref() {
194 "/opt/resource/check" => {
195 let input: CheckInput<
196 <$resource as Resource>::Source,
197 <$resource as Resource>::Version,
198 > = serde_json::from_str(&input_buffer).expect("error deserializing input");
199 let result =
200 <$resource as Resource>::resource_check(input.source, input.version);
201 println!(
202 "{}",
203 serde_json::to_string(&result).expect("error serializing output")
204 );
205 }
206 "/opt/resource/in" => {
207 let input: InInput<
208 <$resource as Resource>::Source,
209 <$resource as Resource>::Version,
210 <$resource as Resource>::InParams,
211 > = serde_json::from_str(&input_buffer).expect("error deserializing input");
212 let result = <$resource as Resource>::resource_in(
213 input.source,
214 input.version,
215 input.params,
216 &args.next().expect("expected path as first parameter"),
217 );
218 match result {
219 Err(error) => {
220 eprintln!("Error! {}", error);
221 std::process::exit(1);
222 }
223 Ok(InOutput { version, metadata }) => println!(
224 "{}",
225 serde_json::to_string(&InOutputKV {
226 version,
227 metadata: metadata.map(|md| md.into_metadata_kv())
228 })
229 .expect("error serializing output")
230 ),
231 };
232 }
233 "/opt/resource/out" => {
234 let input: OutInput<
235 <$resource as Resource>::Source,
236 <$resource as Resource>::OutParams,
237 > = serde_json::from_str(&input_buffer).expect("error deserializing input");
238 let result = <$resource as Resource>::resource_out(
239 input.source,
240 input.params,
241 &args.next().expect("expected path as first parameter"),
242 );
243 println!(
244 "{}",
245 serde_json::to_string(&OutOutputKV {
246 version: result.version,
247 metadata: result.metadata.map(|md| md.into_metadata_kv())
248 })
249 .expect("error serializing output")
250 );
251 }
252 v => eprintln!("unexpected being called as '{}'", v),
253 }
254 }
255 };
256}