diff --git a/.github/workflows/cd-pipeline.yml b/.github/workflows/cd-pipeline.yml index 565fe6a347..410376d586 100644 --- a/.github/workflows/cd-pipeline.yml +++ b/.github/workflows/cd-pipeline.yml @@ -64,6 +64,7 @@ on: # rebuild any PRs and main branch changes - 'bitnami/spark/**' - 'bitnami/spring-cloud-dataflow/**' - 'bitnami/suitecrm/**' + - 'bitnami/tensorflow-resnet/**' - 'bitnami/tomcat/**' - 'bitnami/wildfly/**' - 'bitnami/wordpress/**' diff --git a/.vib/tensorflow-resnet/cypress/cypress.json b/.vib/tensorflow-resnet/cypress/cypress.json new file mode 100644 index 0000000000..c202974ea1 --- /dev/null +++ b/.vib/tensorflow-resnet/cypress/cypress.json @@ -0,0 +1,3 @@ +{ + "baseUrl": "http://localhost" +} diff --git a/.vib/tensorflow-resnet/cypress/cypress/fixtures/persian_cat.jpeg b/.vib/tensorflow-resnet/cypress/cypress/fixtures/persian_cat.jpeg new file mode 100644 index 0000000000..3522bc954a Binary files /dev/null and b/.vib/tensorflow-resnet/cypress/cypress/fixtures/persian_cat.jpeg differ diff --git a/.vib/tensorflow-resnet/cypress/cypress/integration/tensorflow_resnet_spec.js b/.vib/tensorflow-resnet/cypress/cypress/integration/tensorflow_resnet_spec.js new file mode 100644 index 0000000000..ef30b6343f --- /dev/null +++ b/.vib/tensorflow-resnet/cypress/cypress/integration/tensorflow_resnet_spec.js @@ -0,0 +1,55 @@ +/// + +it('classifies sample image', () => { + cy.fixture('persian_cat.jpeg').then((base64Img) => { + cy.loadB64Image(base64Img).then((loadedImg) => { + const img = loadedImg.get()[0]; + let inputImgWidth = img.width; + let inputImgHeight = img.height; + + // Convert the image to an array of RGB values + // Based on the tutorial: + // https://developers.google.com/codelabs/classify-images-tensorflow-serving#6 + let canvas = document.createElement('canvas'); + let context = canvas.getContext('2d'); + canvas.width = inputImgWidth; + canvas.height = inputImgHeight; + + let imgTensor = new Array(); + let pixelArray = new Array(); + context.drawImage(img, 0, 0); + for (let i = 0; i < inputImgHeight; i++) { + pixelArray[i] = new Array(); + for (let j = 0; j < inputImgWidth; j++) { + pixelArray[i][j] = new Array(); + pixelArray[i][j].push(context.getImageData(i, j, 1, 1).data[0] / 255); + pixelArray[i][j].push(context.getImageData(i, j, 1, 1).data[1] / 255); + pixelArray[i][j].push(context.getImageData(i, j, 1, 1).data[2] / 255); + } + } + imgTensor.push(pixelArray); + let data = JSON.stringify({ + instances: imgTensor, + }); + + cy.request({ + method: 'POST', + headers: { 'Content-Type': 'application/json; charset=utf-8' }, + url: `v1/models/resnet:predict`, + body: data, + }).then((response) => { + expect(response.status).to.eq(200); + + const predictionByCategory = response.body.predictions[0]; + expect( + Array.isArray(predictionByCategory) && + !predictionByCategory.includes('') && + !predictionByCategory.includes(null) + ).to.be.true; + + const maxValue = Math.max(...predictionByCategory); + expect(maxValue !== -Infinity && !isNaN(maxValue)).to.be.true; + }); + }); + }); +}); diff --git a/.vib/tensorflow-resnet/cypress/cypress/support/commands.js b/.vib/tensorflow-resnet/cypress/cypress/support/commands.js new file mode 100644 index 0000000000..24c884c981 --- /dev/null +++ b/.vib/tensorflow-resnet/cypress/cypress/support/commands.js @@ -0,0 +1,12 @@ +Cypress.Commands.add('loadB64Image', (base64) => { + const loadImage = () => { + const img = new Image(); + return new Promise((resolve) => { + img.onload = () => { + resolve(img); + }; + img.src = 'data:image/jpeg;base64,' + base64; + }); + }; + return loadImage(); +}); diff --git a/.vib/tensorflow-resnet/cypress/cypress/support/index.js b/.vib/tensorflow-resnet/cypress/cypress/support/index.js new file mode 100644 index 0000000000..37a498fb5b --- /dev/null +++ b/.vib/tensorflow-resnet/cypress/cypress/support/index.js @@ -0,0 +1,20 @@ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands'; + +// Alternatively you can use CommonJS syntax: +// require('./commands') diff --git a/.vib/tensorflow-resnet/goss/goss.yaml b/.vib/tensorflow-resnet/goss/goss.yaml new file mode 100644 index 0000000000..619b6ead5b --- /dev/null +++ b/.vib/tensorflow-resnet/goss/goss.yaml @@ -0,0 +1,21 @@ +addr: + tcp://tensorflow-resnet:{{ .Vars.service.ports.server }}: + reachable: true +port: + tcp:{{ .Vars.containerPorts.server }}: + listening: true + tcp:{{ .Vars.containerPorts.restApi }}: + listening: true +file: + /bitnami/model-data: + exists: true + filetype: directory + mode: "2777" + owner: root +command: + check-user-info: + exec: id + exit-status: 0 + stdout: + - uid={{ .Vars.containerSecurityContext.runAsUser }} + - /groups=.*{{ .Vars.podSecurityContext.fsGroup }}/ diff --git a/.vib/tensorflow-resnet/goss/vars.yaml b/.vib/tensorflow-resnet/goss/vars.yaml new file mode 100644 index 0000000000..f8a0d9c8b1 --- /dev/null +++ b/.vib/tensorflow-resnet/goss/vars.yaml @@ -0,0 +1,11 @@ +containerPorts: + server: 8500 + restApi: 8501 +service: + ports: + server: 8501 + restApi: 80 +containerSecurityContext: + runAsUser: 1002 +podSecurityContext: + fsGroup: 1002 \ No newline at end of file diff --git a/.vib/tensorflow-resnet/vib-publish.json b/.vib/tensorflow-resnet/vib-publish.json index 3606f8c885..a99ff8469c 100644 --- a/.vib/tensorflow-resnet/vib-publish.json +++ b/.vib/tensorflow-resnet/vib-publish.json @@ -16,6 +16,52 @@ } ] }, + "verify": { + "context": { + "resources": { + "url": "{SHA_ARCHIVE}", + "path": "/bitnami/tensorflow-resnet" + }, + "runtime_parameters": "Y29udGFpbmVyUG9ydHM6CiAgc2VydmVyOiA4NTAwCiAgcmVzdEFwaTogODUwMQpzZXJ2aWNlOgogIHR5cGU6IExvYWRCYWxhbmNlcgogIHBvcnRzOgogICAgc2VydmVyOiA4NTAxCiAgICByZXN0QXBpOiA4MApjb250YWluZXJTZWN1cml0eUNvbnRleHQ6CiAgZW5hYmxlZDogdHJ1ZQogIHJ1bkFzVXNlcjogMTAwMgogIHJ1bkFzTm9uUm9vdDogdHJ1ZQpwb2RTZWN1cml0eUNvbnRleHQ6CiAgZW5hYmxlZDogdHJ1ZQogIGZzR3JvdXA6IDEwMDIK", + "target_platform": { + "target_platform_id": "{VIB_ENV_TARGET_PLATFORM}", + "size": { + "name": "S4" + } + } + }, + "actions": [ + { + "action_id": "health-check", + "params": { + "endpoint": "lb-tensorflow-resnet-tf-serving-api", + "app_protocol": "HTTP" + } + }, + { + "action_id": "goss", + "params": { + "resources": { + "path": "/.vib/tensorflow-resnet/goss" + }, + "remote": { + "workload": "deploy-tensorflow-resnet" + }, + "vars_file": "vars.yaml" + } + }, + { + "action_id": "cypress", + "params": { + "resources": { + "path": "/.vib/tensorflow-resnet/cypress" + }, + "endpoint": "lb-tensorflow-resnet-tf-serving-api", + "app_protocol": "HTTP" + } + } + ] + }, "publish": { "actions": [ { diff --git a/.vib/tensorflow-resnet/vib-verify.json b/.vib/tensorflow-resnet/vib-verify.json index ea4ac0d525..9f0c9e68c5 100644 --- a/.vib/tensorflow-resnet/vib-verify.json +++ b/.vib/tensorflow-resnet/vib-verify.json @@ -15,6 +15,52 @@ "action_id": "helm-lint" } ] + }, + "verify": { + "context": { + "resources": { + "url": "{SHA_ARCHIVE}", + "path": "/bitnami/tensorflow-resnet" + }, + "runtime_parameters": "Y29udGFpbmVyUG9ydHM6CiAgc2VydmVyOiA4NTAwCiAgcmVzdEFwaTogODUwMQpzZXJ2aWNlOgogIHR5cGU6IExvYWRCYWxhbmNlcgogIHBvcnRzOgogICAgc2VydmVyOiA4NTAxCiAgICByZXN0QXBpOiA4MApjb250YWluZXJTZWN1cml0eUNvbnRleHQ6CiAgZW5hYmxlZDogdHJ1ZQogIHJ1bkFzVXNlcjogMTAwMgogIHJ1bkFzTm9uUm9vdDogdHJ1ZQpwb2RTZWN1cml0eUNvbnRleHQ6CiAgZW5hYmxlZDogdHJ1ZQogIGZzR3JvdXA6IDEwMDIK", + "target_platform": { + "target_platform_id": "{VIB_ENV_TARGET_PLATFORM}", + "size": { + "name": "S4" + } + } + }, + "actions": [ + { + "action_id": "health-check", + "params": { + "endpoint": "lb-tensorflow-resnet-tf-serving-api", + "app_protocol": "HTTP" + } + }, + { + "action_id": "goss", + "params": { + "resources": { + "path": "/.vib/tensorflow-resnet/goss" + }, + "remote": { + "workload": "deploy-tensorflow-resnet" + }, + "vars_file": "vars.yaml" + } + }, + { + "action_id": "cypress", + "params": { + "resources": { + "path": "/.vib/tensorflow-resnet/cypress" + }, + "endpoint": "lb-tensorflow-resnet-tf-serving-api", + "app_protocol": "HTTP" + } + } + ] } } }