diff --git a/.env.dev b/.env.dev new file mode 100644 index 0000000..0c3250f --- /dev/null +++ b/.env.dev @@ -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 diff --git a/.env.local b/.env.local new file mode 100644 index 0000000..6760ebe --- /dev/null +++ b/.env.local @@ -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 diff --git a/.env.prd b/.env.prd new file mode 100644 index 0000000..9ec77df --- /dev/null +++ b/.env.prd @@ -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 diff --git a/.env.stg b/.env.stg new file mode 100644 index 0000000..f400e02 --- /dev/null +++ b/.env.stg @@ -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 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..daa6fae --- /dev/null +++ b/.github/workflows/deploy.yml @@ -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() diff --git a/.github/workflows/test.workflow.yml b/.github/workflows/test.yml similarity index 100% rename from .github/workflows/test.workflow.yml rename to .github/workflows/test.yml diff --git a/Dockerfile b/Dockerfile index ba18273..688ae2a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/Dockerfile.dev b/Dockerfile.dev deleted file mode 100644 index cc5ee57..0000000 --- a/Dockerfile.dev +++ /dev/null @@ -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 \ No newline at end of file diff --git a/Jenkinsfile b/Jenkinsfile deleted file mode 100644 index f597a6d..0000000 --- a/Jenkinsfile +++ /dev/null @@ -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') - } - } - } - } -} \ No newline at end of file diff --git a/README.md b/README.md index 1b79fe4..6328356 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/k8s/values-dev.yaml b/k8s/values-dev.yaml index 1e69f1c..da4e345 100644 --- a/k8s/values-dev.yaml +++ b/k8s/values-dev.yaml @@ -9,4 +9,4 @@ resources: cpu: 250m memory: 256Mi -replicas: 1 \ No newline at end of file +replicas: 1 diff --git a/k8s/values-prd.yaml b/k8s/values-prd.yaml index 139cf6c..5533a67 100644 --- a/k8s/values-prd.yaml +++ b/k8s/values-prd.yaml @@ -9,4 +9,4 @@ resources: cpu: 250m memory: 256Mi -replicas: 1 \ No newline at end of file +replicas: 1 diff --git a/k8s/values-stg.yaml b/k8s/values-stg.yaml new file mode 100644 index 0000000..dc22136 --- /dev/null +++ b/k8s/values-stg.yaml @@ -0,0 +1,12 @@ +ingress: + hostname: stg-ota-admin.fiskerdps.com + +resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 250m + memory: 256Mi + +replicas: 1 diff --git a/package-lock.json b/package-lock.json index 249ff80..e0a4b0b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index b25e941..3e9d325 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/src/services/__mocks__/auth.js b/src/services/__mocks__/auth.js index 8375478..72f931c 100644 --- a/src/services/__mocks__/auth.js +++ b/src/services/__mocks__/auth.js @@ -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; }, -} \ No newline at end of file + setSignInResponse: (value) => { + signInResponse = value; + }, + setVerifyResponse: (value) => { + verifyResponse = value; + }, + setRefreshResponse: (value) => { + refreshResponse = value; + }, +}; diff --git a/src/services/auth.js b/src/services/auth.js index 9bbfda6..aee4615 100644 --- a/src/services/auth.js +++ b/src/services/auth.js @@ -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; diff --git a/src/services/manifestsAPI.js b/src/services/manifestsAPI.js index 05daa8b..473b6ab 100644 --- a/src/services/manifestsAPI.js +++ b/src/services/manifestsAPI.js @@ -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) => diff --git a/src/services/updatesAPI.js b/src/services/updatesAPI.js index 4cad939..7090b1f 100644 --- a/src/services/updatesAPI.js +++ b/src/services/updatesAPI.js @@ -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); }, }; diff --git a/src/services/uploadFile.js b/src/services/uploadFile.js index 13fadf6..7e9f11a 100644 --- a/src/services/uploadFile.js +++ b/src/services/uploadFile.js @@ -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") { diff --git a/src/services/vehiclesAPI.js b/src/services/vehiclesAPI.js index 88fc0b3..cd92eb2 100644 --- a/src/services/vehiclesAPI.js +++ b/src/services/vehiclesAPI.js @@ -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) =>