diff --git a/.cloudbuild.yaml b/.cloudbuild.yaml new file mode 100644 index 0000000..7837d15 --- /dev/null +++ b/.cloudbuild.yaml @@ -0,0 +1,29 @@ +steps: + - id: 'build-image' + name: 'gcr.io/cloud-builders/docker' + args: + - 'build' + - '-t' + - 'eu.gcr.io/$PROJECT_ID/$REPO_NAME/$BRANCH_NAME/$REPO_NAME:$SHORT_SHA' + - '-t' + - 'eu.gcr.io/$PROJECT_ID/$REPO_NAME/$BRANCH_NAME/$REPO_NAME:latest' + - '.' + - '-f' + - './Dockerfile' + timeout: 1200s + - id: 'push-to-registry' + name: 'gcr.io/cloud-builders/docker' + args: + - 'push' + - 'eu.gcr.io/$PROJECT_ID/$REPO_NAME/$BRANCH_NAME/$REPO_NAME' + - id: 'deploy-to-gke' + name: 'gcr.io/cloud-builders/gcloud' + env: + - 'KUBECONFIG=/.kube/config' + entrypoint: 'bash' + args: + - '-c' + - | + gcloud container clusters get-credentials soils-revealed-cluster --project=$PROJECT_ID --zone=europe-west4-a + kubectl set image deployment/$REPO_NAME --namespace=$BRANCH_NAME soils-revealed=eu.gcr.io/$PROJECT_ID/$REPO_NAME/$BRANCH_NAME/$REPO_NAME:$SHORT_SHA + kubectl rollout restart deployment $REPO_NAME --namespace=$BRANCH_NAME diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9eadd8e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,18 @@ +# ignore folder on copy (more for local build) +*.md +.git +.cache +.history +node_modules +npm-debug.log +.prettierrc +.gitignore +.github +.git +.eslintrc +.env.default +.editorconfig +.dockerignore +.env +.Dockerfile +.cloudbuild.yaml diff --git a/.env.default b/.env.default index 7c1e2ce..cbda645 100644 --- a/.env.default +++ b/.env.default @@ -1,11 +1,13 @@ -PORT=3000 +PORT=3001 MAPBOX_API_KEY= API_URL= ANALYSIS_API_URL= -DEPLOYMENT_KEY= GOOGLE_ANALYTICS_KEY= AWS_REGION= AWS_BUCKET_NAME= AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= AWS_MAX_Z_TILE_STORAGE= +DEPLOYMENT_KEY= +AIRTABLE_API_KEY= +AIRTABLE_USER_ID= diff --git a/.github/workflows/production.yml b/.github/workflows/production.yml deleted file mode 100644 index 81c7fa3..0000000 --- a/.github/workflows/production.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: Production build - -on: - push: - branches: - - master - -jobs: - deploy: - runs-on: ubuntu-latest - - steps: - - name: Run SSH commands - # Documentation: https://github.com/marketplace/actions/ssh-remote-commands - uses: appleboy/ssh-action@master - env: - NODE_PORT: 3000 - MAPBOX_API_KEY: pk.eyJ1IjoidG5jc29pbHNjaWVuY2UiLCJhIjoiY2s5Y3dweXg0MDBlZjNkbXE5ZGk4Y294biJ9.nGYVoUKe7Z8MJoi5GwCGjg - API_URL: http://35.233.41.65/user/skydipper/api/v2 - ANALYSIS_API_URL: https://soilsrevealed.org/api/v1/analysis - GEE_KEY: ${{ secrets.GEE_KEY }} - GOOGLE_ANALYTICS_KEY: UA-179817360-1 - AWS_REGION: ${{ secrets.AWS_REGION }} - AWS_BUCKET_NAME: ${{ secrets.AWS_BUCKET_NAME }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_MAX_Z_TILE_STORAGE: 5 - with: - host: ${{ secrets.SSH_HOST }} - key: ${{ secrets.SSH_KEY }} - port: ${{ secrets.SSH_PORT }} - username: ${{ secrets.SSH_USER }} - envs: NODE_PORT,MAPBOX_API_KEY,API_URL,ANALYSIS_API_URL,GEE_KEY,GOOGLE_ANALYTICS_KEY,AWS_REGION,AWS_BUCKET_NAME,AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_MAX_Z_TILE_STORAGE - script: | - echo '> Source nvm' - export NVM_DIR=~/.nvm - source ~/.nvm/nvm.sh - echo '> Export the path' - export PATH=$PATH:/home/ubuntu/.nvm/versions/node/v12.16.1/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin - echo '> Kill previous node server' - pm2 stop production - echo '> Open production folder' - cd ~/soils-revealed - echo '> Pull from git' - git fetch origin master - git reset --hard origin/master - echo '> Use correct node version' - nvm install - nvm use - echo '> Install dependencies' - yarn install --frozen-lockfile - echo '> Generate a deployment key' - DEPLOYMENT_KEY=`date +%s` - echo '> Create .env file' - echo -e "PORT=$NODE_PORT\nMAPBOX_API_KEY=$MAPBOX_API_KEY\nAPI_URL=$API_URL\nANALYSIS_API_URL=$ANALYSIS_API_URL\nDEPLOYMENT_KEY=$DEPLOYMENT_KEY\nGOOGLE_ANALYTICS_KEY=$GOOGLE_ANALYTICS_KEY\nAWS_REGION=$AWS_REGION\nAWS_BUCKET_NAME=$AWS_BUCKET_NAME\nAWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID\nAWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY\nAWS_MAX_Z_TILE_STORAGE=$AWS_MAX_Z_TILE_STORAGE" > .env - echo '> Create gee.key.json file' - echo $GEE_KEY > gee.key.json - echo '> Build the app' - yarn build - echo '> Run the server' - pm2 start production diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml deleted file mode 100644 index b102368..0000000 --- a/.github/workflows/staging.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: Staging build - -on: - push: - branches: - - develop - -jobs: - deploy: - runs-on: ubuntu-latest - - steps: - - name: Run SSH commands - # Documentation: https://github.com/marketplace/actions/ssh-remote-commands - uses: appleboy/ssh-action@master - env: - NODE_PORT: 3001 - MAPBOX_API_KEY: pk.eyJ1IjoidG5jc29pbHNjaWVuY2UiLCJhIjoiY2s5Y3dweXg0MDBlZjNkbXE5ZGk4Y294biJ9.nGYVoUKe7Z8MJoi5GwCGjg - API_URL: http://35.233.41.65/user/skydipper/api/v2 - ANALYSIS_API_URL: https://soilsrevealed.org/api/v1/analysis - GEE_KEY: ${{ secrets.GEE_KEY }} - GOOGLE_ANALYTICS_KEY: - AWS_REGION: ${{ secrets.AWS_REGION }} - AWS_BUCKET_NAME: ${{ secrets.AWS_BUCKET_NAME }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_MAX_Z_TILE_STORAGE: 5 - with: - host: ${{ secrets.SSH_HOST }} - key: ${{ secrets.SSH_KEY }} - port: ${{ secrets.SSH_PORT }} - username: ${{ secrets.SSH_USER }} - envs: NODE_PORT,MAPBOX_API_KEY,API_URL,ANALYSIS_API_URL,GEE_KEY,GOOGLE_ANALYTICS_KEY,AWS_REGION,AWS_BUCKET_NAME,AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_MAX_Z_TILE_STORAGE - script: | - echo '> Source nvm' - export NVM_DIR=~/.nvm - source ~/.nvm/nvm.sh - echo '> Export the path' - export PATH=$PATH:/home/ubuntu/.nvm/versions/node/v12.16.1/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin - echo '> Kill previous node server' - pm2 stop staging - echo '> Open staging folder' - cd ~/soils-revealed-staging - echo '> Pull from git' - git fetch origin develop - git reset --hard origin/develop - echo '> Use correct node version' - nvm install - nvm use - echo '> Install dependencies' - yarn install --frozen-lockfile - echo '> Generate a deployment key' - DEPLOYMENT_KEY=`date +%s` - echo '> Create .env file' - echo -e "PORT=$NODE_PORT\nMAPBOX_API_KEY=$MAPBOX_API_KEY\nAPI_URL=$API_URL\nANALYSIS_API_URL=$ANALYSIS_API_URL\nDEPLOYMENT_KEY=$DEPLOYMENT_KEY\nGOOGLE_ANALYTICS_KEY=$GOOGLE_ANALYTICS_KEY\nAWS_REGION=$AWS_REGION\nAWS_BUCKET_NAME=$AWS_BUCKET_NAME\nAWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID\nAWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY\nAWS_MAX_Z_TILE_STORAGE=$AWS_MAX_Z_TILE_STORAGE" > .env - echo '> Create gee.key.json file' - echo $GEE_KEY > gee.key.json - echo '> Build the app' - yarn build - echo '> Run the server' - pm2 start staging diff --git a/.gitignore b/.gitignore index 756b8f6..d89545e 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,8 @@ yarn-error.log* # keys gee.key.json + +#VScode +.history/ + + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ede5796 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +FROM docker.io/debian:stable-slim@sha256:a939c03c4d3e3f53e3ef4ef6e75cb681a3ad56537842f95bf89755da86559b13 + +# Source: https://www.kabisa.nl/tech/nvm-in-docker/ +# docker build --force-rm --no-cache -t soils-revealed:latest . +# docker run -p3001:3001 --env-file .env soils-revealed:latest + +SHELL ["/bin/bash","-l","-c"] +ENV DEBIAN_FRONTEND noninteractive + +RUN apt-get update \ + && apt-get install -y tini curl \ + && rm -rf /var/lib/apt/lists/* + +RUN mkdir /soils-revealed +WORKDIR /soils-revealed +COPY . . + +RUN curl --silent -o- https://raw.githubusercontent.com/creationix/nvm/master/install.sh | bash +RUN nvm install && nvm use +RUN echo -e "Implementing node: $(node --version)\nFrom: $(which node)" + +RUN npm install -g yarn +RUN npm install -g pm2 +RUN yarn install --frozen-lockfile + +RUN DEPLOYMENT_KEY=$(date +%s) && echo "DEPLOYMENT_KEY=$DEPLOYMENT_KEY" > .env + +ENTRYPOINT ["/usr/bin/tini","-g","--"] +CMD ["/soils-revealed/run.sh","production"] diff --git a/README.md b/README.md index a21af21..033cd36 100644 --- a/README.md +++ b/README.md @@ -17,14 +17,15 @@ In order to start modifying the app, please make sure to correctly configure you 5. Use the correct Node.js version for this app by running `nvm use`; if you didn't install NVM (step 2), then manually install the Node.js version described in `.nvmrc` 6. Install the dependencies: `yarn` 7. Create a `.env` file at the root of the project by copying `.env.default` and giving a value for each of the variables (see next section for details) -8. Create a `gee.key.json` file at the root of the project with the Google Earth Engine's private key inside +8. Create a gee.key.json file at the root of the project with the Google Earth Engine's private key inside. + 9. Run the server: `yarn dev` You can access a hot-reloaded version of the app on [http://localhost:3000](http://localhost:3000). The application is built using [React](https://reactjs.org/) and the framework [Next.js](https://nextjs.org/). The styles use [Sass](https://sass-lang.com/) and the [Bootstrap](https://getbootstrap.com/) framework. -A continuous deployment system is in place. Each time you push to the `master` branch, the application is deployed to production through a GitHub Action defined in `.github/workflows/production.yml`. Each time you push to `develop`, the application is deployed to staging through an action defined in `.github/workflows/staging.yml`. You can see the status of the build in the “Actions” tab of the repository on GitHub. +A continuous deployment system is in place. Each time you push to the `master` branch, the application is deployed to production. Github will trigger an event on google cloud run and make a deployment. The same for the `develop` branch. ## Environment variables @@ -44,18 +45,42 @@ Below is a description of each of the keys. | AWS_ACCESS_KEY_ID | Access key ID of the AWS server storing the tiles of the soils layers | | AWS_SECRET_ACCESS_KEY | Secret access key of the AWS server storing the tiles of the soils layers | | AWS_MAX_Z_TILE_STORAGE | Maximum zoom at which tiles generated on-the-fly will be saved in the AWS S3 bucket | +| AIRTABLE_API_KEY | Secret access key for [Airtable](https://airtable.com/) | +| AIRTABLE_USER_ID | Airtable User ID | + ## Deployment -As explained before, the application is automatically deployed to staging when pushing new changes to the `develop` branch, and deployed to production when pushing changes to `master`. This is achieved through GitHub Actions defined in `.github/workflows`. +### Local computer + +It is possible to run a local test deployment using the docker image. Docker implement an agnostic build and then during run it will pickup the container's env variables, and properly set the system + +`run.sh` accepts 2 arguments: production or develop. Production argument will run `yarn start` and the code will production ready, while `develop` runs nodejs in development mode, necessary if you are testing content and changes. + +```bash +docker build --force-rm --no-cache -t soils-revealed:latest . +docker run -p3001:3001 --env-file .env soils-revealed:latest /soils-revealed/run.sh production +``` + +**Note:** We have created a `.env` file on the project root with all variables, and this will be used to run nodejs in production mode. This is a deployment to test nodejs in production mode. + +**Note:** Dockerfile has `CMD` implementing production. + -When an action is executed, it connects via SSH to the server hosting the application. The server's credentials are stored in GitHub's “secrets” vault. A script is then executed: the running instance of the application is stopped, the code is pulled, the correct version of node is selected, the dependencies are installed, a local `.env` file is generated, a local `gee.key.json` file is generated, and the application is restarted. +### Google GKE -The `.env` file is programmatically generated on the server because it differs for each environment. Some of the keys are hard coded in the `.github/workflows/XXX.yml` file and others are pulled from GitHub's “secrets” vault. +Public deployment is based on Google Cloud build and file `.cloudbuild.yaml`. Up on push to `master` or `develop`, the following steps will happen: -The `gee.key.json` file contains the credentials for the Google Earth Engine library. It is also programmatically generated as its value is stored in GitHub's “secrets” vault. +1. Github will trigger a Google Cloud run trigger +2. Google cloud will pull the branch content. +3. Docker build will be iniciated, using `Dockerfile` and `.cloudbuild.yaml` +4. After completed Docker image is stored on a private repository, using tags `latest` and `$SHORT_SHA` +5. Google Cloud build will update the image on GKE and make a `kubectl rollout restart` +6. GKE contains a specific `ConfigMap` with all .env necessary for deployment. +7. `gee.key.json` is added to the pods using a `ConfigMap` mount + -Overall, deploying to either environment takes between 1 to 2 minutes to complete. +Overall, deploying to either environment takes between 5 to 10 minutes to complete. If deployment is not successful GKE will continue implementing the previous deployment. ## Architecture diff --git a/components/explore/component.js b/components/explore/component.js index fe1ba2b..e0a9d70 100644 --- a/components/explore/component.js +++ b/components/explore/component.js @@ -6,6 +6,7 @@ import throttle from 'lodash/debounce'; import { Router } from 'lib/routes'; import { logEvent } from 'utils/analytics'; +import { isFirstVisit } from 'utils/explore'; import { useHasMounted, useDesktop } from 'utils/hooks'; import { toggleBasemap, toggleLabels, toggleRoads } from 'utils/map'; import { @@ -28,6 +29,7 @@ import InfoModal from './info-modal'; import InteractiveFeaturePopup from './interactive-feature-popup'; import DrawBoard from './draw-board'; import MapContainer from './map-container'; +import UserModal from 'components/user-modal'; import './style.scss'; @@ -76,6 +78,14 @@ const Explore = ({ const [interactiveFeatures, setInteractiveFeatures] = useState(null); const [showTour, setShowTour] = useState(false); + // User recruitment modal. This modal should appear just the first time the user + // visits the map section + const [userModalOpen, setUserModalOpen] = useState(isFirstVisit()); + + const handleModalClose = () => { + setUserModalOpen(false); + }; + // When the user clicks the popup's button that triggers its close, the map also receives the // event and it opens a new popup right after // This is a bug of react-map-gl's library @@ -222,6 +232,7 @@ const Explore = ({ className="c-explore" style={isDesktop ? { backgroundColor: BASEMAPS[basemap].backgroundColor } : undefined} > + {isDesktop && ( <> {showTour && } diff --git a/components/footer/component.js b/components/footer/component.js index 0151bbd..4708091 100644 --- a/components/footer/component.js +++ b/components/footer/component.js @@ -42,7 +42,7 @@ const Footer = () => {
  • - + Contact
  • diff --git a/components/forms/radio/index.js b/components/forms/radio/index.js index b8db140..d5302eb 100644 --- a/components/forms/radio/index.js +++ b/components/forms/radio/index.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import './style.scss'; -const Radio = ({ id, name, disabled, checked, onChange, children, className }) => ( +const Radio = ({ id, name, disabled, checked, onChange, children, className, required }) => (