├── .dockerignore ├── .env.example ├── .gitignore ├── Dockerfile ├── Dockerfile.ui ├── README.md ├── bin ├── deploy.sh └── dev_server.sh ├── catURL-container.postman_environment.json ├── catURL-local.postman_environment.json ├── catURL-prod.postman_environment.json ├── catURL.postman_collection.json ├── catUrl.gif ├── deployment-prod.yaml ├── deployment-ui-prod.yaml ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── src ├── components │ ├── App.js │ ├── App.test.js │ └── serviceWorker.js ├── css │ ├── App.css │ ├── index.css │ └── reset.css ├── img │ ├── cat-face2.jpg │ ├── logo.svg │ └── screenshot.png ├── index.js ├── seed.txt └── services │ └── server.js └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | node_modules -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | POSTMAN_API_KEY=paste-your-postman-api-key-here 2 | REACT_APP_HOST=paste-your-deployment-domain-here-meaning-server-hosting-your-APIs 3 | DOCKER_HUB_USERNAME=joycelin1600 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env 18 | # .env.development.local 19 | # .env.test.local 20 | # .env.production.local 21 | # prod_postgres.sh 22 | # prod_server.sh 23 | 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | FROM node:alpine 3 | 4 | WORKDIR /app 5 | 6 | ENV NODE_ENV="production" 7 | 8 | COPY package.json yarn.loc[k] package-lock.jso[n] /app/ 9 | 10 | RUN \ 11 | # apk add build-base make gcc g++ linux-headers python-dev libc-dev libc6-compat && \ 12 | yarn install --no-cache --production && \ 13 | adduser -S nodejs && \ 14 | chown -R nodejs /app && \ 15 | chown -R nodejs /home/nodejs 16 | 17 | COPY . /app/ 18 | 19 | USER nodejs 20 | 21 | CMD ["node", "src/services/server.js"] 22 | -------------------------------------------------------------------------------- /Dockerfile.ui: -------------------------------------------------------------------------------- 1 | 2 | FROM nginx 3 | 4 | COPY build /usr/share/nginx/html -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This project was originally published in [Deploying a scalable web application with Docker and Kubernetes](https://medium.com/@joycelin.codes/deploying-a-scalable-web-application-with-docker-and-kubernetes-a5000a06c4e9) from the [Postman Engineering blog](https://medium.com/postman-engineering). 2 | 3 | ### Pre-requisites for local development 4 | 1. Download and install [Node.js](https://nodejs.org/en/) and a package manager like [npm](https://www.npmjs.com/), and 5 | 1. A container platform like [Docker](https://www.docker.com/get-started). Remember to start Docker on your machine. 6 | 7 | ### For Development 8 | 9 | npm install // install dependencies 10 | npm start // start app on 3000 11 | npm run start-server // start server on 5500 12 | 13 | ### For Production 14 | 15 | 1. **Environment variables**: Create a new file called `.env` located in the root of your project directory. Add your production domain and docker hub username. See the example in `.env.example`. 16 | 1. **API tests**: If you plan to run Postman tests, then update the `bin/deploy.sh` file by selecting a method of running your Postman tests. To run either of the last 2 options, update your Postman collection UID and environment UID in `bin/deploy.sh` and also add your [Postman API](https://docs.api.getpostman.com/) key to the `.env` file you created in the previous step. 17 | 1. **Describe deployment**: Update the `deployment-prod.yaml` file to describe your backend deployment. Update the `deployment-ui-prod.yaml` file to describe your frontend deployment. These files will include your container image, resource allocation, the desired number of replicas, and other important information. 18 | 1. **Deploy**: Run the deployment script 19 | 20 | `npm run deploy // run API tests, then deploy frontend and backend to production` 21 | 22 | For the deployment, I used a hosted Kubernetes provider called [Kubesail](https://kubesail.com/) that creates a free managed namespace. However, the underlying deployment utility [`npx deploy-to-kube`](https://github.com/kubesail/deploy-to-kube) supports any Kubernetes cluster. By running it inside your app's directory, this utility will automatically generate a Dockerfile (if it doesn't exist), build and push deployment images, generate Kubernetes configuration files (if it doesn't exist), and trigger a deployment on your Kubernetes cluster. 23 | 24 | ![[catURL website](https://github.com/postmanlabs/node-doc-kube/blob/master/catUrl.gif)](https://github.com/postmanlabs/node-doc-kube/blob/master/catUrl.gif) 25 | -------------------------------------------------------------------------------- /bin/deploy.sh: -------------------------------------------------------------------------------- 1 | # terminate on any error 2 | set -e 3 | 4 | . .env 5 | 6 | npm run build 7 | 8 | # ********* RUN TESTS ********* 9 | 10 | # run API tests against a local server, failed test(s) return a 1 (error) and terminate this script 11 | # if you don't plan to run any Postman tests, ensure this entire section is commented out 12 | 13 | # # [GOOD PRACTICE] run your tests against a local server 14 | # # requires your Postman collection and environment files to be located within the project directory 15 | # # formatted like: newman run -e --ignore-redirects 16 | # newman run catURL.postman_collection.json -e catURL-local.postman_environment.json --ignore-redirects 17 | 18 | # # [BETTER PRACTICE] run your tests using the Postman API (https://docs.api.getpostman.com/) to retrieve the latest versions of your collection and environment 19 | # # requires collection UID, environment UID, and your Postman API key 20 | # # formatted like: newman run -e --ignore-redirects 21 | # newman run https://api.getpostman.com/collections/1559645-9f5aa83b-666d-41ef-9b91-6c3ec25ef789?apikey=ac2c5f4081644e5aa666e151494a7992 -e https://api.getpostman.com/environments/1559645-b5c093e1-bf72-4f7e-b18a-11263f21ec32?apikey=${POSTMAN_API_KEY} --ignore-redirects 22 | 23 | # [BEST PRACTICE] run your tests against a local container that exactly replicates your production environment 24 | # also requires collection UID, environment UID, and your Postman API key 25 | # build and run backend on a local container on port 5501, then run tests against localhost:5501 26 | docker build -f Dockerfile . -t backend 27 | docker run -p 5501:5500 -d --name test_backend_run backend 28 | # use a Postman environment switch tests over to run against container on http://localhost:5501 29 | set +e 30 | newman run https://api.getpostman.com/collections/1559645-9f5aa83b-666d-41ef-9b91-6c3ec25ef789?apikey=${POSTMAN_API_KEY} -e https://api.getpostman.com/environments/1559645-b5c093e1-bf72-4f7e-b18a-11263f21ec32?apikey=${POSTMAN_API_KEY} --ignore-redirects 31 | EXIT_CODE=$? 32 | docker stop test_backend_run 33 | docker rm test_backend_run 34 | # # if the previous tests failed, remember to shut down the local container 35 | if [[ $EXIT_CODE != 0 ]]; then exit $EXIT_CODE; fi 36 | set -e 37 | 38 | 39 | # ********* DEPLOY TO PRODUCTION ********* 40 | 41 | # git revision 42 | GIT_REV=$(git rev-parse --short HEAD) 43 | 44 | # build docker container for front end, and tag container name 45 | docker build -f Dockerfile.ui . -t ${DOCKER_HUB_USERNAME}/cat-ui:${GIT_REV} -t ${DOCKER_HUB_USERNAME}/cat-ui:prod 46 | docker push ${DOCKER_HUB_USERNAME}/cat-ui:${GIT_REV} 47 | 48 | # deploy backend to kubernetes 49 | # will look for a kube config, and if none exists prompt to set up via kubesail 50 | npx deploy-to-kube --no-confirm 51 | 52 | # deploy frontend and tell kubernetes to use newest UI image 53 | kubectl apply -f deployment-ui-prod.yaml 54 | kubectl set image deployment/cat-ui cat-ui=${DOCKER_HUB_USERNAME}/cat-ui:${GIT_REV} 55 | -------------------------------------------------------------------------------- /bin/dev_server.sh: -------------------------------------------------------------------------------- 1 | PROJECT="caturl" 2 | 3 | # Ensure node dependencies 4 | NODE_ENV="development" yarn --no-progress --no-emoji --prefer-offline 5 | 6 | ./node_modules/.bin/nodemon \ 7 | --inspect \ 8 | --ignore src/components \ 9 | --ignore src/css \ 10 | src/services/server.js 11 | -------------------------------------------------------------------------------- /catURL-container.postman_environment.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "be4012af-b66d-420b-8464-665618e9c48e", 3 | "name": "catURL-container", 4 | "values": [ 5 | { 6 | "key": "url", 7 | "value": "http://localhost:5501", 8 | "description": "", 9 | "enabled": true 10 | } 11 | ], 12 | "_postman_variable_scope": "environment", 13 | "_postman_exported_at": "2019-03-03T21:36:13.526Z", 14 | "_postman_exported_using": "Postman/7.0.3" 15 | } -------------------------------------------------------------------------------- /catURL-local.postman_environment.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "b5c093e1-bf72-4f7e-b18a-11263f21ec32", 3 | "name": "catURL-local", 4 | "values": [ 5 | { 6 | "key": "url", 7 | "value": "http://localhost:5500", 8 | "description": "", 9 | "enabled": true 10 | }, 11 | { 12 | "key": "originalUrl", 13 | "value": "", 14 | "enabled": true 15 | }, 16 | { 17 | "key": "path", 18 | "value": "", 19 | "enabled": true 20 | } 21 | ], 22 | "_postman_variable_scope": "environment", 23 | "_postman_exported_at": "2019-03-03T21:36:20.754Z", 24 | "_postman_exported_using": "Postman/7.0.3" 25 | } -------------------------------------------------------------------------------- /catURL-prod.postman_environment.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "545ecaf5-f8a3-41cb-94e3-953afea67d23", 3 | "name": "catURL-prod", 4 | "values": [ 5 | { 6 | "key": "url", 7 | "value": "https://cat-kube-stateless-prod-2ea747e90b.kubesail.io", 8 | "description": "", 9 | "enabled": true 10 | }, 11 | { 12 | "key": "originalUrl", 13 | "value": "", 14 | "enabled": true 15 | }, 16 | { 17 | "key": "path", 18 | "value": "", 19 | "enabled": true 20 | } 21 | ], 22 | "_postman_variable_scope": "environment", 23 | "_postman_exported_at": "2019-03-03T21:36:26.908Z", 24 | "_postman_exported_using": "Postman/7.0.3" 25 | } -------------------------------------------------------------------------------- /catURL.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "9f5aa83b-666d-41ef-9b91-6c3ec25ef789", 4 | "name": "catURL", 5 | "description": "We'll start with a simple Node app that functions like a URL shortener. In our case, we'll transform one URL into a different one using cat verbs, cat adjectives, and cat emojis 🐱 - and when you input your custom URL into a browser, you'll be redirected back to the original website.\n\n![[catURL website](https://i.imgur.com/yypLUxZ.jpg)](https://i.imgur.com/yypLUxZ.jpg)\n\n### Get started\n\nThis collection and environment was originally used as an example in [Deploying a scalable web application with Docker and Kubernetes](link) from the [Postman Engineering blog](https://medium.com/postman-engineering).\n\nIf you want to follow along, go ahead and fork [this example](link to github) and follow the README steps to spin up a local version of these APIs. \n\nThe beauty of using containers is that even if I'm developing this example on a machine with my Operating System and a different version of Node, you can rely on my container image to prescribe the exact specifications you'll need to run the same application seamlessly on your machine, or in the cloud, or wherever you choose to deploy.\n\n### Test the redirect\n\nSince we're working with redirects, let's set up Postman so we can properly test these requests. By default, Postman will follow a redirect -- unless we tell it not to. If we update our general Postman settings to disallow redirects, we can inspect our server's response headers before the redirection.\n\n* To run the `redirect test` request, make sure you have `Automatically follow redirects` toggled OFF under the general [Postman app settings](https://learning.getpostman.com/docs/postman/launching_postman/settings#general-settings). \n* To run the collection in the Postman app via [collection runner](https://learning.getpostman.com/docs/postman/collection_runs/starting_a_collection_run), make sure you have the same `Automatically follow redirects` toggled OFF.\n* To run the collection using Postman's command line runner [Newman](https://github.com/postmanlabs/newman), make sure you include the flag `--ignore-redirects ` with your command.\n* To run the collection from the Postman cloud using a [monitor](https://learning.getpostman.com/docs/postman/monitors/setting_up_monitor), make sure you check the box that says `Don't follow redirects`.\n\n", 6 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 7 | }, 8 | "item": [ 9 | { 10 | "name": "urlize", 11 | "event": [ 12 | { 13 | "listen": "test", 14 | "script": { 15 | "id": "7594b33d-1078-46ff-b5d9-9ff0fd874188", 16 | "exec": [ 17 | "pm.test(\"Status code is 200\", function () {", 18 | " pm.response.to.have.status(200);", 19 | "});", 20 | "", 21 | "pm.test(\"Body contains catpath\", function () {", 22 | " pm.expect(pm.response.json()).to.have.ownProperty(\"catpath\");", 23 | "});", 24 | "", 25 | "// save data as environment variables to verify the next request redirects as expected", 26 | "pm.environment.set(\"originalUrl\", request.data[\"originalUrl\"]);", 27 | "pm.environment.set(\"path\", pm.response.json().catpath);", 28 | "" 29 | ], 30 | "type": "text/javascript" 31 | } 32 | } 33 | ], 34 | "request": { 35 | "method": "POST", 36 | "header": [ 37 | { 38 | "key": "Content-Type", 39 | "name": "Content-Type", 40 | "value": "application/x-www-form-urlencoded", 41 | "type": "text" 42 | } 43 | ], 44 | "body": { 45 | "mode": "urlencoded", 46 | "urlencoded": [ 47 | { 48 | "key": "originalUrl", 49 | "value": "https://www.getpostman.com", 50 | "type": "text" 51 | } 52 | ] 53 | }, 54 | "url": { 55 | "raw": "{{url}}/encode", 56 | "host": [ 57 | "{{url}}" 58 | ], 59 | "path": [ 60 | "encode" 61 | ] 62 | }, 63 | "description": "This endpoint will encode your original url into a new catURL, returning the new path.\n\nFor example, try submitting `https://www.getpostman.com` as the `originalUrl` in the request body. The response is a json object containing a `catpath` composed of cat words and emojis like `\"stalk.sleepy-bombay-scratch.whisker\"`. You can also review the saved example.\n\nNow change the value of `originalUrl` to a different URL, hit send, and inspect the response." 64 | }, 65 | "response": [ 66 | { 67 | "name": "Successful response", 68 | "originalRequest": { 69 | "method": "POST", 70 | "header": [ 71 | { 72 | "key": "Content-Type", 73 | "name": "Content-Type", 74 | "value": "application/x-www-form-urlencoded", 75 | "type": "text" 76 | } 77 | ], 78 | "body": { 79 | "mode": "urlencoded", 80 | "urlencoded": [ 81 | { 82 | "key": "originalUrl", 83 | "value": "https://postman.aha.io/products/DEVSOL/feature_cards", 84 | "type": "text" 85 | } 86 | ] 87 | }, 88 | "url": { 89 | "raw": "{{url}}/encode", 90 | "host": [ 91 | "{{url}}" 92 | ], 93 | "path": [ 94 | "encode" 95 | ] 96 | } 97 | }, 98 | "status": "OK", 99 | "code": 200, 100 | "_postman_previewlanguage": "json", 101 | "header": [ 102 | { 103 | "key": "X-Powered-By", 104 | "value": "Express" 105 | }, 106 | { 107 | "key": "Access-Control-Allow-Origin", 108 | "value": "*" 109 | }, 110 | { 111 | "key": "Access-Control-Allow-Headers", 112 | "value": "Origin, X-Requested-With, Content-Type, Accept" 113 | }, 114 | { 115 | "key": "Access-Control-Allow-Methods", 116 | "value": "GET, PUT, POST, DELETE, OPTIONS" 117 | }, 118 | { 119 | "key": "Content-Type", 120 | "value": "application/json; charset=utf-8" 121 | }, 122 | { 123 | "key": "Content-Length", 124 | "value": "56" 125 | }, 126 | { 127 | "key": "ETag", 128 | "value": "W/\"38-6j2mGVZYscuM2imZ/fY6K/PoXIs\"" 129 | }, 130 | { 131 | "key": "Date", 132 | "value": "Sun, 24 Feb 2019 22:28:52 GMT" 133 | }, 134 | { 135 | "key": "Connection", 136 | "value": "keep-alive" 137 | } 138 | ], 139 | "cookie": [], 140 | "body": "{\n \"error\": null,\n \"catpath\": \"meow-saunter.paw.scratch.yawn\"\n}" 141 | } 142 | ] 143 | }, 144 | { 145 | "name": "redirect test", 146 | "event": [ 147 | { 148 | "listen": "test", 149 | "script": { 150 | "id": "c6a8f33f-ed76-46c1-8e15-6295e39ecd4b", 151 | "exec": [ 152 | "// under your Postman Settings, make sure you toggle OFF `Automatically follow redirects` so you can examine the location before the redirect", 153 | "", 154 | "const target = responseHeaders.location || responseHeaders.Location;", 155 | "", 156 | "pm.test(\"Redirect to the original URL\", () => {", 157 | " const original = pm.environment.get(\"originalUrl\");", 158 | " pm.expect(target).to.equal(original);", 159 | "});", 160 | "", 161 | "// terminate the collection run if running for testing purposes", 162 | "postman.setNextRequest(null);", 163 | "// pm.environment.unset(\"originalUrl\");", 164 | "// pm.environment.unset(\"path\");", 165 | "" 166 | ], 167 | "type": "text/javascript" 168 | } 169 | } 170 | ], 171 | "request": { 172 | "method": "GET", 173 | "header": [], 174 | "body": { 175 | "mode": "raw", 176 | "raw": "" 177 | }, 178 | "url": { 179 | "raw": "{{url}}/{{path}}", 180 | "host": [ 181 | "{{url}}" 182 | ], 183 | "path": [ 184 | "{{path}}" 185 | ] 186 | }, 187 | "description": "This endpoint will begin redirecting your cat URL to your originally submitted URL. We will stop it before following the redirect to verify the redirection is going to the originally submitted URL.\n\nFor example, update the `path` environment variable with the cat path you received in the previous request. You will be redirected to your originally submitted URL.\n\n*Note*: Make sure you have Postman set up properly to [not follow the redirect](#test-the-redirect). Then, when you send this request, look under the Headers tab of the response viewer down at the bottom. Find the Location header to inspect the target for the redirect." 188 | }, 189 | "response": [ 190 | { 191 | "name": "Successfully paused redirection", 192 | "originalRequest": { 193 | "method": "GET", 194 | "header": [], 195 | "body": { 196 | "mode": "raw", 197 | "raw": "" 198 | }, 199 | "url": { 200 | "raw": "{{url}}/{{path}}", 201 | "host": [ 202 | "{{url}}" 203 | ], 204 | "path": [ 205 | "{{path}}" 206 | ] 207 | } 208 | }, 209 | "status": "Found", 210 | "code": 302, 211 | "_postman_previewlanguage": "plain", 212 | "header": [ 213 | { 214 | "key": "X-Powered-By", 215 | "value": "Express" 216 | }, 217 | { 218 | "key": "Access-Control-Allow-Origin", 219 | "value": "*" 220 | }, 221 | { 222 | "key": "Access-Control-Allow-Headers", 223 | "value": "Origin, X-Requested-With, Content-Type, Accept" 224 | }, 225 | { 226 | "key": "Access-Control-Allow-Methods", 227 | "value": "GET, PUT, POST, DELETE, OPTIONS" 228 | }, 229 | { 230 | "key": "Content-Type", 231 | "value": "text/plain; charset=utf-8" 232 | }, 233 | { 234 | "key": "Location", 235 | "value": "https://www.getpostman.com" 236 | }, 237 | { 238 | "key": "Vary", 239 | "value": "Accept" 240 | }, 241 | { 242 | "key": "Content-Length", 243 | "value": "48" 244 | }, 245 | { 246 | "key": "Date", 247 | "value": "Mon, 25 Feb 2019 01:36:39 GMT" 248 | }, 249 | { 250 | "key": "Connection", 251 | "value": "keep-alive" 252 | } 253 | ], 254 | "cookie": [], 255 | "body": "Found. Redirecting to https://www.getpostman.com" 256 | } 257 | ] 258 | }, 259 | { 260 | "name": "redirect allow", 261 | "event": [ 262 | { 263 | "listen": "test", 264 | "script": { 265 | "id": "d3e531c4-a052-4b67-9d5d-f7682cd5b7d0", 266 | "exec": [ 267 | "// under your Postman Settings, make sure you toggle ON `Automatically follow redirects` so you allow the redirect", 268 | "", 269 | "pm.test(\"Status code is 200\", function () {", 270 | " pm.response.to.have.status(200);", 271 | "});" 272 | ], 273 | "type": "text/javascript" 274 | } 275 | } 276 | ], 277 | "request": { 278 | "method": "GET", 279 | "header": [], 280 | "body": { 281 | "mode": "raw", 282 | "raw": "" 283 | }, 284 | "url": { 285 | "raw": "{{url}}/{{path}}", 286 | "host": [ 287 | "{{url}}" 288 | ], 289 | "path": [ 290 | "{{path}}" 291 | ] 292 | }, 293 | "description": "This endpoint will redirect your cat URL to your originally submitted URL.\n\n*Note*: To run this request, make sure you have `Automatically follow redirects` toggled ON under your general Postman settings.\n\nFor example, update the `path` environment variable with the cat path you received in the previous request. You will be redirected to your originally submitted URL, and can review the webpage in the response body.\n" 294 | }, 295 | "response": [ 296 | { 297 | "name": "Successfully allow redirect", 298 | "originalRequest": { 299 | "method": "GET", 300 | "header": [], 301 | "body": { 302 | "mode": "raw", 303 | "raw": "" 304 | }, 305 | "url": { 306 | "raw": "{{url}}/{{path}}", 307 | "host": [ 308 | "{{url}}" 309 | ], 310 | "path": [ 311 | "{{path}}" 312 | ] 313 | } 314 | }, 315 | "status": "OK", 316 | "code": 200, 317 | "_postman_previewlanguage": "html", 318 | "header": [ 319 | { 320 | "key": "Content-Type", 321 | "value": "text/html" 322 | }, 323 | { 324 | "key": "Transfer-Encoding", 325 | "value": "chunked" 326 | }, 327 | { 328 | "key": "Connection", 329 | "value": "keep-alive" 330 | }, 331 | { 332 | "key": "Date", 333 | "value": "Wed, 20 Feb 2019 23:40:14 GMT" 334 | }, 335 | { 336 | "key": "Last-Modified", 337 | "value": "Wed, 20 Feb 2019 23:40:08 GMT" 338 | }, 339 | { 340 | "key": "Cache-Control", 341 | "value": "max-age=1209600" 342 | }, 343 | { 344 | "key": "Server", 345 | "value": "AmazonS3" 346 | }, 347 | { 348 | "key": "Strict-Transport-Security", 349 | "value": "max-age=300; includeSubDomains" 350 | }, 351 | { 352 | "key": "X-XSS-Protection", 353 | "value": "1; mode=block" 354 | }, 355 | { 356 | "key": "X-Content-Type-Options", 357 | "value": "nosniff" 358 | }, 359 | { 360 | "key": "X-Frame-Options", 361 | "value": "DENY" 362 | }, 363 | { 364 | "key": "Content-Encoding", 365 | "value": "gzip" 366 | }, 367 | { 368 | "key": "Vary", 369 | "value": "Accept-Encoding" 370 | }, 371 | { 372 | "key": "Age", 373 | "value": "352658" 374 | }, 375 | { 376 | "key": "X-Cache", 377 | "value": "Hit from cloudfront" 378 | }, 379 | { 380 | "key": "Via", 381 | "value": "1.1 0576b942ae9f4fc9c0b62b0736e9bfd6.cloudfront.net (CloudFront)" 382 | }, 383 | { 384 | "key": "X-Amz-Cf-Id", 385 | "value": "Gx73nzzbe2cffFOMNQFqFq2h8eTZq-DwAa4uDf55RigzR_u3RFBt3w==" 386 | } 387 | ], 388 | "cookie": [], 389 | "body": "\n\n \n \n \n \n \n \n Postman | API Development Environment\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
\n
\n \n \"Postman\n \n \n
\n
\n \n
\n
\n
\n \n
\n
\n \n \"Postman\n \n
\n \n
\n
\n
\n \n \n \n
\n
\n
\n
\n
\n
\n Postman Simplifies API Development.\n
\n
\n Get easy, API-First solutions with the industry's only complete API Development Environment.\n
\n \n
\n
\n \"\"\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n 6 million +\n
\n
\n Developers\n
\n
\n
\n
\n 200,000 +\n
\n
\n Companies\n
\n
\n
\n
\n 130 million +\n
\n
\n APIs\n
\n
\n
\n
\n
\n
\n
\n How Postman Improves API Development\n
\n
\n
\n
\n \"complete\n \n
\n
\n
\n The Most Complete ADE\n
\n
\n Postman is the only complete API development environment, and flexibly integrates with the software development cycle.\n
\n \n
\n
\n
\n
\n \"integrated\n \n
\n
\n
\n Integrated Tools\n
\n
\n Postman's built-in tools offer solutions for managing APIs in every stage of development — designing & mock, testing, documentation and monitoring.\n
\n \n
\n
\n
\n
\n \"plans\n \n
\n
\n
\n Plans Sizes for Everyone\n
\n
\n Postman has a plan that works for everybody — from individual contributors to small development teams to large scale enterprise teams.\n
\n \n
\n
\n
\n
\n
\n
\n \"\"\n \"\"\n \"\"\n \"\"\n \"\"\n \n
\n
\n
\n
\n
\n
\n
\n Postman is for Teams\n
\n
\n Postman empowers teams of all sizes to seamlessly collaborate in real time across shared workspaces and\n collections. Postman team workspaces ensure teams stay organized and maintain a single source of truth\n throughout the entire API development lifecycle.\n
\n \n
\n
\n \"Postman\n \n
\n
\n
\n
\n
\n
\n Postman Delivers the \n ONLY Complete ADE System.\n
\n
\n Postman streamlines the development process and captures a single source of truth about your APIs while also\n simplifying collaboration across your teams and organizations.\n
\n \n
\n
\n
\n \"space\n \"spaceman\"\n \n
\n
\n \"space\n \"spaceman\"\n \n
\n
\n
\n
\n
\n
\n Postman Leads the Way in the API-first Universe.\n
\n
\n Postman is used by 6 million developers and more than 200,000 companies to access 130 million APIs every month.\n
\n
\n
\n
\n \"BigCommerce\n \n
\n
\n “Postman has been essential to us in rapidly developing new APIs - internally we use it to debug and share\n particular contexts easily across our environments.”\n
\n
\n
\n
Nathan Booker
\n
API Product Manager
\n
\n
\n
\n
\n \"Clarifai\n \"AMC\n \"BigCommerce\n \"TYK\n \n
\n
\n \"Adobe\n \"Twitter\n \"Hotelbeds\n \"docu-sign\n \n
\n
\n \"PayPal\n \"MuleSoft\n \"Square\n \"Imgur\n \n
\n
\n \"Shopify\n \"Hootsuite\n \"Best\n \"Coursera\n \n
\n
\n \"Microsoft\n \"Atlassian\n \"Sikka\n \"Synapse\n \n
\n
\n
\n \n
\n
\n
\n
\n
\n What's Happening at Postman\n
\n
\n Stay up to date with everything going on with Postman\n
\n
\n \n
\n
\n \"\"\n Recent Tweets\n \n
\n
\n
\n \n
\n
\n \"\"\n Recent Releases\n \n
\n
\n
\n
\n
\n \"\"\n In The News\n \n
\n
\n
\n
\n
\n
\n
\n
\n
\n \"spaceman\n \n
\n
\n
\n Which Postman Plan Will Deliver for You?\n
\n
\n Postman has solutions for everyone, from individuals to teams and enterprises.\n
\n \n
\n
\n
\n
\n \n \n
\n \n
\n \n \n \n \n \n \n \n \n " 390 | } 391 | ] 392 | } 393 | ], 394 | "event": [ 395 | { 396 | "listen": "prerequest", 397 | "script": { 398 | "id": "f9c5d32c-5495-4860-861f-1d5f8cee21d8", 399 | "type": "text/javascript", 400 | "exec": [ 401 | "" 402 | ] 403 | } 404 | }, 405 | { 406 | "listen": "test", 407 | "script": { 408 | "id": "0b5ff4b7-7fa4-4178-a09b-85c054343ffb", 409 | "type": "text/javascript", 410 | "exec": [ 411 | "" 412 | ] 413 | } 414 | } 415 | ] 416 | } -------------------------------------------------------------------------------- /catUrl.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/postmanlabs/node-doc-kube/920377f87d75456ea9716d19fc2f9fbe89b74aa6/catUrl.gif -------------------------------------------------------------------------------- /deployment-prod.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: cat-kube-stateless-prod 5 | spec: 6 | selector: 7 | matchLabels: 8 | app: cat-kube-stateless 9 | env: prod 10 | minReadySeconds: 5 11 | strategy: 12 | type: RollingUpdate 13 | rollingUpdate: 14 | maxSurge: 1 15 | maxUnavailable: 0 16 | replicas: 1 17 | template: 18 | metadata: 19 | labels: 20 | deployedBy: deploy-to-kube 21 | app: cat-kube-stateless 22 | env: prod 23 | spec: 24 | volumes: [] 25 | containers: 26 | - name: cat-kube-stateless-prod 27 | image: 'joycelin1600/cat-kube-stateless:prod' 28 | imagePullPolicy: Always 29 | ports: 30 | - name: http 31 | containerPort: 5500 32 | resources: 33 | requests: 34 | cpu: 1m 35 | memory: 32Mi 36 | limits: 37 | cpu: 100m 38 | memory: 64Mi 39 | -------------------------------------------------------------------------------- /deployment-ui-prod.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: cat-ui 5 | spec: 6 | selector: 7 | matchLabels: 8 | app: cat-ui 9 | env: prod 10 | minReadySeconds: 5 11 | strategy: 12 | type: RollingUpdate 13 | rollingUpdate: 14 | maxSurge: 1 15 | maxUnavailable: 0 16 | replicas: 1 17 | template: 18 | metadata: 19 | labels: 20 | deployedBy: deploy-to-kube 21 | app: cat-ui 22 | env: prod 23 | spec: 24 | volumes: [] 25 | containers: 26 | - name: cat-ui 27 | image: 'joycelin1600/cat-ui:prod' 28 | imagePullPolicy: Always 29 | ports: 30 | - name: http 31 | containerPort: 80 32 | resources: 33 | requests: 34 | cpu: 1m 35 | memory: 32Mi 36 | limits: 37 | cpu: 100m 38 | memory: 64Mi 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cat-kube-stateless", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "body-parser": "^1.18.3", 7 | "express": "^4.16.4", 8 | "fs": "0.0.1-security", 9 | "nodemon": "^1.18.10", 10 | "prettier": "^1.16.4", 11 | "react": "^16.7.0", 12 | "react-copy-to-clipboard": "^5.0.1", 13 | "react-dom": "^16.7.0", 14 | "react-scripts": "2.1.3", 15 | "request": "^2.88.0" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "start-server": "./bin/dev_server.sh", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject", 23 | "deploy": "./bin/deploy.sh" 24 | }, 25 | "eslintConfig": { 26 | "extends": "react-app" 27 | }, 28 | "browserslist": [ 29 | ">0.2%", 30 | "not dead", 31 | "not ie <= 11", 32 | "not op_mini all" 33 | ], 34 | "deploy-to-kube": { 35 | "prod": { 36 | "entrypoint": "src/services/server.js", 37 | "context": "kubesail-loopdelicious", 38 | "registry": "index.docker.io/v1/", 39 | "port": 5500, 40 | "protocol": "http", 41 | "registryUsername": "joycelin1600", 42 | "confirm": true 43 | } 44 | }, 45 | "devDependencies": { 46 | "newman": "^4.4.0" 47 | } 48 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/postmanlabs/node-doc-kube/920377f87d75456ea9716d19fc2f9fbe89b74aa6/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | React App 26 | 27 | 28 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { CopyToClipboard } from 'react-copy-to-clipboard'; 3 | import '../css/App.css'; 4 | 5 | const redirectTarget = process.env.NODE_ENV == 'development' ? 'http://localhost:5500' : process.env.REACT_APP_HOST; 6 | 7 | class App extends Component { 8 | 9 | state = { 10 | value: "", 11 | validUrl: true, 12 | encodedUrlReady: false, 13 | encodedUrl: "", 14 | copied: false 15 | } 16 | 17 | handleChange = (event) => { 18 | this.setState({ 19 | value: event.target.value 20 | }); 21 | } 22 | 23 | handleSubmit = (event) => { 24 | event.preventDefault(); 25 | 26 | let data = { 27 | originalUrl: this.refs['new-url'].value 28 | }; 29 | 30 | fetch(`${redirectTarget}/encode`, { 31 | method: 'POST', 32 | body: JSON.stringify(data), 33 | headers:{ 34 | 'Content-Type': 'application/json' 35 | } 36 | }).then(response => { 37 | // console.log(response); 38 | if (response.ok) { 39 | response.json().then(json => { 40 | if (json === "Try again") { 41 | this.setState({ 42 | validUrl: false 43 | }); 44 | } else { 45 | let path = json.catpath; 46 | this.setState({ 47 | encodedUrlReady: true, 48 | validUrl: true, 49 | encodedUrl: `${redirectTarget}/${path}` 50 | }) 51 | } 52 | }); 53 | } 54 | }) 55 | } 56 | 57 | render() { 58 | return ( 59 |
60 | 61 |
62 | catURL 63 |
64 | 65 | 66 | Fork me on GitHub 67 | 68 | 69 |
70 | 71 |
72 |
73 |
74 |

enter an original url to create a custom cat url:

75 | 76 | 77 |
78 |
79 |
80 | 81 | {!this.state.validUrl ? 82 |
83 |

Please enter a valid URL.

84 |
85 | : null } 86 | 87 | {this.state.encodedUrlReady && this.state.validUrl ? 88 |
89 |
90 |

is now:

91 |
92 |
93 | 94 | 95 | this.setState({copied: true})}> 98 | 99 | 100 | 101 |
102 |
103 | : null } 104 | 105 |
created by: Postman
106 |
107 | Intended for entertainment use only. No guarantees made. No one endorses anything contained in the URLs. 108 |

{"\n"}{"\n"}

109 | CatURL is not responsible for any content linked through its service. Use at your own risk. It's a litter bit amazing. 110 |
111 |
112 | 113 |
114 | 115 |
116 | ); 117 | } 118 | } 119 | 120 | export default App; 121 | -------------------------------------------------------------------------------- /src/components/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/components/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read http://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit http://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See http://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/css/App.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | font-family: 'Helvetica Neue'; 4 | background: linear-gradient(transparent 60%, white), url(../img/cat-face2.jpg) no-repeat; 5 | background-size: cover; 6 | } 7 | 8 | html, body { 9 | height: 100%; 10 | } 11 | 12 | h3 { 13 | padding: 5px; 14 | } 15 | 16 | .snipe { 17 | position: absolute; 18 | top: 0; 19 | right: 0; 20 | border: 0; 21 | } 22 | 23 | .nav { 24 | width: auto; 25 | font-weight: bold; 26 | display: block; 27 | border-style: solid; 28 | border-width: thin; 29 | border-color: lightgray; 30 | padding: 25px; 31 | margin: 0; 32 | } 33 | 34 | .nav-brand { 35 | width: 100px; 36 | height: auto; 37 | display: block; 38 | font-size: 30px; 39 | color: #5096FF; 40 | } 41 | 42 | .guts { 43 | padding: 20px; 44 | } 45 | 46 | .hidden { 47 | display: none; 48 | } 49 | 50 | .guts { 51 | margin: 0 0 50px 0; 52 | } 53 | 54 | 55 | /*initial call to action to enter original_url*/ 56 | .message-contents { 57 | margin: 0 0 50px 0; 58 | height: 30px; 59 | font-size: 15px; 60 | } 61 | #notes { 62 | margin: 0 0 0 5px; 63 | width: 400px; 64 | line-height: 30px; 65 | font-size: 15px; 66 | background-color: rgba(255, 255, 255, 0.5); 67 | border: solid #5096FF 1px; 68 | /* display: inline-block; */ 69 | float: left; 70 | padding: 5px; 71 | border-radius: 6px 0 0 6px; 72 | } 73 | /*button styling*/ 74 | .btn { 75 | width: 100px; 76 | border-radius: 0 6px 6px 0; 77 | background-color: #5096FF; 78 | font-size: 15px; 79 | border: solid #5096FF 1px; 80 | float: left; 81 | padding: 11px; 82 | cursor: pointer; 83 | } 84 | .submit-btn { 85 | background-color: #5096FF; 86 | border: solid #5096FF 1px; 87 | } 88 | .copy-btn { 89 | background-color: darkgray; 90 | color: black; 91 | border-color: darkgray; 92 | } 93 | .btn:hover { 94 | background-color: #CCC72F; 95 | color: black; 96 | border-color: #CCC72F; 97 | } 98 | 99 | .error-message { 100 | /* display: none; */ 101 | } 102 | 103 | /*dynamic display of encode_url*/ 104 | .display-contents { 105 | /* margin: 0 0 50px 0; 106 | line-height: 30px; */ 107 | margin: 0 0 50px 0; 108 | height: 30px; 109 | font-size: 15px; 110 | } 111 | .display { 112 | float: left; 113 | padding: 5px; 114 | } 115 | .new-url { 116 | width: 400px; 117 | float: left; 118 | border: solid 1px lightgray; 119 | background-color: rgba(255, 255, 255, 0.5); 120 | border-radius: 6px 0 0 6px; 121 | font-size: 15px; 122 | line-height: 30px; 123 | padding: 5px; 124 | } 125 | 126 | .copied { 127 | background-color: whitesmoke; 128 | } 129 | 130 | /*tooltip styling*/ 131 | .tooltip .tooltiptext::after { 132 | content: " "; 133 | position: absolute; 134 | bottom: 100%; /* At the top of the tooltip */ 135 | left: 50%; 136 | margin-left: -5px; 137 | border-width: 5px; 138 | border-style: solid; 139 | border-color: transparent transparent black transparent; 140 | } 141 | 142 | /*footer elements*/ 143 | #attribution { 144 | padding: 5px; 145 | margin: 250px 0 0 0; 146 | position: absolute; 147 | bottom: 35px; 148 | } 149 | 150 | .disclaimer { 151 | font-size: 10px; 152 | padding: 10px; 153 | margin: 0 0 0 15px; 154 | display: inline-block; 155 | position: absolute; 156 | bottom: 0; 157 | left: 0; 158 | } 159 | 160 | /*link styling*/ 161 | a { 162 | color: #5096FF; 163 | text-decoration: none; 164 | } 165 | 166 | /* unvisited link */ 167 | a:link { 168 | color: #5096FF 169 | } 170 | 171 | /* visited link */ 172 | a:visited { 173 | color: #5096FF; 174 | } 175 | 176 | /* mouse over link */ 177 | a:hover { 178 | color: #CCC72F; 179 | text-decoration: underline; 180 | } 181 | 182 | /* selected link */ 183 | a:active { 184 | color: #CCC72F; 185 | } 186 | 187 | .cf:before, 188 | .cf:after { 189 | content: " "; /* 1 */ 190 | display: table; /* 2 */ 191 | } 192 | 193 | .cf:after { 194 | clear: both; 195 | } -------------------------------------------------------------------------------- /src/css/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 5 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 13 | monospace; 14 | } 15 | -------------------------------------------------------------------------------- /src/css/reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | /* padding: 0; */ 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | /* line-height: 0; */ 26 | } 27 | /* HTML5 display-role reset for older browsers */ 28 | article, aside, details, figcaption, figure, 29 | footer, header, hgroup, menu, nav, section { 30 | display: block; 31 | } 32 | body { 33 | line-height: 1; 34 | } 35 | ol, ul { 36 | list-style: none; 37 | } 38 | blockquote, q { 39 | quotes: none; 40 | } 41 | blockquote:before, blockquote:after, 42 | q:before, q:after { 43 | content: ''; 44 | content: none; 45 | } 46 | table { 47 | border-collapse: collapse; 48 | border-spacing: 0; 49 | } 50 | 51 | 52 | /*http://www.paulirish.com/2012/box-sizing-border-box-ftw/*/ 53 | /* apply a natural box layout model to all elements, but allowing components to change */ 54 | html, body { 55 | box-sizing: border-box; 56 | } 57 | *, *:before, *:after { 58 | box-sizing: inherit; 59 | } 60 | 61 | 62 | 63 | /*http://nicolasgallagher.com/micro-clearfix-hack/*/ 64 | /** 65 | * For modern browsers 66 | * 1. The space content is one way to avoid an Opera bug when the 67 | * contenteditable attribute is included anywhere else in the document. 68 | * Otherwise it causes space to appear at the top and bottom of elements 69 | * that are clearfixed. 70 | * 2. The use of `table` rather than `block` is only necessary if using 71 | * `:before` to contain the top-margins of child elements. 72 | */ 73 | .cf:before, 74 | .cf:after { 75 | content: " "; /* 1 */ 76 | display: table; /* 2 */ 77 | } 78 | 79 | .cf:after { 80 | clear: both; 81 | } -------------------------------------------------------------------------------- /src/img/cat-face2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/postmanlabs/node-doc-kube/920377f87d75456ea9716d19fc2f9fbe89b74aa6/src/img/cat-face2.jpg -------------------------------------------------------------------------------- /src/img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/img/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/postmanlabs/node-doc-kube/920377f87d75456ea9716d19fc2f9fbe89b74aa6/src/img/screenshot.png -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './css/reset.css'; 4 | import './css/index.css'; 5 | import App from './components/App'; 6 | import * as serviceWorker from './components/serviceWorker'; 7 | 8 | ReactDOM.render(, document.getElementById('root')); 9 | 10 | // If you want your app to work offline and load faster, you can change 11 | // unregister() to register() below. Note this comes with some pitfalls. 12 | // Learn more about service workers: http://bit.ly/CRA-PWA 13 | serviceWorker.unregister(); 14 | -------------------------------------------------------------------------------- /src/seed.txt: -------------------------------------------------------------------------------- 1 | lazy 2 | sleepy 3 | fluffy 4 | plump 5 | tabby 6 | calico 7 | tuxedo 8 | siamese 9 | bombay 10 | pounce 11 | chase 12 | stalk 13 | jump 14 | saunter 15 | screech 16 | lick 17 | yawn 18 | rub 19 | meow 20 | mew 21 | purr 22 | hiss 23 | scamper 24 | scratch 25 | stretch 26 | whisker 27 | paw 28 | tail 29 | ennui 30 | feline 31 | kitty 32 | floof 33 | derp 34 | ,,,^..^,,, 35 | =^.^= 36 | =^_^= -------------------------------------------------------------------------------- /src/services/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const bodyParser = require('body-parser'); 3 | const fs = require ('fs'); 4 | 5 | const app = express(); 6 | 7 | // allow CORS access 8 | app.use(function(req, res, next) { 9 | res.header("Access-Control-Allow-Origin", "*"); 10 | res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); 11 | res.header("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE, OPTIONS"); 12 | res.header("Content-Type", "application/json"); 13 | next(); 14 | }); 15 | 16 | app.use(bodyParser.json()); 17 | app.use(bodyParser.urlencoded({ extended: false })); 18 | 19 | // to maintain state of encoded URLs 20 | const urlMap = {}; 21 | 22 | // https://stackoverflow.com/a/3975573/6815074 23 | const validateURL = (textval) => { 24 | 25 | const urlregex = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/; 26 | return urlregex.test(textval); 27 | 28 | } 29 | 30 | // POST request to add a new url to db 31 | app.post('/encode', function(req, res) { 32 | 33 | const originalUrl = req.body.originalUrl.toLowerCase(); 34 | 35 | // in postman 36 | // raw body, json >> form data 37 | // response "text" >> json object 38 | 39 | 40 | // check if valid URL 41 | if (!validateURL(originalUrl)) { 42 | 43 | res.json("Try again"); 44 | 45 | } else { 46 | 47 | // generate a new URL 48 | const catFile = fs.readFileSync("./src/seed.txt"); 49 | const catWords = catFile.toString().split("\n") 50 | 51 | let need_new_url = true; 52 | let catpath = ''; 53 | 54 | while (need_new_url) { 55 | 56 | for (i=0; i<5; i++) { 57 | let word = catWords[Math.floor(Math.random() * catWords.length)]; // random cat word 58 | let shouldUpper = Math.floor(Math.random() * 2); // initiate randomizer for upper / lower case 59 | shouldUpper ? word.toLowerCase() : word.toUpperCase(); // upper or lower case 60 | let punctuation = Math.floor(Math.random() * 2); // initiate randomizer for punctuation 61 | punctuation ? word += '.' : word += '-'; // separate by . or - 62 | catpath += word; 63 | } 64 | 65 | catpath = catpath.slice(0, -1); 66 | 67 | need_new_url = false; 68 | 69 | } 70 | 71 | // save the association between the original url and new url path 72 | if (catpath) { 73 | 74 | urlMap[catpath] = originalUrl; 75 | res.json({ 76 | error: null, 77 | catpath: catpath 78 | }); 79 | 80 | } 81 | 82 | } 83 | 84 | }); 85 | 86 | // For a user who enters encoded cat url in a browser, redirect to the original url. 87 | app.get('/:catpath', function(req, res) { 88 | 89 | if (urlMap[req.params.catpath]) { 90 | 91 | return res.redirect(urlMap[req.params.catpath]); 92 | 93 | } else { 94 | 95 | return "No URL found." 96 | 97 | } 98 | }) 99 | 100 | 101 | app.listen(process.env.PORT || 5500); 102 | console.log('app running on port ', process.env.PORT || 5500); 103 | --------------------------------------------------------------------------------