Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 11 additions & 15 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "technique"
version = "0.6.0"
version = "0.6.1"
edition = "2021"
description = "A domain specific language for procedures."
authors = [ "Andrew Cowie" ]
Expand Down
102 changes: 102 additions & 0 deletions examples/prototype/NetworkProbe.tq
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
% technique v1
! Proprietary and Confidential; © 2024 ACME, Inc

connectivity_check(e, s) : LocalEnvironment, TargetService -> NetworkHealth

# Network Connectivity Check

We check the health of the network path between a machine at a customer site
and a service running in a datacenter by establishing functionality at each
layer between our device and the remote server.

I. Local network connectivity <local_network>

local_network :

# Local Network Connectivity

Establish that the local network environment is functioning.

1. Check physical network interface { exec(
```bash
ip addr
```
) } and look for eth0 being marked UP.
2. Check local network connectivity
3. Check local DHCP is working
4. Check local DNS responding
5. Verify reachability of local network gateway

II. Reachability of site border relay <home_border>

home_border :

# Check site border router

Can we traverse from the immediate local network to the edge of the network at
this site?

1. Verify reachability of this-side border router inside interface.

III. Check internet connectivity <public_network>

public_network :

# Check connectivity to public internet

Ensure we have a working internet uplink!

1. Verify reachability known first hop upstream
2. Check reachability of known global services
3. Check global DNS responding.
'Reachable'

IV. Check overlay network <overlay_network>

overlay_network :

# Check overlay network

Verify that the overlay network control plane services are responding and that
outbound packets are able to reach known hub nodes on that network.

1. Check overlay control plane is responding with valid status.
2. Verify reachability of known hub and exit nodes on overlay network.

V. Reachability of away border relay <away_border>

away_border :

# Check connectivity to away-side border

1. Verify reachability of away-side border router or relay node

VI. Traversal of away side local network <away_network>

away_network :

# Check away-side network

Now that we have connectivity to our foothold in the remote datacenter, we
need to work out if we can get any further. At this point we establish that we
are able to reach major services of the cloud infrastructure provider,
specifically RDS and S3.

1. Check away-side local network connectivity
2. Check known cloud services responding <check_aws_health>

check_aws_health :

VII. Check response from remote service <service_endpoint>

service_endpoint : Target -> Health

# Confirm remote service health

Finally run a sequence of checks to establish the remote service is reachable,
answering, and healthy.

1. Verify reachability of load balancer
2. Verify health of load balancer
3. Verify service responds to health check request
4. Validate service responds correctly to test transaction
32 changes: 28 additions & 4 deletions src/formatting/formatter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,16 @@ pub fn render_expression<'i>(expression: &'i Expression, renderer: &dyn Render)

pub fn render_procedure_declaration<'i>(procedure: &'i Procedure, renderer: &dyn Render) -> String {
render_declaration(
procedure.name.value,
procedure.parameters.as_ref().map(|v| v.as_slice()),
procedure.signature.as_ref(),
procedure
.name
.value,
procedure
.parameters
.as_ref()
.map(|v| v.as_slice()),
procedure
.signature
.as_ref(),
renderer,
)
}
Expand Down Expand Up @@ -552,6 +559,7 @@ impl<'i> Formatter<'i> {
let mut elements = procedure
.elements
.iter();
let mut preceded = false;
if let Some(Element::Title(_, _)) = procedure
.elements
.first()
Expand All @@ -561,14 +569,23 @@ impl<'i> Formatter<'i> {
.next()
.unwrap(),
);
preceded = true;
}

self.add_fragment_reference(Syntax::BlockEnd, "");

// remaining elements

for element in elements {
// A code block separates from a preceding title or description with
// a blank line, but collapses onto the declaration as the first element.
if let Element::CodeBlock(..) = element {
if preceded {
self.add_fragment_reference(Syntax::Newline, "\n");
}
}
self.append_element(element);
preceded = true;
}
}

Expand All @@ -588,7 +605,7 @@ impl<'i> Formatter<'i> {
self.add_fragment_reference(Syntax::Newline, "\n");
self.append_steps(steps);
}
Element::CodeBlock(expressions, _) => {
Element::CodeBlock(expressions, subscopes, _) => {
self.add_fragment_reference(Syntax::Structure, "{");
self.add_fragment_reference(Syntax::Newline, "\n");

Expand All @@ -601,6 +618,13 @@ impl<'i> Formatter<'i> {
self.decrease(4);

self.add_fragment_reference(Syntax::Structure, "}");

if !subscopes.is_empty() {
self.add_fragment_reference(Syntax::Newline, "\n");
self.increase(4);
self.append_scopes(subscopes);
self.decrease(4);
}
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/language/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ pub enum Element<'i> {
Title(&'i str, Span),
Description(Vec<Paragraph<'i>>, Span),
Steps(Vec<Scope<'i>>, Span),
CodeBlock(Vec<Expression<'i>>, Span),
CodeBlock(Vec<Expression<'i>>, Vec<Scope<'i>>, Span),
}

impl PartialEq for Element<'_> {
Expand All @@ -73,7 +73,7 @@ impl PartialEq for Element<'_> {
(Element::Title(a, _), Element::Title(b, _)) => a == b,
(Element::Description(a, _), Element::Description(b, _)) => a == b,
(Element::Steps(a, _), Element::Steps(b, _)) => a == b,
(Element::CodeBlock(a, _), Element::CodeBlock(b, _)) => a == b,
(Element::CodeBlock(a, sa, _), Element::CodeBlock(b, sb, _)) => a == b && sa == sb,
_ => false,
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/parsing/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ mod parser;
mod scope;

// Export the actual public API
pub use parser::{parse_with_recovery, Parser, ParsingError};
pub use parser::{parse_numeric, parse_with_recovery, Parser, ParsingError};

/// Read a file and return an owned String. We pass that ownership back to the
/// main function so that the Technique object created by parse() below can
Expand Down
52 changes: 51 additions & 1 deletion src/parsing/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1158,8 +1158,26 @@ impl<'i> Parser<'i> {
} else if is_code_block(content) {
match parser.read_code_block() {
Ok(expressions) => {
// A loop code block owns the steps below it as its body,
// the way an attribute block owns its scope; `read_scopes`
// stops at a trailing description, leaving it to be flagged.
// A plain code block owns nothing, so following content
// stays at this level.
let subscopes = if is_loop_block(&expressions) {
match parser.read_scopes() {
Ok(subscopes) => subscopes,
Err(error) => {
self.problems
.push(error);
parser.skip_to_next_line();
vec![]
}
}
} else {
vec![]
};
let span = parser.span_since(elem_start);
elements.push(Element::CodeBlock(expressions, span));
elements.push(Element::CodeBlock(expressions, subscopes, span));
}
Err(error) => {
self.problems
Expand Down Expand Up @@ -3159,6 +3177,18 @@ fn is_code_block(content: &str) -> bool {
re.is_match(content)
}

/// Is this code block is a control structure (`foreach` or `repeat`) which
/// owns the steps below it as its body. A plain code does not.
fn is_loop_block(expressions: &[Expression]) -> bool {
if expressions.len() != 1 {
return false;
}
match &expressions[0] {
Expression::Foreach(..) | Expression::Repeat(..) => true,
_ => false,
}
}

#[allow(unused)]
fn is_code_inline(content: &str) -> bool {
let content = content.trim_ascii_start();
Expand Down Expand Up @@ -3257,6 +3287,26 @@ fn malformed_response_pattern(content: &str) -> bool {
re.is_match(content)
}

/// Parse a standalone numeric literal with the same grammar the parser uses
/// inside a document. Used by the runner to validate a numeric value when
/// input or edited.
pub fn parse_numeric(text: &str) -> Option<Numeric<'_>> {
let mut parser = Parser::new();
parser.initialize(text);
let numeric = parser
.read_numeric()
.ok()?;
parser.trim_whitespace();
if parser
.source
.is_empty()
{
Some(numeric)
} else {
None
}
}

fn is_numeric(content: &str) -> bool {
is_numeric_integral(content) || is_numeric_quantity(content)
}
Expand Down
4 changes: 4 additions & 0 deletions src/problem/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1227,6 +1227,10 @@ you can iterate over.
format!("Unknown function {}()", function),
format!("The function {}() is undefined. This should have been caught during the linking phase!", function),
),
RunnerError::FunctionArityMismatch { function, expected, actual } => (
format!("Wrong number of arguments to {}()", function),
format!("The function {}() expects {} but {} given.", function, expected, actual),
),
RunnerError::ExecError(error) => (
"Could not run external command".to_string(),
format!("Launching or reading from the external command failed: {}.", error),
Expand Down
Loading