Merge branch 'develop'

master
Jef Roosens 2021-04-12 21:45:14 +02:00
commit dddde5074a
Signed by: Jef Roosens
GPG Key ID: B580B976584B5F30
13 changed files with 164 additions and 98 deletions

View File

@ -6,12 +6,12 @@ make lint &> /dev/null 2>&1 || {
exit 1; exit 1;
} }
branch=`git rev-parse --abbrev-ref HEAD` # branch=`git rev-parse --abbrev-ref HEAD`
# TODO should we add release branches here as well? # # TODO should we add release branches here as well?
if [[ "$branch" =~ ^master|develop$ ]]; then # if [[ "$branch" =~ ^master|develop$ ]]; then
make test > /dev/null 2>&1 || { # make test > /dev/null 2>&1 || {
>&2 echo "Tests failed. check 'make test' for more info."; # >&2 echo "Tests failed. check 'make test' for more info.";
exit 1; # exit 1;
} # }
fi # fi

View File

@ -1,37 +0,0 @@
# syntax = docker/dockerfile:1.2
# We use a multi-stage build to end up with a very small final image
FROM alpine:latest AS builder
ENV PATH "$PATH:/root/.cargo/bin"
WORKDIR /usr/src/app
# Install build dependencies, rustup & rust's nightly build & toolchain
RUN apk update && apk add --no-cache openssl-dev build-base curl && \
{ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain nightly; }
# Copy source code over to builder
COPY Cargo.toml Cargo.lock ./
COPY src/ ./src/
# Run the tests, don't want no broken docker image
# And then finally, build the project
# Thank the lords that this article exists
# https://users.rust-lang.org/t/sigsegv-with-program-linked-against-openssl-in-an-alpine-container/52172
# TODO add what these flags do & why they work
# NOTE: cargo install auto-appends bin to the path
RUN --mount=type=cache,target=/usr/src/app/target RUSTFLAGS="-C target-feature=-crt-static" cargo test && \
RUSTFLAGS="-C target-feature=-crt-static" cargo install --path . --bin fej --root /usr/local
# Now, we create the actual image
FROM alpine:latest
# Install some dynamic libraries needed for everything to work
RUN apk update && apk add --no-cache openssl libgcc
# Copy binary over to final image
COPY --from=builder /usr/local/bin/fej /usr/local/bin/fej
CMD ["/usr/local/bin/fej"]

View File

@ -1,35 +1,36 @@
IMAGE := chewingbever/fej
all: debug all: debug
.PHONY: all .PHONY: all
# Builds # Builds
debug: debug:
@ cargo build @ ./build -m dev
.PHONY: debug .PHONY: debug
release: release:
@ cargo build --release @ ./build -m rel
.PHONY: release .PHONY: release
image: Dockerfile
@ ./build '$(IMAGE)'
.PHONY: image
push: push:
@ ./build '$(IMAGE)' push @ ./build -m prod -a push
.PHONY: push .PHONY: push
# Run # Run
run: run:
@ RUST_BACKTRACE=1 cargo run --bin fej @ ./build -m dev -a run
.PHONY: run .PHONY: run
stop:
@ docker stop -t 2 fej
.PHONY: stop
logs:
@ docker logs -f fej
.PHONY: logs
# Testing # Testing
test: test:
@ cargo test --no-fail-fast @ ./build -m dev -a run -l -- test --no-fail-fast
.PHONY: test .PHONY: test
format: format:

76
build
View File

@ -1,18 +1,27 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Simple guard to check input args image="chewingbever/fej"
[[ $# -eq 1 ]] || [[ $# -eq 2 ]] || { # Should be either dev or rel
>&2 echo "Usage: ./build IMAGE [ACTION]" mode="dev"
exit 1 action=""
} attach="--detach"
while getopts ":i:m:a:l" c; do
case $c in
i ) image="$OPTARG" ;;
m ) mode="$OPTARG" ;;
a ) action="$OPTARG" ;;
l ) attach="" ;;
? ) exit 1 ;;
esac
done
shift $((OPTIND-1))
# Extract current version from Cargo.toml & get current branch # Extract current version from Cargo.toml & get current branch
patch_version="$(grep -Po '(?<=version = ").*(?=")' Cargo.toml | head -n1)" patch_version=`grep -Po '(?<=version = ").*(?=")' Cargo.toml | head -n1`
major_version="$(echo "$patch_version" | major_version=`echo "$patch_version" | sed -E 's/([0-9]+)\.([0-9]+)\.([0-9]+)/\1/'`
sed -E 's/([0-9]+)\.([0-9]+)\.([0-9]+)/\1/')" minor_version=`echo "$patch_version" | sed -E 's/([0-9]+).([0-9]+).([0-9]+)/\1.\2/'`
minor_version="$(echo "$patch_version" | branch=`git rev-parse --abbrev-ref HEAD`
sed -E 's/([0-9]+).([0-9]+).([0-9]+)/\1.\2/')"
branch="$(git branch --show-current)"
if [[ "$branch" = "master" ]]; then if [[ "$branch" = "master" ]]; then
tags=("$patch_version" "$minor_version" "$major_version" "latest") tags=("$patch_version" "$minor_version" "$major_version" "latest")
@ -25,31 +34,58 @@ else
fi fi
# Run the actual build command # First, we build the builder
DOCKER_BUILDKIT=1 docker build -t "$1:$tags" . DOCKER_BUILDKIT=1 docker build -f docker/Dockerfile.builder -t "$image-builder:latest" .
if [[ "$2" = push ]]; then # Run the actual build command
if [ "$mode" = "rel" ]; then
DOCKER_BUILDKIT=1 docker build -t "$image:$tags" -f docker/Dockerfile.rel .
elif [[ "$mode" = "dev" ]]; then
DOCKER_BUILDKIT=1 docker build -t "$image-dev:$tags" -f docker/Dockerfile.dev .
else
>&2 echo "Invalid mode."
exit 1
fi
if [[ "$action" = push ]]; then
[[ "$branch" =~ ^develop|master$ ]] || { [[ "$branch" =~ ^develop|master$ ]] || {
>&2 echo "You can only push from develop or master." >&2 echo "You can only push from develop or master."
exit 2 exit 2
} }
[[ "$mode" = "rel" ]] || {
>&2 echo "You can only push release builds."
exit 3
}
for tag in "${tags[@]}"; do for tag in "${tags[@]}"; do
# Create the tag # Create the tag
docker tag "$1:$tags" "$1:$tag" docker tag "$image:$tags" "$image:$tag"
# Push the tag # Push the tag
docker push "$1:$tag" docker push "$image:$tag"
# Remove the tag again, if it's not the main tag # Remove the tag again, if it's not the main tag
[[ "$tag" != "$tags" ]] && docker rmi "$1:$tag" [[ "$tag" != "$tags" ]] && docker rmi "$image:$tag"
done done
elif [[ "$2" = run ]]; then elif [[ "$action" = run ]]; then
docker run \ if [[ "$mode" = "dev" ]]; then
# Create caching volumes if needed (they need to be named)
docker volume create fej_build-cache
docker volume create fej_registry-cache
flags="-v fej_build-cache:/usr/src/app/target -v fej_registry-cache:/root/.cargo/registry"
fi
docker run $attach $flags \
--rm \ --rm \
--interactive \ --interactive \
--tty \ --tty \
--publish 8000:8000 \ --publish 8000:8000 \
"$1:$tags" --name fej \
"$image$([[ "$mode" != "rel" ]] && echo "-dev"):$tags" "$@"
fi fi

View File

@ -0,0 +1,19 @@
# We use a multi-stage build to end up with a very small final image
FROM alpine:latest AS builder
ARG MODE
ARG RUN_TESTS
ENV PATH "$PATH:/root/.cargo/bin"
# Needed for proper compiling of openssl-dev
ENV RUSTFLAGS="-C target-feature=-crt-static"
WORKDIR /usr/src/app
# Install build dependencies, rustup & rust's nightly build & toolchain
RUN apk update && apk add --no-cache openssl-dev build-base curl && \
{ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain nightly; }
# Copy source code over to builder
COPY Cargo.toml Cargo.lock ./
COPY src/ ./src/

View File

@ -0,0 +1,6 @@
FROM chewingbever/fej-builder:latest
ENV RUST_BACKTRACE=1
ENTRYPOINT ["cargo"]
CMD ["run"]

View File

@ -0,0 +1,22 @@
FROM chewingbever/fej-builder:latest AS builder
# And then finally, build the project
# Thank the lords that this article exists
# https://users.rust-lang.org/t/sigsegv-with-program-linked-against-openssl-in-an-alpine-container/52172
# TODO add what these flags do & why they work
# NOTE: cargo install auto-appends bin to the path
RUN --mount=type=cache,target=/usr/src/app/target \
--mount=type=cache,target=/root/.cargo/registry \
cargo install --path . --bin fej --root /usr/local
# Now, we create the actual image
FROM alpine:latest
# Install some dynamic libraries needed for everything to work
RUN apk update && apk add --no-cache openssl libgcc
# Copy binary over to final image
COPY --from=builder /usr/local/bin/fej /usr/local/bin/fej
CMD ["/usr/local/bin/fej"]

View File

@ -1,6 +1,9 @@
// I can probably do this way easier using an external crate, I should do that // I can probably do this way easier using an external crate, I should do that
use rocket::http::Status; use rocket::http::Status;
/// Represents any general error that the API can encounter during execution.
/// It allows us to more easily process errors, and hopefully allows for
/// more clear error messages.
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub enum FejError { pub enum FejError {
InvalidArgument, InvalidArgument,

View File

@ -1,5 +0,0 @@
# Ivago
This part of the API is a wrapper around the Ivago website (Ivago being the
company that collects the trash in my city). Their site isn't exactly RESTful,
so this endpoint simply wraps it in a RESTful wrapper.

View File

@ -20,15 +20,14 @@ const BASE_URL: &str = "https://www.ivago.be/nl/particulier/afval/ophaling";
/// Endpoint for the actual calendar output /// Endpoint for the actual calendar output
const CAL_URL: &str = "https://www.ivago.be/nl/particulier/garbage/pick-up/pickups"; const CAL_URL: &str = "https://www.ivago.be/nl/particulier/garbage/pick-up/pickups";
/// Searches the Ivago API for streets in the given city /// Searches the Ivago API for streets in the given city.
/// ///
/// # Arguments /// # Arguments
/// ///
/// * `street` - name of the street /// * `search_term` - Search term to use to look for streets
/// * `city` - city the street is in pub fn search_streets(search_term: &str) -> Result<Vec<Street>, FejError> {
pub fn search_streets(street_name: &str) -> Result<Vec<Street>, FejError> {
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let response = client.get(SEARCH_URL).query(&[("q", street_name)]).send()?; let response = client.get(SEARCH_URL).query(&[("q", search_term)]).send()?;
let data: Vec<HashMap<String, String>> = response.json()?; let data: Vec<HashMap<String, String>> = response.json()?;
// This is pretty cool, filter_map first does get() on all the maps, and // This is pretty cool, filter_map first does get() on all the maps, and
@ -41,14 +40,14 @@ pub fn search_streets(street_name: &str) -> Result<Vec<Street>, FejError> {
.collect()) .collect())
} }
/// Returns the pickup times for the various trash types /// Returns the pickup times for the various trash types.
/// ///
/// # Arguments /// # Arguments
/// ///
/// * `street` - desired street /// * `street` - Street to look up
/// * `number` - house number /// * `number` - House number in given street
/// * `start_date` - earliest date for the results /// * `start_date` - Earliest date for the results
/// * `end_date` - latest date for the results /// * `end_date` - Latest date for the results
pub fn get_pickup_times( pub fn get_pickup_times(
street: &Street, street: &Street,
number: &u32, number: &u32,

View File

@ -1,14 +1,20 @@
use super::BasicDate; use super::BasicDate;
use serde::ser::{Serialize, SerializeStruct, Serializer}; use serde::ser::{Serialize, SerializeStruct, Serializer};
/// Represents a pickup time instance. All fields are a direct map of the /// Represents a date when a pickup will occur. Label describes which type of
/// original API /// trash will be picked up.
pub struct PickupTime { pub struct PickupTime {
date: BasicDate, date: BasicDate,
label: String, label: String,
} }
impl PickupTime { impl PickupTime {
/// Creates a new PickupTime instance.
///
/// # Arguments
///
/// * `date` - Date of pickup time
/// * `label` - Type of trash
pub fn new(date: BasicDate, label: String) -> PickupTime { pub fn new(date: BasicDate, label: String) -> PickupTime {
PickupTime { PickupTime {
date: date, date: date,

View File

@ -4,7 +4,7 @@ use rocket::request::FromFormValue;
use serde::ser::{Serialize, SerializeStruct, Serializer}; use serde::ser::{Serialize, SerializeStruct, Serializer};
use std::convert::TryFrom; use std::convert::TryFrom;
/// Represents a street /// Represents a street in a given city
pub struct Street { pub struct Street {
name: String, name: String,
city: String, city: String,
@ -76,6 +76,7 @@ impl<'v> FromFormValue<'v> for Street {
mod tests { mod tests {
use super::*; use super::*;
/// Tests the conversion to string
#[test] #[test]
fn test_to_string() { fn test_to_string() {
let street = Street::new(String::from("testname"), String::from("city")); let street = Street::new(String::from("testname"), String::from("city"));

View File

@ -9,11 +9,26 @@ pub fn routes() -> Vec<rocket::Route> {
routes![route_search_streets, route_get_pickup_times] routes![route_search_streets, route_get_pickup_times]
} }
#[get("/search?<street>")] /// This route handles the Ivago search endpoint. It returns a list of streets,
pub fn route_search_streets(street: String) -> Result<Json<Vec<Street>>, Status> { /// consisting of a street name & a city.
Ok(Json(search_streets(street.as_str())?)) ///
/// # Arguments
///
/// * `search_term` - Search term to use to look for streets
#[get("/search?<search_term>")]
pub fn route_search_streets(search_term: String) -> Result<Json<Vec<Street>>, Status> {
Ok(Json(search_streets(search_term.as_str())?))
} }
/// Handles returning of pickup times for a specific address. It returns a list
/// of pickup times, containing a date and a description of the trash type.
///
/// # Arguments
///
/// * `street` - Street to look up
/// * `number` - House number in the given street
/// * `start_date` - Earliest date that can be returned
/// * `end_date` - Latest date that can be returned
#[get("/?<street>&<number>&<start_date>&<end_date>")] #[get("/?<street>&<number>&<start_date>&<end_date>")]
pub fn route_get_pickup_times( pub fn route_get_pickup_times(
street: Street, street: Street,