difftreelog
feat(fmt) single-line objects
in: master
6 files changed
crates/jrsonnet-formatter/src/children.rsdiffbeforeafterboth--- a/crates/jrsonnet-formatter/src/children.rs
+++ b/crates/jrsonnet-formatter/src/children.rs
@@ -80,14 +80,19 @@
)
}
-pub fn should_start_with_newline(prev_inline: Option<&ChildTrivia>, tt: &ChildTrivia) -> bool {
- count_newlines_before(tt)
- + prev_inline
- .map(count_newlines_after)
- .unwrap_or_default()
+// Should start, triggers multi-line render
+pub fn should_start_with_newline(
+ prev_inline: Option<&ChildTrivia>,
+ tt: &ChildTrivia,
+) -> (bool, bool) {
+ let count =
+ count_newlines_before(tt) + prev_inline.map(count_newlines_after).unwrap_or_default();
+ (
// First for previous item end, second for current item
- >= 2
+ count >= 2,
+ count >= 1,
+ )
}
fn count_newlines_before(tt: &ChildTrivia) -> usize {
@@ -147,13 +152,14 @@
} else {
mem::take(&mut next)
};
+ let (should_start_with_newline, triggers_multiline) = should_start_with_newline(
+ current_child.as_ref().map(|c| &c.inline_trivia),
+ &before_trivia,
+ );
let last_child = current_child.replace(Child {
// First item should not start with newline
- should_start_with_newline: had_some
- && should_start_with_newline(
- current_child.as_ref().map(|c| &c.inline_trivia),
- &before_trivia,
- ),
+ should_start_with_newline: had_some && should_start_with_newline,
+ triggers_multiline,
before_trivia,
value,
inline_trivia: Vec::new(),
@@ -203,7 +209,8 @@
should_start_with_newline: should_start_with_newline(
current_child.as_ref().map(|c| &c.inline_trivia),
&next,
- ),
+ )
+ .0,
trivia: next,
};
@@ -234,6 +241,9 @@
/// item2,
/// ```
pub inline_trivia: ChildTrivia,
+ /// Is this child has whitespace that is considered significant, meaning
+ /// user has inserted it to split the value into multiple lines.
+ pub triggers_multiline: bool,
}
pub struct EndingComments {
crates/jrsonnet-formatter/src/comments.rsdiffbeforeafterboth1use std::string::String;23use dprint_core::formatting::PrintItems;4use jrsonnet_rowan_parser::{nodes::TriviaKind, AstToken};56use crate::{children::ChildTrivia, p, pi};78pub enum CommentLocation {9 /// Above local, field, other things10 AboveItem,11 /// After item12 ItemInline,13 /// After all items in object14 EndOfItems,15}1617#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]18pub fn format_comments(comments: &ChildTrivia, loc: CommentLocation, out: &mut PrintItems) {19 for c in comments {20 let Ok(c) = c else {21 let mut text = c.as_ref().unwrap_err() as &str;22 while !text.is_empty() {23 let pos = text.find(['\n', '\t']).unwrap_or(text.len());24 let sliced = &text[..pos];25 p!(out, string(sliced.to_string()));26 text = &text[pos..];27 if !text.is_empty() {28 match text.as_bytes()[0] {29 b'\n' => p!(out, nl),30 b'\t' => p!(out, tab),31 _ => unreachable!(),32 }33 text = &text[1..];34 }35 }36 continue;37 };38 match c.kind() {39 TriviaKind::Whitespace => {}40 TriviaKind::MultiLineComment => {41 let mut text = c42 .text()43 .strip_prefix("/*")44 .expect("ml comment starts with /*")45 .strip_suffix("*/")46 .expect("ml comment ends with */");47 // doc-style comment, /**48 let doc = if text.starts_with('*') {49 text = &text[1..];50 true51 } else {52 false53 };54 // Is comment starts with text immediatly, i.e /*text55 let mut immediate_start = true;56 let mut lines = text57 .split('\n')58 .map(|l| l.trim_end().to_string())59 .skip_while(|l| {60 if l.is_empty() {61 immediate_start = false;62 true63 } else {64 false65 }66 })67 .collect::<Vec<_>>();68 while lines.last().is_some_and(String::is_empty) {69 lines.pop();70 }71 if lines.len() == 1 && !doc {72 if matches!(loc, CommentLocation::ItemInline) {73 p!(out, str(" "));74 }75 p!(out, str("/* ") string(lines[0].trim().to_string()) str(" */") nl);76 } else if !lines.is_empty() {77 fn common_ws_prefix<'a>(a: &'a str, b: &str) -> &'a str {78 let offset = a79 .bytes()80 .zip(b.bytes())81 .take_while(|(a, b)| a == b && (a.is_ascii_whitespace() || *a == b'*'))82 .count();83 &a[..offset]84 }85 // First line is not empty, extract ws prefix of it86 let mut common_ws_padding = (if immediate_start && lines.len() > 1 {87 common_ws_prefix(&lines[1], &lines[1])88 } else {89 common_ws_prefix(&lines[0], &lines[0])90 })91 .to_string();92 for line in lines93 .iter()94 .skip(if immediate_start { 2 } else { 1 })95 .filter(|l| !l.is_empty())96 {97 common_ws_padding = common_ws_prefix(&common_ws_padding, line).to_string();98 }99 for line in lines100 .iter_mut()101 .skip(usize::from(immediate_start))102 .filter(|l| !l.is_empty())103 {104 *line = line105 .strip_prefix(&common_ws_padding)106 .expect("all non-empty lines start with this padding")107 .to_string();108 }109110 p!(out, str("/*"));111 if doc {112 p!(out, str("*"));113 }114 p!(out, nl);115 for mut line in lines {116 if doc {117 p!(out, str(" *"));118 }119 if line.is_empty() {120 p!(out, nl);121 } else {122 if doc {123 p!(out, str(" "));124 }125 while let Some(new_line) = line.strip_prefix('\t') {126 if doc {127 p!(out, str(" "));128 } else {129 p!(out, tab);130 }131 line = new_line.to_string();132 }133 p!(out, string(line.to_string()) nl);134 }135 }136 if doc {137 p!(out, str(" "));138 }139 p!(out, str("*/") nl);140 }141 }142 // TODO: Keep common padding for multiple continous lines of single-line comments143 // I.e144 // ```145 // # Line1146 // # Line2147 // ```148 // Should be reformatted as149 // ```150 // # Line1151 // # Line2152 // ```153 // But currently comment formatter is not aware of continous comment lines, and reformats it as154 // ```155 // # Line1156 // # Line2157 // ```158 TriviaKind::SingleLineHashComment => {159 if matches!(loc, CommentLocation::ItemInline) {160 p!(out, str(" "));161 }162 p!(out, str("# ") string(c.text().strip_prefix('#').expect("hash comment starts with #").trim().to_string()));163 if !matches!(loc, CommentLocation::ItemInline) {164 p!(out, nl);165 }166 }167 TriviaKind::SingleLineSlashComment => {168 if matches!(loc, CommentLocation::ItemInline) {169 p!(out, str(" "));170 }171 p!(out, str("// ") string(c.text().strip_prefix("//").expect("comment starts with //").trim().to_string()));172 if !matches!(loc, CommentLocation::ItemInline) {173 p!(out, nl);174 }175 }176 // Garbage in - garbage out177 TriviaKind::ErrorCommentTooShort => p!(out, str("/*/")),178 TriviaKind::ErrorCommentUnterminated => p!(out, string(c.text().to_string())),179 }180 }181}1use std::string::String;23use dprint_core::formatting::PrintItems;4use jrsonnet_rowan_parser::{nodes::TriviaKind, AstToken};56use crate::{children::ChildTrivia, p, pi};78pub enum CommentLocation {9 /// Above local, field, other things10 AboveItem,11 /// After item12 ItemInline,13 /// After all items in object14 EndOfItems,15}1617#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]18pub fn format_comments(comments: &ChildTrivia, loc: CommentLocation, out: &mut PrintItems) {19 for c in comments {20 let Ok(c) = c else {21 let mut text = c.as_ref().unwrap_err() as &str;22 while !text.is_empty() {23 let pos = text.find(['\n', '\t']).unwrap_or(text.len());24 let sliced = &text[..pos];25 p!(out, string(sliced.to_string()));26 text = &text[pos..];27 if !text.is_empty() {28 match text.as_bytes()[0] {29 b'\n' => p!(out, nl),30 b'\t' => p!(out, tab),31 _ => unreachable!(),32 }33 text = &text[1..];34 }35 }36 continue;37 };38 match c.kind() {39 TriviaKind::Whitespace => {}40 TriviaKind::MultiLineComment => {41 let mut text = c42 .text()43 .strip_prefix("/*")44 .expect("ml comment starts with /*")45 .strip_suffix("*/")46 .expect("ml comment ends with */");47 // doc-style comment, /**48 let doc = if text.starts_with('*') {49 text = &text[1..];50 true51 } else {52 false53 };54 // Is comment starts with text immediatly, i.e /*text55 let mut immediate_start = true;56 let mut lines = text57 .split('\n')58 .map(|l| l.trim_end().to_string())59 .skip_while(|l| {60 if l.is_empty() {61 immediate_start = false;62 true63 } else {64 false65 }66 })67 .collect::<Vec<_>>();68 while lines.last().is_some_and(String::is_empty) {69 lines.pop();70 }71 if lines.len() == 1 && !doc {72 if matches!(loc, CommentLocation::ItemInline) {73 p!(out, str(" "));74 }75 p!(out, str("/* ") string(lines[0].trim().to_string()) str(" */"));76 if matches!(loc, CommentLocation::AboveItem | CommentLocation::EndOfItems) {77 p!(out, nl);78 }79 } else if !lines.is_empty() {80 fn common_ws_prefix<'a>(a: &'a str, b: &str) -> &'a str {81 let offset = a82 .bytes()83 .zip(b.bytes())84 .take_while(|(a, b)| a == b && (a.is_ascii_whitespace() || *a == b'*'))85 .count();86 &a[..offset]87 }88 // First line is not empty, extract ws prefix of it89 let mut common_ws_padding = (if immediate_start && lines.len() > 1 {90 common_ws_prefix(&lines[1], &lines[1])91 } else {92 common_ws_prefix(&lines[0], &lines[0])93 })94 .to_string();95 for line in lines96 .iter()97 .skip(if immediate_start { 2 } else { 1 })98 .filter(|l| !l.is_empty())99 {100 common_ws_padding = common_ws_prefix(&common_ws_padding, line).to_string();101 }102 for line in lines103 .iter_mut()104 .skip(usize::from(immediate_start))105 .filter(|l| !l.is_empty())106 {107 *line = line108 .strip_prefix(&common_ws_padding)109 .expect("all non-empty lines start with this padding")110 .to_string();111 }112113 p!(out, str("/*"));114 if doc {115 p!(out, str("*"));116 }117 p!(out, nl);118 for mut line in lines {119 if doc {120 p!(out, str(" *"));121 }122 if line.is_empty() {123 p!(out, nl);124 } else {125 if doc {126 p!(out, str(" "));127 }128 while let Some(new_line) = line.strip_prefix('\t') {129 if doc {130 p!(out, str(" "));131 } else {132 p!(out, tab);133 }134 line = new_line.to_string();135 }136 p!(out, string(line.to_string()) nl);137 }138 }139 if doc {140 p!(out, str(" "));141 }142 p!(out, str("*/") nl);143 }144 }145 // TODO: Keep common padding for multiple continous lines of single-line comments146 // I.e147 // ```148 // # Line1149 // # Line2150 // ```151 // Should be reformatted as152 // ```153 // # Line1154 // # Line2155 // ```156 // But currently comment formatter is not aware of continous comment lines, and reformats it as157 // ```158 // # Line1159 // # Line2160 // ```161 TriviaKind::SingleLineHashComment => {162 if matches!(loc, CommentLocation::ItemInline) {163 p!(out, str(" "));164 }165 p!(out, str("# ") string(c.text().strip_prefix('#').expect("hash comment starts with #").trim().to_string()));166 if !matches!(loc, CommentLocation::ItemInline) {167 p!(out, nl);168 }169 }170 TriviaKind::SingleLineSlashComment => {171 if matches!(loc, CommentLocation::ItemInline) {172 p!(out, str(" "));173 }174 p!(out, str("// ") string(c.text().strip_prefix("//").expect("comment starts with //").trim().to_string()));175 if !matches!(loc, CommentLocation::ItemInline) {176 p!(out, nl);177 }178 }179 // Garbage in - garbage out180 TriviaKind::ErrorCommentTooShort => p!(out, str("/*/")),181 TriviaKind::ErrorCommentUnterminated => p!(out, string(c.text().to_string())),182 }183 }184}crates/jrsonnet-formatter/src/lib.rsdiffbeforeafterboth--- a/crates/jrsonnet-formatter/src/lib.rs
+++ b/crates/jrsonnet-formatter/src/lib.rs
@@ -3,8 +3,10 @@
use children::{children_between, trivia_before};
use dprint_core::formatting::{
condition_helpers::is_multiple_lines,
+ condition_resolvers::true_resolver,
ir_helpers::{new_line_group, with_indent},
- ConditionResolver, ConditionResolverContext, LineNumber, PrintItems, PrintOptions,
+ ConditionResolver, ConditionResolverContext, LineNumber, PrintItemPath, PrintItems,
+ PrintOptions, Signal,
};
use hi_doc::{Formatting, SnippetBuilder};
use jrsonnet_rowan_parser::{
@@ -12,13 +14,13 @@
Arg, ArgsDesc, Assertion, BinaryOperator, Bind, CompSpec, Destruct, DestructArrayPart,
DestructRest, Expr, ExprBase, FieldName, ForSpec, IfSpec, ImportKind, Literal, Member,
Name, Number, ObjBody, ObjLocal, ParamsDesc, SliceDesc, SourceFile, Stmt, Suffix, Text,
- UnaryOperator, Visibility,
+ Trivia, TriviaKind, UnaryOperator, Visibility,
},
AstNode, AstToken as _, SyntaxToken,
};
use crate::{
- children::{trivia_after, Child},
+ children::{trivia_after, Child, EndingComments},
comments::{format_comments, CommentLocation},
};
@@ -27,6 +29,23 @@
#[cfg(test)]
mod tests;
+fn with_indent_eoi(cond: ConditionResolver, o: PrintItems, e: EndingComments) -> PrintItems {
+ let end_comments_items = {
+ let mut items = PrintItems::new();
+ if e.should_start_with_newline {
+ p!(&mut items, nl);
+ }
+ format_comments(&e.trivia, CommentLocation::EndOfItems, &mut items);
+ items.into_rc_path()
+ };
+ let items =
+ new_line_group(pi!(@i; items(o.into()) items(end_comments_items.into()))).into_rc_path();
+
+ let indented = with_indent(pi!(@i; nl items(items.into())));
+
+ pi!(@i; if_else("indented body", cond, items(indented))(str(" ") items(items.into())))
+}
+
pub trait Printable {
fn print(&self, out: &mut PrintItems);
}
@@ -147,6 +166,10 @@
($o:ident, $($t:tt)*) => {
pi!(@s; $o: $($t)*)
};
+ (&mut $o:ident, $($t:tt)*) => {
+ let om = &mut $o;
+ pi!(@s; om: $($t)*)
+ };
}
pub(crate) use p;
pub(crate) use pi;
@@ -461,22 +484,53 @@
p!(out, str("{ }"));
return;
}
- p!(out, str("{") >i nl);
- for (i, mem) in children.into_iter().enumerate() {
- if mem.should_start_with_newline && i != 0 {
- p!(out, nl);
+
+ let source_is_multiline = children.iter().any(|c| c.triggers_multiline)
+ || end_comments.should_start_with_newline;
+
+ let start = LineNumber::new("obj start line");
+ let end = LineNumber::new("obj end line");
+ let multi_line: ConditionResolver = if source_is_multiline {
+ true_resolver()
+ } else {
+ Rc::new(move |ctx: &mut ConditionResolverContext| {
+ is_multiple_lines(ctx, start, end)
+ })
+ };
+
+ fn gen_members(
+ children: Vec<Child<Member>>,
+ multi_line: ConditionResolver,
+ ) -> PrintItems {
+ let mut _out = PrintItems::new();
+ let out = &mut _out;
+ let mut members = children.into_iter().peekable();
+ while let Some(mem) = members.next() {
+ if mem.should_start_with_newline {
+ p!(out, nl);
+ }
+ format_comments(&mem.before_trivia, CommentLocation::AboveItem, out);
+ p!(out, { mem.value });
+ let has_more = members.peek().is_some();
+ if has_more {
+ p!(out, str(","));
+ } else {
+ p!(out, if("trailing comma", multi_line, str(",")));
+ }
+ format_comments(&mem.inline_trivia, CommentLocation::ItemInline, out);
+ p!(out, if_else("member separator", multi_line, nl)(sonl));
}
- format_comments(&mem.before_trivia, CommentLocation::AboveItem, out);
- p!(out, {mem.value} str(","));
- format_comments(&mem.inline_trivia, CommentLocation::ItemInline, out);
- p!(out, nl);
+ _out
}
- if end_comments.should_start_with_newline {
- p!(out, nl);
- }
- format_comments(&end_comments.trivia, CommentLocation::EndOfItems, out);
- p!(out, <i str("}"));
+ let members_items =
+ new_line_group(gen_members(children, multi_line.clone())).into_rc_path();
+
+ let members = with_indent_eoi(multi_line, members_items.into(), end_comments);
+
+ p!(out, str("{") info(start));
+ p!(out, items(members));
+ p!(out, str("}") info(end));
}
}
}
crates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__complex_comments.snapdiffbeforeafterboth--- a/crates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__complex_comments.snap
+++ b/crates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__complex_comments.snap
@@ -1,6 +1,6 @@
---
source: crates/jrsonnet-formatter/src/tests.rs
-expression: "reformat(indoc!(\"{\n\t\t comments: {\n\t\t\t_: '',\n\t\t\t// Plain comment\n\t\t\ta: '',\n\n\t\t\t# Plain comment with empty line before\n\t\t\tb: '',\n\t\t\t/*Single-line multiline comment\n\n\t\t\t*/\n\t\t\tc: '',\n\n\t\t\t/**Single-line multiline doc comment\n\n\t\t\t*/\n\t\t\tc: '',\n\n\t\t\t/**Multiline doc\n\t\t\tComment\n\t\t\t*/\n\t\t\tc: '',\n\n\t\t\t/*\n\n\tMulti-line\n\n\tcomment\n\t\t\t*/\n\t\t\td: '',\n\n\t\t\te: '', // Inline comment\n\n\t\t\tk: '',\n\n\t\t\t// Text after everything\n\t\t },\n\t\t comments2: {\n\t\t\tk: '',\n\t\t\t// Text after everything, but no newline above\n\t\t },\n spacing: {\n a: '',\n\n b: '',\n },\n noSpacing: {\n a: '',\n b: '',\n },\n }\"))"
+expression: "reformat(indoc!(\"{\n\t\t comments: {\n\t\t\t_: '',\n\t\t\t// Plain comment\n\t\t\ta: '',\n\n\t\t\t# Plain comment with empty line before\n\t\t\tb: '',\n\t\t\t/*Single-line multiline comment\n\n\t\t\t*/\n\t\t\tc: '',\n\n\t\t\t/**Single-line multiline doc comment\n\n\t\t\t*/\n\t\t\tc: '',\n\n\t\t\t/**Multiline doc\n\t\t\tComment\n\t\t\t*/\n\t\t\tc: '',\n\n\t\t\t/*\n\n\tMulti-line\n\n\tcomment\n\t\t\t*/\n\t\t\td: '',\n\n\t\t\te: '', // Inline comment\n\n\t\t\tk: '',\n\n\t\t\t// Text after everything\n\t\t },\n\t\t comments2: {\n\t\t\tk: '',\n\t\t\t// Text after everything, but no newline above\n\t\t },\n spacing: {\n a: '',\n\n b: '',\n },\n noSpacing: {\n a: '',\n b: '',\n },\n\n\t\t\t smallObjectWithEnding: {/*Ending comment*/},\n\t\t\t smallObjectWithFieldAndEnding: {a: 11/*Ending comment*/},\n\t\t\t smallObjectWithFieldAndEnding2: {/*Start*/a: 11/*Ending comment*/},\n }\"))"
---
{
comments: {
@@ -50,4 +50,13 @@
a: '',
b: '',
},
+
+ smallObjectWithEnding: {
+ /* Ending comment */
+ },
+ smallObjectWithFieldAndEnding: { a: 11 /* Ending comment */ },
+ smallObjectWithFieldAndEnding2: {
+ /* Start */
+ a: 11, /* Ending comment */
+ },
}
crates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__complex_nested.snapdiffbeforeafterboth--- /dev/null
+++ b/crates/jrsonnet-formatter/src/snapshots/jrsonnet_formatter__tests__complex_nested.snap
@@ -0,0 +1,41 @@
+---
+source: crates/jrsonnet-formatter/src/tests.rs
+expression: "reformat(indoc!(\"\n\t\t\t{\n\t\t\t\tkubernetes: {\n\t\t\t\t deployment: {\n\t\t\t\t\tapiVersion: 'apps/v1',\n\t\t\t\t\tkind: 'Deployment',\n\t\t\t\t\tmetadata: {\n\t\t\t\t\t name: 'myapp',\n\t\t\t\t\t labels: { app: 'myapp', version: 'v1' },\n\t\t\t\t\t},\n\t\t\t\t\tspec: {\n\t\t\t\t\t replicas: 3,\n\t\t\t\t\t selector: { matchLabels: { app: 'myapp' } },\n\t\t\t\t\t template: {\n\t\t\t\t\t\t metadata: { labels: { app: 'myapp' } },\n\t\t\t\t\t\t spec: {\n\t\t\t\t\t\t\tcontainers: [\n\t\t\t\t\t\t\t {\n\t\t\t\t\t\t\t\t name: 'myapp',\n\t\t\t\t\t\t\t\t image: 'myapp:latest',\n\t\t\t\t\t\t\t\t ports: [{ containerPort: 8080 }],\n\t\t\t\t\t\t\t\t env: [\n\t\t\t\t\t\t\t\t\t{ name: 'FOO', value: 'bar' },\n\t\t\t\t\t\t\t\t\t{ name: 'BAZ', valueFrom: { secretKeyRef: { name: 'mysecret', key: 'password' } } },\n\t\t\t\t\t\t\t\t ],\n\t\t\t\t\t\t\t },\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t },\n\t\t\t\t\t },\n\t\t\t\t\t},\n\t\t\t\t },\n\t\t\t },\n\t\t\t}\n\t\t\"))"
+---
+{
+ kubernetes: {
+ deployment: {
+ apiVersion: 'apps/v1',
+ kind: 'Deployment',
+ metadata: {
+ name: 'myapp',
+ labels: { app: 'myapp', version: 'v1' },
+ },
+ spec: {
+ replicas: 3,
+ selector: { matchLabels: { app: 'myapp' } },
+ template: {
+ metadata: { labels: { app: 'myapp' } },
+ spec: {
+ containers: [
+ {
+ name: 'myapp',
+ image: 'myapp:latest',
+ ports: [
+ { containerPort: 8080 },
+ ],
+ env: [
+ { name: 'FOO', value: 'bar' },
+ {
+ name: 'BAZ',
+ valueFrom: { secretKeyRef: { name: 'mysecret', key: 'password' } },
+ },
+ ],
+ },
+ ],
+ },
+ },
+ },
+ },
+ },
+}
crates/jrsonnet-formatter/src/tests.rsdiffbeforeafterboth--- a/crates/jrsonnet-formatter/src/tests.rs
+++ b/crates/jrsonnet-formatter/src/tests.rs
@@ -74,6 +74,10 @@
a: '',
b: '',
},
+
+ smallObjectWithEnding: {/*Ending comment*/},
+ smallObjectWithFieldAndEnding: {a: 11/*Ending comment*/},
+ smallObjectWithFieldAndEnding2: {/*Start*/a: 11/*Ending comment*/},
}"
)));
}
@@ -104,3 +108,43 @@
"
)));
}
+
+#[test]
+fn complex_nested() {
+ insta::assert_snapshot!(reformat(indoc!(
+ "
+ {
+ kubernetes: {
+ deployment: {
+ apiVersion: 'apps/v1',
+ kind: 'Deployment',
+ metadata: {
+ name: 'myapp',
+ labels: { app: 'myapp', version: 'v1' },
+ },
+ spec: {
+ replicas: 3,
+ selector: { matchLabels: { app: 'myapp' } },
+ template: {
+ metadata: { labels: { app: 'myapp' } },
+ spec: {
+ containers: [
+ {
+ name: 'myapp',
+ image: 'myapp:latest',
+ ports: [{ containerPort: 8080 }],
+ env: [
+ { name: 'FOO', value: 'bar' },
+ { name: 'BAZ', valueFrom: { secretKeyRef: { name: 'mysecret', key: 'password' } } },
+ ],
+ },
+ ],
+ },
+ },
+ },
+ },
+ },
+ }
+ "
+ )));
+}