Skip to content

Commit e728790

Browse files
Merge pull request #123 from OneBusAway/deployment
Add a new README section about deploying immutable Docker images
2 parents 3da9d4a + 0f0ff5e commit e728790

File tree

11 files changed

+535
-0
lines changed

11 files changed

+535
-0
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ for use with [Docker](https://www.docker.com/).
1010

1111
Check out our [onebusaway-deployment](https://github.com/oneBusAway/onebusaway-deployment) repository, which features OpenTofu (Terraform) IaC configuration for deploying OneBusAway to AWS, Azure, Google Cloud Platform, Render, Kubernetes, and other platforms.
1212

13+
### Deploying Docker Images
14+
15+
The 'simplest' way to deploy to services compatible with Docker image deployment is by creating immutable Docker images with your static data bundle pre-generated inside of the image. [See deployment-examples/README.md for more information](deployment-examples/README.md).
16+
1317
### Deploy to Render
1418

1519
[Render](https://www.render.com) is an easy-to-use Platform-as-a-Service (PaaS) provider. You can host OneBusAway on Render by either manually configuring it or by clicking the button below.

deployment-examples/README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Building and Deploying Immutable Docker Images
2+
3+
The simplest way to deploy a OneBusAway server to a compatible cloud provider (e.g. Render, Heroku) is by creating an immutable Docker image prebuilt with your transit agency's static GTFS data feed.
4+
5+
With a little hosting provider-specific automation, you should be able to automatically deploy the latest version of your image from the container registry to which you upload the image.
6+
7+
The Open Transit Software Foundation uses this approach with [OBACloud](https://onebusawaycloud.com), our OneBusAway as a Service offering, to build and deploy new containers in an easy, predictable, and horizontally scalable manner.
8+
9+
## Instructions
10+
11+
*All of the files referenced below can be found in the [immutable](./immutable) directory.*
12+
13+
### Setup and Image Building
14+
15+
1. Create a private repository on GitHub that will be the source for your images.
16+
1. Copy `Dockerfile.mbta`, `docker-compose.yaml`, `bin`, and `config` into your repository.
17+
1. Run `mkdir -p .github/workflows` in your repository and then copy `docker.yaml` to `.github/workflows/docker.yaml`
18+
1. Test your new Docker image by running it with `docker compose up oba_app`
19+
1. Assuming it builds successfully, access it at http://localhost:8080 and validate it using the `bin/validate.sh` script at the root of this repo.
20+
1. Once you have successfully validated your image, commit your changes to GitHub and create a new Release of your repository.
21+
1. The creation of the Release will kick off a new Action to build your Docker images. It will take 10-15 minutes to create the new images. Once it finishes, you'll be able to find them in your organization's Packages page.
22+
23+
### Update the Image
24+
25+
To update your image—for instance to force an update to the static GTFS feed, simply increment the `"REVISION"` value. You can automate this using `sed` or manually change the value. Then, create a new Release via the GitHub UI, command line, or API.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Inherit from the base onebusaway image
2+
FROM opentransitsoftwarefoundation/onebusaway-api-webapp:2.6.0-latest
3+
4+
COPY config/tdf-log4j2.xml /usr/local/tomcat/webapps/onebusaway-transit-data-federation-webapp/WEB-INF/classes/log4j2.xml
5+
6+
# Set environment variables permanently
7+
ENV JDBC_DRIVER="org.postgresql.Driver"
8+
ENV TZ="America/New_York"
9+
ENV GTFS_URL="https://cdn.mbta.com/MBTA_GTFS.zip"
10+
ENV ALERTS_URL="https://cdn.mbta.com/realtime/Alerts.pb"
11+
ENV TRIP_UPDATES_URL="https://cdn.mbta.com/realtime/TripUpdates.pb"
12+
ENV VEHICLE_POSITIONS_URL="https://cdn.mbta.com/realtime/VehiclePositions.pb"
13+
ENV REFRESH_INTERVAL="30"
14+
ENV AGENCY_ID_LIST='["1","3"]'
15+
ENV REVISION="1"
16+
17+
# Set these at runtime:
18+
# ENV JDBC_URL=""
19+
# ENV JDBC_USER=""
20+
# ENV JDBC_PASSWORD=""
21+
22+
# Create a directory for the custom data bundle
23+
RUN mkdir -p /bundle
24+
RUN /oba/build_bundle.sh
25+
26+
# Unset GTFS_URL so that build_bundle.sh doesn't run after build time
27+
ENV GTFS_URL=""
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#!/bin/bash
2+
# detect-changed-agencies.sh
3+
4+
# Usage information
5+
function usage {
6+
echo "Usage: $0 [--current-tag TAG] [--previous-tag TAG] [--json]" >&2
7+
echo " --current-tag TAG : Specify the current release tag (default: latest tag)" >&2
8+
echo " --previous-tag TAG : Specify the previous release tag (default: auto-detect)" >&2
9+
echo " --json : Output results as JSON array" >&2
10+
exit 1
11+
}
12+
13+
# Parse command line arguments
14+
JSON_OUTPUT=false
15+
CURRENT_TAG=""
16+
PREVIOUS_TAG=""
17+
18+
while [[ "$#" -gt 0 ]]; do
19+
case $1 in
20+
--current-tag) CURRENT_TAG="$2"; shift ;;
21+
--previous-tag) PREVIOUS_TAG="$2"; shift ;;
22+
--json) JSON_OUTPUT=true ;;
23+
-h|--help) usage ;;
24+
*) echo "Unknown parameter: $1" >&2; usage ;;
25+
esac
26+
shift
27+
done
28+
29+
# If no current tag specified, use the latest tag
30+
if [ -z "$CURRENT_TAG" ]; then
31+
CURRENT_TAG=$(git tag --sort=-creatordate | head -n 1)
32+
if [ -z "$CURRENT_TAG" ]; then
33+
echo "No tags found in repository. Please specify --current-tag." >&2
34+
exit 1
35+
fi
36+
echo "Using latest tag: $CURRENT_TAG" >&2
37+
fi
38+
39+
# If no previous tag specified, find the previous one
40+
if [ -z "$PREVIOUS_TAG" ]; then
41+
PREVIOUS_TAG=$(git tag --sort=-creatordate | grep -v "$CURRENT_TAG" | head -n 1)
42+
if [ -z "$PREVIOUS_TAG" ]; then
43+
echo "No previous tag found. Please specify --previous-tag." >&2
44+
exit 1
45+
fi
46+
echo "Using previous tag: $PREVIOUS_TAG" >&2
47+
fi
48+
49+
# Get changed Dockerfiles between the tags
50+
CHANGED_FILES=$(git diff --name-only "$PREVIOUS_TAG" "$CURRENT_TAG" | grep "^Dockerfile\." || true)
51+
52+
# Extract agency names
53+
AGENCIES=()
54+
while read -r file; do
55+
if [[ "$file" == Dockerfile.* ]]; then
56+
AGENCY=$(echo "$file" | sed 's/Dockerfile\.//')
57+
AGENCIES+=("$AGENCY")
58+
fi
59+
done <<< "$CHANGED_FILES"
60+
61+
# Output results
62+
if [ "${#AGENCIES[@]}" -eq 0 ]; then
63+
echo "No Dockerfile changes detected between $PREVIOUS_TAG and $CURRENT_TAG" >&2
64+
if [ "$JSON_OUTPUT" = true ]; then
65+
echo "[]"
66+
fi
67+
exit 0
68+
fi
69+
70+
if [ "$JSON_OUTPUT" = true ]; then
71+
# Output as JSON array (requires jq) - on a single line
72+
printf '%s\n' "${AGENCIES[@]}" | sort | uniq | jq -R . | jq -s -c .
73+
else
74+
# Output as simple list
75+
echo "Changed agencies between $PREVIOUS_TAG and $CURRENT_TAG:" >&2
76+
printf '%s\n' "${AGENCIES[@]}" | sort | uniq
77+
fi
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#!/bin/bash
2+
# detect-dockerfile-changes.sh
3+
4+
# Get the previous and current tags (passed as arguments)
5+
PREVIOUS_TAG=$1
6+
CURRENT_TAG=$2
7+
8+
if [ -z "$PREVIOUS_TAG" ] || [ -z "$CURRENT_TAG" ]; then
9+
echo "Usage: $0 <previous-tag> <current-tag>"
10+
exit 1
11+
fi
12+
13+
# Check if both tags exist
14+
if ! git rev-parse --verify "$PREVIOUS_TAG" > /dev/null 2>&1; then
15+
echo "Tag $PREVIOUS_TAG does not exist"
16+
exit 1
17+
fi
18+
19+
if ! git rev-parse --verify "$CURRENT_TAG" > /dev/null 2>&1; then
20+
echo "Tag $CURRENT_TAG does not exist"
21+
exit 1
22+
fi
23+
24+
# Get changed Dockerfiles between the two tags
25+
CHANGED_FILES=$(git diff --name-only "$PREVIOUS_TAG" "$CURRENT_TAG" | grep "^Dockerfile\.")
26+
27+
# Output the list of files
28+
if [ -z "$CHANGED_FILES" ]; then
29+
echo "No Dockerfile changes detected"
30+
exit 0
31+
fi
32+
33+
echo "$CHANGED_FILES"
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#!/bin/bash
2+
# extract-agencies.sh
3+
4+
# Read the list of changed Dockerfiles (either from stdin or a file)
5+
if [ -n "$1" ]; then
6+
CHANGED_FILES=$(cat "$1")
7+
else
8+
CHANGED_FILES=$(cat)
9+
fi
10+
11+
# Extract agency names from Dockerfile changes
12+
AGENCIES=()
13+
while read -r file; do
14+
if [[ "$file" == Dockerfile.* ]]; then
15+
AGENCY=$(echo "$file" | sed 's/Dockerfile\.//')
16+
AGENCIES+=("$AGENCY")
17+
fi
18+
done <<< "$CHANGED_FILES"
19+
20+
# Output as a simple list by default
21+
if [ "${#AGENCIES[@]}" -eq 0 ]; then
22+
echo "No agencies extracted"
23+
exit 0
24+
fi
25+
26+
# Format output
27+
if [ "$2" = "--json" ]; then
28+
# Output as JSON array (requires jq)
29+
printf '%s\n' "${AGENCIES[@]}" | jq -R . | jq -s .
30+
else
31+
# Output as simple list
32+
printf '%s\n' "${AGENCIES[@]}" | sort | uniq
33+
fi
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#!/bin/bash
2+
# get-previous-tag.sh
3+
4+
# Get the current tag (you would pass this as an argument when testing)
5+
CURRENT_TAG=$1
6+
7+
if [ -z "$CURRENT_TAG" ]; then
8+
echo "Usage: $0 <current-tag>"
9+
exit 1
10+
fi
11+
12+
# Get the previous tag (excluding the current one)
13+
PREVIOUS_TAG=$(git tag --sort=-creatordate | grep -v "$CURRENT_TAG" | head -n 1)
14+
15+
if [ -z "$PREVIOUS_TAG" ]; then
16+
echo "No previous tag found"
17+
exit 1
18+
fi
19+
20+
echo "$PREVIOUS_TAG"
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
#!/usr/bin/env ruby
2+
3+
require 'yaml'
4+
require 'open-uri'
5+
require 'json'
6+
require 'minitest/autorun'
7+
8+
API_KEY="org.onebusaway.iphone"
9+
BASE_URL="http://localhost:8080/api/where"
10+
11+
SERVICES = {
12+
dash: {
13+
agency_id: '1',
14+
route_id: '1_35',
15+
stop_id: '1_512',
16+
lat: 38.83334,
17+
lon: -77.09186,
18+
route_min_count: 10
19+
},
20+
raba: {
21+
agency_id: '25',
22+
route_id: '25_24',
23+
stop_id: '25_2000',
24+
lat: 40.3061885,
25+
lon: -122.121985,
26+
search_radius: 20_000,
27+
route_min_count: 10
28+
},
29+
sdmts: {
30+
agency_id: 'MTS',
31+
route_id: 'MTS_992',
32+
stop_id: 'MTS_60499',
33+
lat: 32.899853,
34+
lon: -116.731188,
35+
route_min_count: 100,
36+
search_radius: 20_000,
37+
agencies_count: 4
38+
},
39+
sta: {
40+
agency_id: 'STA',
41+
route_id: 'STA_63',
42+
stop_id: 'STA_CONC',
43+
lat: 47.622782,
44+
lon: -117.390875,
45+
route_min_count: 50
46+
},
47+
tampa: {
48+
agency_id: '1',
49+
route_id: '1_10',
50+
stop_id: '1_4340',
51+
lat: 27.950712,
52+
lon: -82.397875,
53+
route_min_count: 30,
54+
agencies_count: 2
55+
},
56+
unitrans: {
57+
agency_id: 'unitrans',
58+
route_id: 'unitrans_O',
59+
stop_id: 'unitrans_22102',
60+
lat: 38.555308,
61+
lon: -121.73599,
62+
route_min_count: 20
63+
}
64+
}
65+
66+
def call_api(endpoint, params = {})
67+
url = "#{BASE_URL}/#{endpoint}?key=#{API_KEY}"
68+
url += "&#{URI.encode_www_form(params)}" unless params.empty?
69+
response = URI.open(url).read
70+
JSON.parse(response)
71+
end
72+
73+
def service_name
74+
compose = YAML.load_file(File.join(__dir__, '..', 'docker-compose.yaml'))
75+
compose.dig('services', 'oba_app', 'build', 'dockerfile').split('.').last.to_sym
76+
end
77+
78+
def load_data
79+
service = SERVICES[service_name]
80+
raise "Service not found: #{service_name}" unless service
81+
service
82+
end
83+
84+
puts "Running API tests for service: #{service_name}"
85+
86+
class ApiTests < Minitest::Test
87+
def setup
88+
@service = load_data
89+
end
90+
91+
def test_current_time
92+
response = call_api("current-time.json")
93+
assert_equal(200, response["code"])
94+
assert_match(/\d+/, response["currentTime"].to_s)
95+
end
96+
97+
def test_agencies_with_coverage
98+
response = call_api("agencies-with-coverage.json")
99+
agencies = response.dig('data', 'list')
100+
assert_equal(@service[:agencies_count] || 1, agencies.length)
101+
agency = agencies.filter {|a| a['agencyId'] == @service[:agency_id] }.first
102+
assert_in_delta(@service[:lat], agency['lat'])
103+
assert_in_delta(@service[:lon], agency['lon'])
104+
end
105+
106+
def test_routes_for_agency
107+
response = call_api("routes-for-agency/#{@service[:agency_id]}.json")
108+
routes = response.dig('data', 'list')
109+
assert_operator(@service[:route_min_count], :<, routes.length)
110+
route = routes.first
111+
assert_equal(@service[:agency_id], route['agencyId'])
112+
refute_empty(route['nullSafeShortName'])
113+
end
114+
115+
def test_stops_for_route
116+
response = call_api("stops-for-route/#{@service[:route_id]}.json")
117+
entry = response.dig('data', 'entry')
118+
assert_equal(["polylines", "routeId", "stopGroupings", "stopIds"], entry.keys)
119+
assert_equal(@service[:route_id], entry['routeId'])
120+
end
121+
122+
def test_stop
123+
response = call_api("stop/#{@service[:stop_id]}.json")
124+
data = response.dig('data', 'entry')
125+
assert_equal(@service[:stop_id], data['id'])
126+
end
127+
128+
def test_stops_for_location
129+
params = {lat: @service[:lat], lon: @service[:lon]}
130+
if @service[:search_radius]
131+
params[:radius] = @service[:search_radius]
132+
end
133+
response = call_api("stops-for-location.json", params)
134+
assert_operator(0, :<, response.dig('data', 'list').count)
135+
end
136+
137+
def test_arrivals_and_departures_for_stop
138+
response = call_api("arrivals-and-departures-for-stop/#{@service[:stop_id]}.json")
139+
data = response.dig('data', 'entry', 'arrivalsAndDepartures')
140+
141+
departures = data.collect {|d| d['predictedDepartureTime'] }.compact.select {|d| d > 0 }
142+
assert_operator(0, :<, departures.count, 'has real time data')
143+
end
144+
end

0 commit comments

Comments
 (0)