diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index c9479e6..30fb163 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -23,3 +23,33 @@ jobs: - name: 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}" diff --git a/README.md b/README.md index 203eb8b..b75d09a 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,13 @@ Required when `SEND_EMAIL=true`: Recommended settings: -- `ALMA_CREDS_FILE=/config/alma.creds` - `ALMA_ASSIGNMENTS_URL=https://example.invalid/children/student-id/assignments` - `ALMA_SCHEDULE_URL=https://example.invalid/children/student-id/schedule` - `ALMA_START_DATE=2026-01-20` - `ALMA_UPCOMING_DAYS=14` +- `ALMA_CREDS_FILE=/config/alma.creds` +- `ALMA_USERNAME` +- `ALMA_PASSWORD` - `SMTP_PORT=587` - `SMTP_STARTTLS=true` - `SMTP_USERNAME` @@ -26,7 +28,10 @@ Recommended settings: 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 username: your-alma-username @@ -48,8 +53,9 @@ It runs on pushes to `main` and pull requests, and currently: - runs `go test ./...` - runs `go build .` +- builds and pushes `:main` and `:sha-` 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 @@ -63,4 +69,4 @@ The container image uses a static Go binary in `scratch`, with only the CA bundl ## 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. diff --git a/cronjob.example.yaml b/cronjob.example.yaml index 49efc6a..600a8a0 100644 --- a/cronjob.example.yaml +++ b/cronjob.example.yaml @@ -2,53 +2,21 @@ apiVersion: batch/v1 kind: CronJob metadata: name: alma-assignments-reporter - namespace: default + namespace: email spec: + timeZone: America/Los_Angeles schedule: "0 14 * * *" jobTemplate: spec: template: spec: + imagePullSecrets: + - name: alma-assignments-reporter-registry restartPolicy: Never containers: - name: reporter - image: example.invalid/alma-assignments-reporter:latest - env: - - name: ALMA_ASSIGNMENTS_URL - value: https://example.invalid/children/student-id/assignments - - name: ALMA_SCHEDULE_URL - 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 + image: example.invalid/alma-assignments-reporter:main + imagePullPolicy: Always + envFrom: + - secretRef: + name: alma-assignments-reporter diff --git a/main.go b/main.go index 3095a60..f6c0de2 100644 --- a/main.go +++ b/main.go @@ -147,7 +147,7 @@ func loadConfig() (Config, error) { if err != nil { 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 { return Config{}, err } @@ -269,6 +269,20 @@ func splitCSV(value string) []string { } 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) if err != nil { return nil, fmt.Errorf("read alma creds: %w", err) @@ -291,13 +305,6 @@ func loadAlmaCreds(filePath string) (map[string]string, error) { 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"] == "" { return nil, errors.New("alma creds file must contain username and password") } diff --git a/main_test.go b/main_test.go index f923ca5..9f3589c 100644 --- a/main_test.go +++ b/main_test.go @@ -1,6 +1,7 @@ package main import ( + "os" "strings" "testing" "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 { doc, err := html.Parse(strings.NewReader(text)) if err != nil {