Add container publishing and env-based runtime
This commit is contained in:
@@ -23,3 +23,33 @@ jobs:
|
|||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: go build .
|
run: go build .
|
||||||
|
|
||||||
|
publish:
|
||||||
|
if: gitea.event_name == 'push' && gitea.ref == 'refs/heads/main'
|
||||||
|
needs: test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Docker CLI
|
||||||
|
run: |
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y docker.io
|
||||||
|
|
||||||
|
- name: Login to Gitea Registry
|
||||||
|
env:
|
||||||
|
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
registry_host="${GITHUB_SERVER_URL#http://}"
|
||||||
|
registry_host="${registry_host#https://}"
|
||||||
|
printf '%s' "${REGISTRY_PASSWORD}" | docker login "${registry_host}" -u "${GITHUB_REPOSITORY_OWNER}" --password-stdin
|
||||||
|
|
||||||
|
- name: Build and Push Image
|
||||||
|
run: |
|
||||||
|
registry_host="${GITHUB_SERVER_URL#http://}"
|
||||||
|
registry_host="${registry_host#https://}"
|
||||||
|
image="${registry_host}/${GITHUB_REPOSITORY}"
|
||||||
|
docker build -t "${image}:main" -t "${image}:sha-${GITHUB_SHA}" .
|
||||||
|
docker push "${image}:main"
|
||||||
|
docker push "${image}:sha-${GITHUB_SHA}"
|
||||||
|
|||||||
@@ -12,11 +12,13 @@ Required when `SEND_EMAIL=true`:
|
|||||||
|
|
||||||
Recommended settings:
|
Recommended settings:
|
||||||
|
|
||||||
- `ALMA_CREDS_FILE=/config/alma.creds`
|
|
||||||
- `ALMA_ASSIGNMENTS_URL=https://example.invalid/children/student-id/assignments`
|
- `ALMA_ASSIGNMENTS_URL=https://example.invalid/children/student-id/assignments`
|
||||||
- `ALMA_SCHEDULE_URL=https://example.invalid/children/student-id/schedule`
|
- `ALMA_SCHEDULE_URL=https://example.invalid/children/student-id/schedule`
|
||||||
- `ALMA_START_DATE=2026-01-20`
|
- `ALMA_START_DATE=2026-01-20`
|
||||||
- `ALMA_UPCOMING_DAYS=14`
|
- `ALMA_UPCOMING_DAYS=14`
|
||||||
|
- `ALMA_CREDS_FILE=/config/alma.creds`
|
||||||
|
- `ALMA_USERNAME`
|
||||||
|
- `ALMA_PASSWORD`
|
||||||
- `SMTP_PORT=587`
|
- `SMTP_PORT=587`
|
||||||
- `SMTP_STARTTLS=true`
|
- `SMTP_STARTTLS=true`
|
||||||
- `SMTP_USERNAME`
|
- `SMTP_USERNAME`
|
||||||
@@ -26,7 +28,10 @@ Recommended settings:
|
|||||||
|
|
||||||
Any setting can also be supplied via a `*_FILE` variant such as `SMTP_PASSWORD_FILE`.
|
Any setting can also be supplied via a `*_FILE` variant such as `SMTP_PASSWORD_FILE`.
|
||||||
|
|
||||||
The Alma credentials file format is:
|
The Alma credentials can be supplied either by:
|
||||||
|
|
||||||
|
- `ALMA_USERNAME` and `ALMA_PASSWORD`
|
||||||
|
- `ALMA_CREDS_FILE` containing:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
username: your-alma-username
|
username: your-alma-username
|
||||||
@@ -48,8 +53,9 @@ It runs on pushes to `main` and pull requests, and currently:
|
|||||||
|
|
||||||
- runs `go test ./...`
|
- runs `go test ./...`
|
||||||
- runs `go build .`
|
- runs `go build .`
|
||||||
|
- builds and pushes `:main` and `:sha-<commit>` container tags on pushes to `main`
|
||||||
|
|
||||||
The workflow expects a runner with the `ubuntu-latest` label. The cluster runner deployed for this repo provides that label.
|
The workflow expects a runner with the `ubuntu-latest` label and a repository Actions secret named `REGISTRY_PASSWORD` that can push to the Gitea container registry. The cluster runner deployed for this repo provides the required runner label.
|
||||||
|
|
||||||
## Container
|
## Container
|
||||||
|
|
||||||
@@ -63,4 +69,4 @@ The container image uses a static Go binary in `scratch`, with only the CA bundl
|
|||||||
|
|
||||||
## Kubernetes
|
## Kubernetes
|
||||||
|
|
||||||
Use a Secret for Alma and SMTP credentials. The example manifest in `cronjob.example.yaml` mounts Alma credentials at `/config/alma.creds` and reads SMTP credentials from secret-backed environment variables.
|
Use a Secret for Alma and SMTP credentials. The example manifest in `cronjob.example.yaml` reads all runtime settings from Kubernetes secrets and does not require a credentials file mount.
|
||||||
|
|||||||
+9
-41
@@ -2,53 +2,21 @@ apiVersion: batch/v1
|
|||||||
kind: CronJob
|
kind: CronJob
|
||||||
metadata:
|
metadata:
|
||||||
name: alma-assignments-reporter
|
name: alma-assignments-reporter
|
||||||
namespace: default
|
namespace: email
|
||||||
spec:
|
spec:
|
||||||
|
timeZone: America/Los_Angeles
|
||||||
schedule: "0 14 * * *"
|
schedule: "0 14 * * *"
|
||||||
jobTemplate:
|
jobTemplate:
|
||||||
spec:
|
spec:
|
||||||
template:
|
template:
|
||||||
spec:
|
spec:
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: alma-assignments-reporter-registry
|
||||||
restartPolicy: Never
|
restartPolicy: Never
|
||||||
containers:
|
containers:
|
||||||
- name: reporter
|
- name: reporter
|
||||||
image: example.invalid/alma-assignments-reporter:latest
|
image: example.invalid/alma-assignments-reporter:main
|
||||||
env:
|
imagePullPolicy: Always
|
||||||
- name: ALMA_ASSIGNMENTS_URL
|
envFrom:
|
||||||
value: https://example.invalid/children/student-id/assignments
|
- secretRef:
|
||||||
- name: ALMA_SCHEDULE_URL
|
name: alma-assignments-reporter
|
||||||
value: https://example.invalid/children/student-id/schedule
|
|
||||||
- name: ALMA_START_DATE
|
|
||||||
value: "2026-01-20"
|
|
||||||
- name: ALMA_UPCOMING_DAYS
|
|
||||||
value: "14"
|
|
||||||
- name: ALMA_CREDS_FILE
|
|
||||||
value: /config/alma.creds
|
|
||||||
- name: SMTP_HOST
|
|
||||||
value: smtp.email.svc.cluster.local
|
|
||||||
- name: SMTP_PORT
|
|
||||||
value: "587"
|
|
||||||
- name: SMTP_STARTTLS
|
|
||||||
value: "false"
|
|
||||||
- name: EMAIL_FROM
|
|
||||||
value: alma-reporter@example.invalid
|
|
||||||
- name: EMAIL_TO
|
|
||||||
value: parent@example.invalid
|
|
||||||
- name: SMTP_USERNAME
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: alma-assignments-reporter-smtp
|
|
||||||
key: username
|
|
||||||
- name: SMTP_PASSWORD
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: alma-assignments-reporter-smtp
|
|
||||||
key: password
|
|
||||||
volumeMounts:
|
|
||||||
- name: alma-creds
|
|
||||||
mountPath: /config
|
|
||||||
readOnly: true
|
|
||||||
volumes:
|
|
||||||
- name: alma-creds
|
|
||||||
secret:
|
|
||||||
secretName: alma-assignments-reporter-alma
|
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ func loadConfig() (Config, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return Config{}, fmt.Errorf("parse ALMA_UPCOMING_DAYS: %w", err)
|
return Config{}, fmt.Errorf("parse ALMA_UPCOMING_DAYS: %w", err)
|
||||||
}
|
}
|
||||||
almaCredsFile, err := readValue("ALMA_CREDS_FILE", "/config/alma.creds", true)
|
almaCredsFile, err := readValue("ALMA_CREDS_FILE", "/config/alma.creds", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Config{}, err
|
return Config{}, err
|
||||||
}
|
}
|
||||||
@@ -269,6 +269,20 @@ func splitCSV(value string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func loadAlmaCreds(filePath string) (map[string]string, error) {
|
func loadAlmaCreds(filePath string) (map[string]string, error) {
|
||||||
|
if envUser, _ := readValue("ALMA_USERNAME", "", false); envUser != "" {
|
||||||
|
if envPass, _ := readValue("ALMA_PASSWORD", "", false); envPass != "" {
|
||||||
|
return map[string]string{
|
||||||
|
"username": envUser,
|
||||||
|
"password": envPass,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("ALMA_PASSWORD is required when ALMA_USERNAME is set")
|
||||||
|
}
|
||||||
|
|
||||||
|
if filePath == "" {
|
||||||
|
return nil, errors.New("missing Alma credentials: set ALMA_USERNAME/ALMA_PASSWORD or ALMA_CREDS_FILE")
|
||||||
|
}
|
||||||
|
|
||||||
data, err := os.ReadFile(filePath)
|
data, err := os.ReadFile(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("read alma creds: %w", err)
|
return nil, fmt.Errorf("read alma creds: %w", err)
|
||||||
@@ -291,13 +305,6 @@ func loadAlmaCreds(filePath string) (map[string]string, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if envUser, _ := readValue("ALMA_USERNAME", "", false); envUser != "" {
|
|
||||||
values["username"] = envUser
|
|
||||||
}
|
|
||||||
if envPass, _ := readValue("ALMA_PASSWORD", "", false); envPass != "" {
|
|
||||||
values["password"] = envPass
|
|
||||||
}
|
|
||||||
|
|
||||||
if values["username"] == "" || values["password"] == "" {
|
if values["username"] == "" || values["password"] == "" {
|
||||||
return nil, errors.New("alma creds file must contain username and password")
|
return nil, errors.New("alma creds file must contain username and password")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -93,6 +94,44 @@ func TestExtractLikeLiveMissingTable(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoadAlmaCredsFromEnv(t *testing.T) {
|
||||||
|
t.Setenv("ALMA_USERNAME", "student")
|
||||||
|
t.Setenv("ALMA_PASSWORD", "secret")
|
||||||
|
|
||||||
|
got, err := loadAlmaCreds("")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("loadAlmaCreds returned error: %v", err)
|
||||||
|
}
|
||||||
|
if got["username"] != "student" || got["password"] != "secret" {
|
||||||
|
t.Fatalf("unexpected creds: %#v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadAlmaCredsRequiresPasswordWithEnvUsername(t *testing.T) {
|
||||||
|
t.Setenv("ALMA_USERNAME", "student")
|
||||||
|
t.Setenv("ALMA_PASSWORD", "")
|
||||||
|
|
||||||
|
if _, err := loadAlmaCreds(""); err == nil {
|
||||||
|
t.Fatal("expected error when ALMA_PASSWORD is missing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadAlmaCredsFromFile(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
path := dir + "/alma.creds"
|
||||||
|
if err := os.WriteFile(path, []byte("username: student\npassword: secret\n"), 0o600); err != nil {
|
||||||
|
t.Fatalf("write creds file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := loadAlmaCreds(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("loadAlmaCreds returned error: %v", err)
|
||||||
|
}
|
||||||
|
if got["username"] != "student" || got["password"] != "secret" {
|
||||||
|
t.Fatalf("unexpected creds: %#v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func mustParseHTML(text string) *html.Node {
|
func mustParseHTML(text string) *html.Node {
|
||||||
doc, err := html.Parse(strings.NewReader(text))
|
doc, err := html.Parse(strings.NewReader(text))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user