mirror of
https://github.com/mgrove36/ical-filter-proxy.git
synced 2026-03-03 01:47:07 +00:00
Merge branch 'develop' into 'main'
release 0.1.0
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/ical-filter-proxy
|
||||
/config.yaml
|
||||
19
.gitlab-ci.yml
Normal file
19
.gitlab-ci.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
include:
|
||||
- project: gitlab/includes # private gitlab instance
|
||||
file:
|
||||
- docker.yml
|
||||
- go.yml
|
||||
- helm.yml
|
||||
variables:
|
||||
CI_DOCKER_BUILD: "true"
|
||||
CI_DOCKER_DHUB_REPO: "yungwood/ical-filter-proxy"
|
||||
|
||||
go:build:
|
||||
artifacts:
|
||||
paths:
|
||||
- ical-filter-proxy
|
||||
|
||||
docker:build:
|
||||
needs:
|
||||
- go:build
|
||||
37
Dockerfile
Normal file
37
Dockerfile
Normal file
@@ -0,0 +1,37 @@
|
||||
FROM alpine:3.20.2
|
||||
|
||||
# build args and defaults
|
||||
ARG BUILD_DATE="not-set"
|
||||
ARG REVISION="unknown"
|
||||
ARG VERSION="dev-build"
|
||||
|
||||
# set some labels
|
||||
LABEL org.opencontainers.image.created="$BUILD_DATE" \
|
||||
org.opencontainers.image.base.name="alpine:3.20.2" \
|
||||
org.opencontainers.image.documentation="https://github.com/yungwood/ical-filter-proxy/tree/main/README.md" \
|
||||
org.opencontainers.image.licenses="MIT" \
|
||||
org.opencontainers.image.source="https://github.com/yungwood/ical-filter-proxy" \
|
||||
org.opencontainers.image.revision="$REVISION" \
|
||||
org.opencontainers.image.title="iCal Filter Proxy" \
|
||||
org.opencontainers.image.description="iCal proxy with support for user-defined filtering rules" \
|
||||
org.opencontainers.image.version="$VERSION"
|
||||
|
||||
# install dependencies
|
||||
RUN apk --no-cache add gcompat=1.1.0-r4
|
||||
|
||||
# create a group and user
|
||||
RUN addgroup -S icalfilterproxy && adduser -S -G icalfilterproxy icalfilterproxy
|
||||
|
||||
# switch to app user
|
||||
USER icalfilterproxy
|
||||
|
||||
# set working dir
|
||||
WORKDIR /app
|
||||
|
||||
# copy binary
|
||||
COPY ical-filter-proxy /usr/bin/ical-filter-proxy
|
||||
|
||||
# expose port, define entrypoint
|
||||
EXPOSE 8080/tcp
|
||||
ENTRYPOINT ["/usr/bin/ical-filter-proxy"]
|
||||
CMD ["-config", "/app/config.yaml"]
|
||||
213
README.md
Normal file
213
README.md
Normal file
@@ -0,0 +1,213 @@
|
||||
<!-- PROJECT LOGO -->
|
||||
<br />
|
||||
<div align="center">
|
||||
<a href="https://github.com/yungwood/ical-filter-proxy">
|
||||
<img src="logo.png" alt="Logo" width="120" height="120">
|
||||
</a>
|
||||
|
||||
<h3 align="center">iCal Filter Proxy</h3>
|
||||
|
||||
<p align="center">
|
||||
iCal proxy with support for user-defined filtering rules
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
## What's this thing?
|
||||
|
||||
Do you have iCal feeds with a bunch of stuff you *don't* need? Do you want to modify events generated by your rostering system?
|
||||
|
||||
iCal Filter Proxy is a simple service for proxying multiple iCal feeds while applying a list of filters to remove or modify events to suit your use case.
|
||||
|
||||
### Features
|
||||
|
||||
* Proxy multiple calendars
|
||||
* Define a list of filters per calendar
|
||||
* Match events using basic text and regex conditions
|
||||
* Remove or modify events as they are proxied
|
||||
|
||||
|
||||
### Built With
|
||||
|
||||
* Go
|
||||
* [golang-ical](https://github.com/arran4/golang-ical)
|
||||
* [yaml.v3](https://github.com/go-yaml/yaml/tree/v3.0.1)
|
||||
* [DALL-E 2](https://openai.com/index/dall-e-2/) (app icon)
|
||||
|
||||
|
||||
## Setup
|
||||
|
||||
### Docker
|
||||
|
||||
Docker images are published to [Docker Hub](https://hub.docker.com/repository/docker/yungwood/ical-filter-proxy). You'll need a config file (see below) mounted into the container at `/app/config.yaml`.
|
||||
|
||||
For example:
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name=ical-filter-proxy \
|
||||
-v config.yaml:/app/config.yaml \
|
||||
-p 8080:8080/tcp \
|
||||
--restart unless-stopped \
|
||||
yungwood/ical-filter-proxy:latest
|
||||
```
|
||||
|
||||
You can also adapt the included [`docker-compose.yaml`](./docker-compose.yaml) example.
|
||||
|
||||
### Kubernetes
|
||||
|
||||
You can deploy iCal Filter Proxy using the included helm chart from [`charts/ical-filter-proxy`](charts/ical-filter-proxy).
|
||||
|
||||
### Build from source
|
||||
|
||||
You can also build the app and container from source.
|
||||
|
||||
```bash
|
||||
# clone this repo
|
||||
git clone git@github.com:yungwood/ical-filter-proxy.git
|
||||
cd ical-filter-proxy
|
||||
|
||||
# build the ical-filter-proxy binary
|
||||
go build .
|
||||
|
||||
# build container image
|
||||
docker build \
|
||||
--build-arg BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \
|
||||
--build-arg REVISION=$(git rev-parse HEAD) \
|
||||
--build-arg VERSION=$(git rev-parse --short HEAD) \
|
||||
-t ical-filter-proxy:latest .
|
||||
```
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
Calendars and filters are defined in a yaml config file. By default this is `config.yaml` (use the `-config` switch to change this). The configuration must define at least one calendar for ical-filter-proxy to start.
|
||||
|
||||
Example configuration (with comments):
|
||||
|
||||
```yaml
|
||||
calendars:
|
||||
|
||||
# basic example
|
||||
- name: example # used as slug in URL - e.g. ical-filter-proxy:8080/calendars/example/feed?token=changeme
|
||||
token: "changeme" # token used to pull iCal feed - authentication is disabled when blank
|
||||
feed_url: "https://my-upstream-calendar.url/feed.ics" # URL for the upstream iCal feed
|
||||
filters: # optional - if no filters defined the upstream calendar is proxied as parsed
|
||||
- description: "Remove an event based on a regex"
|
||||
remove: true # events matching this filter will be removed
|
||||
match: # optional - all events will match if no rules defined
|
||||
summary: # match on event summary (title)
|
||||
contains: "deleteme" # must contain 'deleteme'
|
||||
- description: "Remove descriptions from all events"
|
||||
transform: # optional
|
||||
description: # modify event description
|
||||
remove: true # replace with a blank string
|
||||
|
||||
# example: removing noise from an Office 365 calendar
|
||||
- name: outlook
|
||||
token: "changeme"
|
||||
feed_url: "https://outlook.office365.com/owa/calendar/.../reachcalendar.ics"
|
||||
filters:
|
||||
- description: "Remove canceled events" # canceled events remain with a 'Canceled:' prefix until removed
|
||||
remove: true
|
||||
match:
|
||||
summary:
|
||||
prefix: "Canceled: "
|
||||
- description: "Remove optional events"
|
||||
remove: true
|
||||
match:
|
||||
summary:
|
||||
prefix: "[Optional]"
|
||||
- description: "Remove public holidays"
|
||||
remove: true
|
||||
match:
|
||||
summary:
|
||||
regex_match: ".*[Pp]ublic [Hh]oliday.*"
|
||||
|
||||
# example: cleaning up an OpsGenie feed
|
||||
- name: opsgenie
|
||||
token: "changeme"
|
||||
feed_url: "https://company.app.opsgenie.com/webapi/webcal/getRecentSchedule?webcalToken=..."
|
||||
filters:
|
||||
- description: "Keep oncall schedule events and fix names"
|
||||
match:
|
||||
summary:
|
||||
contains: "schedule: oncall"
|
||||
stop: true # stops processing any more filters
|
||||
transform:
|
||||
summary:
|
||||
replace: "On-Call" # replace the event summary (title)
|
||||
- description: "Remove all other events"
|
||||
remove: true
|
||||
|
||||
unsafe: false # optional - must be enabled if any calendars do not have a token
|
||||
```
|
||||
|
||||
|
||||
### Filters
|
||||
|
||||
Calendar events are filtered using a similar concept to email filtering. A list of filters is defined for each calendar in the config.
|
||||
|
||||
Each event parsed from `feed_url` is evaluated against the filters in sequence.
|
||||
|
||||
* All `match` rules for a filter must be true to match an event
|
||||
* A filter with no `match` rules will *always* match
|
||||
* When a match is found:
|
||||
* if `remove` is `true` the event is discarded
|
||||
* `transform` rules are applied to the event
|
||||
* if `stop` is `true` no more filters are processed
|
||||
* If no match is found the event is retained by default
|
||||
|
||||
#### Match conditions
|
||||
|
||||
Each filter can spcify match conditions against the following event properties:
|
||||
|
||||
* `summary` (string value)
|
||||
* `location` (string value)
|
||||
* `description` (string value)
|
||||
|
||||
These match conditions are available for a string value:
|
||||
|
||||
* `contains` - property must contain this value
|
||||
* `prefix` - property must start with this value
|
||||
* `suffix` - property must end with this value
|
||||
* `regex_match` - property must match the given regular expression (an invalid regex will result in no matches)
|
||||
|
||||
#### Transformations
|
||||
|
||||
Transformations can be applied to the following event properties:
|
||||
|
||||
* `summary` - string value
|
||||
* `location` - string value
|
||||
* `description` - string value
|
||||
|
||||
The following transformations are available for strings:
|
||||
|
||||
* `replace` - the property is replace with this value
|
||||
* `remove` - if `true` the property is set to a blank string
|
||||
|
||||
|
||||
## Roadmap to 1.0
|
||||
|
||||
There are a few more features I would like to add before I call the project "stable" and release version 1.0.
|
||||
|
||||
- [ ] Time based event conditions
|
||||
- [ ] Caching
|
||||
- [ ] Support for `ical_url_file` and `token_file` in config (vault secrets)
|
||||
- [ ] Prometheus metrics
|
||||
- [ ] Testing
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
If you have a suggestion that would make this better, please feel free to open an issue or send a pull request.
|
||||
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for details.
|
||||
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
This project was inspired by [darkphnx/ical-filter-proxy](https://github.com/darkphnx/ical-filter-proxy). I needed more flexibility with filtering rules and the ability to modify event descriptions... plus I wanted an excuse to finally write something in Go.
|
||||
259
calendar.go
Normal file
259
calendar.go
Normal file
@@ -0,0 +1,259 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
ics "github.com/arran4/golang-ical"
|
||||
)
|
||||
|
||||
// All structs defined in this file are used to unmarshall yaml configuration and
|
||||
// provide helper functions that are used to fetch and filter events
|
||||
|
||||
// CalendarConfig definition
|
||||
type CalendarConfig struct {
|
||||
Name string `yaml:"name"`
|
||||
Token string `yaml:"token"`
|
||||
FeedURL string `yaml:"feed_url"`
|
||||
Filters []Filter `yaml:"filters"`
|
||||
}
|
||||
|
||||
// Downloads iCal feed from the URL and applies filtering rules
|
||||
func (calendarConfig CalendarConfig) fetch() ([]byte, error) {
|
||||
|
||||
// get the iCal feed
|
||||
slog.Debug("Fetching iCal feed", "url", calendarConfig.FeedURL)
|
||||
resp, err := http.Get(calendarConfig.FeedURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
feedData, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// parse calendar
|
||||
cal, err := ics.ParseCalendar(strings.NewReader(string(feedData)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// process filters
|
||||
if len(calendarConfig.Filters) > 0 {
|
||||
slog.Debug("Processing filters", "calendar", calendarConfig.Name)
|
||||
for _, event := range cal.Events() {
|
||||
if !calendarConfig.ProcessEvent(event) {
|
||||
cal.RemoveEvent(event.Id())
|
||||
}
|
||||
}
|
||||
slog.Debug("Filter processing completed", "calendar", calendarConfig.Name)
|
||||
} else {
|
||||
slog.Debug("No filters to evaluate", "calendar", calendarConfig.Name)
|
||||
}
|
||||
|
||||
// serialize output
|
||||
var buf bytes.Buffer
|
||||
err = cal.SerializeTo(&buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// return
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// Evaluate the filters for a calendar against a given VEvent and
|
||||
// perform any transformations directly to the VEvent (pointer)
|
||||
// This function returns false if an event should be deleted
|
||||
func (calendarConfig CalendarConfig) ProcessEvent(event *ics.VEvent) bool {
|
||||
|
||||
// Get the Summary (the "title" of the event)
|
||||
// In case we cannot parse the event summary it should get dropped
|
||||
summary := event.GetProperty(ics.ComponentPropertySummary) // summary only for logging
|
||||
if summary == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Iterate through the Filter rules
|
||||
for id, filter := range calendarConfig.Filters {
|
||||
|
||||
// Does the filter match the event?
|
||||
if filter.matchesEvent(*event) {
|
||||
slog.Debug("Filter match found", "rule_id", id, "filter_description", filter.Description, "event_summary", summary.Value)
|
||||
|
||||
// The event should get dropped if RemoveEvent is set
|
||||
if filter.RemoveEvent {
|
||||
slog.Debug("Event to be removed, no more rules will be processed", "action", "DELETE", "rule_id", id, "filter_description", filter.Description, "event_summary", summary.Value)
|
||||
return false
|
||||
}
|
||||
|
||||
// Apply transformation rules to event
|
||||
filter.transformEvent(event)
|
||||
|
||||
// Check if we should stop processing rules
|
||||
if filter.Stop {
|
||||
slog.Debug("Stop option is set, no more rules will be processed", "rule_id", id, "filter_description", filter.Description, "event_summary", summary.Value)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keep event by default if all Filter rules are processed
|
||||
slog.Debug("Rule processing complete, event will be kept", "rule_id", nil, "event_summary", summary.Value)
|
||||
return true
|
||||
|
||||
}
|
||||
|
||||
// Filter definition
|
||||
type Filter struct {
|
||||
Description string `yaml:"description"`
|
||||
RemoveEvent bool `yaml:"remove"`
|
||||
Stop bool `yaml:"stop"`
|
||||
Match EventMatchRules `yaml:"match"`
|
||||
Transform EventTransformRules `yaml:"transform"`
|
||||
}
|
||||
|
||||
// Returns true if a VEvent matches the Filter conditions
|
||||
func (filter Filter) matchesEvent(event ics.VEvent) bool {
|
||||
|
||||
// If an event property is not defined golang-ical returns a nil pointer
|
||||
|
||||
// Get event Summary - only used for debug logging
|
||||
eventSummary := event.GetProperty(ics.ComponentPropertySummary)
|
||||
if eventSummary == nil {
|
||||
slog.Warn("Unable to process event summary. Event will be dropped")
|
||||
return false // never match if VEvent has no summary
|
||||
}
|
||||
|
||||
// Check Summary filters against VEvent
|
||||
if filter.Match.Summary.hasConditions() {
|
||||
if !filter.Match.Summary.matchesString(eventSummary.Value) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Check Description filters against VEvent
|
||||
if filter.Match.Description.hasConditions() {
|
||||
eventDescription := event.GetProperty(ics.ComponentPropertyDescription)
|
||||
if eventDescription == nil {
|
||||
slog.Debug("Event has no Description so cannot not match filter", "event_summary", eventSummary.Value, "filter", filter.Description)
|
||||
return false // if VEvent has no description it cannot match filter
|
||||
}
|
||||
if !filter.Match.Description.matchesString(eventDescription.Value) {
|
||||
slog.Debug("Event Description does not match filter conditions", "event_summary", eventSummary.Value, "filter", filter.Description)
|
||||
return false // event doesn't match
|
||||
}
|
||||
}
|
||||
|
||||
// Check Description filters against VEvent
|
||||
if filter.Match.Location.hasConditions() {
|
||||
eventLocation := event.GetProperty(ics.ComponentPropertyLocation)
|
||||
if eventLocation == nil {
|
||||
slog.Warn("Event has no Location so cannot match filter", "event_summary", eventSummary.Value, "filter", filter.Description)
|
||||
return false // if VEvent has no location it cannot match filter
|
||||
}
|
||||
if !filter.Match.Location.matchesString(eventLocation.Value) {
|
||||
slog.Debug("Event Location does not match filter conditions", "event_summary", eventSummary.Value, "filter", filter.Description)
|
||||
return false // event doesn't match
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// VEvent must match if we get here
|
||||
slog.Debug("Event matches filter conditions", "event_summary", eventSummary.Value, "filter", filter.Description)
|
||||
return true
|
||||
}
|
||||
|
||||
// Applies filter transformations to a VEvent pointer
|
||||
func (filter Filter) transformEvent(event *ics.VEvent) {
|
||||
|
||||
// Summary transformations
|
||||
if filter.Transform.Summary.Remove {
|
||||
event.SetSummary("")
|
||||
} else if filter.Transform.Summary.Replace != "" {
|
||||
event.SetSummary(filter.Transform.Summary.Replace)
|
||||
}
|
||||
|
||||
// Description transformations
|
||||
if filter.Transform.Description.Remove {
|
||||
event.SetDescription("")
|
||||
} else if filter.Transform.Description.Replace != "" {
|
||||
event.SetDescription(filter.Transform.Description.Replace)
|
||||
}
|
||||
}
|
||||
|
||||
// EventMatchRules contains VEvent properties that user can match against
|
||||
type EventMatchRules struct {
|
||||
Summary StringMatchRule `yaml:"summary"`
|
||||
Description StringMatchRule `yaml:"description"`
|
||||
Location StringMatchRule `yaml:"location"`
|
||||
}
|
||||
|
||||
// StringMatchRule defines match rules for VEvent properties with string values
|
||||
type StringMatchRule struct {
|
||||
Contains string `yaml:"contains"`
|
||||
Prefix string `yaml:"prefix"`
|
||||
Suffix string `yaml:"suffix"`
|
||||
RegexMatch string `yaml:"regex"`
|
||||
}
|
||||
|
||||
// Returns true if StringMatchRule has any conditions
|
||||
func (smr StringMatchRule) hasConditions() bool {
|
||||
return smr.Contains != "" ||
|
||||
smr.Prefix != "" ||
|
||||
smr.Suffix != "" ||
|
||||
smr.RegexMatch != ""
|
||||
}
|
||||
|
||||
// Returns true if a given string (data) matches ALL StringMatchRule conditions
|
||||
func (smr StringMatchRule) matchesString(data string) bool {
|
||||
// check contains if set
|
||||
if smr.Contains != "" {
|
||||
if data == "" || !strings.Contains(data, smr.Contains) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
// check prefix if set
|
||||
if smr.Prefix != "" {
|
||||
if data == "" || !strings.HasPrefix(data, smr.Prefix) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
// check suffix if set
|
||||
if smr.Suffix != "" {
|
||||
if data == "" || !strings.HasSuffix(data, smr.Suffix) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
// check regex match if set
|
||||
if smr.RegexMatch != "" {
|
||||
match, err := regexp.MatchString(data, smr.RegexMatch)
|
||||
if err != nil {
|
||||
slog.Warn("error processing regex rule", "value", smr.RegexMatch)
|
||||
return false // regex error is considered a failure to match
|
||||
}
|
||||
if !match {
|
||||
return false // regex didn't match
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// EventTransformRules contains VEvent properties that user can modify
|
||||
type EventTransformRules struct {
|
||||
Summary StringTransformRule `yaml:"summary"`
|
||||
Description StringTransformRule `yaml:"description"`
|
||||
Location StringTransformRule `yaml:"location"`
|
||||
}
|
||||
|
||||
// StringTransformRule defines changes for VEvent properties with string values
|
||||
type StringTransformRule struct {
|
||||
Replace string `yaml:"replace"`
|
||||
Remove bool `yaml:"remove"`
|
||||
}
|
||||
23
charts/ical-filter-proxy/.helmignore
Normal file
23
charts/ical-filter-proxy/.helmignore
Normal file
@@ -0,0 +1,23 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
8
charts/ical-filter-proxy/Chart.yaml
Normal file
8
charts/ical-filter-proxy/Chart.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
apiVersion: v2
|
||||
name: ical-filter-proxy
|
||||
description: iCal proxy with support for user-defined filtering rules
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "0.1.0"
|
||||
maintainers:
|
||||
- name: Yung Wood
|
||||
22
charts/ical-filter-proxy/templates/NOTES.txt
Normal file
22
charts/ical-filter-proxy/templates/NOTES.txt
Normal file
@@ -0,0 +1,22 @@
|
||||
1. Get the application URL by running these commands:
|
||||
{{- if .Values.ingress.enabled }}
|
||||
{{- range $host := .Values.ingress.hosts }}
|
||||
{{- range .paths }}
|
||||
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- else if contains "NodePort" .Values.service.type }}
|
||||
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "ical-filter-proxy.fullname" . }})
|
||||
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
||||
echo http://$NODE_IP:$NODE_PORT
|
||||
{{- else if contains "LoadBalancer" .Values.service.type }}
|
||||
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
||||
You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "ical-filter-proxy.fullname" . }}'
|
||||
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "ical-filter-proxy.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
|
||||
echo http://$SERVICE_IP:{{ .Values.service.port }}
|
||||
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "ical-filter-proxy.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
|
||||
echo "Visit http://127.0.0.1:8080 to use your application"
|
||||
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
|
||||
{{- end }}
|
||||
62
charts/ical-filter-proxy/templates/_helpers.tpl
Normal file
62
charts/ical-filter-proxy/templates/_helpers.tpl
Normal file
@@ -0,0 +1,62 @@
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "ical-filter-proxy.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "ical-filter-proxy.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "ical-filter-proxy.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "ical-filter-proxy.labels" -}}
|
||||
helm.sh/chart: {{ include "ical-filter-proxy.chart" . }}
|
||||
{{ include "ical-filter-proxy.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "ical-filter-proxy.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "ical-filter-proxy.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
{{- define "ical-filter-proxy.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
{{- default (include "ical-filter-proxy.fullname" .) .Values.serviceAccount.name }}
|
||||
{{- else }}
|
||||
{{- default "default" .Values.serviceAccount.name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
15
charts/ical-filter-proxy/templates/config.yaml
Normal file
15
charts/ical-filter-proxy/templates/config.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
{{- if .Values.config.enabled -}}
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "ical-filter-proxy.fullname" . }}-config
|
||||
labels:
|
||||
{{- include "ical-filter-proxy.labels" . | nindent 4 }}
|
||||
data:
|
||||
config.yaml: |
|
||||
{{- if .Values.config.insecure }}
|
||||
insecure: true
|
||||
{{- end }}
|
||||
calendars:
|
||||
{{ .Values.config.calendars | toYaml | indent 6 }}
|
||||
{{- end }}
|
||||
79
charts/ical-filter-proxy/templates/deployment.yaml
Normal file
79
charts/ical-filter-proxy/templates/deployment.yaml
Normal file
@@ -0,0 +1,79 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "ical-filter-proxy.fullname" . }}
|
||||
labels:
|
||||
{{- include "ical-filter-proxy.labels" . | nindent 4 }}
|
||||
spec:
|
||||
{{- if not .Values.autoscaling.enabled }}
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "ical-filter-proxy.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
{{- with .Values.podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "ical-filter-proxy.labels" . | nindent 8 }}
|
||||
{{- with .Values.podLabels }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "ical-filter-proxy.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
args: {{ .Values.args }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.service.port }}
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
{{- toYaml .Values.livenessProbe | nindent 12 }}
|
||||
readinessProbe:
|
||||
{{- toYaml .Values.readinessProbe | nindent 12 }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
volumeMounts:
|
||||
{{- if .Values.config.enabled }}
|
||||
- name: config
|
||||
mountPath: /app/config.yaml
|
||||
subPath: config.yaml
|
||||
{{- end }}
|
||||
{{- with .Values.volumeMounts }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
volumes:
|
||||
{{- if .Values.config.enabled }}
|
||||
- name: config
|
||||
configMap:
|
||||
name: {{ include "ical-filter-proxy.fullname" . }}-config
|
||||
{{- end }}
|
||||
{{- with .Values.volumes }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
32
charts/ical-filter-proxy/templates/hpa.yaml
Normal file
32
charts/ical-filter-proxy/templates/hpa.yaml
Normal file
@@ -0,0 +1,32 @@
|
||||
{{- if .Values.autoscaling.enabled }}
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "ical-filter-proxy.fullname" . }}
|
||||
labels:
|
||||
{{- include "ical-filter-proxy.labels" . | nindent 4 }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ include "ical-filter-proxy.fullname" . }}
|
||||
minReplicas: {{ .Values.autoscaling.minReplicas }}
|
||||
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
|
||||
metrics:
|
||||
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
61
charts/ical-filter-proxy/templates/ingress.yaml
Normal file
61
charts/ical-filter-proxy/templates/ingress.yaml
Normal file
@@ -0,0 +1,61 @@
|
||||
{{- if .Values.ingress.enabled -}}
|
||||
{{- $fullName := include "ical-filter-proxy.fullname" . -}}
|
||||
{{- $svcPort := .Values.service.port -}}
|
||||
{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
|
||||
{{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
|
||||
{{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
{{- else -}}
|
||||
apiVersion: extensions/v1beta1
|
||||
{{- end }}
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $fullName }}
|
||||
labels:
|
||||
{{- include "ical-filter-proxy.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
|
||||
ingressClassName: {{ .Values.ingress.className }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.tls }}
|
||||
tls:
|
||||
{{- range .Values.ingress.tls }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.ingress.hosts }}
|
||||
- host: {{ .host | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ .path }}
|
||||
{{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
|
||||
pathType: {{ .pathType }}
|
||||
{{- end }}
|
||||
backend:
|
||||
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
service:
|
||||
name: {{ $fullName }}
|
||||
port:
|
||||
number: {{ $svcPort }}
|
||||
{{- else }}
|
||||
serviceName: {{ $fullName }}
|
||||
servicePort: {{ $svcPort }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
15
charts/ical-filter-proxy/templates/service.yaml
Normal file
15
charts/ical-filter-proxy/templates/service.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "ical-filter-proxy.fullname" . }}
|
||||
labels:
|
||||
{{- include "ical-filter-proxy.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{- include "ical-filter-proxy.selectorLabels" . | nindent 4 }}
|
||||
13
charts/ical-filter-proxy/templates/serviceaccount.yaml
Normal file
13
charts/ical-filter-proxy/templates/serviceaccount.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
{{- if .Values.serviceAccount.create -}}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "ical-filter-proxy.serviceAccountName" . }}
|
||||
labels:
|
||||
{{- include "ical-filter-proxy.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
|
||||
{{- end }}
|
||||
119
charts/ical-filter-proxy/values.yaml
Normal file
119
charts/ical-filter-proxy/values.yaml
Normal file
@@ -0,0 +1,119 @@
|
||||
replicaCount: 1
|
||||
|
||||
image:
|
||||
repository: yungwood/ical-filter-proxy
|
||||
pullPolicy: IfNotPresent
|
||||
tag: "" # defaults to chart appVersion
|
||||
|
||||
imagePullSecrets: []
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
args: []
|
||||
|
||||
config:
|
||||
enabled: false # enable to generate config from values (example below)
|
||||
# calendars:
|
||||
# - name: outlook
|
||||
# token: "changeme"
|
||||
# feed_url: "https://outlook.office365.com/owa/calendar/.../reachcalendar.ics"
|
||||
# filters:
|
||||
# - description: "Remove cancelled events"
|
||||
# remove: true
|
||||
# match:
|
||||
# summary:
|
||||
# prefix: "Canceled: "
|
||||
# - description: "Remove optional events"
|
||||
# remove: true
|
||||
# match:
|
||||
# summary:
|
||||
# prefix: "[Optional]"
|
||||
# - description: "Remove public holidays"
|
||||
# remove: true
|
||||
# match:
|
||||
# summary:
|
||||
# regex_match: ".*[Pp]ublic [Hh]oliday.*"
|
||||
# insecure: false
|
||||
|
||||
serviceAccount:
|
||||
create: true
|
||||
automount: true
|
||||
annotations: {}
|
||||
name: ""
|
||||
|
||||
podAnnotations: {}
|
||||
podLabels: {}
|
||||
|
||||
podSecurityContext: {}
|
||||
# fsGroup: 2000
|
||||
|
||||
securityContext:
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
runAsGroup: 3000
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 8080
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
className: ""
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
hosts:
|
||||
- host: ical-filter-proxy.local
|
||||
paths:
|
||||
- path: /
|
||||
pathType: ImplementationSpecific
|
||||
tls: []
|
||||
# - secretName: ical-filter-proxy-tls
|
||||
# hosts:
|
||||
# - ical-filter-proxy.local
|
||||
|
||||
resources: {}
|
||||
# limits:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
# requests:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /liveness
|
||||
port: http
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /readiness
|
||||
port: http
|
||||
|
||||
autoscaling:
|
||||
enabled: false
|
||||
minReplicas: 1
|
||||
maxReplicas: 100
|
||||
targetCPUUtilizationPercentage: 80
|
||||
# targetMemoryUtilizationPercentage: 80
|
||||
|
||||
volumes: []
|
||||
# - name: foo
|
||||
# secret:
|
||||
# secretName: mysecret
|
||||
# optional: false
|
||||
|
||||
volumeMounts: []
|
||||
# - name: foo
|
||||
# mountPath: "/etc/foo"
|
||||
# readOnly: true
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
||||
26
docker-compose.yaml
Normal file
26
docker-compose.yaml
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
services:
|
||||
ical-filter-proxy:
|
||||
image: yungwood/ical-filter-proxy:latest
|
||||
container_name: ical-filter-proxy
|
||||
# # specify additional command arguments
|
||||
# command: --debug
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./config.yaml:/app/config.yaml
|
||||
restart: always
|
||||
|
||||
## the below example might be used with traefik docker provider
|
||||
## https://doc.traefik.io/traefik/providers/docker/
|
||||
# labels:
|
||||
# traefik.enable: true
|
||||
# traefik.http.routers.ical.rule: "Host(`ical.mydomain.com`)"
|
||||
# traefik.http.routers.ical.entrypoints: web
|
||||
# traefik.http.services.ical.loadbalancer.server.port: 8080
|
||||
# networks:
|
||||
# - traefik
|
||||
# networks:
|
||||
# traefik:
|
||||
# name: traefik
|
||||
# external: true
|
||||
18
go.mod
Normal file
18
go.mod
Normal file
@@ -0,0 +1,18 @@
|
||||
module github.com/yungwood/ical-filter-proxy
|
||||
|
||||
go 1.22.5
|
||||
|
||||
require (
|
||||
github.com/arran4/golang-ical v0.3.1
|
||||
github.com/gookit/color v1.5.4 // indirect
|
||||
github.com/gookit/goutil v0.6.15 // indirect
|
||||
github.com/gookit/gsr v0.1.0 // indirect
|
||||
github.com/gookit/slog v0.5.6 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
golang.org/x/sync v0.5.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
29
go.sum
Normal file
29
go.sum
Normal file
@@ -0,0 +1,29 @@
|
||||
github.com/arran4/golang-ical v0.3.1 h1:v13B3eQZ9VDHTAvT6M11vVzxYgcYmjyPBE2eAZl3VZk=
|
||||
github.com/arran4/golang-ical v0.3.1/go.mod h1:LZWxF8ZIu/sjBVUCV0udiVPrQAgq3V0aa0RfbO99Qkk=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
|
||||
github.com/gookit/goutil v0.6.15/go.mod h1:qdKdYEHQdEtyH+4fNdQNZfJHhI0jUZzHxQVAV3DaMDY=
|
||||
github.com/gookit/gsr v0.1.0/go.mod h1:7wv4Y4WCnil8+DlDYHBjidzrEzfHhXEoFjEA0pPPWpI=
|
||||
github.com/gookit/slog v0.5.6/go.mod h1:RfIwzoaQ8wZbKdcqG7+3EzbkMqcp2TUn3mcaSZAw2EQ=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
182
main.go
Normal file
182
main.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var Version = "development"
|
||||
|
||||
// this struct used to parse config.yaml
|
||||
type Config struct {
|
||||
Calendars []CalendarConfig `yaml:"calendars"`
|
||||
AllowUnsafe bool `yaml:"unsafe"`
|
||||
}
|
||||
|
||||
// This function loads the configuration file and does some basic validation
|
||||
// Returns false if the config is not valid or an error occurs
|
||||
func (config *Config) LoadConfig(file string) bool {
|
||||
data, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
slog.Error("Unable to open config file! You can use -config to specify a different file", "file", file)
|
||||
return false
|
||||
}
|
||||
err = yaml.Unmarshal(data, &config)
|
||||
if err != nil {
|
||||
slog.Error("Error while unmarshalling yaml! Check config file is valid", "file", file)
|
||||
return false
|
||||
}
|
||||
|
||||
// ensure calendars exist
|
||||
if len(config.Calendars) == 0 {
|
||||
slog.Error("No calendars found! Configuration should define at least one calendar")
|
||||
return false
|
||||
}
|
||||
|
||||
// validate calendar configs
|
||||
for _, calendarConfig := range config.Calendars {
|
||||
|
||||
// check if url seems valid
|
||||
if !strings.HasPrefix(calendarConfig.FeedURL, "http://") && !strings.HasPrefix(calendarConfig.FeedURL, "https://") {
|
||||
slog.Debug("Calendar URL must begin with http:// or https://", "calendar", calendarConfig.Name, "feed_url", len(calendarConfig.Filters))
|
||||
return false
|
||||
}
|
||||
|
||||
// Check to see if auth is disabled (token not set)
|
||||
// If so print a warning message and make sure unsafe is enabled in config
|
||||
if len(calendarConfig.Token) == 0 {
|
||||
slog.Warn("Calendar has no token set. Authentication will be disabled", "calendar", calendarConfig.Name)
|
||||
if !config.AllowUnsafe {
|
||||
slog.Error("Calendar cannot have authentication disabled without unsafe optionenabled in the configuration", "calendar", calendarConfig.Name)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Print a warning if the calendar has no filters
|
||||
if len(calendarConfig.Filters) == 0 {
|
||||
slog.Warn("Calendar has no filters and will be proxy-only", "calendar", calendarConfig.Name)
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return true // config is parsed successfully
|
||||
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
// command-line args
|
||||
var (
|
||||
configFile string
|
||||
debugLogging bool
|
||||
jsonLogging bool
|
||||
listenPort int
|
||||
validateConfig bool
|
||||
printVersion bool
|
||||
)
|
||||
flag.StringVar(&configFile, "config", "config.yaml", "config file")
|
||||
flag.BoolVar(&debugLogging, "debug", false, "enable debug logging")
|
||||
flag.BoolVar(&printVersion, "version", false, "print version and exit")
|
||||
flag.BoolVar(&jsonLogging, "json", false, "output logging in JSON format")
|
||||
flag.IntVar(&listenPort, "port", 8080, "listening port for api")
|
||||
flag.BoolVar(&validateConfig, "validate", false, "validate config and exit")
|
||||
flag.Parse()
|
||||
|
||||
// print version and exit
|
||||
if printVersion {
|
||||
fmt.Println("version:", Version)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// setup logging options
|
||||
loggingLevel := slog.LevelInfo // default loglevel
|
||||
if debugLogging {
|
||||
loggingLevel = slog.LevelDebug // debug logging enabled
|
||||
}
|
||||
opts := &slog.HandlerOptions{
|
||||
Level: loggingLevel,
|
||||
}
|
||||
|
||||
// create json or text logger based on args
|
||||
var logger *slog.Logger
|
||||
if jsonLogging {
|
||||
logger = slog.New(slog.NewJSONHandler(os.Stdout, opts))
|
||||
} else {
|
||||
logger = slog.New(slog.NewTextHandler(os.Stdout, opts))
|
||||
}
|
||||
slog.SetDefault(logger)
|
||||
|
||||
// load configuration
|
||||
slog.Debug("reading config", "configFile", configFile)
|
||||
var config Config
|
||||
if !config.LoadConfig(configFile) {
|
||||
os.Exit(1) // fail if config is not valid
|
||||
}
|
||||
slog.Debug("loaded config")
|
||||
|
||||
// print a message and exit if validate arg was specified
|
||||
if validateConfig {
|
||||
slog.Info("configuration was validated successfully")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// iterate through calendars in the config and setup a handler for each
|
||||
// todo: consider refactor to route requests dynamically?
|
||||
for _, calendarConfig := range config.Calendars {
|
||||
|
||||
// configure HTTP endpoint
|
||||
httpPath := "/calendars/" + calendarConfig.Name + "/feed"
|
||||
slog.Debug("Configuring endpoint", "calendar", calendarConfig.Name, "http_path", httpPath)
|
||||
http.HandleFunc(httpPath, func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
slog.Debug("Received request for calendar", "http_path", httpPath, "calendar", calendarConfig.Name, "client_ip", r.RemoteAddr)
|
||||
|
||||
// validate token
|
||||
token := r.URL.Query().Get("token")
|
||||
if token != calendarConfig.Token {
|
||||
slog.Warn("Unauthorized access attempt", "client_ip", r.RemoteAddr)
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// fetch and filter upstream calendar
|
||||
feed, err := calendarConfig.fetch()
|
||||
if err != nil {
|
||||
slog.Error("Error fetching and filtering feed", "error", err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// return calendar
|
||||
w.Header().Set("Content-Type", "text/calendar")
|
||||
_, err = w.Write(feed)
|
||||
if err != nil {
|
||||
slog.Error("Error writing response", "error", err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
slog.Info("Calendar request processed", "http_path", httpPath, "calendar", calendarConfig.Name, "client_ip", r.RemoteAddr)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// add a readiness and liveness check endpoint (return blank 200 OK response)
|
||||
http.HandleFunc("/liveness", func(w http.ResponseWriter, r *http.Request) {})
|
||||
http.HandleFunc("/readiness", func(w http.ResponseWriter, r *http.Request) {})
|
||||
|
||||
// start the webserver
|
||||
slog.Info("Starting web server", "port", listenPort)
|
||||
if err := http.ListenAndServe(":"+strconv.Itoa(listenPort), nil); err != nil {
|
||||
slog.Error("Error starting web server", "error", err)
|
||||
}
|
||||
|
||||
}
|
||||
4
renovate.json
Normal file
4
renovate.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"baseBranches": ["develop"]
|
||||
}
|
||||
Reference in New Issue
Block a user