Skip to content
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

309 Enhancement of Environment Variable Handling #315

Merged
merged 18 commits into from
Feb 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions .github/workflows/firebase-deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ jobs:
- name: Install Dependencies
run: npm ci
working-directory: firebase-functions/functions
- name: Create .env file
run: |
echo "GMAIL_USER=${{ secrets.GMAIL_USER }}" >> .env
echo "GMAIL_PASS=${{ secrets.GMAIL_PASS }}" >> .env
echo "DATACITE_AUTH_HASH=${{ secrets.DATACITE_AUTH_HASH }}" >> .env
echo "AWS_REGION=${{ secrets.AWS_REGION }}" >> .env
echo "AWS_ACCESSKEYID=${{ secrets.AWS_ACCESSKEYID }}" >> .env
echo "AWS_SECRETACCESSKEY=${{ secrets.AWS_SECRETACCESSKEY }}" >> .env
echo "GITHUB_AUTH=${{ secrets.ISSUE_CREATOR_PAT }}" >> .env
working-directory: firebase-functions/functions
- name: Deploy to Firebase
uses: w9jds/firebase-action@master
with:
Expand All @@ -28,8 +38,7 @@ jobs:
FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}
GMAIL_USER: ${{ secrets.GMAIL_USER }}
GMAIL_PASS: ${{ secrets.GMAIL_PASS }}
DATACITE_USER: ${{ secrets.DATACITE_USER }}
DATACITE_PASS: ${{ secrets.DATACITE_PASS }}
DATACITE_AUTH_HASH: ${{ secrets.DATACITE_AUTH_HASH }}
AWS_REGION: ${{ secrets.AWS_REGION }}
AWS_ACCESSKEYID: ${{ secrets.AWS_ACCESSKEYID }}
AWS_SECRETACCESSKEY: ${{ secrets.AWS_SECRETACCESSKEY }}
Expand Down
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,54 @@ Pushes to master automatically deploy to <https://cioos-siooc.github.io/metadata
Or manually deploy any branch with

`npm run deploy`

## Deployment and Configuration of Firebase Functions

Our Firebase Functions infrastructure utilizes both GitHub Actions for automated and manual deployments and parameterized configuration for management of environment variables and credentials.

### Automated and Manual Deployment with GitHub Actions

We use a GitHub Actions workflow named `firebase-deploy` for deploying Firebase Functions. This workflow is triggered automatically on push to the main branch but can also be executed manually for feature branches.

#### Workflow Features

- **Automated Deployments on Push to Main**: Ensures that any changes merged into the main branch are automatically deployed to Firebase.
- **Manual Deployment Option**: Allows for manual deployments of specific branches, useful for testing changes in feature branches.
- **Environment Variables and Secrets**: Uses GitHub Secrets to populate a virtual `.env` file with necessary configurations for the deployment process.

#### Manual Deployment Steps

1. Go to the "Actions" tab in the GitHub repository.
2. Select the `firebase-deploy` workflow.
3. Click "Run workflow", select the branch to deploy, and initiate the workflow.

#### GitHub Secrets and .env File Creation

The workflow utilizes the following secrets to create the virtual `.env` file for the deployment process:

- `GMAIL_USER`
- `GMAIL_PASS`
- `DATACITE_AUTH_HASH`
- `AWS_REGION`
- `AWS_ACCESSKEYID`
- `AWS_SECRETACCESSKEY`
- `GITHUB_AUTH`

### Using Parameterized Configuration in Firebase Functions

Our Firebase Functions leverage parameterized configuration for managing sensitive information. This helps prevent functions from being deployed with missing configurations/credentials.

For details on defining and accessing these parameters, refer to the [official Firebase documentation](https://firebase.google.com/docs/functions/config-env?gen=1st).

#### Deployment Considerations with Parameters

- **Local Development**: Use a `.env` file within the `functions` directory for local development, mirroring the setup of parameters used in production environments.
- **Firebase CLI Prompt**: The CLI may prompt for parameter values during deployment if they are not preset, ensuring functions are correctly configured.
- **Firebase Console Management**: Parameters can also be managed within the Firebase Console.

### Security Considerations

The use of GitHub Secrets and the creation of a virtual `.env` file during the workflow run ensures that sensitive information is handled securely, without persisting in the repository or exposing it beyond the lifecycle of the workflow execution.

- **Exclude `.env` Files from Version Control**: Ensure `.env` files are not included in version control to prevent exposure of sensitive data.
- **Temporary `.env` Files**: The `.env` file created during the GitHub Actions workflow is virtual and transient. It exists only for the duration of the workflow run and is not committed to the repository.
25 changes: 20 additions & 5 deletions firebase-functions/functions/datacite.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
const baseUrl = "https://api.datacite.org/dois/";
const { DATACITE_AUTH_HASH } = process.env;
const functions = require("firebase-functions");
const { defineString } = require('firebase-functions/params');
const axios = require("axios");

const dataciteAuthHash = defineString('DATACITE_AUTH_HASH');

exports.createDraftDoi = functions.https.onCall(async (record) => {
// Fallback to process.env.DATACITE_AUTH_HASH for local dev or deployment-specific configurations,
// otherwise use Firebase's parameterized configuration at runtime.
const dataciteCred = process.env.DATACITE_AUTH_HASH || dataciteAuthHash.value()

try{
const url = `${baseUrl}`;
const response = await axios.post(url, record, {
headers: {
'Authorization': `Basic ${DATACITE_AUTH_HASH}`,
'Authorization': `Basic ${dataciteCred}`,
'Content-Type': 'application/json',
},
});
Expand Down Expand Up @@ -47,11 +53,14 @@ exports.createDraftDoi = functions.https.onCall(async (record) => {
});

exports.updateDraftDoi = functions.https.onCall(async (data) => {

const dataciteCred = process.env.DATACITE_AUTH_HASH || dataciteAuthHash.value()

try {
const url = `${baseUrl}${data.doi}/`;
const response = await axios.put(url, data.data, {
headers: {
'Authorization': `Basic ${DATACITE_AUTH_HASH}`,
'Authorization': `Basic ${dataciteCred}`,
'Content-Type': "application/json",
},
});
Expand Down Expand Up @@ -93,10 +102,13 @@ exports.updateDraftDoi = functions.https.onCall(async (data) => {
});

exports.deleteDraftDoi = functions.https.onCall(async (draftDoi) => {

const dataciteCred = process.env.DATACITE_AUTH_HASH || dataciteAuthHash.value()

try {
const url = `${baseUrl}${draftDoi}/`;
const response = await axios.delete(url, {
headers: { 'Authorization': `Basic ${DATACITE_AUTH_HASH}` },
headers: { 'Authorization': `Basic ${dataciteCred}` },
});
return response.status;
} catch (err) {
Expand Down Expand Up @@ -131,12 +143,15 @@ exports.deleteDraftDoi = functions.https.onCall(async (draftDoi) => {
});

exports.getDoiStatus = functions.https.onCall(async (data) => {

const dataciteCred = process.env.DATACITE_AUTH_HASH || dataciteAuthHash.value()

try {
const url = `${baseUrl}${data.doi}/`;
// TODO: limit response to just the state field. elasticsearch query syntax?
const response = await axios.get(url, {
headers: {
'Authorization': `Basic ${DATACITE_AUTH_HASH}`
'Authorization': `Basic ${dataciteCred}`
},
});
return response.data.data.attributes.state;
Expand Down
7 changes: 5 additions & 2 deletions firebase-functions/functions/issue.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
const { Octokit } = require("octokit");
const fs = require("fs");
const { defineString } = require('firebase-functions/params');

const githubAuth = defineString('GITHUB_AUTH');
const githubAuthCred = process.env.GITHUB_AUTH || githubAuth.value()

const { GITHUB_AUTH } = process.env;
function readIssueText(filename) {
try {
return fs.readFileSync(filename, "utf8");
Expand All @@ -13,7 +16,7 @@ function readIssueText(filename) {

async function createIssue(title, url) {
const octokit = new Octokit({
auth: GITHUB_AUTH,
auth: githubAuthCred,
});
const issueText = readIssueText("dataset-name.md");
const input = {
Expand Down
9 changes: 7 additions & 2 deletions firebase-functions/functions/notify.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
const functions = require("firebase-functions");
const admin = require("firebase-admin");
const { defineString } = require('firebase-functions/params');
const nodemailer = require("nodemailer");
const { mailOptionsReviewer, mailOptionsAuthor } = require("./mailoutText");
const createIssue = require("./issue");

/**
* Here we're using Gmail to send
*/
const { GMAIL_USER, GMAIL_PASS } = process.env;
const gmailUser = defineString('GMAIL_USER');
const gmailPass = defineString('GMAIL_PASS');

const gmailUserCred = process.env.GMAIL_USER || gmailUser.value()
const gmailPassCred = process.env.GMAIL_PASS || gmailPass.value()

const transporter = nodemailer.createTransport({
service: "gmail",
auth: { user: GMAIL_USER, pass: GMAIL_PASS },
auth: { user: gmailUserCred, pass: gmailPassCred },
});
/*
Email the reviewers for the region when a form is submitted for review
Expand Down
15 changes: 11 additions & 4 deletions firebase-functions/functions/translate.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
const functions = require("firebase-functions");
const { defineString } = require('firebase-functions/params');
const AWS = require("aws-sdk");

const { AWS_REGION, AWS_ACCESSKEYID, AWS_SECRETACCESSKEY } = process.env;
const awsRegion = defineString('AWS_REGION');
const awsAccessKeyId = defineString('AWS_ACCESSKEYID');
const awsSecretAccessKey = defineString('AWS_SECRETACCESSKEY');

const awsRegionCred = process.env.AWS_REGION || awsRegion.value()
const awsAccessKeyIdCred = process.env.AWS_ACCESSKEYID || awsAccessKeyId.value()
const awsSecretAccessKeyCred = process.env.AWS_SECRETACCESSKEY || awsSecretAccessKey.value()

const awsAuth = {
region: AWS_REGION,
accessKeyId: AWS_ACCESSKEYID,
secretAccessKey: AWS_SECRETACCESSKEY,
region: awsRegionCred,
accessKeyId: awsAccessKeyIdCred,
secretAccessKey: awsSecretAccessKeyCred,
};

AWS.config = new AWS.Config(awsAuth);
Expand Down
Loading