Skip to content

Commit

Permalink
Merge pull request #315 from cioos-siooc/309-improve-env-config
Browse files Browse the repository at this point in the history
309 Enhancement of Environment Variable Handling
  • Loading branch information
sorochak authored Feb 8, 2024
2 parents 625e639 + a7b0d9e commit ac17aee
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 15 deletions.
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

0 comments on commit ac17aee

Please sign in to comment.