-
Notifications
You must be signed in to change notification settings - Fork 0
DOCSP-55611: Add Kanopy cronjob deployment for GitHub metrics collection #7
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
Changes from 2 commits
70c4b21
d8c121c
2c6bd43
a90dcae
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| --- | ||
| kind: pipeline | ||
| type: kubernetes | ||
| name: github-metrics | ||
|
|
||
| trigger: | ||
| branch: | ||
| - main | ||
| event: | ||
| - push | ||
|
|
||
| steps: | ||
| - name: check-changes | ||
| image: alpine/git | ||
| commands: | ||
| - | | ||
| # Check if any files in github-metrics/ directory changed | ||
| git diff --name-only $DRONE_COMMIT_BEFORE $DRONE_COMMIT_AFTER | grep -q "^github-metrics/" && echo "Changes detected" || (echo "No changes in github-metrics/, skipping" && exit 78) | ||
|
|
||
| - name: test | ||
| image: node:20-alpine | ||
| commands: | ||
| - cd github-metrics | ||
| - npm ci | ||
| - node --version | ||
| - npm --version | ||
| - echo "Validating package.json and dependencies..." | ||
|
|
||
| - name: publish | ||
| image: plugins/kaniko-ecr | ||
| settings: | ||
| create_repository: true | ||
| registry: 795250896452.dkr.ecr.us-east-1.amazonaws.com | ||
| repo: docs/github-metrics | ||
| tags: | ||
| - git-${DRONE_COMMIT_SHA:0:7} | ||
| - latest | ||
| access_key: | ||
| from_secret: ecr_access_key | ||
| secret_key: | ||
| from_secret: ecr_secret_key | ||
| context: github-metrics | ||
| dockerfile: github-metrics/Dockerfile | ||
|
|
||
| - name: deploy | ||
| image: quay.io/mongodb/drone-helm:v3 | ||
| settings: | ||
| chart: mongodb/cronjobs | ||
| chart_version: 1.21.2 | ||
| add_repos: [ mongodb=https://10gen.github.io/helm-charts ] | ||
| namespace: docs | ||
| release: github-metrics | ||
| values: image.tag=git-${DRONE_COMMIT_SHA:0:7},image.repository=795250896452.dkr.ecr.us-east-1.amazonaws.com/docs/github-metrics | ||
| values_files: [ 'github-metrics/cronjobs.yml' ] | ||
| api_server: https://api.prod.corp.mongodb.com | ||
| kubernetes_token: | ||
| from_secret: kubernetes_token |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| FROM node:20-alpine | ||
|
|
||
| # Set working directory | ||
| WORKDIR /app | ||
|
|
||
| # Copy package files first (for better Docker layer caching) | ||
| COPY package.json package-lock.json ./ | ||
|
|
||
| # Install dependencies (use ci for reproducible builds) | ||
| RUN npm ci --only=production | ||
|
|
||
| # Copy the rest of the application files | ||
| COPY . . | ||
|
|
||
| # Create a non-root user for security best practices | ||
| RUN addgroup -g 1001 -S nodejs && \ | ||
| adduser -S nodejs -u 1001 && \ | ||
| chown -R nodejs:nodejs /app | ||
|
|
||
| # Switch to non-root user | ||
| USER nodejs | ||
|
|
||
| # Set NODE_ENV to production | ||
| ENV NODE_ENV=production | ||
|
|
||
| # Command to run the application | ||
| # This will be executed by the Kubernetes CronJob | ||
| CMD ["node", "index.js"] | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| import fs from 'fs'; | ||
| import path from 'path'; | ||
|
|
||
| // Path to the state file (mounted from persistent volume) | ||
| const STATE_FILE_PATH = process.env.STATE_FILE_PATH || '/data/last-run.json'; | ||
|
|
||
| // Minimum days between runs | ||
| const MIN_DAYS_BETWEEN_RUNS = parseInt(process.env.MIN_DAYS_BETWEEN_RUNS || '14', 10); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we make this 13, instead? I'm wondering if any possibility of weirdness related to timing of test runs, etc. If it's only running on Monday and it's been close to 14 it should probably run. |
||
|
|
||
| /** | ||
| * Check if enough time has passed since the last run | ||
| * @returns {boolean} true if should run, false if should skip | ||
| */ | ||
| export function shouldRun() { | ||
| try { | ||
| // Check if state file exists | ||
| if (!fs.existsSync(STATE_FILE_PATH)) { | ||
| console.log('No previous run found. Running for the first time.'); | ||
| return true; | ||
| } | ||
|
|
||
| // Read the last run timestamp | ||
| const stateData = JSON.parse(fs.readFileSync(STATE_FILE_PATH, 'utf8')); | ||
| const lastRunTime = new Date(stateData.lastRun); | ||
| const now = new Date(); | ||
|
|
||
| // Calculate days since last run | ||
| const daysSinceLastRun = (now - lastRunTime) / (1000 * 60 * 60 * 24); | ||
|
|
||
| console.log(`Last run: ${lastRunTime.toISOString()}`); | ||
| console.log(`Days since last run: ${daysSinceLastRun.toFixed(2)}`); | ||
| console.log(`Minimum days required: ${MIN_DAYS_BETWEEN_RUNS}`); | ||
|
|
||
| if (daysSinceLastRun < MIN_DAYS_BETWEEN_RUNS) { | ||
| console.log(`⏭️ Skipping run - only ${daysSinceLastRun.toFixed(2)} days since last run (need ${MIN_DAYS_BETWEEN_RUNS})`); | ||
| return false; | ||
| } | ||
|
|
||
| console.log(`✅ Proceeding with run - ${daysSinceLastRun.toFixed(2)} days since last run`); | ||
| return true; | ||
|
|
||
| } catch (error) { | ||
| console.error('Error checking last run time:', error.message); | ||
| console.log('Proceeding with run due to error reading state file'); | ||
| return true; // Run if we can't read the state file | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Update the state file with the current timestamp | ||
| */ | ||
| export function updateLastRun() { | ||
| try { | ||
| const now = new Date(); | ||
| const stateData = { | ||
| lastRun: now.toISOString(), | ||
| timestamp: now.getTime() | ||
| }; | ||
|
|
||
| // Ensure the directory exists | ||
| const dir = path.dirname(STATE_FILE_PATH); | ||
| if (!fs.existsSync(dir)) { | ||
| fs.mkdirSync(dir, { recursive: true }); | ||
| } | ||
|
|
||
| // Write the state file | ||
| fs.writeFileSync(STATE_FILE_PATH, JSON.stringify(stateData, null, 2), 'utf8'); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't know if there's any possibility of weirdness if the file already exists. Have you tested this manually with an extant file to ensure the behavior is correct - i.e. overwrites file contents? I'm wondering if we should remove the old file during the update func just for safety, or if that's overkill.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it seems like overkill, esp for this tiny doc. @krollins-mdb does this align with common practice around
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The method completely replaces the file if it already exists, so it should be fine. I am curious about using the sync version here instead of the default async
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thanks for catching. I'll update |
||
| console.log(`✅ Updated last run timestamp: ${now.toISOString()}`); | ||
|
|
||
| } catch (error) { | ||
| console.error('Error updating last run time:', error.message); | ||
| // Don't throw - we don't want to fail the job just because we can't write the state file | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Get the last run information | ||
| * @returns {Object|null} Object with lastRun date and timestamp, or null if no previous run | ||
| */ | ||
| export function getLastRunInfo() { | ||
|
cbullinger marked this conversation as resolved.
Outdated
|
||
| try { | ||
| if (!fs.existsSync(STATE_FILE_PATH)) { | ||
| return null; | ||
| } | ||
|
|
||
| const stateData = JSON.parse(fs.readFileSync(STATE_FILE_PATH, 'utf8')); | ||
| return { | ||
| lastRun: new Date(stateData.lastRun), | ||
| timestamp: stateData.timestamp | ||
| }; | ||
|
|
||
| } catch (error) { | ||
| console.error('Error reading last run info:', error.message); | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| export { MIN_DAYS_BETWEEN_RUNS }; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| --- | ||
| # `image` can be skipped if the values are being set in your .drone.yml file | ||
| image: | ||
| repository: 795250896452.dkr.ecr.us-east-1.amazonaws.com/docs/github-metrics | ||
| tag: latest | ||
|
|
||
| # global secrets are references to k8s Secrets | ||
| globalEnvSecrets: | ||
| GITHUB_TOKEN: github-token | ||
| ATLAS_CONNECTION_STRING: atlas-connection-string | ||
|
|
||
| cronJobs: | ||
| - name: github-metrics-collection | ||
| # Run weekly on Mondays at 8am UTC | ||
| # The application checks if it ran in the last 14 days and skips if so | ||
| # Cron format: minute hour day-of-month month day-of-week | ||
| # 0 = Sunday, 1 = Monday, etc. | ||
| schedule: "0 8 * * 1" | ||
| command: | ||
| - node | ||
| - index.js | ||
| resources: | ||
| requests: | ||
| cpu: 100m | ||
| memory: 256Mi | ||
| limits: | ||
| cpu: 500m | ||
| memory: 512Mi | ||
| # Persistent volume to store last run timestamp | ||
| persistence: | ||
| enabled: true | ||
| storageClass: "standard" | ||
| accessMode: ReadWriteOnce | ||
| size: 1Gi | ||
| mountPath: /data |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, it looks like we're trying to read from
process.envhere but I don't see where we're setting these vars in the env setup?