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}