Setup environment vars for staging and production (#99)

* CEC-371 Car ECU display (#79)

* Merge Development (#53)

* Use responsive iframe control for charts (#49)

* Use responsive iframe control to charts

* Move external Grafana link to Dashboard page

* Remove unused embedded style class

* Add button label

* added delete button to deploy packages

* Fix unit test warning
Remove unused route from test

* Fix styling of button

* minor fixes per pr review

Co-authored-by: jcw-fisker <jwatson@fiskerinc.com>
Co-authored-by: John Cotten Watson <83605808+jcw-fisker@users.noreply.github.com>

* Development Merge (#57)

* CEC-287 Car connection status (#59) (#60)

* Car connection status

* Formatting

* Merge Development (#64)

* Add connection status to vehicles page

* ConnectedIcon control

* Handle Style

* Development (#67)

* preliminary map for vehicles

* weird zoom bug

* passing react tests

* fixing warnings and updating snapshots

* update node environment to 14

* addressing comments by changing variable types and adding styles to home page title

* adding CODEOWNERS file

* fixing token error

* CEC-371 Update car ECUs display (#78)

* Clean up className styles
Update car status page to show update and ECUs

* Add update ecu version button
Show all ECUs on car status page
Only show car ecus for search

Co-authored-by: jcw-fisker <jwatson@fiskerinc.com>
Co-authored-by: John Cotten Watson <83605808+jcw-fisker@users.noreply.github.com>
Co-authored-by: Drew Taylor <69828061+drew-fisker@users.noreply.github.com>

* CEC-394 Car update log (#81)

* CEC-394 Car update status control

* Remove Datadog RUM
Remove package update components
Move control components into Controls folder
Add Car update status page

* Display update status log
Clean up unused update package code

* Remove console.logs

* no vars

* adding timestamp to vehicle popup

* modifying vehicle data query

* removing extraneous code

* removing console log

* Clean up SonarCloud warnings (#83)

* Clean up SonarCloud warnings

* Bogus security warning

* Fix another warning

* Fix unauthorized locations request

* Fix update progress control

* CEC-563 New manifest format (#88)

* Add ManifestCreateContext
Update create manifest page

* Finish UI changes and API integration

* Fixes

* Fix test

* Remove manifest ECU file version and type

* Fixes

* Add manifest ecu file type control

* Fix Sonar warnings

* Fix test

* Update codeowners

* Formatting

* CEC-553 Change file type to string (#90)

* CEC-553 File type uses string enum

* Fix test timeout

* Fix

* Clean up (#95)

* Clean up
Mock missing methods

* Smell

* Setup environment vars

* fix

* Load env file
Remove env var default values

* Dockerfile requires environment value
Do not need dev Dockerfile

* Github Actions pipeline + git flow (#100)

* test workflow

* oops

* latest slack action for custom message

* this works

Co-authored-by: jcw-fisker <jwatson@fiskerinc.com>
Co-authored-by: John Cotten Watson <83605808+jcw-fisker@users.noreply.github.com>
Co-authored-by: Drew Taylor <69828061+drew-fisker@users.noreply.github.com>
Co-authored-by: Drew Taylor <dtaylor@fiskerinc.com>
Co-authored-by: Rafi Greenberg <72412693+rafi-fisker@users.noreply.github.com>
This commit is contained in:
John Wu
2021-11-03 17:50:17 -07:00
committed by GitHub
parent ec3a31a15a
commit 16c3e2902b
21 changed files with 249 additions and 171 deletions

3
.env.dev Normal file
View File

@@ -0,0 +1,3 @@
REACT_APP_AUTH_SERVICE_URL=https://gw-dev.fiskerdps.com/compute_auth
REACT_APP_UPLOAD_SERVICE_URL=https://gw-dev.fiskerdps.com/ota_update
REACT_APP_AUTH_CALLBACK_URL=https://dev-ota-admin.fiskerdps.com

3
.env.local Normal file
View File

@@ -0,0 +1,3 @@
REACT_APP_AUTH_SERVICE_URL=http://localhost/compute_auth
REACT_APP_UPLOAD_SERVICE_URL=http://localhost/ota_update
REACT_APP_AUTH_CALLBACK_URL=http://localhost:3000

3
.env.prd Normal file
View File

@@ -0,0 +1,3 @@
REACT_APP_AUTH_SERVICE_URL=https://gw.fiskerdps.com/compute_auth
REACT_APP_UPLOAD_SERVICE_URL=https://gw.fiskerdps.com/ota_update
REACT_APP_AUTH_CALLBACK_URL=https://ota-admin.fiskerdps.com

3
.env.stg Normal file
View File

@@ -0,0 +1,3 @@
REACT_APP_AUTH_SERVICE_URL=https://gw-stg.fiskerdps.com/compute_auth
REACT_APP_UPLOAD_SERVICE_URL=https://gw-stg.fiskerdps.com/ota_update
REACT_APP_AUTH_CALLBACK_URL=https://stg-ota-admin.fiskerdps.com

72
.github/workflows/deploy.yml vendored Normal file
View File

@@ -0,0 +1,72 @@
on:
push:
branches:
- develop
- main
- 'release/**'
- 'hotfix/**'
jobs:
deploy:
name: Deploy
runs-on: self-hosted
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
TAG: ${{ github.sha }}
PROJECT: ota-admin-portal
steps:
- name: Slack Notify
uses: act10ns/slack@master
with:
channel: "#cloud-builds"
status: starting
- name: Checkout
uses: actions/checkout@v2
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-region: us-west-2
- name: Create ECR Repo
run: aws ecr create-repository --region us-west-2 --repository-name ${PROJECT} || true
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
- name: Set Env
run: |
case ${GITHUB_REF} in
refs/heads/develop)
ENVIRONMENT=dev;;
refs/heads/release/*)
ENVIRONMENT=stg;;
refs/heads/hotfix/*)
ENVIRONMENT=stg;;
refs/heads/main)
ENVIRONMENT=prd;;
*)
ENVIRONMENT=dev;;
esac
echo "ENVIRONMENT=${ENVIRONMENT}" >> $GITHUB_ENV
- name: Build, tag, and push image to Amazon ECR
id: build-tag-push-image
env:
REGISTRY: ${{ steps.login-ecr.outputs.registry }}
run: |
docker build --build-arg ENVIRONMENT=$ENVIRONMENT -t $REGISTRY/$PROJECT:$TAG-$ENVIRONMENT .
docker push $REGISTRY/$PROJECT:$TAG-$ENVIRONMENT
- name: Deploy
id: deploy
env:
REGISTRY: ${{ steps.login-ecr.outputs.registry }}
run: |-
helm upgrade \
--kube-context $ENVIRONMENT \
--set image.registry=$REGISTRY \
--set image.name=$PROJECT \
--set image.tag=$TAG-$ENVIRONMENT \
--wait -i -f k8s/values-$ENVIRONMENT.yaml $PROJECT k8s/
- uses: act10ns/slack@master
with:
channel: "#cloud-builds"
status: ${{ job.status }}
message: Successfully deployed to ${{ env.ENVIRONMENT }}!
if: always()

View File

@@ -1,9 +1,11 @@
FROM node:14-alpine as builder
ARG ENVIRONMENT
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
RUN npm run build:${ENVIRONMENT}
FROM nginx:alpine

View File

@@ -1,12 +0,0 @@
FROM node:14-alpine as builder
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder build /usr/share/nginx/html
COPY .env /usr/share/nginx/html/.env
COPY nginx.conf /etc/nginx/conf.d/default.conf

81
Jenkinsfile vendored
View File

@@ -1,81 +0,0 @@
@Library('fisker') _
pipeline {
agent none
options {
ansiColor('xterm')
}
environment {
PROJECT = getProject()
ENV = getEnv()
}
stages {
stage('Build') {
when {
beforeAgent true
allOf {
not {
changeRequest()
}
anyOf {
branch 'development'
branch 'main'
}
}
}
agent {
kubernetes {
cloud 'dev'
inheritFrom 'fisker'
}
}
steps {
slack("Build Started - ${env.JOB_NAME} (${env.BUILD_URL})", 'info', '#team-eng-compute-jenkins')
slack(getChanges(), 'info', '#team-eng-compute-jenkins')
container('awscli') {
ecr()
}
container('kaniko') {
buildImage()
}
}
post {
failure {
slack("${env.JOB_NAME} build failed!", 'error', '#team-eng-compute-jenkins')
}
}
}
stage('Deploy') {
when {
beforeAgent true
allOf {
not {
changeRequest()
}
anyOf {
branch 'development'
branch 'main'
}
}
}
agent {
kubernetes {
cloud getEnv()
inheritFrom 'fisker'
}
}
steps {
slack("Deploying ${PROJECT} to ${ENV}... :partydeploy: ", 'info', '#team-eng-compute-jenkins')
container('helm') {
deploy(getEnv())
}
slack("Successfully deployed ${PROJECT} to ${ENV}! :tada: ", 'info', '#team-eng-compute-jenkins')
}
post {
failure {
slack("${PROJECT} deploy to ${ENV} failed!", 'error', '#team-eng-compute-jenkins')
}
}
}
}
}

View File

@@ -15,7 +15,7 @@ Running locally
Running Docker container
1. Copy .env.template to .env and edit the service urls for authentication and api services
2. Build the image `docker build -t fiskerinc/portal --file Dockerfile.dev .`
2. Build the image `docker build --build-arg ENVIRONMENT=local -t fiskerinc/portal .`
3. Start the container `docker run -p 3000:80 fiskerinc/portal`
4. Access portal at localhost:3000

View File

@@ -9,4 +9,4 @@ resources:
cpu: 250m
memory: 256Mi
replicas: 1
replicas: 1

View File

@@ -9,4 +9,4 @@ resources:
cpu: 250m
memory: 256Mi
replicas: 1
replicas: 1

12
k8s/values-stg.yaml Normal file
View File

@@ -0,0 +1,12 @@
ingress:
hostname: stg-ota-admin.fiskerdps.com
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 250m
memory: 256Mi
replicas: 1

53
package-lock.json generated
View File

@@ -3906,9 +3906,9 @@
}
},
"caniuse-lite": {
"version": "1.0.30001223",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001223.tgz",
"integrity": "sha512-k/RYs6zc/fjbxTjaWZemeSmOjO0JJV+KguOBA3NwPup8uzxM1cMhR2BD9XmO86GuqaqTCO8CgkgH9Rz//vdDiA=="
"version": "1.0.30001275",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001275.tgz",
"integrity": "sha512-ihJVvj8RX0kn9GgP43HKhb5q9s2XQn4nEQhdldEJvZhCsuiB2XOq6fAMYQZaN6FPWfsr2qU0cdL0CSbETwbJAg=="
},
"capture-exit": {
"version": "2.0.0",
@@ -5406,6 +5406,53 @@
"resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
"integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="
},
"env-cmd": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/env-cmd/-/env-cmd-10.1.0.tgz",
"integrity": "sha512-mMdWTT9XKN7yNth/6N6g2GuKuJTsKMDHlQFUDacb/heQRRWOTIZ42t1rMHnQu4jYxU1ajdTeJM+9eEETlqToMA==",
"requires": {
"commander": "^4.0.0",
"cross-spawn": "^7.0.0"
},
"dependencies": {
"cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"requires": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
}
},
"path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="
},
"shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"requires": {
"shebang-regex": "^3.0.0"
}
},
"shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
},
"which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"requires": {
"isexe": "^2.0.0"
}
}
}
},
"errno": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz",

View File

@@ -11,6 +11,7 @@
"@testing-library/user-event": "^13.2.1",
"axios": "^0.21.1",
"clsx": "^1.1.1",
"env-cmd": "^10.1.0",
"leaflet": "^1.7.1",
"material-ui-dropzone": "^3.5.0",
"react": "^17.0.2",
@@ -21,9 +22,13 @@
"web-vitals": "^2.1.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"start": "env-cmd -f .env.local react-scripts start",
"build": "env-cmd -f .env.local react-scripts build",
"build:dev": "env-cmd -f .env.dev react-scripts build",
"build:stg": "env-cmd -f .env.stg react-scripts build",
"build:prd": "env-cmd -f .env.prd react-scripts build",
"build:local": "env-cmd -f .env.local react-scripts build",
"test": "env-cmd -f .env.local react-scripts test",
"test:debug": "react-scripts --inspect-brk test --runInBand --no-cache",
"eject": "react-scripts eject"
},

View File

@@ -1,5 +1,5 @@
const AUTH_URL = process.env.REACT_APP_AUTH_SERVICE_URL || "https://gw-dev.fiskerdps.com/compute_auth";
const CALLBACK_URL = process.env.REACT_APP_AUTH_CALLBACK_URL || "";
const AUTH_URL = process.env.REACT_APP_AUTH_SERVICE_URL;
const CALLBACK_URL = process.env.REACT_APP_AUTH_CALLBACK_URL;
let signInResponse = {};
let verifyResponse = {};
@@ -15,7 +15,13 @@ export default {
signIn: async (username, password) => logResponse(signInResponse),
verify: async (idToken) => logResponse(verifyResponse),
refresh: async (refreshToken) => logResponse(refreshResponse),
setSignInResponse: (value) => { signInResponse = value; },
setVerifyResponse: (value) => { verifyResponse = value; },
setRefreshResponse: (value) => { refreshResponse = value; },
}
setSignInResponse: (value) => {
signInResponse = value;
},
setVerifyResponse: (value) => {
verifyResponse = value;
},
setRefreshResponse: (value) => {
refreshResponse = value;
},
};

View File

@@ -1,37 +1,40 @@
import { fetchRespHandler } from "../utils/http";
const AUTH_URL = process.env.REACT_APP_AUTH_SERVICE_URL || "https://gw-dev.fiskerdps.com/compute_auth";
const CALLBACK_URL = process.env.REACT_APP_AUTH_CALLBACK_URL || "https://dev-ota-admin.fiskerdps.com";
const AUTH_URL = process.env.REACT_APP_AUTH_SERVICE_URL;
const CALLBACK_URL = process.env.REACT_APP_AUTH_CALLBACK_URL;
const auth = {
ssoAuthorize: () => `${AUTH_URL}/authorize?redirect=${CALLBACK_URL}`,
ssoLogout: () => `${AUTH_URL}/logout?redirect=${CALLBACK_URL}`,
signIn: (code) => fetch(`${AUTH_URL}/token`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
code,
redirect: CALLBACK_URL,
})
}).then(fetchRespHandler),
signIn: (code) =>
fetch(`${AUTH_URL}/token`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
code,
redirect: CALLBACK_URL,
}),
}).then(fetchRespHandler),
verify: (idToken) => fetch(`${AUTH_URL}/verify`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ token: idToken })
}).then(fetchRespHandler),
verify: (idToken) =>
fetch(`${AUTH_URL}/verify`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ token: idToken }),
}).then(fetchRespHandler),
refresh: (refreshToken) => fetch(`${AUTH_URL}/refresh`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ refresh_token: refreshToken })
}).then(fetchRespHandler),
refresh: (refreshToken) =>
fetch(`${AUTH_URL}/refresh`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ refresh_token: refreshToken }),
}).then(fetchRespHandler),
};
export default auth;

View File

@@ -4,9 +4,7 @@ import {
addQueryParams,
} from "../utils/http";
const API_ENDPOINT =
process.env.REACT_APP_UPLOAD_SERVICE_URL ||
"https://gw-dev.fiskerdps.com/ota_update";
const API_ENDPOINT = process.env.REACT_APP_UPLOAD_SERVICE_URL;
const manifestsAPI = {
deleteManifest: async (manifest_id, token) =>

View File

@@ -1,49 +1,64 @@
import { getAuthHeaderOptions, fetchRespHandler, addQueryParams } from "../utils/http";
import {
getAuthHeaderOptions,
fetchRespHandler,
addQueryParams,
} from "../utils/http";
const API_ENDPOINT = process.env.REACT_APP_UPLOAD_SERVICE_URL || "https://gw-dev.fiskerdps.com/ota_update";
const API_ENDPOINT = process.env.REACT_APP_UPLOAD_SERVICE_URL;
const updatesAPI = {
createCarUpdates: async (data, token) => fetch(`${API_ENDPOINT}/carupdate`, {
method: "POST",
headers: Object.assign({ "Content-Type": "application/json" }, getAuthHeaderOptions(token)),
body: JSON.stringify(data),
})
.then(fetchRespHandler),
createCarUpdates: async (data, token) =>
fetch(`${API_ENDPOINT}/carupdate`, {
method: "POST",
headers: Object.assign(
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
body: JSON.stringify(data),
}).then(fetchRespHandler),
getCarUpdateLog: async (query, token) => {
const u = addQueryParams(`${API_ENDPOINT}/carupdateslog`, query);
return fetch(u, {
method: "GET",
headers: Object.assign({ "Content-Type": "application/json" }, getAuthHeaderOptions(token)),
})
.then(fetchRespHandler);
headers: Object.assign(
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
}).then(fetchRespHandler);
},
getCarUpdateProgress: async (carupdateids, token) => {
const u = `${API_ENDPOINT}/carupdatesstatuses?carupdateids=${carupdateids}`;
return fetch(u, {
method: "GET",
headers: Object.assign({ "Content-Type": "application/json" }, getAuthHeaderOptions(token)),
})
.then(fetchRespHandler);
headers: Object.assign(
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
}).then(fetchRespHandler);
},
getCarUpdates: async (search, token) => {
const u = addQueryParams(`${API_ENDPOINT}/carupdates`, search);
return fetch(u, {
method: "GET",
headers: Object.assign({ "Content-Type": "application/json" }, getAuthHeaderOptions(token)),
})
.then(fetchRespHandler);
headers: Object.assign(
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
}).then(fetchRespHandler);
},
getVINUpdates: async (vin, token) => {
const u = addQueryParams(`${API_ENDPOINT}/carupdates`, { vin });
return fetch(u, {
method: "GET",
headers: Object.assign({ "Content-Type": "application/json" }, getAuthHeaderOptions(token)),
})
.then(fetchRespHandler);
headers: Object.assign(
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
}).then(fetchRespHandler);
},
};

View File

@@ -1,12 +1,12 @@
import axios from 'axios';
import axios from "axios";
const UPLOAD_ENDPOINT = process.env.REACT_APP_UPLOAD_SERVICE_URL || "https://gw-dev.fiskerdps.com/ota_update";
const UPLOAD_ENDPOINT = process.env.REACT_APP_UPLOAD_SERVICE_URL;
const fileField = "file";
export const getCancelToken = () => {
const token = axios.CancelToken;
return token.source();
}
};
export const uploadFile = (data, token, onProgress, cancelToken) => {
const form = new FormData();
@@ -14,18 +14,18 @@ export const uploadFile = (data, token, onProgress, cancelToken) => {
method: "POST",
headers: {
"Content-Type": "multipart/form-data",
"Authorization": `Bearer ${token}`,
Authorization: `Bearer ${token}`,
},
cancelToken,
};
if (onProgress) {
options = {
options = {
...options,
onUploadProgress: (event) => {
onProgress(event.loaded / event.total);
}
}
},
};
}
for (let key in data) {
@@ -34,7 +34,8 @@ export const uploadFile = (data, token, onProgress, cancelToken) => {
form.append(fileField, data[fileField]);
return axios.post(`${UPLOAD_ENDPOINT}/manifestfile`, form, options)
return axios
.post(`${UPLOAD_ENDPOINT}/manifestfile`, form, options)
.then((response) => response.data)
.catch((error) => {
if (typeof error.response.data === "string") {

View File

@@ -4,9 +4,7 @@ import {
addQueryParams,
} from "../utils/http";
const API_ENDPOINT =
process.env.REACT_APP_UPLOAD_SERVICE_URL ||
"https://gw-dev.fiskerdps.com/ota_update";
const API_ENDPOINT = process.env.REACT_APP_UPLOAD_SERVICE_URL;
const vehiclesAPI = {
addVehicle: async (vehicle, token) =>