From 41b1b096a8df344b9565aabf978f84958b6522eb Mon Sep 17 00:00:00 2001 From: Thomas DA ROCHA Date: Tue, 1 Oct 2024 19:35:36 +0200 Subject: [PATCH] feat: Permissive chmod (#285) Also renamed an enum variant --- docs/dofigen.schema.json | 50 ++++++++++++++++++++++++++----- docs/struct.md | 4 +-- src/deserialize.rs | 63 ++++++++++++++++++++++++++++++++++++++++ src/dockerfile_struct.rs | 8 ++--- src/dofigen_struct.rs | 38 +++++++++++++++++++++++- src/generator.rs | 35 +++++++++++++++++++++- tests/lib_test.rs | 10 +++++-- tests/regression_test.rs | 6 ++-- 8 files changed, 193 insertions(+), 21 deletions(-) diff --git a/docs/dofigen.schema.json b/docs/dofigen.schema.json index 5708296..a404cdd 100644 --- a/docs/dofigen.schema.json +++ b/docs/dofigen.schema.json @@ -457,6 +457,48 @@ "Cache": { "title": "Cache", "type": "object", + "oneOf": [ + { + "type": "object", + "required": [ + "fromImage" + ], + "properties": { + "fromImage": { + "$ref": "#/definitions/ParsableStruct" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "fromBuilder" + ], + "properties": { + "fromBuilder": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "fromContext" + ], + "properties": { + "fromContext": { + "type": [ + "string", + "null" + ], + "nullable": true + } + }, + "additionalProperties": false + } + ], "properties": { "chmod": { "default": null, @@ -485,14 +527,6 @@ ], "nullable": true }, - "from": { - "default": null, - "type": [ - "string", - "null" - ], - "nullable": true - }, "id": { "default": null, "type": [ diff --git a/docs/struct.md b/docs/struct.md index 1c1ed64..2b28e5a 100644 --- a/docs/struct.md +++ b/docs/struct.md @@ -120,7 +120,7 @@ It can be parsed from string. | `sharing` | "shared" or "private" or "locked" | The sharing strategy of the cache. | | `from...` | [FromContext](#fromcontext) | The base of the cache mount. | | `source` | string | Subpath in the from to mount. | -| `chmod` | string | The permissions of the cache. | +| `chmod` | string or integer | The permissions of the cache. | | `chown` | [User](#user) | The user and group that own the cache. | ## Bind @@ -213,7 +213,7 @@ This represents the options of a COPY/ADD instructions. | --- | --- | --- | | `target` | string | The target path of the copied files. | | `chown` | [User](#user) | The user and group that own the copied files. See https://docs.docker.com/reference/dockerfile/#copy---chown---chmod | -| `chmod` | string | The permissions of the copied files. See https://docs.docker.com/reference/dockerfile/#copy---chown---chmod | +| `chmod` | string or integer | The permissions of the copied files. See https://docs.docker.com/reference/dockerfile/#copy---chown---chmod | | `link` | boolean | Use of the link flag. See https://docs.docker.com/reference/dockerfile/#copy---link | ## Port diff --git a/src/deserialize.rs b/src/deserialize.rs index c3440a5..9cc0190 100644 --- a/src/deserialize.rs +++ b/src/deserialize.rs @@ -1069,6 +1069,21 @@ where deserializer.deserialize_any(visitor) } +#[cfg(feature = "permissive")] +pub(crate) fn deserialize_from_optional_string_or_number<'de, D>( + deserializer: D, +) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + let val: Option = Deserialize::deserialize(deserializer)?; + + Ok(Some(val.map(|val| match val { + StringOrNumber::String(s) => s, + StringOrNumber::Number(n) => n.to_string(), + }))) +} + fn sort_commands(a: &VecDeepPatchCommand, b: &VecDeepPatchCommand) -> Ordering where T: Clone + From

, @@ -2380,5 +2395,53 @@ mod test { ) } } + + #[cfg(feature = "permissive")] + mod from_optional_string_or_number { + use super::*; + + #[derive(Deserialize, Debug, Clone, PartialEq, Default)] + struct TestStruct { + #[serde( + deserialize_with = "deserialize_from_optional_string_or_number", + default + )] + pub test: Option>, + } + + #[test] + fn string() { + let ret: TestStruct = serde_yaml::from_str("test: \"123\"").unwrap(); + assert_eq_sorted!( + ret, + TestStruct { + test: Some(Some("123".into())) + } + ) + } + + #[test] + fn number() { + let ret: TestStruct = serde_yaml::from_str("test: 123").unwrap(); + assert_eq_sorted!( + ret, + TestStruct { + test: Some(Some("123".into())) + } + ) + } + + #[test] + fn null() { + let ret: TestStruct = serde_yaml::from_str("test: null").unwrap(); + assert_eq_sorted!(ret, TestStruct { test: Some(None) }) + } + + #[test] + fn absent() { + let ret: TestStruct = serde_yaml::from_str("").unwrap(); + assert_eq_sorted!(ret, TestStruct { test: None }) + } + } } } diff --git a/src/dockerfile_struct.rs b/src/dockerfile_struct.rs index 2c7bb83..851eab1 100644 --- a/src/dockerfile_struct.rs +++ b/src/dockerfile_struct.rs @@ -22,7 +22,7 @@ pub struct DockerfileInsctruction { #[derive(Debug, Clone, PartialEq)] pub enum InstructionOption { - NameOnly(String), + Flag(String), WithValue(String, String), WithOptions(String, Vec), } @@ -82,7 +82,7 @@ impl DockerfileContent for DockerfileInsctruction { impl DockerfileContent for InstructionOption { fn generate_content(&self) -> String { match self { - InstructionOption::NameOnly(name) => format!("--{}", name), + InstructionOption::Flag(name) => format!("--{}", name), InstructionOption::WithValue(name, value) => format!("--{}={}", name, value), InstructionOption::WithOptions(name, options) => format!( "--{}={}", @@ -123,7 +123,7 @@ mod test { command: "RUN".into(), content: "echo 'Hello, World!'".into(), options: vec![ - InstructionOption::NameOnly("arg1".into()), + InstructionOption::Flag("arg1".into()), InstructionOption::WithValue("arg2".into(), "value2".into()), ], }; @@ -147,7 +147,7 @@ mod test { #[test] fn test_generate_content_name_only_option() { - let option = InstructionOption::NameOnly("arg1".into()); + let option = InstructionOption::Flag("arg1".into()); assert_eq_sorted!(option.generate_content(), "--arg1"); } diff --git a/src/dofigen_struct.rs b/src/dofigen_struct.rs index b305ea0..7550031 100644 --- a/src/dofigen_struct.rs +++ b/src/dofigen_struct.rs @@ -258,6 +258,13 @@ pub struct Cache { pub source: Option, /// The permissions of the cache + #[cfg_attr( + feature = "permissive", + patch(attribute(serde( + deserialize_with = "deserialize_from_optional_string_or_number", + default + ))) + )] #[serde(skip_serializing_if = "Option::is_none")] pub chmod: Option, @@ -507,6 +514,13 @@ pub struct CopyOptions { /// The permissions of the copied files /// See https://docs.docker.com/reference/dockerfile/#copy---chown---chmod + #[cfg_attr( + feature = "permissive", + patch(attribute(serde( + deserialize_with = "deserialize_from_optional_string_or_number", + default + ))) + )] #[serde(skip_serializing_if = "Option::is_none")] pub chmod: Option, @@ -873,7 +887,6 @@ mod test { ); } - // #[ignore = "Not managed yet by serde because of multilevel flatten: https://serde.rs/field-attrs.html#flatten"] #[test] fn copy_simple() { let json_data = r#"{ @@ -903,6 +916,29 @@ mod test { ); } + #[cfg(feature = "permissive")] + #[test] + fn copy_chmod_int() { + let json_data = r#"{ + "paths": ["file1.txt"], + "chmod": 755 +}"#; + + let copy_resource: CopyPatch = serde_yaml::from_str(json_data).unwrap(); + + assert_eq_sorted!( + copy_resource, + CopyPatch { + paths: Some(vec!["file1.txt".into()].into_patch()), + options: Some(CopyOptionsPatch { + chmod: Some(Some("755".into())), + ..Default::default() + }), + ..Default::default() + } + ); + } + #[cfg(feature = "permissive")] #[test] fn deserialize_copy_from_str() { diff --git a/src/generator.rs b/src/generator.rs index 1f5d127..3a96095 100644 --- a/src/generator.rs +++ b/src/generator.rs @@ -207,7 +207,7 @@ fn add_copy_options( inst_options.push(InstructionOption::WithValue("chmod".into(), chmod.into())); } if *copy_options.link.as_ref().unwrap_or(&true) { - inst_options.push(InstructionOption::NameOnly("link".into())); + inst_options.push(InstructionOption::Flag("link".into())); } } @@ -836,6 +836,39 @@ mod test { } } + mod copy { + use super::*; + + #[test] + fn with_chmod() { + let copy = Copy { + paths: vec!["/path/to/file".into()], + options: CopyOptions { + target: Some("/app/".into()), + chmod: Some("755".into()), + ..Default::default() + }, + ..Default::default() + }; + + let lines = copy + .generate_dockerfile_lines(&GenerationContext::default()) + .unwrap(); + + assert_eq_sorted!( + lines, + vec![DockerfileLine::Instruction(DockerfileInsctruction { + command: "COPY".into(), + content: "\"/path/to/file\" \"/app/\"".into(), + options: vec![ + InstructionOption::WithValue("chmod".into(), "755".into()), + InstructionOption::Flag("link".into()) + ], + })] + ); + } + } + mod image_name { use super::*; diff --git a/tests/lib_test.rs b/tests/lib_test.rs index a521e5f..28d1cb7 100644 --- a/tests/lib_test.rs +++ b/tests/lib_test.rs @@ -49,13 +49,15 @@ arg: APP_NAME: template-rust env: fprocess: /app -artifacts: +copy: - fromBuilder: builder - source: /home/rust/src/target/x86_64-unknown-linux-musl/release/${APP_NAME} + paths: /home/rust/src/target/x86_64-unknown-linux-musl/release/${APP_NAME} target: /app + chmod: "555" - fromImage: ghcr.io/openfaas/of-watchdog:0.9.6 - source: /fwatchdog + paths: /fwatchdog target: /fwatchdog + chmod: 555 expose: 8080 healthcheck: interval: 3s @@ -100,11 +102,13 @@ ENV fprocess="/app" COPY \ --from=builder \ --chown=1000:1000 \ + --chmod=555 \ --link \ "/home/rust/src/target/x86_64-unknown-linux-musl/release/${APP_NAME}" "/app" COPY \ --from=ghcr.io/openfaas/of-watchdog:0.9.6 \ --chown=1000:1000 \ + --chmod=555 \ --link \ "/fwatchdog" "/fwatchdog" USER 1000:1000 diff --git a/tests/regression_test.rs b/tests/regression_test.rs index 8229d92..08269eb 100644 --- a/tests/regression_test.rs +++ b/tests/regression_test.rs @@ -1,7 +1,9 @@ -use std::collections::HashMap; - +#[cfg(feature = "permissive")] use dofigen_lib::*; +#[cfg(feature = "permissive")] use pretty_assertions_sorted::assert_eq_sorted; +#[cfg(feature = "permissive")] +use std::collections::HashMap; #[cfg(feature = "permissive")] #[test]