-
Notifications
You must be signed in to change notification settings - Fork 0
feat(mentorship-sync): implement mentorship-sync CronJob with fixture source for DEV #141
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 36 commits
Commits
Show all changes
41 commits
Select commit
Hold shift + click to select a range
552e5fc
docs(mentorship-sync): add implementation plan
mlehotskylf 49a0cb1
feat(mentorship-sync): add MentorshipProgram domain model and reposit…
mlehotskylf 16ee1bc
feat(mentorship-sync): add FixtureSource for DEV environment testing
mlehotskylf 994e4e3
feat(mentorship-sync): add Snowflake client with mock-driver unit test
mlehotskylf 8f4016d
feat(mentorship-sync): add MentorshipRepository with upsert and benef…
mlehotskylf 5873635
feat(mentorship-sync): add fixture data file for DEV environment testing
mlehotskylf 5d10c50
feat(mentorship-sync): implement syncer and CronJob entry point
mlehotskylf 959f06d
feat(mentorship-sync): add Dockerfile for CronJob container image
mlehotskylf 0235cb3
fix(mentorship-sync): document FetchPrograms beneficiary limitation t…
mlehotskylf dc20ecb
fix(mentorship-sync): align Snowflake query with actual MENTORSHIP_PR…
mlehotskylf 7b48d07
feat(mentorship-sync): add Helm CronJob template and values
mlehotskylf 6241180
fix(review): address PR #141 review feedback
mlehotskylf 09cd611
fix(review): address PR #141 second-round review feedback
mlehotskylf 6af85a4
fix(review): address PR #141 third-round review feedback
mlehotskylf 2cd46b8
feat(mentorship-sync): add owner_lf_username from Snowflake gold model
mlehotskylf ea4cafa
Merge branch 'main' into feat/mentorship-sync
mlehotskylf 9ede94a
fix(mentorship-sync): address code review findings
mlehotskylf 466d83a
fix(review): address PR #141 fifth-round review feedback
mlehotskylf ca6d366
Merge branch 'main' into feat/mentorship-sync
mlehotskylf b154c89
feat(mentorship-sync): implement full sync from Snowflake to PostgreSQL
lewisojile 7b9842d
ci(checkov): fix helm chart rendering failures in MegaLinter
lewisojile 53e0227
Merge branch 'main' into feat/mentorship-sync
lewisojile 35dec7e
fix: address PR #141 review comments
lewisojile 16fa4aa
Merge branch 'main' into feat/mentorship-sync
lewisojile a5d241e
fix: add HEALTHCHECK NONE to mentorship-sync Dockerfile
lewisojile 4bf6e6f
fix: provide Checkov helm values to unblock chart rendering
lewisojile f4f836c
Potential fix for pull request finding
lewisojile bd68357
fix: remove unused UPDATED_AT column from Snowflake query
lewisojile c85110d
fix: remove invalid helm-values-file from .checkov.yml
lewisojile e5841a9
feat: filter Snowflake query to programs updated in last 30 days
lewisojile f799d90
feat: filter Snowflake query to programs updated in last 30 days
lewisojile 019b133
Merge branch 'main' into feat/mentorship-sync
lewisojile c7c0a3f
fix: include jobspring_project_id in upsert DO UPDATE clause
lewisojile 45967c6
feat: default initiative list endpoints to published status
lewisojile 5bf9682
fix(chart): require mentorshipSyncCronJob.image.tag when CronJob is e…
lewisojile f9633fb
fix(mentorship-sync): trim OwnerLFUsername before validation and upsert
lewisojile 73020b4
Potential fix for pull request finding
lewisojile b59165b
Potential fix for pull request finding
lewisojile 005979f
Potential fix for pull request finding
lewisojile da30a52
chore: merge main into feat/mentorship-sync, resolve go.sum conflict
lewisojile bca7d7c
feat(mentorship-sync): add owner profile fields from Snowflake
lewisojile File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| # Copyright The Linux Foundation and each contributor to LFX. | ||
| # SPDX-License-Identifier: MIT | ||
|
|
||
| # ── Build stage ────────────────────────────────────────────────────────────── | ||
| FROM golang:1.25-alpine AS builder | ||
|
|
||
| WORKDIR /build | ||
|
|
||
| # Cache dependencies separately from source code | ||
| COPY go.mod go.sum ./ | ||
| RUN go mod download | ||
|
|
||
| COPY . . | ||
| RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ | ||
| go build -trimpath -ldflags "-s -w" \ | ||
| -o /build/mentorship-sync ./cmd/mentorship-sync | ||
|
|
||
| # ── Final stage ─────────────────────────────────────────────────────────────── | ||
| FROM alpine:3.20 | ||
|
|
||
| # ca-certificates is required for outbound TLS (Auth0 JWKS, Stripe, Ledger, OTLP). | ||
| # Security: run as non-root user | ||
| RUN apk add --no-cache ca-certificates \ | ||
| && addgroup -S appgroup && adduser -S appuser -G appgroup -u 65532 | ||
|
|
||
| WORKDIR /app | ||
| COPY --from=builder /build/mentorship-sync ./mentorship-sync | ||
| COPY cmd/mentorship-sync/testdata/ /app/testdata/ | ||
|
|
||
| USER appuser | ||
|
|
||
| # This is a CronJob — it runs to completion and exits. Health checks are not | ||
| # applicable; HEALTHCHECK NONE explicitly satisfies the Checkov CKV_DOCKER_2 check. | ||
| HEALTHCHECK NONE | ||
|
|
||
| ENTRYPOINT ["/app/mentorship-sync"] |
14 changes: 14 additions & 0 deletions
14
backend/charts/lfx-crowdfunding-backend/ci/checkov-values.yaml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| # Copyright The Linux Foundation and each contributor to LFX. | ||
| # SPDX-License-Identifier: MIT | ||
| --- | ||
| # Checkov CI values — used by Checkov's helm runner to render the chart for | ||
| # static analysis. These values satisfy required guards in validate.yaml so | ||
| # that `helm template` can complete without aborting. They are never used in | ||
| # a real deployment; ArgoCD overrides all values via environment-specific | ||
| # values files and ExternalSecrets. | ||
|
|
||
| image: | ||
| tag: "ci-test" | ||
|
|
||
| config: | ||
| STRIPE_RETURN_URL: "https://crowdfunding.linuxfoundation.org/payment/complete" |
57 changes: 57 additions & 0 deletions
57
backend/charts/lfx-crowdfunding-backend/templates/cronjob-mentorship-sync.yaml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| # Copyright The Linux Foundation and each contributor to LFX. | ||
| # SPDX-License-Identifier: MIT | ||
| {{- if .Values.mentorshipSyncCronJob.enabled }} | ||
| apiVersion: batch/v1 | ||
| kind: CronJob | ||
| metadata: | ||
| name: {{ include "lfx-v2-initiatives-service.fullname" . }}-mentorship-sync | ||
| namespace: {{ .Release.Namespace }} | ||
| labels: | ||
| {{- include "lfx-v2-initiatives-service.labels" . | nindent 4 }} | ||
| app.kubernetes.io/component: mentorship-sync | ||
| spec: | ||
| schedule: {{ .Values.mentorshipSyncCronJob.schedule | quote }} | ||
| concurrencyPolicy: {{ .Values.mentorshipSyncCronJob.concurrencyPolicy }} | ||
| successfulJobsHistoryLimit: {{ .Values.mentorshipSyncCronJob.successfulJobsHistoryLimit }} | ||
| failedJobsHistoryLimit: {{ .Values.mentorshipSyncCronJob.failedJobsHistoryLimit }} | ||
| jobTemplate: | ||
| spec: | ||
| template: | ||
| metadata: | ||
| labels: | ||
| app.kubernetes.io/component: mentorship-sync | ||
| spec: | ||
| restartPolicy: OnFailure | ||
| serviceAccountName: {{ include "lfx-v2-initiatives-service.serviceAccountName" . }} | ||
| automountServiceAccountToken: false | ||
| {{- with .Values.imagePullSecrets }} | ||
| imagePullSecrets: | ||
| {{- toYaml . | nindent 12 }} | ||
| {{- end }} | ||
| securityContext: | ||
| {{- toYaml .Values.podSecurityContext | nindent 12 }} | ||
| containers: | ||
| - name: mentorship-sync | ||
| securityContext: | ||
| {{- toYaml .Values.securityContext | nindent 16 }} | ||
| image: "{{ .Values.mentorshipSyncCronJob.image.repository }}:{{ .Values.mentorshipSyncCronJob.image.tag }}" | ||
| imagePullPolicy: {{ .Values.image.pullPolicy }} | ||
| command: ["/app/mentorship-sync"] | ||
| {{- if or .Values.env .Values.mentorshipSyncCronJob.fixtureFile }} | ||
| env: | ||
| {{- with .Values.env }} | ||
| {{- toYaml . | nindent 16 }} | ||
| {{- end }} | ||
| {{- if .Values.mentorshipSyncCronJob.fixtureFile }} | ||
| - name: MENTORSHIP_SYNC_FIXTURE_FILE | ||
| value: {{ .Values.mentorshipSyncCronJob.fixtureFile | quote }} | ||
| {{- end }} | ||
| {{- end }} | ||
| envFrom: | ||
| - configMapRef: | ||
| name: {{ include "lfx-v2-initiatives-service.fullname" . }}-config | ||
| - secretRef: | ||
| name: {{ .Values.secretName }} | ||
| resources: | ||
|
lewisojile marked this conversation as resolved.
|
||
| {{- toYaml .Values.mentorshipSyncCronJob.resources | nindent 16 }} | ||
| {{- end }} | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| // Copyright The Linux Foundation and each contributor to LFX. | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| package main | ||
|
|
||
| import "log/slog" | ||
|
|
||
| func discardLogger() *slog.Logger { | ||
| return slog.New(slog.NewTextHandler(nopWriter{}, nil)) | ||
| } | ||
|
|
||
| type nopWriter struct{} | ||
|
|
||
| func (nopWriter) Write(p []byte) (int, error) { return len(p), nil } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| // Copyright The Linux Foundation and each contributor to LFX. | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| // mentorship-sync pulls Mentorship program data from Snowflake (or a JSON | ||
| // fixture in DEV) and upserts initiative_type=mentorship rows into CF Postgres. | ||
| // | ||
| // See backend/docs/rewrite/10-mentorship-sync-dev-testing.md for the DEV testing strategy. | ||
| // | ||
| // Usage: run as a K8s CronJob (daily schedule). Exits 0 on success, | ||
| // non-zero on any error. K8s uses the exit code to track CronJob health. | ||
| package main | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "log/slog" | ||
| "os" | ||
| "time" | ||
|
|
||
| "github.com/linuxfoundation/lfx-v2-initiatives-service/internal/infrastructure/db" | ||
| "github.com/linuxfoundation/lfx-v2-initiatives-service/internal/infrastructure/snowflake" | ||
| ) | ||
|
|
||
| func main() { | ||
| logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) | ||
|
|
||
| if err := run(logger); err != nil { | ||
| logger.Error("mentorship-sync failed", "error", err) | ||
| os.Exit(1) | ||
| } | ||
| } | ||
|
|
||
| func run(logger *slog.Logger) error { | ||
| ctx := context.Background() | ||
| start := time.Now() | ||
|
|
||
| cfg, err := loadConfig() | ||
| if err != nil { | ||
| return fmt.Errorf("config: %w", err) | ||
| } | ||
|
|
||
| pool, err := db.NewPool(ctx, db.PoolConfig{ | ||
| DSN: cfg.DatabaseURL, | ||
| MaxConns: 5, | ||
| MinConns: 1, | ||
| ConnMaxLifetime: cfg.DBConnMaxLifetime, | ||
| }) | ||
|
lewisojile marked this conversation as resolved.
|
||
| if err != nil { | ||
| return fmt.Errorf("database pool: %w", err) | ||
| } | ||
| defer pool.Close() | ||
|
|
||
| var src mentorshipSource | ||
| if cfg.FixtureFile != "" { | ||
| logger.Info("using fixture source", "path", cfg.FixtureFile) | ||
| src = snowflake.NewFixtureSource(cfg.FixtureFile) | ||
| } else { | ||
| client, err := snowflake.NewClient(snowflake.ClientConfig{ | ||
| Account: cfg.SnowflakeAccount, | ||
| User: cfg.SnowflakeUser, | ||
| Warehouse: cfg.SnowflakeWarehouse, | ||
| Database: cfg.SnowflakeDatabase, | ||
| Role: cfg.SnowflakeRole, | ||
| PrivateKey: cfg.SnowflakePrivateKey, | ||
| }) | ||
| if err != nil { | ||
| return fmt.Errorf("snowflake client: %w", err) | ||
| } | ||
| defer client.Close() | ||
| src = client | ||
| } | ||
|
|
||
| repo := db.NewMentorshipRepository(pool) | ||
| syncer := newSyncer(repo, src, logger) | ||
|
|
||
| logger.Info("mentorship-sync starting") | ||
|
|
||
| result, err := syncer.Run(ctx) | ||
| if err != nil { | ||
| return fmt.Errorf("sync run: %w", err) | ||
| } | ||
|
|
||
| logger.Info("mentorship-sync complete", | ||
| "duration", time.Since(start).String(), | ||
| "total", result.total, | ||
| "upserted", result.upserted, | ||
| "errors", result.errors, | ||
| ) | ||
| if result.errors > 0 { | ||
| return fmt.Errorf("sync completed with %d error(s) out of %d programs", result.errors, result.total) | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| type config struct { | ||
| DatabaseURL string | ||
| DBConnMaxLifetime time.Duration | ||
| FixtureFile string | ||
| SnowflakeAccount string | ||
| SnowflakeUser string | ||
| SnowflakeWarehouse string | ||
| SnowflakeDatabase string | ||
| SnowflakeRole string | ||
| SnowflakePrivateKey string | ||
| } | ||
|
|
||
| func loadConfig() (*config, error) { | ||
| dbURL := os.Getenv("DATABASE_URL") | ||
| if dbURL == "" { | ||
| return nil, fmt.Errorf("DATABASE_URL is required") | ||
| } | ||
|
|
||
| cfg := &config{ | ||
| DatabaseURL: dbURL, | ||
| DBConnMaxLifetime: 5 * time.Minute, | ||
| FixtureFile: os.Getenv("MENTORSHIP_SYNC_FIXTURE_FILE"), | ||
| } | ||
|
|
||
| if cfg.FixtureFile == "" { | ||
| cfg.SnowflakeAccount = os.Getenv("SNOWFLAKE_ACCOUNT") | ||
| if cfg.SnowflakeAccount == "" { | ||
| return nil, fmt.Errorf("SNOWFLAKE_ACCOUNT is required when MENTORSHIP_SYNC_FIXTURE_FILE is not set") | ||
| } | ||
| cfg.SnowflakeUser = os.Getenv("SNOWFLAKE_USER") | ||
| if cfg.SnowflakeUser == "" { | ||
| return nil, fmt.Errorf("SNOWFLAKE_USER is required when MENTORSHIP_SYNC_FIXTURE_FILE is not set") | ||
| } | ||
| cfg.SnowflakeWarehouse = os.Getenv("SNOWFLAKE_WAREHOUSE") | ||
| if cfg.SnowflakeWarehouse == "" { | ||
| return nil, fmt.Errorf("SNOWFLAKE_WAREHOUSE is required when MENTORSHIP_SYNC_FIXTURE_FILE is not set") | ||
| } | ||
| cfg.SnowflakeDatabase = os.Getenv("SNOWFLAKE_DATABASE") | ||
| if cfg.SnowflakeDatabase == "" { | ||
| return nil, fmt.Errorf("SNOWFLAKE_DATABASE is required when MENTORSHIP_SYNC_FIXTURE_FILE is not set") | ||
| } | ||
| cfg.SnowflakeRole = os.Getenv("SNOWFLAKE_ROLE") | ||
| if cfg.SnowflakeRole == "" { | ||
| return nil, fmt.Errorf("SNOWFLAKE_ROLE is required when MENTORSHIP_SYNC_FIXTURE_FILE is not set") | ||
| } | ||
| cfg.SnowflakePrivateKey = os.Getenv("SNOWFLAKE_PRIVATE_KEY") | ||
| if cfg.SnowflakePrivateKey == "" { | ||
| return nil, fmt.Errorf("SNOWFLAKE_PRIVATE_KEY is required when MENTORSHIP_SYNC_FIXTURE_FILE is not set") | ||
| } | ||
| } | ||
|
|
||
| return cfg, nil | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.