├── tekton ├── persistent-volume.yaml ├── canary-templates │ ├── destrule-service.yaml │ ├── virtualservice-service.yaml │ └── deployment.yaml ├── auth │ └── serviceaccount.yaml ├── test-pipeline-run.sh ├── workspace-pvc.yaml ├── alpha │ ├── pipeline-resources.yaml │ ├── task-git-clone.yaml │ └── pipeline.yaml ├── taskrun.yaml ├── pipeline-run.yaml ├── task-git-clone.yaml ├── pipeline-build-deploy-canary.yaml ├── task-build-service.yaml └── task-canary-rollout.yaml ├── .gitignore ├── src ├── reviews │ ├── reviews-wlpcfg │ │ ├── shared │ │ │ └── .gitkeep │ │ ├── build.gradle │ │ ├── src │ │ │ └── test │ │ │ │ └── java │ │ │ │ └── it │ │ │ │ ├── TestApplication.java │ │ │ │ ├── rest │ │ │ │ └── LibertyRestEndpointTest.java │ │ │ │ └── EndpointTest.java │ │ ├── Dockerfile │ │ └── servers │ │ │ └── LibertyProjectServer │ │ │ └── server.xml │ ├── .gitignore │ ├── settings.gradle │ ├── build.gradle │ └── reviews-application │ │ ├── src │ │ ├── main │ │ │ ├── java │ │ │ │ └── application │ │ │ │ │ ├── ReviewsApplication.java │ │ │ │ │ └── rest │ │ │ │ │ └── LibertyRestEndpoint.java │ │ │ └── webapp │ │ │ │ └── WEB-INF │ │ │ │ ├── web.xml │ │ │ │ └── ibm-web-ext.xml │ │ └── test │ │ │ └── java │ │ │ └── test │ │ │ └── TestApplication.java │ │ └── build.gradle ├── mongodb │ ├── ratings_data.json │ ├── script.sh │ └── Dockerfile ├── productpage │ ├── test-requirements.txt │ ├── static │ │ └── bootstrap │ │ │ ├── fonts │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ └── glyphicons-halflings-regular.woff2 │ │ │ ├── js │ │ │ ├── npm.js │ │ │ └── bootstrap.min.js │ │ │ └── css │ │ │ ├── bootstrap-theme.min.css │ │ │ └── bootstrap-theme.css │ ├── requirements.txt │ ├── Dockerfile │ ├── templates │ │ ├── index.html │ │ └── productpage.html │ ├── tests │ │ └── unit │ │ │ └── test_productpage.py │ └── productpage.py ├── ratings │ ├── package.json │ ├── Dockerfile │ └── ratings.js ├── mysql │ ├── mysqldb-init.sql │ └── Dockerfile ├── details │ ├── Dockerfile │ └── details.rb └── build-services.sh ├── .DS_Store ├── istio ├── .DS_Store ├── mesh-mTLS.yaml ├── dest-rule-mTLS.yaml ├── gateway.yaml ├── canary.yaml ├── virtualservice.yaml └── destinationrules.yaml ├── images ├── argo-ui.png ├── argo-operator.png ├── operators-ossm.png ├── flow-tekton-istio.png ├── ossm-control-plane.png └── ossm-member-roll.png ├── manifests ├── destrule-details.yaml ├── destrule-productpage.yaml ├── destrule-ratings.yaml ├── destrule-reviews.yaml ├── gateway.yaml ├── virtualservice-canary-ratings-v2.yaml ├── virtualservice-gateway.yaml ├── deployment-ratings-v2.yaml └── default-deployment.yaml ├── kubernetes ├── deployment.yaml └── default-deployment.yaml └── README.md /tekton/persistent-volume.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | secret.yaml 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /src/reviews/reviews-wlpcfg/shared/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tekton/canary-templates/destrule-service.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/mongodb/ratings_data.json: -------------------------------------------------------------------------------- 1 | {rating: 5} 2 | {rating: 4} 3 | -------------------------------------------------------------------------------- /src/productpage/test-requirements.txt: -------------------------------------------------------------------------------- 1 | requests-mock==1.5.2 2 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jkapl/ci-cd-istio-tekton/HEAD/.DS_Store -------------------------------------------------------------------------------- /istio/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jkapl/ci-cd-istio-tekton/HEAD/istio/.DS_Store -------------------------------------------------------------------------------- /images/argo-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jkapl/ci-cd-istio-tekton/HEAD/images/argo-ui.png -------------------------------------------------------------------------------- /images/argo-operator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jkapl/ci-cd-istio-tekton/HEAD/images/argo-operator.png -------------------------------------------------------------------------------- /images/operators-ossm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jkapl/ci-cd-istio-tekton/HEAD/images/operators-ossm.png -------------------------------------------------------------------------------- /images/flow-tekton-istio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jkapl/ci-cd-istio-tekton/HEAD/images/flow-tekton-istio.png -------------------------------------------------------------------------------- /images/ossm-control-plane.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jkapl/ci-cd-istio-tekton/HEAD/images/ossm-control-plane.png -------------------------------------------------------------------------------- /images/ossm-member-roll.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jkapl/ci-cd-istio-tekton/HEAD/images/ossm-member-roll.png -------------------------------------------------------------------------------- /src/reviews/.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | reviews-application/build/ 3 | reviews-wlpcfg/servers/LibertyProjectServer/apps/ 4 | -------------------------------------------------------------------------------- /src/reviews/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'reviews' 2 | 3 | include 'reviews-application' 4 | include 'reviews-wlpcfg' 5 | -------------------------------------------------------------------------------- /src/reviews/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | group = 'org.istio' 3 | version = '1.0' 4 | repositories { 5 | mavenCentral() 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tekton/auth/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: build-bot 5 | secrets: 6 | - name: basic-user-pass 7 | - name: basic-user-pass-2 -------------------------------------------------------------------------------- /istio/mesh-mTLS.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: "authentication.maistra.io/v1" 2 | kind: "ServiceMeshPolicy" 3 | metadata: 4 | name: "default" 5 | namespace: dev-jk-cp 6 | spec: 7 | peers: 8 | - mtls: {} -------------------------------------------------------------------------------- /src/productpage/static/bootstrap/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jkapl/ci-cd-istio-tekton/HEAD/src/productpage/static/bootstrap/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /src/productpage/static/bootstrap/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jkapl/ci-cd-istio-tekton/HEAD/src/productpage/static/bootstrap/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /src/productpage/static/bootstrap/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jkapl/ci-cd-istio-tekton/HEAD/src/productpage/static/bootstrap/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /src/productpage/static/bootstrap/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jkapl/ci-cd-istio-tekton/HEAD/src/productpage/static/bootstrap/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /tekton/canary-templates/virtualservice-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1alpha3 2 | kind: VirtualService 3 | metadata: 4 | name: reviews 5 | spec: 6 | hosts: 7 | - reviews 8 | http: 9 | - route: -------------------------------------------------------------------------------- /tekton/test-pipeline-run.sh: -------------------------------------------------------------------------------- 1 | tkn pipeline start build-deploy-canary --param="GIT_URL=https://github.com/jkapl/ci-cd-istio-tekton" --param="BUILDER_IMAGE=quay.io/buildah/stable:v1.14.8" --param="REVISION=master" --showlog -------------------------------------------------------------------------------- /src/ratings/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "start": "node ratings.js" 4 | }, 5 | "dependencies": { 6 | "httpdispatcher": "1.0.0", 7 | "mongodb": "^2.2.31", 8 | "mysql": "^2.15.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /manifests/destrule-details.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1alpha3 2 | kind: DestinationRule 3 | metadata: 4 | name: details 5 | spec: 6 | host: details 7 | subsets: 8 | - name: v1 9 | labels: 10 | version: v1 11 | -------------------------------------------------------------------------------- /istio/dest-rule-mTLS.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: "networking.istio.io/v1alpha3" 2 | kind: "DestinationRule" 3 | metadata: 4 | name: "default" 5 | namespace: dev-jk-cp 6 | spec: 7 | host: "*.local" 8 | trafficPolicy: 9 | tls: 10 | mode: ISTIO_MUTUAL -------------------------------------------------------------------------------- /manifests/destrule-productpage.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1alpha3 2 | kind: DestinationRule 3 | metadata: 4 | name: productpage 5 | spec: 6 | host: productpage 7 | subsets: 8 | - name: v1 9 | labels: 10 | version: v1 11 | -------------------------------------------------------------------------------- /tekton/workspace-pvc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolumeClaim 3 | metadata: 4 | name: joel-ci-cd 5 | spec: 6 | #storageClassName: rook-cephfs 7 | accessModes: 8 | - ReadWriteOnce 9 | resources: 10 | requests: 11 | storage: 1Gi 12 | -------------------------------------------------------------------------------- /src/reviews/reviews-application/src/main/java/application/ReviewsApplication.java: -------------------------------------------------------------------------------- 1 | package application; 2 | import javax.ws.rs.ApplicationPath; 3 | import javax.ws.rs.core.Application; 4 | 5 | @ApplicationPath("/") 6 | public class ReviewsApplication extends Application { 7 | } 8 | -------------------------------------------------------------------------------- /manifests/destrule-ratings.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1alpha3 2 | kind: DestinationRule 3 | metadata: 4 | name: ratings 5 | spec: 6 | host: ratings 7 | subsets: 8 | - name: v1 9 | labels: 10 | version: v1 11 | - name: v2 12 | labels: 13 | version: v2 14 | -------------------------------------------------------------------------------- /tekton/alpha/pipeline-resources.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: tekton.dev/v1alpha1 2 | kind: PipelineResource 3 | metadata: 4 | name: git-source 5 | spec: 6 | type: git 7 | params: 8 | - name: url 9 | value: https://github.com/jkapl/ci-cd-tekton-istio 10 | - name: revision 11 | value: master -------------------------------------------------------------------------------- /tekton/taskrun.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: tekton.dev/v1beta1 2 | kind: TaskRun 3 | metadata: 4 | generateName: mytaskrun- 5 | spec: 6 | taskRef: 7 | name: git-clone 8 | params: 9 | - name: GIT_URL 10 | value: https://github.com/jkapl/ci-cd-istio-tekton 11 | - name: REVISION 12 | value: master -------------------------------------------------------------------------------- /istio/gateway.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1alpha3 2 | kind: Gateway 3 | metadata: 4 | name: bookinfo-gateway 5 | spec: 6 | selector: 7 | istio: ingressgateway # use istio default controller 8 | servers: 9 | - port: 10 | number: 80 11 | name: http 12 | protocol: HTTP 13 | hosts: 14 | - "*" 15 | -------------------------------------------------------------------------------- /manifests/destrule-reviews.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1alpha3 2 | kind: DestinationRule 3 | metadata: 4 | name: reviews 5 | spec: 6 | host: reviews 7 | subsets: 8 | - name: v1 9 | labels: 10 | version: v1 11 | - name: v2 12 | labels: 13 | version: v2 14 | - name: v3 15 | labels: 16 | version: v3 -------------------------------------------------------------------------------- /manifests/gateway.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1alpha3 2 | kind: Gateway 3 | metadata: 4 | name: bookinfo-gateway 5 | spec: 6 | selector: 7 | istio: ingressgateway # use istio default controller 8 | servers: 9 | - port: 10 | number: 80 11 | name: http 12 | protocol: HTTP 13 | hosts: 14 | - "*" 15 | -------------------------------------------------------------------------------- /manifests/virtualservice-canary-ratings-v2.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1alpha3 2 | kind: VirtualService 3 | metadata: 4 | name: ratings-v2 5 | spec: 6 | hosts: 7 | - ratings 8 | http: 9 | - route: 10 | - destination: 11 | host: ratings 12 | subset: v2 13 | weight: 10 14 | - destination: 15 | host: ratings 16 | subset: v1 17 | weight: 90 18 | -------------------------------------------------------------------------------- /src/mysql/mysqldb-init.sql: -------------------------------------------------------------------------------- 1 | # Initialize a mysql db with a 'test' db and be able test productpage with it. 2 | # mysql -h 127.0.0.1 -ppassword < mysqldb-init.sql 3 | 4 | CREATE DATABASE test; 5 | USE test; 6 | 7 | CREATE TABLE `ratings` ( 8 | `ReviewID` INT NOT NULL, 9 | `Rating` INT, 10 | PRIMARY KEY (`ReviewID`) 11 | ); 12 | INSERT INTO ratings (ReviewID, Rating) VALUES (1, 5); 13 | INSERT INTO ratings (ReviewID, Rating) VALUES (2, 4); 14 | -------------------------------------------------------------------------------- /istio/canary.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1alpha3 2 | kind: VirtualService 3 | metadata: 4 | name: reviews 5 | spec: 6 | hosts: 7 | - reviews 8 | http: 9 | - route: 10 | - destination: 11 | host: reviews 12 | subset: v1 13 | weight: 45 14 | - destination: 15 | host: reviews 16 | subset: v2 17 | weight: 10 18 | - destination: 19 | host: reviews 20 | subset: v3 21 | weight: 43 -------------------------------------------------------------------------------- /src/reviews/reviews-application/src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | Liberty Project 6 | 7 | 8 | index.html 9 | 10 | -------------------------------------------------------------------------------- /istio/virtualservice.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1alpha3 2 | kind: VirtualService 3 | metadata: 4 | name: bookinfo 5 | spec: 6 | hosts: 7 | - "*" 8 | gateways: 9 | - bookinfo-gateway 10 | http: 11 | - match: 12 | - uri: 13 | exact: /productpage 14 | - uri: 15 | exact: /login 16 | - uri: 17 | exact: /logout 18 | - uri: 19 | prefix: /api/v1/products 20 | route: 21 | - destination: 22 | host: productpage 23 | port: 24 | number: 9080 -------------------------------------------------------------------------------- /manifests/virtualservice-gateway.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1alpha3 2 | kind: VirtualService 3 | metadata: 4 | name: bookinfo 5 | spec: 6 | hosts: 7 | - "*" 8 | gateways: 9 | - bookinfo-gateway 10 | http: 11 | - match: 12 | - uri: 13 | exact: /productpage 14 | - uri: 15 | exact: /login 16 | - uri: 17 | exact: /logout 18 | - uri: 19 | prefix: /api/v1/products 20 | route: 21 | - destination: 22 | host: productpage 23 | port: 24 | number: 9080 -------------------------------------------------------------------------------- /src/productpage/static/bootstrap/js/npm.js: -------------------------------------------------------------------------------- 1 | // This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment. 2 | require('../../js/transition.js') 3 | require('../../js/alert.js') 4 | require('../../js/button.js') 5 | require('../../js/carousel.js') 6 | require('../../js/collapse.js') 7 | require('../../js/dropdown.js') 8 | require('../../js/modal.js') 9 | require('../../js/tooltip.js') 10 | require('../../js/popover.js') 11 | require('../../js/scrollspy.js') 12 | require('../../js/tab.js') 13 | require('../../js/affix.js') -------------------------------------------------------------------------------- /manifests/deployment-ratings-v2.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: ratings-v2 5 | labels: 6 | app: ratings 7 | version: v2 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: ratings 13 | version: v2 14 | template: 15 | metadata: 16 | annotations: 17 | sidecar.istio.io/inject: "true" 18 | labels: 19 | app: ratings 20 | version: v2 21 | spec: 22 | containers: 23 | - name: ratings 24 | image: quay.io/jkap/ratings:v2 25 | imagePullPolicy: IfNotPresent 26 | -------------------------------------------------------------------------------- /kubernetes/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: details-v1 5 | labels: 6 | app: details 7 | version: v1 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: details 13 | version: v1 14 | template: 15 | metadata: 16 | annotations: 17 | sidecar.istio.io/inject: "true" 18 | labels: 19 | app: details 20 | version: v1 21 | spec: 22 | containers: 23 | - name: details 24 | image: docker.io/maistra/examples-bookinfo-details-v1:0.12.0 25 | imagePullPolicy: IfNotPresent -------------------------------------------------------------------------------- /tekton/canary-templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: details-v1 5 | labels: 6 | app: details 7 | version: v1 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: details 13 | version: v1 14 | template: 15 | metadata: 16 | annotations: 17 | sidecar.istio.io/inject: "true" 18 | labels: 19 | app: details 20 | version: v1 21 | spec: 22 | containers: 23 | - name: details 24 | image: docker.io/maistra/examples-bookinfo-details-v1:0.12.0 25 | imagePullPolicy: IfNotPresent -------------------------------------------------------------------------------- /src/reviews/reviews-wlpcfg/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | } 5 | } 6 | 7 | apply plugin: 'eclipse' 8 | 9 | task copyApplication(type: Copy) { 10 | from '../reviews-application/build/libs/reviews-application-1.0.war' 11 | into 'servers/LibertyProjectServer/apps/' 12 | } 13 | 14 | task build(dependsOn: ['copyApplication']){ 15 | } 16 | 17 | task clean { 18 | delete "servers/LibertyProjectServer/apps" 19 | delete "servers/LibertyProjectServer/lib" 20 | delete "servers/LibertyProjectServer/logs" 21 | delete "servers/LibertyProjectServer/workarea" 22 | delete "servers/LibertyProjectServer/resources" 23 | } 24 | -------------------------------------------------------------------------------- /src/productpage/requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2019.3.9 2 | chardet==3.0.4 3 | Click==7.0 4 | contextlib2==0.5.5 5 | dominate==2.3.5 6 | Flask==1.0.2 7 | Flask-Bootstrap==3.3.7.1 8 | Flask-JSON==0.3.3 9 | future==0.17.1 10 | futures==3.1.1 11 | gevent==1.4.0 12 | greenlet==0.4.15 13 | idna==2.8 14 | itsdangerous==1.1.0 15 | jaeger-client==3.13.0 16 | Jinja2==2.10.1 17 | json2html==1.2.1 18 | MarkupSafe==0.23 19 | nose==1.3.7 20 | opentracing==1.2.2 21 | opentracing-instrumentation==2.4.3 22 | requests==2.21.0 23 | simplejson==3.16.0 24 | six==1.12.0 25 | threadloop==1.0.2 26 | thrift==0.11.0 27 | tornado==4.5.3 28 | urllib3==1.24.2 29 | visitor==0.1.3 30 | Werkzeug==0.15.5 31 | wrapt==1.11.1 32 | -------------------------------------------------------------------------------- /tekton/alpha/task-git-clone.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: tekton.dev/v1alpha1 2 | kind: Task 3 | metadata: 4 | name: git-clone-alpha 5 | spec: 6 | inputs: 7 | # params: 8 | resources: 9 | - name: source 10 | type: git 11 | steps: 12 | - name: git-clone 13 | image: gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init:v0.12.1 14 | script: | 15 | git clone -b $REVISION $GIT_URL 16 | cd ci-cd-istio-tekton 17 | RESULT_SHA="$(git rev-parse HEAD | tr -d '\n')" 18 | echo -n "$RESULT_SHA" 19 | env: 20 | - name: GIT_URL 21 | value: "$(inputs.resources.source.url)" 22 | - name: REVISION 23 | value: "$(inputs.resources.source.revision)" -------------------------------------------------------------------------------- /tekton/pipeline-run.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: tekton.dev/v1beta1 2 | kind: PipelineRun 3 | metadata: 4 | generateName: test-run- 5 | spec: 6 | serviceAccountName: build-bot 7 | pipelineRef: 8 | name: build-deploy-canary 9 | params: 10 | - name: GIT_URL 11 | value: https://github.com/jkapl/ci-cd-istio-tekton 12 | - name: BUILDER_IMAGE 13 | value: https://quay.io/buildah/stable:v1.14.0 14 | - name: REVISION 15 | value: master 16 | - name: SERVICE_NAME 17 | value: ratings 18 | - name: IMAGE_REPOSITORY 19 | value: quay.io/jkap 20 | - name: SERVICE_VERSION 21 | value: v2 22 | workspaces: 23 | - name: build-deploy-workspace 24 | persistentVolumeClaim: 25 | claimName: joel-ci-cd -------------------------------------------------------------------------------- /src/mongodb/script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Copyright Istio Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -e 18 | mongoimport --host localhost --db test --collection ratings --drop --file /app/data/ratings_data.json 19 | -------------------------------------------------------------------------------- /src/mysql/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Istio Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | FROM mysql:8.0.17 16 | # MYSQL_ROOT_PASSWORD must be supplied as an env var 17 | 18 | COPY ./mysqldb-init.sql /docker-entrypoint-initdb.d 19 | -------------------------------------------------------------------------------- /src/reviews/reviews-application/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'war' 2 | 3 | sourceCompatibility = 1.8 4 | 5 | repositories { 6 | mavenCentral() 7 | } 8 | 9 | dependencies { 10 | providedCompile group:'javax.websocket', name:'javax.websocket-api', version:'1.1' 11 | providedCompile group:'javax.ws.rs', name:'javax.ws.rs-api', version:'2.0' 12 | providedCompile group:'javax.json', name:'javax.json-api', version:'1.0' 13 | providedCompile 'javax.servlet:javax.servlet-api:3.1.0' 14 | providedCompile 'javax.annotation:javax.annotation-api:1.2' 15 | providedCompile 'javax.inject:javax.inject:1' 16 | providedCompile 'javax.enterprise.concurrent:javax.enterprise.concurrent-api:1.0' 17 | providedCompile 'javax.enterprise:cdi-api:1.2' 18 | providedCompile 'io.swagger:swagger-annotations:1.5.0' 19 | } 20 | -------------------------------------------------------------------------------- /src/mongodb/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Istio Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | FROM mongo:4.0.12-xenial 16 | WORKDIR /app/data/ 17 | COPY ratings_data.json /app/data/ 18 | COPY script.sh /docker-entrypoint-initdb.d/ 19 | RUN chmod +x /docker-entrypoint-initdb.d/script.sh 20 | -------------------------------------------------------------------------------- /tekton/task-git-clone.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: tekton.dev/v1beta1 2 | kind: Task 3 | metadata: 4 | name: git-clone 5 | spec: 6 | params: 7 | - name: GIT_URL 8 | type: string 9 | default: https://github.com/jkapl/ci-cd-istio-tekton 10 | - name: REVISION 11 | type: string 12 | default: master 13 | steps: 14 | - name: git-clone 15 | image: gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init:v0.12.1 16 | securityContext: 17 | privileged: true 18 | script: | 19 | cd $(workspaces.source.path) 20 | rm -rf ci-cd-istio-tekton 21 | git clone -b "$(params.REVISION)" "$(params.GIT_URL)" 22 | cd ci-cd-istio-tekton 23 | RESULT_SHA="$(git rev-parse HEAD | tr -d '\n')" 24 | pwd 25 | echo -n "$RESULT_SHA" 26 | workspaces: 27 | - name: source 28 | description: Location of source code -------------------------------------------------------------------------------- /src/reviews/reviews-application/src/test/java/test/TestApplication.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2017 Istio Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | *******************************************************************************/ 16 | package test; 17 | 18 | public class TestApplication { 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/ratings/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Istio Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | FROM node:12.9.0-slim 16 | 17 | COPY package.json /opt/microservices/ 18 | COPY ratings.js /opt/microservices/ 19 | WORKDIR /opt/microservices 20 | RUN npm install 21 | 22 | ARG service_version 23 | ENV SERVICE_VERSION ${service_version:-v1} 24 | 25 | EXPOSE 9080 26 | CMD ["node", "/opt/microservices/ratings.js", "9080"] 27 | -------------------------------------------------------------------------------- /istio/destinationrules.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1alpha3 2 | kind: DestinationRule 3 | metadata: 4 | name: productpage 5 | spec: 6 | host: productpage 7 | subsets: 8 | - name: v1 9 | labels: 10 | version: v1 11 | --- 12 | apiVersion: networking.istio.io/v1alpha3 13 | kind: DestinationRule 14 | metadata: 15 | name: reviews 16 | spec: 17 | host: reviews 18 | subsets: 19 | - name: v1 20 | labels: 21 | version: v1 22 | - name: v2 23 | labels: 24 | version: v2 25 | - name: v3 26 | labels: 27 | version: v3 28 | --- 29 | apiVersion: networking.istio.io/v1alpha3 30 | kind: DestinationRule 31 | metadata: 32 | name: ratings 33 | spec: 34 | host: ratings 35 | subsets: 36 | - name: v1 37 | labels: 38 | version: v1 39 | --- 40 | apiVersion: networking.istio.io/v1alpha3 41 | kind: DestinationRule 42 | metadata: 43 | name: details 44 | spec: 45 | host: details 46 | subsets: 47 | - name: v1 48 | labels: 49 | version: v1 50 | --- -------------------------------------------------------------------------------- /src/details/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Istio Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | FROM ruby:2.7-rc-slim 16 | 17 | COPY details.rb /opt/microservices/ 18 | 19 | ARG service_version 20 | ENV SERVICE_VERSION ${service_version:-v1} 21 | ARG enable_external_book_service 22 | ENV ENABLE_EXTERNAL_BOOK_SERVICE ${enable_external_book_service:-false} 23 | 24 | EXPOSE 9080 25 | WORKDIR /opt/microservices 26 | 27 | CMD ["ruby", "details.rb", "9080"] 28 | -------------------------------------------------------------------------------- /src/reviews/reviews-application/src/main/webapp/WEB-INF/ibm-web-ext.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/reviews/reviews-wlpcfg/src/test/java/it/TestApplication.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2017 Istio Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | *******************************************************************************/ 16 | package it; 17 | 18 | import org.junit.Test; 19 | 20 | public class TestApplication extends EndpointTest { 21 | 22 | @Test 23 | public void testDeployment() { 24 | testEndpoint("/index.html", "

Welcome to your Liberty Application

"); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/reviews/reviews-wlpcfg/src/test/java/it/rest/LibertyRestEndpointTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2017 Istio Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | *******************************************************************************/ 16 | package it.rest; 17 | 18 | import it.EndpointTest; 19 | 20 | import org.junit.Test; 21 | 22 | public class LibertyRestEndpointTest extends EndpointTest { 23 | 24 | @Test 25 | public void testDeployment() { 26 | testEndpoint("/rest", "Hello from the REST endpoint!"); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/reviews/reviews-wlpcfg/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Istio Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | FROM websphere-liberty:19.0.0.6-javaee8 16 | 17 | ENV SERVERDIRNAME reviews 18 | 19 | COPY ./servers/LibertyProjectServer /opt/ibm/wlp/usr/servers/defaultServer/ 20 | 21 | RUN /opt/ibm/wlp/bin/installUtility install --acceptLicense /opt/ibm/wlp/usr/servers/defaultServer/server.xml && \ 22 | chmod -R g=rwx /opt/ibm/wlp/output/defaultServer/ 23 | 24 | ARG service_version 25 | ARG enable_ratings 26 | ARG star_color 27 | ENV SERVICE_VERSION ${service_version:-v1} 28 | ENV ENABLE_RATINGS ${enable_ratings:-false} 29 | ENV STAR_COLOR ${star_color:-black} 30 | 31 | CMD ["/opt/ibm/wlp/bin/server", "run", "defaultServer"] 32 | -------------------------------------------------------------------------------- /src/productpage/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Istio Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | FROM python:3.7.4-slim 16 | 17 | COPY requirements.txt ./ 18 | RUN pip install --no-cache-dir -r requirements.txt 19 | 20 | COPY test-requirements.txt ./ 21 | RUN pip install --no-cache-dir -r test-requirements.txt 22 | 23 | COPY productpage.py /opt/microservices/ 24 | COPY tests/unit/* /opt/microservices/ 25 | COPY templates /opt/microservices/templates 26 | COPY static /opt/microservices/static 27 | COPY requirements.txt /opt/microservices/ 28 | 29 | ARG flood_factor 30 | ENV FLOOD_FACTOR ${flood_factor:-0} 31 | 32 | EXPOSE 9080 33 | WORKDIR /opt/microservices 34 | RUN python -m unittest discover 35 | 36 | CMD ["python", "productpage.py", "9080"] 37 | -------------------------------------------------------------------------------- /src/reviews/reviews-wlpcfg/servers/LibertyProjectServer/server.xml: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 | jaxrs-2.0 18 | jsonp-1.0 19 | 20 | 21 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/productpage/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "bootstrap/base.html" %} 2 | {% block metas %} 3 | 4 | 5 | 6 | {% endblock %} 7 | 8 | {% block styles %} 9 | 10 | 11 | 12 | 13 | 14 | {% endblock %} 15 | {% block scripts %} 16 | 17 | 18 | 19 | 20 | 21 | {% endblock %} 22 | {% block title %}Simple Bookstore App{% endblock %} 23 | {% block content %} 24 |

25 |

Hello! This is a simple bookstore application consisting of three services as shown below

26 |

27 | {% autoescape false %} 28 | {{ serviceTable }} 29 | {% endautoescape %} 30 |

31 |

Click on one of the links below to auto generate a request to the backend as a real user or a tester 32 |

33 |

34 |

Normal user

35 |

Test user

36 | {% endblock %} -------------------------------------------------------------------------------- /tekton/alpha/pipeline.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: tekton.dev/v1alpha1 2 | kind: Pipeline 3 | metadata: 4 | name: pipeline-git-clone-alpha 5 | spec: 6 | # params: 7 | # - name: appName 8 | # description: the deployment name, this value will be set as deployment name, version label and contaier name 9 | # - name: color 10 | # description: the hexa decimal color value 11 | # - name: message 12 | # description: the message like Hello, Bonjour or Hola 13 | # - name: contextDir 14 | # description: the context directory of the Java sources 15 | # default: . 16 | resources: 17 | - name: appSource 18 | type: git 19 | tasks: 20 | - name: git-clone 21 | taskRef: 22 | name: git-clone-alpha 23 | # params: 24 | # - name: contextDir 25 | # value: "$(params.contextDir)" 26 | # - name: dockerFile 27 | # value: src/main/docker/Dockerfile.jvm 28 | resources: 29 | inputs: 30 | - name: source 31 | resource: appSource 32 | # outputs: 33 | # - name: builtImage 34 | # resource: appImage 35 | # - name: deploy-bgc 36 | # taskRef: 37 | # name: yq-deploy 38 | # runAfter: 39 | # - build-java-app 40 | # resources: 41 | # inputs: 42 | # - name: source 43 | # resource: appSource 44 | # - name: appImage 45 | # resource: appImage 46 | # params: 47 | # - name: appName 48 | # value: "$(params.appName)" 49 | # - name: color 50 | # value: "$(params.color)" 51 | # - name: message 52 | # value: "$(params.message)" -------------------------------------------------------------------------------- /tekton/pipeline-build-deploy-canary.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: tekton.dev/v1beta1 2 | kind: Pipeline 3 | metadata: 4 | name: build-deploy-canary 5 | spec: 6 | workspaces: 7 | - name: build-deploy-workspace 8 | params: 9 | - name: GIT_URL 10 | type: string 11 | default: https://github.com/jkapl/ci-cd-istio-tekton 12 | - name: BUILDER_IMAGE 13 | type: string 14 | default: https://quay.io/buildah/stable 15 | - name: REVISION 16 | type: string 17 | default: master 18 | - name: SERVICE_NAME 19 | type: string 20 | default: productpage 21 | - name: SERVICE_VERSION 22 | type: string 23 | default: latest 24 | - name: IMAGE_REPOSITORY 25 | type: string 26 | default: quay.io/jkap 27 | tasks: 28 | - name: git-clone 29 | taskRef: 30 | name: git-clone 31 | params: 32 | - name: GIT_URL 33 | value: "$(params.GIT_URL)" 34 | - name: REVISION 35 | value: "$(params.REVISION)" 36 | workspaces: 37 | - name: source 38 | workspace: build-deploy-workspace 39 | - name: build-service 40 | taskRef: 41 | name: build-service 42 | params: 43 | - name: SERVICE_NAME 44 | value: "$(params.SERVICE_NAME)" 45 | - name: IMAGE_REPOSITORY 46 | value: "$(params.IMAGE_REPOSITORY)" 47 | - name: SERVICE_VERSION 48 | value: "$(params.SERVICE_VERSION)" 49 | workspaces: 50 | - name: source 51 | workspace: build-deploy-workspace 52 | runAfter: 53 | - git-clone 54 | - name: canary-rollout 55 | taskRef: 56 | name: canary-rollout 57 | params: 58 | - name: SERVICE_NAME 59 | value: "$(params.SERVICE_NAME)" 60 | - name: IMAGE_REPOSITORY 61 | value: "$(params.IMAGE_REPOSITORY)" 62 | - name: SERVICE_VERSION 63 | value: "$(params.SERVICE_VERSION)" 64 | workspaces: 65 | - name: source 66 | workspace: build-deploy-workspace 67 | runAfter: 68 | - build-service 69 | 70 | -------------------------------------------------------------------------------- /src/reviews/reviews-wlpcfg/src/test/java/it/EndpointTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2017 Istio Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | *******************************************************************************/ 16 | package it; 17 | 18 | import static org.junit.Assert.assertTrue; 19 | 20 | import javax.ws.rs.client.Client; 21 | import javax.ws.rs.client.ClientBuilder; 22 | import javax.ws.rs.client.Invocation; 23 | import javax.ws.rs.client.WebTarget; 24 | import javax.ws.rs.core.Response; 25 | 26 | public class EndpointTest { 27 | 28 | public void testEndpoint(String endpoint, String expectedOutput) { 29 | String port = System.getProperty("liberty.test.port"); 30 | String war = System.getProperty("war.name"); 31 | String url = "http://localhost:" + port + "/" + war + endpoint; 32 | System.out.println("Testing " + url); 33 | Response response = sendRequest(url, "GET"); 34 | int responseCode = response.getStatus(); 35 | assertTrue("Incorrect response code: " + responseCode, 36 | responseCode == 200); 37 | 38 | String responseString = response.readEntity(String.class); 39 | response.close(); 40 | assertTrue("Incorrect response, response is " + responseString, responseString.contains(expectedOutput)); 41 | } 42 | 43 | public Response sendRequest(String url, String requestType) { 44 | Client client = ClientBuilder.newClient(); 45 | System.out.println("Testing " + url); 46 | WebTarget target = client.target(url); 47 | Invocation.Builder invoBuild = target.request(); 48 | Response response = invoBuild.build(requestType).invoke(); 49 | return response; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tekton/task-build-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: tekton.dev/v1beta1 2 | kind: Task 3 | metadata: 4 | name: build-service 5 | spec: 6 | workspaces: 7 | - name: source 8 | 9 | params: 10 | - name: IMAGE_REPOSITORY 11 | description: Repository where buildah will push new image 12 | - name: BUILDER_IMAGE 13 | description: The location of the buildah builder image 14 | default: quay.io/buildah/stable 15 | - name: DOCKERFILE 16 | description: Path to the Dockerfile to build 17 | default: ./Dockerfile 18 | - name: SERVICE_NAME 19 | description: Path to the microservice directory to use as context 20 | default: details 21 | - name: SERVICE_VERSION 22 | description: Version of newly built service 23 | default: latest 24 | - name: CONTEXT 25 | description: Path to Dockerfile 26 | default: . 27 | - name: TLSVERIFY 28 | description: Verify the TLS on the registry endpoint (for push/pull to a non-TLS registry) 29 | default: "false" 30 | - name: FORMAT 31 | description: The format of the built container, oci or docker 32 | default: "oci" 33 | 34 | results: 35 | - name: IMAGE_DIGEST 36 | description: Digest of the image just built. 37 | 38 | steps: 39 | - name: build 40 | image: $(params.BUILDER_IMAGE) 41 | # workingDir: $(workspaces.source.path) 42 | # command: ['buildah', 'bud', '--format=$(params.FORMAT)', '--tls-verify=$(params.TLSVERIFY)', '--no-cache', '-f', '$(params.DOCKERFILE)', '-t', '$(params.IMAGE)', '$(params.CONTEXT)'] 43 | volumeMounts: 44 | - name: varlibcontainers 45 | mountPath: /var/lib/containers 46 | securityContext: 47 | privileged: true 48 | script: | 49 | cd $(workspaces.source.path)/ci-cd-istio-tekton/src/$(params.SERVICE_NAME) 50 | buildah bud --format=$(params.FORMAT) --tls-verify=$(params.TLSVERIFY) --no-cache -f $(params.DOCKERFILE) -t $(params.IMAGE_REPOSITORY)/$(params.SERVICE_NAME):$(params.SERVICE_VERSION) $(params.CONTEXT) 51 | 52 | - name: push 53 | image: $(params.BUILDER_IMAGE) 54 | env: 55 | - name: REGISTRY_AUTH_FILE 56 | value: /home/builder/.docker/config.json 57 | # workingDir: $(workspaces.source) 58 | # command: ['buildah', 'push', '--tls-verify=$(params.TLSVERIFY)', '$(params.IMAGE)'] 59 | volumeMounts: 60 | - name: varlibcontainers 61 | mountPath: /var/lib/containers 62 | securityContext: 63 | privileged: true 64 | script: | 65 | cd $(workspaces.source.path)/ci-cd-istio-tekton/src/$(params.SERVICE_NAME) 66 | cat ~/.docker/config.json 67 | buildah push --authfile ~/.docker/config.json --tls-verify=$(params.TLSVERIFY) $(params.IMAGE_REPOSITORY)/$(params.SERVICE_NAME):$(params.SERVICE_VERSION) 68 | 69 | volumes: 70 | - name: varlibcontainers 71 | emptyDir: {} -------------------------------------------------------------------------------- /src/productpage/tests/unit/test_productpage.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018 Istio Authors 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | # Run from the top level productpage directory with: 17 | # 18 | # pip install -r test-requirements.txt 19 | # python -m unittest discover tests/unit 20 | 21 | import unittest 22 | 23 | import requests_mock 24 | 25 | import productpage 26 | 27 | 28 | class ApplianceTest(unittest.TestCase): 29 | 30 | def setUp(self): 31 | self.app = productpage.app.test_client() 32 | 33 | @requests_mock.Mocker() 34 | def test_header_propagation_reviews(self, m): 35 | """ Check that tracing headers are forwarded correctly """ 36 | product_id = 0 37 | # Register expected headers with the mock. If the headers 38 | # don't match, the mock won't fire, an E500 will be triggered 39 | # and the test will fail. 40 | expected_headers = { 41 | 'x-request-id': '34eeb41d-d267-9e49-8b84-dde403fc5b72', 42 | 'x-b3-traceid': '40c7fdf104e3de67', 43 | 'x-b3-spanid': '40c7fdf104e3de67', 44 | 'x-b3-sampled': '1' 45 | } 46 | m.get("http://reviews:9080/reviews/%d" % product_id, text='{}', 47 | request_headers=expected_headers) 48 | 49 | uri = "/api/v1/products/%d/reviews" % product_id 50 | headers = { 51 | 'x-request-id': '34eeb41d-d267-9e49-8b84-dde403fc5b72', 52 | 'x-b3-traceid': '40c7fdf104e3de67', 53 | 'x-b3-spanid': '40c7fdf104e3de67', 54 | 'x-b3-sampled': '1' 55 | } 56 | actual = self.app.get(uri, headers=headers) 57 | self.assertEqual(200, actual.status_code) 58 | 59 | @requests_mock.Mocker() 60 | def test_header_propagation_ratings(self, m): 61 | """ Check that tracing headers are forwarded correctly """ 62 | product_id = 0 63 | # Register expected headers with the mock. If the headers 64 | # don't match, the mock won't fire, an E500 will be triggered 65 | # and the test will fail. 66 | expected_headers = { 67 | 'x-request-id': '34eeb41d-d267-9e49-8b84-dde403fc5b73', 68 | 'x-b3-traceid': '30c7fdf104e3de66', 69 | 'x-b3-spanid': '30c7fdf104e3de66', 70 | 'x-b3-sampled': '1' 71 | } 72 | m.get("http://ratings:9080/ratings/%d" % product_id, text='{}', 73 | request_headers=expected_headers) 74 | 75 | uri = "/api/v1/products/%d/ratings" % product_id 76 | headers = { 77 | 'x-request-id': '34eeb41d-d267-9e49-8b84-dde403fc5b73', 78 | 'x-b3-traceid': '30c7fdf104e3de66', 79 | 'x-b3-spanid': '30c7fdf104e3de66', 80 | 'x-b3-sampled': '1' 81 | } 82 | actual = self.app.get(uri, headers=headers) 83 | self.assertEqual(200, actual.status_code) 84 | -------------------------------------------------------------------------------- /src/build-services.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2017 Istio Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -o errexit 18 | 19 | if [ "$#" -ne 2 ]; then 20 | echo "Incorrect parameters" 21 | echo "Usage: build-services.sh " 22 | exit 1 23 | fi 24 | 25 | VERSION=$1 26 | PREFIX=$2 27 | SCRIPTDIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) 28 | 29 | pushd "$SCRIPTDIR/productpage" 30 | docker build --pull -t "${PREFIX}/examples-bookinfo-productpage-v1:${VERSION}" -t "${PREFIX}/examples-bookinfo-productpage-v1:latest" . 31 | #flooding 32 | docker build --pull -t "${PREFIX}/examples-bookinfo-productpage-v-flooding:${VERSION}" -t "${PREFIX}/examples-bookinfo-productpage-v-flooding:latest" --build-arg flood_factor=100 . 33 | popd 34 | 35 | pushd "$SCRIPTDIR/details" 36 | #plain build -- no calling external book service to fetch topics 37 | docker build --pull -t "${PREFIX}/examples-bookinfo-details-v1:${VERSION}" -t "${PREFIX}/examples-bookinfo-details-v1:latest" --build-arg service_version=v1 . 38 | #with calling external book service to fetch topic for the book 39 | docker build --pull -t "${PREFIX}/examples-bookinfo-details-v2:${VERSION}" -t "${PREFIX}/examples-bookinfo-details-v2:latest" --build-arg service_version=v2 \ 40 | --build-arg enable_external_book_service=true . 41 | popd 42 | 43 | pushd "$SCRIPTDIR/reviews" 44 | #java build the app. 45 | docker run --rm -u root -v "$(pwd)":/home/gradle/project -w /home/gradle/project gradle:4.8.1 gradle clean build 46 | pushd reviews-wlpcfg 47 | #plain build -- no ratings 48 | docker build --pull -t "${PREFIX}/examples-bookinfo-reviews-v1:${VERSION}" -t "${PREFIX}/examples-bookinfo-reviews-v1:latest" --build-arg service_version=v1 . 49 | #with ratings black stars 50 | docker build --pull -t "${PREFIX}/examples-bookinfo-reviews-v2:${VERSION}" -t "${PREFIX}/examples-bookinfo-reviews-v2:latest" --build-arg service_version=v2 \ 51 | --build-arg enable_ratings=true . 52 | #with ratings red stars 53 | docker build --pull -t "${PREFIX}/examples-bookinfo-reviews-v3:${VERSION}" -t "${PREFIX}/examples-bookinfo-reviews-v3:latest" --build-arg service_version=v3 \ 54 | --build-arg enable_ratings=true --build-arg star_color=red . 55 | popd 56 | popd 57 | 58 | pushd "$SCRIPTDIR/ratings" 59 | docker build --pull -t "${PREFIX}/examples-bookinfo-ratings-v1:${VERSION}" -t "${PREFIX}/examples-bookinfo-ratings-v1:latest" --build-arg service_version=v1 . 60 | docker build --pull -t "${PREFIX}/examples-bookinfo-ratings-v2:${VERSION}" -t "${PREFIX}/examples-bookinfo-ratings-v2:latest" --build-arg service_version=v2 . 61 | docker build --pull -t "${PREFIX}/examples-bookinfo-ratings-v-faulty:${VERSION}" -t "${PREFIX}/examples-bookinfo-ratings-v-faulty:latest" --build-arg service_version=v-faulty . 62 | docker build --pull -t "${PREFIX}/examples-bookinfo-ratings-v-delayed:${VERSION}" -t "${PREFIX}/examples-bookinfo-ratings-v-delayed:latest" --build-arg service_version=v-delayed . 63 | docker build --pull -t "${PREFIX}/examples-bookinfo-ratings-v-unavailable:${VERSION}" -t "${PREFIX}/examples-bookinfo-ratings-v-unavailable:latest" --build-arg service_version=v-unavailable . 64 | docker build --pull -t "${PREFIX}/examples-bookinfo-ratings-v-unhealthy:${VERSION}" -t "${PREFIX}/examples-bookinfo-ratings-v-unhealthy:latest" --build-arg service_version=v-unhealthy . 65 | popd 66 | 67 | pushd "$SCRIPTDIR/mysql" 68 | docker build --pull -t "${PREFIX}/examples-bookinfo-mysqldb:${VERSION}" -t "${PREFIX}/examples-bookinfo-mysqldb:latest" . 69 | popd 70 | 71 | pushd "$SCRIPTDIR/mongodb" 72 | docker build --pull -t "${PREFIX}/examples-bookinfo-mongodb:${VERSION}" -t "${PREFIX}/examples-bookinfo-mongodb:latest" . 73 | popd 74 | -------------------------------------------------------------------------------- /src/details/details.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | # 3 | # Copyright 2017 Istio Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | require 'webrick' 18 | require 'json' 19 | require 'net/http' 20 | 21 | if ARGV.length < 1 then 22 | puts "usage: #{$PROGRAM_NAME} port" 23 | exit(-1) 24 | end 25 | 26 | port = Integer(ARGV[0]) 27 | 28 | server = WEBrick::HTTPServer.new :BindAddress => '*', :Port => port 29 | 30 | trap 'INT' do server.shutdown end 31 | 32 | server.mount_proc '/health' do |req, res| 33 | res.status = 200 34 | res.body = {'status' => 'Details is healthy'}.to_json 35 | res['Content-Type'] = 'application/json' 36 | end 37 | 38 | server.mount_proc '/details' do |req, res| 39 | pathParts = req.path.split('/') 40 | headers = get_forward_headers(req) 41 | 42 | begin 43 | begin 44 | id = Integer(pathParts[-1]) 45 | rescue 46 | raise 'please provide numeric product id' 47 | end 48 | details = get_book_details(id, headers) 49 | res.body = details.to_json 50 | res['Content-Type'] = 'application/json' 51 | rescue => error 52 | res.body = {'error' => error}.to_json 53 | res['Content-Type'] = 'application/json' 54 | res.status = 400 55 | end 56 | end 57 | 58 | # TODO: provide details on different books. 59 | def get_book_details(id, headers) 60 | if ENV['ENABLE_EXTERNAL_BOOK_SERVICE'] === 'true' then 61 | # the ISBN of one of Comedy of Errors on the Amazon 62 | # that has Shakespeare as the single author 63 | isbn = '0486424618' 64 | return fetch_details_from_external_service(isbn, id, headers) 65 | end 66 | 67 | return { 68 | 'id' => id, 69 | 'author': 'William Shakespeare', 70 | 'year': 1595, 71 | 'type' => 'paperback', 72 | 'pages' => 200, 73 | 'publisher' => 'PublisherA', 74 | 'language' => 'English', 75 | 'ISBN-10' => '1234567890', 76 | 'ISBN-13' => '123-1234567890' 77 | } 78 | end 79 | 80 | def fetch_details_from_external_service(isbn, id, headers) 81 | uri = URI.parse('https://www.googleapis.com/books/v1/volumes?q=isbn:' + isbn) 82 | http = Net::HTTP.new(uri.host, ENV['DO_NOT_ENCRYPT'] === 'true' ? 80:443) 83 | http.read_timeout = 5 # seconds 84 | 85 | # DO_NOT_ENCRYPT is used to configure the details service to use either 86 | # HTTP (true) or HTTPS (false, default) when calling the external service to 87 | # retrieve the book information. 88 | # 89 | # Unless this environment variable is set to true, the app will use TLS (HTTPS) 90 | # to access external services. 91 | unless ENV['DO_NOT_ENCRYPT'] === 'true' then 92 | http.use_ssl = true 93 | end 94 | 95 | request = Net::HTTP::Get.new(uri.request_uri) 96 | headers.each { |header, value| request[header] = value } 97 | 98 | response = http.request(request) 99 | 100 | json = JSON.parse(response.body) 101 | book = json['items'][0]['volumeInfo'] 102 | 103 | language = book['language'] === 'en'? 'English' : 'unknown' 104 | type = book['printType'] === 'BOOK'? 'paperback' : 'unknown' 105 | isbn10 = get_isbn(book, 'ISBN_10') 106 | isbn13 = get_isbn(book, 'ISBN_13') 107 | 108 | return { 109 | 'id' => id, 110 | 'author': book['authors'][0], 111 | 'year': book['publishedDate'], 112 | 'type' => type, 113 | 'pages' => book['pageCount'], 114 | 'publisher' => book['publisher'], 115 | 'language' => language, 116 | 'ISBN-10' => isbn10, 117 | 'ISBN-13' => isbn13 118 | } 119 | 120 | end 121 | 122 | def get_isbn(book, isbn_type) 123 | isbn_dentifiers = book['industryIdentifiers'].select do |identifier| 124 | identifier['type'] === isbn_type 125 | end 126 | 127 | return isbn_dentifiers[0]['identifier'] 128 | end 129 | 130 | def get_forward_headers(request) 131 | headers = {} 132 | incoming_headers = [ 'x-request-id', 133 | 'x-b3-traceid', 134 | 'x-b3-spanid', 135 | 'x-b3-parentspanid', 136 | 'x-b3-sampled', 137 | 'x-b3-flags', 138 | 'x-ot-span-context', 139 | 'x-datadog-trace-id', 140 | 'x-datadog-parent-id', 141 | 'x-datadog-sampled' 142 | ] 143 | 144 | request.each do |header, value| 145 | if incoming_headers.include? header then 146 | headers[header] = value 147 | end 148 | end 149 | 150 | return headers 151 | end 152 | 153 | server.start 154 | -------------------------------------------------------------------------------- /src/productpage/templates/productpage.html: -------------------------------------------------------------------------------- 1 | {% extends "bootstrap/base.html" %} 2 | {% block metas %} 3 | 4 | 5 | 6 | {% endblock %} 7 | 8 | {% block styles %} 9 | 10 | 11 | 12 | 13 | 14 | {% endblock %} 15 | {% block scripts %} 16 | 17 | 18 | 19 | 20 | 21 | 22 | 27 | {% endblock %} 28 | {% block title %}Simple Bookstore App{% endblock %} 29 | {% block content %} 30 | 31 | 47 | 48 | 72 | 73 | 94 | 95 |
96 |
97 |
98 |

{{ product.title }}

99 | {% autoescape false %} 100 |

Summary: {{ product.descriptionHtml }}

101 | {% endautoescape %} 102 |
103 |
104 | 105 |
106 |
107 | {% if detailsStatus == 200: %} 108 |

Book Details

109 |
110 |
Type:
{{ details.type }} 111 |
Pages:
{{ details.pages }} 112 |
Publisher:
{{ details.publisher }} 113 |
Language:
{{ details.language }} 114 |
ISBN-10:
{{ details['ISBN-10'] }} 115 |
ISBN-13:
{{ details['ISBN-13'] }} 116 |
117 | {% else %} 118 |

Error fetching product details!

119 | {% if details: %} 120 |

{{ details.error }}

121 | {% endif %} 122 | {% endif %} 123 |
124 | 125 |
126 | {% if reviewsStatus == 200: %} 127 |

Book Reviews

128 | {% for review in reviews.reviews %} 129 |
130 |

{{ review.text }}

131 | {{ review.reviewer }} 132 | {% if review.rating: %} 133 | {% if review.rating.stars: %} 134 | 135 | 136 | {% for n in range(review.rating.stars) %} 137 | 138 | {% endfor %} 139 | 140 | {% for n in range(5 - review.rating.stars) %} 141 | 142 | {% endfor %} 143 | 144 | {% elif review.rating.error: %} 145 |

{{ review.rating.error }}

146 | {% endif %} 147 | {% endif %} 148 |
149 | {% endfor %} 150 | {% else %} 151 |

Error fetching product reviews!

152 | {% if reviews: %} 153 |

{{ reviews.error }}

154 | {% endif %} 155 | {% endif %} 156 |
157 |
158 |
159 | {% endblock %} -------------------------------------------------------------------------------- /manifests/default-deployment.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Istio Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | ################################################################################################## 16 | # Details service 17 | ################################################################################################## 18 | apiVersion: v1 19 | kind: Service 20 | metadata: 21 | name: details 22 | labels: 23 | app: details 24 | service: details 25 | spec: 26 | ports: 27 | - port: 9080 28 | name: http 29 | selector: 30 | app: details 31 | --- 32 | apiVersion: apps/v1 33 | kind: Deployment 34 | metadata: 35 | name: details-v1 36 | labels: 37 | app: details 38 | version: v1 39 | spec: 40 | replicas: 1 41 | selector: 42 | matchLabels: 43 | app: details 44 | version: v1 45 | template: 46 | metadata: 47 | annotations: 48 | sidecar.istio.io/inject: "true" 49 | labels: 50 | app: details 51 | version: v1 52 | spec: 53 | containers: 54 | - name: details 55 | image: docker.io/maistra/examples-bookinfo-details-v1:0.12.0 56 | imagePullPolicy: IfNotPresent 57 | ports: 58 | - containerPort: 9080 59 | --- 60 | ################################################################################################## 61 | # Ratings service 62 | ################################################################################################## 63 | apiVersion: v1 64 | kind: Service 65 | metadata: 66 | name: ratings 67 | labels: 68 | app: ratings 69 | service: ratings 70 | spec: 71 | ports: 72 | - port: 9080 73 | name: http 74 | selector: 75 | app: ratings 76 | --- 77 | apiVersion: apps/v1 78 | kind: Deployment 79 | metadata: 80 | name: ratings-v1 81 | labels: 82 | app: ratings 83 | version: v1 84 | spec: 85 | replicas: 1 86 | selector: 87 | matchLabels: 88 | app: ratings 89 | version: v1 90 | template: 91 | metadata: 92 | annotations: 93 | sidecar.istio.io/inject: "true" 94 | labels: 95 | app: ratings 96 | version: v1 97 | spec: 98 | containers: 99 | - name: ratings 100 | image: docker.io/maistra/examples-bookinfo-ratings-v1:0.12.0 101 | imagePullPolicy: IfNotPresent 102 | ports: 103 | - containerPort: 9080 104 | --- 105 | apiVersion: v1 106 | kind: ServiceAccount 107 | metadata: 108 | name: bookinfo-reviews 109 | --- 110 | ################################################################################################## 111 | # Reviews service 112 | ################################################################################################## 113 | apiVersion: v1 114 | kind: Service 115 | metadata: 116 | name: reviews 117 | labels: 118 | app: reviews 119 | service: reviews 120 | spec: 121 | ports: 122 | - port: 9080 123 | name: http 124 | selector: 125 | app: reviews 126 | --- 127 | apiVersion: apps/v1 128 | kind: Deployment 129 | metadata: 130 | name: reviews-v1 131 | labels: 132 | app: reviews 133 | version: v1 134 | spec: 135 | replicas: 1 136 | selector: 137 | matchLabels: 138 | app: reviews 139 | version: v1 140 | template: 141 | metadata: 142 | annotations: 143 | sidecar.istio.io/inject: "true" 144 | labels: 145 | app: reviews 146 | version: v1 147 | spec: 148 | containers: 149 | - name: reviews 150 | image: docker.io/maistra/examples-bookinfo-reviews-v1:0.12.0 151 | imagePullPolicy: IfNotPresent 152 | ports: 153 | - containerPort: 9080 154 | --- 155 | apiVersion: apps/v1 156 | kind: Deployment 157 | metadata: 158 | name: reviews-v2 159 | labels: 160 | app: reviews 161 | version: v2 162 | spec: 163 | replicas: 1 164 | selector: 165 | matchLabels: 166 | app: reviews 167 | version: v2 168 | template: 169 | metadata: 170 | annotations: 171 | sidecar.istio.io/inject: "true" 172 | labels: 173 | app: reviews 174 | version: v2 175 | spec: 176 | serviceAccountName: bookinfo-reviews 177 | containers: 178 | - name: reviews 179 | image: docker.io/maistra/examples-bookinfo-reviews-v2:0.12.0 180 | imagePullPolicy: IfNotPresent 181 | ports: 182 | - containerPort: 9080 183 | --- 184 | apiVersion: apps/v1 185 | kind: Deployment 186 | metadata: 187 | name: reviews-v3 188 | labels: 189 | app: reviews 190 | version: v3 191 | spec: 192 | replicas: 1 193 | selector: 194 | matchLabels: 195 | app: reviews 196 | version: v3 197 | template: 198 | metadata: 199 | annotations: 200 | sidecar.istio.io/inject: "true" 201 | labels: 202 | app: reviews 203 | version: v3 204 | spec: 205 | serviceAccountName: bookinfo-reviews 206 | containers: 207 | - name: reviews 208 | image: docker.io/maistra/examples-bookinfo-reviews-v3:0.12.0 209 | imagePullPolicy: IfNotPresent 210 | ports: 211 | - containerPort: 9080 212 | --- 213 | apiVersion: v1 214 | kind: ServiceAccount 215 | metadata: 216 | name: bookinfo-productpage 217 | --- 218 | ################################################################################################## 219 | # Productpage services 220 | ################################################################################################## 221 | apiVersion: v1 222 | kind: Service 223 | metadata: 224 | name: productpage 225 | labels: 226 | app: productpage 227 | service: productpage 228 | spec: 229 | ports: 230 | - port: 9080 231 | name: http 232 | selector: 233 | app: productpage 234 | --- 235 | apiVersion: apps/v1 236 | kind: Deployment 237 | metadata: 238 | name: productpage-v1 239 | labels: 240 | app: productpage 241 | version: v1 242 | spec: 243 | replicas: 1 244 | selector: 245 | matchLabels: 246 | app: productpage 247 | version: v1 248 | template: 249 | metadata: 250 | annotations: 251 | sidecar.istio.io/inject: "true" 252 | labels: 253 | app: productpage 254 | version: v1 255 | spec: 256 | serviceAccountName: bookinfo-productpage 257 | containers: 258 | - name: productpage 259 | image: docker.io/maistra/examples-bookinfo-productpage-v1:0.12.0 260 | imagePullPolicy: IfNotPresent 261 | ports: 262 | - containerPort: 9080 263 | --- -------------------------------------------------------------------------------- /kubernetes/default-deployment.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Istio Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | ################################################################################################## 16 | # Details service 17 | ################################################################################################## 18 | apiVersion: v1 19 | kind: Service 20 | metadata: 21 | name: details 22 | labels: 23 | app: details 24 | service: details 25 | spec: 26 | ports: 27 | - port: 9080 28 | name: http 29 | selector: 30 | app: details 31 | --- 32 | apiVersion: apps/v1 33 | kind: Deployment 34 | metadata: 35 | name: details-v1 36 | labels: 37 | app: details 38 | version: v1 39 | spec: 40 | replicas: 1 41 | selector: 42 | matchLabels: 43 | app: details 44 | version: v1 45 | template: 46 | metadata: 47 | annotations: 48 | sidecar.istio.io/inject: "true" 49 | labels: 50 | app: details 51 | version: v1 52 | spec: 53 | containers: 54 | - name: details 55 | image: docker.io/maistra/examples-bookinfo-details-v1:0.12.0 56 | imagePullPolicy: IfNotPresent 57 | ports: 58 | - containerPort: 9080 59 | --- 60 | ################################################################################################## 61 | # Ratings service 62 | ################################################################################################## 63 | apiVersion: v1 64 | kind: Service 65 | metadata: 66 | name: ratings 67 | labels: 68 | app: ratings 69 | service: ratings 70 | spec: 71 | ports: 72 | - port: 9080 73 | name: http 74 | selector: 75 | app: ratings 76 | --- 77 | apiVersion: apps/v1 78 | kind: Deployment 79 | metadata: 80 | name: ratings-v1 81 | labels: 82 | app: ratings 83 | version: v1 84 | spec: 85 | replicas: 1 86 | selector: 87 | matchLabels: 88 | app: ratings 89 | version: v1 90 | template: 91 | metadata: 92 | annotations: 93 | sidecar.istio.io/inject: "true" 94 | labels: 95 | app: ratings 96 | version: v1 97 | spec: 98 | containers: 99 | - name: ratings 100 | image: docker.io/maistra/examples-bookinfo-ratings-v1:0.12.0 101 | imagePullPolicy: IfNotPresent 102 | ports: 103 | - containerPort: 9080 104 | --- 105 | apiVersion: v1 106 | kind: ServiceAccount 107 | metadata: 108 | name: bookinfo-reviews 109 | --- 110 | ################################################################################################## 111 | # Reviews service 112 | ################################################################################################## 113 | apiVersion: v1 114 | kind: Service 115 | metadata: 116 | name: reviews 117 | labels: 118 | app: reviews 119 | service: reviews 120 | spec: 121 | ports: 122 | - port: 9080 123 | name: http 124 | selector: 125 | app: reviews 126 | --- 127 | apiVersion: apps/v1 128 | kind: Deployment 129 | metadata: 130 | name: reviews-v1 131 | labels: 132 | app: reviews 133 | version: v1 134 | spec: 135 | replicas: 1 136 | selector: 137 | matchLabels: 138 | app: reviews 139 | version: v1 140 | template: 141 | metadata: 142 | annotations: 143 | sidecar.istio.io/inject: "true" 144 | labels: 145 | app: reviews 146 | version: v1 147 | spec: 148 | containers: 149 | - name: reviews 150 | image: docker.io/maistra/examples-bookinfo-reviews-v1:0.12.0 151 | imagePullPolicy: IfNotPresent 152 | ports: 153 | - containerPort: 9080 154 | --- 155 | apiVersion: apps/v1 156 | kind: Deployment 157 | metadata: 158 | name: reviews-v2 159 | labels: 160 | app: reviews 161 | version: v2 162 | spec: 163 | replicas: 1 164 | selector: 165 | matchLabels: 166 | app: reviews 167 | version: v2 168 | template: 169 | metadata: 170 | annotations: 171 | sidecar.istio.io/inject: "true" 172 | labels: 173 | app: reviews 174 | version: v2 175 | spec: 176 | serviceAccountName: bookinfo-reviews 177 | containers: 178 | - name: reviews 179 | image: docker.io/maistra/examples-bookinfo-reviews-v2:0.12.0 180 | imagePullPolicy: IfNotPresent 181 | ports: 182 | - containerPort: 9080 183 | --- 184 | apiVersion: apps/v1 185 | kind: Deployment 186 | metadata: 187 | name: reviews-v3 188 | labels: 189 | app: reviews 190 | version: v3 191 | spec: 192 | replicas: 1 193 | selector: 194 | matchLabels: 195 | app: reviews 196 | version: v3 197 | template: 198 | metadata: 199 | annotations: 200 | sidecar.istio.io/inject: "true" 201 | labels: 202 | app: reviews 203 | version: v3 204 | spec: 205 | serviceAccountName: bookinfo-reviews 206 | containers: 207 | - name: reviews 208 | image: docker.io/maistra/examples-bookinfo-reviews-v3:0.12.0 209 | imagePullPolicy: IfNotPresent 210 | ports: 211 | - containerPort: 9080 212 | --- 213 | apiVersion: v1 214 | kind: ServiceAccount 215 | metadata: 216 | name: bookinfo-productpage 217 | --- 218 | ################################################################################################## 219 | # Productpage services 220 | ################################################################################################## 221 | apiVersion: v1 222 | kind: Service 223 | metadata: 224 | name: productpage 225 | labels: 226 | app: productpage 227 | service: productpage 228 | spec: 229 | ports: 230 | - port: 9080 231 | name: http 232 | selector: 233 | app: productpage 234 | --- 235 | apiVersion: apps/v1 236 | kind: Deployment 237 | metadata: 238 | name: productpage-v1 239 | labels: 240 | app: productpage 241 | version: v1 242 | spec: 243 | replicas: 1 244 | selector: 245 | matchLabels: 246 | app: productpage 247 | version: v1 248 | template: 249 | metadata: 250 | annotations: 251 | sidecar.istio.io/inject: "true" 252 | labels: 253 | app: productpage 254 | version: v1 255 | spec: 256 | serviceAccountName: bookinfo-productpage 257 | containers: 258 | - name: productpage 259 | image: docker.io/maistra/examples-bookinfo-productpage-v1:0.12.0 260 | imagePullPolicy: IfNotPresent 261 | ports: 262 | - containerPort: 9080 263 | --- -------------------------------------------------------------------------------- /src/reviews/reviews-application/src/main/java/application/rest/LibertyRestEndpoint.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2017 Istio Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | *******************************************************************************/ 16 | package application.rest; 17 | 18 | import javax.json.Json; 19 | import javax.json.JsonObject; 20 | import javax.json.JsonReader; 21 | import javax.ws.rs.GET; 22 | import javax.ws.rs.Path; 23 | import javax.ws.rs.PathParam; 24 | import javax.ws.rs.ProcessingException; 25 | import javax.ws.rs.client.Client; 26 | import javax.ws.rs.client.ClientBuilder; 27 | import javax.ws.rs.client.Invocation; 28 | import javax.ws.rs.client.WebTarget; 29 | import javax.ws.rs.core.Application; 30 | import javax.ws.rs.core.Context; 31 | import javax.ws.rs.core.HttpHeaders; 32 | import javax.ws.rs.core.MediaType; 33 | import javax.ws.rs.core.Response; 34 | import java.io.StringReader; 35 | 36 | @Path("/") 37 | public class LibertyRestEndpoint extends Application { 38 | 39 | private final static Boolean ratings_enabled = Boolean.valueOf(System.getenv("ENABLE_RATINGS")); 40 | private final static String star_color = System.getenv("STAR_COLOR") == null ? "black" : System.getenv("STAR_COLOR"); 41 | private final static String services_domain = System.getenv("SERVICES_DOMAIN") == null ? "" : ("." + System.getenv("SERVICES_DOMAIN")); 42 | private final static String ratings_hostname = System.getenv("RATINGS_HOSTNAME") == null ? "ratings" : System.getenv("RATINGS_HOSTNAME"); 43 | private final static String ratings_service = "http://" + ratings_hostname + services_domain + ":9080/ratings"; 44 | // HTTP headers to propagate for distributed tracing are documented at 45 | // https://istio.io/docs/tasks/telemetry/distributed-tracing/overview/#trace-context-propagation 46 | private final static String[] headers_to_proagate = {"x-request-id","x-b3-traceid","x-b3-spanid","x-b3-sampled","x-b3-flags", 47 | "x-ot-span-context","x-datadog-trace-id","x-datadog-parent-id","x-datadog-sampled", "end-user","user-agent"}; 48 | 49 | private String getJsonResponse (String productId, int starsReviewer1, int starsReviewer2) { 50 | String result = "{"; 51 | result += "\"id\": \"" + productId + "\","; 52 | result += "\"reviews\": ["; 53 | 54 | // reviewer 1: 55 | result += "{"; 56 | result += " \"reviewer\": \"Reviewer1\","; 57 | result += " \"text\": \"An extremely entertaining play by Shakespeare. The slapstick humour is refreshing!\""; 58 | if (ratings_enabled) { 59 | if (starsReviewer1 != -1) { 60 | result += ", \"rating\": {\"stars\": " + starsReviewer1 + ", \"color\": \"" + star_color + "\"}"; 61 | } 62 | else { 63 | result += ", \"rating\": {\"error\": \"Ratings service is currently unavailable\"}"; 64 | } 65 | } 66 | result += "},"; 67 | 68 | // reviewer 2: 69 | result += "{"; 70 | result += " \"reviewer\": \"Reviewer2\","; 71 | result += " \"text\": \"Absolutely fun and entertaining. The play lacks thematic depth when compared to other plays by Shakespeare.\""; 72 | if (ratings_enabled) { 73 | if (starsReviewer2 != -1) { 74 | result += ", \"rating\": {\"stars\": " + starsReviewer2 + ", \"color\": \"" + star_color + "\"}"; 75 | } 76 | else { 77 | result += ", \"rating\": {\"error\": \"Ratings service is currently unavailable\"}"; 78 | } 79 | } 80 | result += "}"; 81 | 82 | result += "]"; 83 | result += "}"; 84 | 85 | return result; 86 | } 87 | 88 | private JsonObject getRatings(String productId, HttpHeaders requestHeaders) { 89 | ClientBuilder cb = ClientBuilder.newBuilder(); 90 | Integer timeout = star_color.equals("black") ? 10000 : 2500; 91 | cb.property("com.ibm.ws.jaxrs.client.connection.timeout", timeout); 92 | cb.property("com.ibm.ws.jaxrs.client.receive.timeout", timeout); 93 | Client client = cb.build(); 94 | WebTarget ratingsTarget = client.target(ratings_service + "/" + productId); 95 | Invocation.Builder builder = ratingsTarget.request(MediaType.APPLICATION_JSON); 96 | for (String header : headers_to_proagate) { 97 | String value = requestHeaders.getHeaderString(header); 98 | if (value != null) { 99 | builder.header(header,value); 100 | } 101 | } 102 | try { 103 | Response r = builder.get(); 104 | 105 | int statusCode = r.getStatusInfo().getStatusCode(); 106 | if (statusCode == Response.Status.OK.getStatusCode()) { 107 | try (StringReader stringReader = new StringReader(r.readEntity(String.class)); 108 | JsonReader jsonReader = Json.createReader(stringReader)) { 109 | return jsonReader.readObject(); 110 | } 111 | } else { 112 | System.out.println("Error: unable to contact " + ratings_service + " got status of " + statusCode); 113 | return null; 114 | } 115 | } catch (ProcessingException e) { 116 | System.err.println("Error: unable to contact " + ratings_service + " got exception " + e); 117 | return null; 118 | } 119 | } 120 | 121 | @GET 122 | @Path("/health") 123 | public Response health() { 124 | return Response.ok().type(MediaType.APPLICATION_JSON).entity("{\"status\": \"Reviews is healthy\"}").build(); 125 | } 126 | 127 | @GET 128 | @Path("/reviews/{productId}") 129 | public Response bookReviewsById(@PathParam("productId") int productId, @Context HttpHeaders requestHeaders) { 130 | int starsReviewer1 = -1; 131 | int starsReviewer2 = -1; 132 | 133 | if (ratings_enabled) { 134 | JsonObject ratingsResponse = getRatings(Integer.toString(productId), requestHeaders); 135 | if (ratingsResponse != null) { 136 | if (ratingsResponse.containsKey("ratings")) { 137 | JsonObject ratings = ratingsResponse.getJsonObject("ratings"); 138 | if (ratings.containsKey("Reviewer1")){ 139 | starsReviewer1 = ratings.getInt("Reviewer1"); 140 | } 141 | if (ratings.containsKey("Reviewer2")){ 142 | starsReviewer2 = ratings.getInt("Reviewer2"); 143 | } 144 | } 145 | } 146 | } 147 | 148 | String jsonResStr = getJsonResponse(Integer.toString(productId), starsReviewer1, starsReviewer2); 149 | return Response.ok().type(MediaType.APPLICATION_JSON).entity(jsonResStr).build(); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /tekton/task-canary-rollout.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: tekton.dev/v1beta1 2 | kind: Task 3 | metadata: 4 | name: canary-rollout 5 | spec: 6 | workspaces: 7 | - name: source 8 | 9 | params: 10 | - name: SERVICE_NAME 11 | description: Path to the microservice directory to use as context. 12 | default: details 13 | - name: IMAGE_REPOSITORY 14 | description: Repository where buildah will push new image 15 | - name: SERVICE_VERSION 16 | description: Version of newly built service 17 | 18 | steps: 19 | - name: create-files 20 | image: docker.io/mikefarah/yq 21 | securityContext: 22 | privileged: true 23 | script: | 24 | echo Editing destination rule.... 25 | RULE_INDEX=$(yq r $(workspaces.source.path)/ci-cd-istio-tekton/manifests/destrule-$(params.SERVICE_NAME).yaml --length spec.subsets) 26 | yq w -i $(workspaces.source.path)/ci-cd-istio-tekton/manifests/destrule-$(params.SERVICE_NAME).yaml "spec.subsets[+]" name: 27 | yq w -i $(workspaces.source.path)/ci-cd-istio-tekton/manifests/destrule-$(params.SERVICE_NAME).yaml spec.subsets[$RULE_INDEX].name $(params.SERVICE_VERSION) 28 | yq w -i $(workspaces.source.path)/ci-cd-istio-tekton/manifests/destrule-$(params.SERVICE_NAME).yaml spec.subsets[$RULE_INDEX].labels.version $(params.SERVICE_VERSION) 29 | 30 | echo Adding deployment... 31 | cp $(workspaces.source.path)/ci-cd-istio-tekton/tekton/canary-templates/deployment.yaml $(workspaces.source.path)/ci-cd-istio-tekton/manifests/deployment-$(params.SERVICE_NAME)-$(params.SERVICE_VERSION).yaml 32 | yq w -i $(workspaces.source.path)/ci-cd-istio-tekton/manifests/deployment-$(params.SERVICE_NAME)-$(params.SERVICE_VERSION).yaml metadata.name $(params.SERVICE_NAME)-$(params.SERVICE_VERSION) 33 | yq w -i $(workspaces.source.path)/ci-cd-istio-tekton/manifests/deployment-$(params.SERVICE_NAME)-$(params.SERVICE_VERSION).yaml metadata.labels.app $(params.SERVICE_NAME) 34 | yq w -i $(workspaces.source.path)/ci-cd-istio-tekton/manifests/deployment-$(params.SERVICE_NAME)-$(params.SERVICE_VERSION).yaml metadata.labels.version $(params.SERVICE_VERSION) 35 | yq w -i $(workspaces.source.path)/ci-cd-istio-tekton/manifests/deployment-$(params.SERVICE_NAME)-$(params.SERVICE_VERSION).yaml spec.selector.matchLabels.app $(params.SERVICE_NAME) 36 | yq w -i $(workspaces.source.path)/ci-cd-istio-tekton/manifests/deployment-$(params.SERVICE_NAME)-$(params.SERVICE_VERSION).yaml spec.selector.matchLabels.version $(params.SERVICE_VERSION) 37 | yq w -i $(workspaces.source.path)/ci-cd-istio-tekton/manifests/deployment-$(params.SERVICE_NAME)-$(params.SERVICE_VERSION).yaml spec.template.metadata.labels.app $(params.SERVICE_NAME) 38 | yq w -i $(workspaces.source.path)/ci-cd-istio-tekton/manifests/deployment-$(params.SERVICE_NAME)-$(params.SERVICE_VERSION).yaml spec.template.metadata.labels.version $(params.SERVICE_VERSION) 39 | yq w -i $(workspaces.source.path)/ci-cd-istio-tekton/manifests/deployment-$(params.SERVICE_NAME)-$(params.SERVICE_VERSION).yaml spec.template.spec.containers[0].name $(params.SERVICE_NAME) 40 | yq w -i $(workspaces.source.path)/ci-cd-istio-tekton/manifests/deployment-$(params.SERVICE_NAME)-$(params.SERVICE_VERSION).yaml spec.template.spec.containers[0].image $(params.IMAGE_REPOSITORY)/$(params.SERVICE_NAME):$(params.SERVICE_VERSION) 41 | 42 | echo Adding virtual service... 43 | cp $(workspaces.source.path)/ci-cd-istio-tekton/tekton/canary-templates/virtualservice-service.yaml $(workspaces.source.path)/ci-cd-istio-tekton/manifests/virtualservice-canary-$(params.SERVICE_NAME)-$(params.SERVICE_VERSION).yaml 44 | yq w -i $(workspaces.source.path)/ci-cd-istio-tekton/manifests/virtualservice-canary-$(params.SERVICE_NAME)-$(params.SERVICE_VERSION).yaml metadata.name $(params.SERVICE_NAME)-$(params.SERVICE_VERSION) 45 | yq w -i $(workspaces.source.path)/ci-cd-istio-tekton/manifests/virtualservice-canary-$(params.SERVICE_NAME)-$(params.SERVICE_VERSION).yaml spec.hosts[0] $(params.SERVICE_NAME) 46 | yq w -i $(workspaces.source.path)/ci-cd-istio-tekton/manifests/virtualservice-canary-$(params.SERVICE_NAME)-$(params.SERVICE_VERSION).yaml "spec.http[0].route[+]" destination: 47 | yq w -i $(workspaces.source.path)/ci-cd-istio-tekton/manifests/virtualservice-canary-$(params.SERVICE_NAME)-$(params.SERVICE_VERSION).yaml spec.http[0].route[0].destination.host $(params.SERVICE_NAME) 48 | yq w -i $(workspaces.source.path)/ci-cd-istio-tekton/manifests/virtualservice-canary-$(params.SERVICE_NAME)-$(params.SERVICE_VERSION).yaml spec.http[0].route[0].destination.subset $(params.SERVICE_VERSION) 49 | yq w -i $(workspaces.source.path)/ci-cd-istio-tekton/manifests/virtualservice-canary-$(params.SERVICE_NAME)-$(params.SERVICE_VERSION).yaml spec.http[0].route[0].weight 10 50 | WEIGHT=$((90/$RULE_INDEX)) 51 | dest=0 52 | while [ "$dest" -lt $RULE_INDEX ]; do 53 | SUBSET=$(yq r $(workspaces.source.path)/ci-cd-istio-tekton/manifests/destrule-$(params.SERVICE_NAME).yaml spec.subsets[$dest].name) 54 | yq w -i $(workspaces.source.path)/ci-cd-istio-tekton/manifests/virtualservice-canary-$(params.SERVICE_NAME)-$(params.SERVICE_VERSION).yaml "spec.http[0].route[+]" destination: 55 | yq w -i $(workspaces.source.path)/ci-cd-istio-tekton/manifests/virtualservice-canary-$(params.SERVICE_NAME)-$(params.SERVICE_VERSION).yaml spec.http[0].route[$(($dest + 1))].destination.host $(params.SERVICE_NAME) 56 | yq w -i $(workspaces.source.path)/ci-cd-istio-tekton/manifests/virtualservice-canary-$(params.SERVICE_NAME)-$(params.SERVICE_VERSION).yaml spec.http[0].route[$(($dest + 1))].destination.subset $SUBSET 57 | yq w -i $(workspaces.source.path)/ci-cd-istio-tekton/manifests/virtualservice-canary-$(params.SERVICE_NAME)-$(params.SERVICE_VERSION).yaml spec.http[0].route[$(($dest + 1))].weight $WEIGHT 58 | dest=$((dest+1)) 59 | done 60 | TOTAL_WEIGHT=$(($RULE_INDEX*$WEIGHT)) 61 | TOTAL_WEIGHT=$(($TOTAL_WEIGHT+10)) 62 | if [ $TOTAL_WEIGHT -lt 100 ] 63 | then 64 | WEIGHT_REMAIN=$((100-$TOTAL_WEIGHT)) 65 | CURRENT_WEIGHT=$(yq r $(workspaces.source.path)/ci-cd-istio-tekton/manifests/virtualservice-canary-$(params.SERVICE_NAME)-$(params.SERVICE_VERSION).yaml spec.http[0].route[1].weight) 66 | yq w -i $(workspaces.source.path)/ci-cd-istio-tekton/manifests/virtualservice-canary-$(params.SERVICE_NAME)-$(params.SERVICE_VERSION).yaml spec.http[0].route[1].weight $(($CURRENT_WEIGHT+$WEIGHT_REMAIN)) 67 | fi 68 | 69 | - name: git-push 70 | image: gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init:v0.12.1 71 | securityContext: 72 | privileged: true 73 | script: | 74 | cd $(workspaces.source.path)/ci-cd-istio-tekton 75 | echo $(git status) 76 | cat ~/.gitconfig 77 | git config --global user.email "joelkaplan1@gmail.com" 78 | git config --global user.name "Joel Kaplan" 79 | git add . 80 | MESSAGE="Automated commit by Tekton: $(params.SERVICE_NAME):$(params.SERVICE_VERSION)" 81 | git commit -m "$MESSAGE" 82 | git push 83 | # - name: rollout 84 | # image: quay.io/openshift/origin-cli:latest 85 | # securityContext: 86 | # privileged: true 87 | # script: | 88 | # oc apply -f $(workspaces.source.path)/ci-cd-istio-tekton/kubernetes/deployment.yaml -n joel-ci-cd 89 | # # oc apply -f $(workspaces.source.path)/ci-cd-istio-tekton/istio/canary.yaml -n joel-ci-cd -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CI/CD on OpenShift with Tekton and Istio 2 | ## Introduction 3 | OpenShift is a powerful and secure platform for deploying containerized workloads. Combined with OpenShift Service Mesh and OpenShift Pipelines, it becomes a platform for enterprise agility, enabling continuous integration and continuous deployment via pipelines that build and push new code into managed canary rollouts.

With OpenShift Service Mesh, based on the Istio project; OpenShift Pipelines, based on Tekton; and Argo CD, a project that enables a GitOps approach to application management, developers can push changes to source code and within minutes see those changes deployed to a small subset of users. This iterative approach to software development means enterprises can rapidly and safely build new features and deploy them to end-users. 4 | 5 | ## Architecture 6 | 7 | This GitHub repository provides a demonstration of how a Tekton pipeline can be used in tandem with a service mesh to deploy workloads automatically with a canary rollout. 8 | 9 | ![pipeline-flow](images/flow-tekton-istio.png) 10 | 11 | ### Installation 12 | 13 | #### Software Versions Used 14 | - OpenShift 4.4.3 15 | - Argo CD Operator 0.0.8 16 | - Elasticsearch Operator 4.4.0 17 | - Red Hat OpenShift Jaeger Operator 1.17.2 18 | - Kiali Operator 1.12.11 19 | - OpenShift Pipelines Operator 1.0.1 20 | - Red Hat OpenShift Service Mesh 1.1.2 21 | 22 | Each of the operators above are available via OperatorHub in the OpenShift web console. 23 | 24 | #### OpenShift Service Mesh (OSSM) 25 | Use the OperatorHub tab in OpenShift to install the service mesh. Elasticsearch, Kiali, and Jaeger must be installed prior to Red Hat OpenShift Service Mesh. For more detailed instructions on the installation process, check out the [Istio demo on our GitHub page](https://github.ibm.com/cpat/ocp-chapter/blob/master/features/istio_guide.md) or consult the following link on the OpenShift official website: 26 | 27 | [Installation steps for OpenShift Service Mesh Operator](https://docs.openshift.com/container-platform/4.4/service_mesh/service_mesh_install/installing-ossm.html) 28 | 29 | Create a new project to hold the service mesh control plane. Then click on the Istio Service Mesh Control Plane tab in the OSSM operator details page. Click create ServiceMeshControlPlane. Click create to apply the default settings.

30 | ![ossm-control-plane](images/ossm-control-plane.png) 31 | 32 | Create a new project to hold the Bookinfo application, and add the name of that project to the service mesh member roll. Add the project under `spec.members`, then click create. This notifies the service mesh control plane to inject sidecar proxies into the pods running in the application project namespace. 33 | 34 | ![ossm-member-roll](images/ossm-member-roll.png) 35 | #### OpenShift Pipelines 36 | [Installation steps for OpenShift Pipelines Operator](https://docs.openshift.com/container-platform/4.4/pipelines/installing-pipelines.html) 37 | #### ArgoCD 38 | Create a new project to hold the Argo CD resources. Click on Installed Operators, then select the ArgoCD tab and click Create ArgoCD. Click create to apply the default settings. 39 | 40 | ![argo-operator](images/argo-operator.png) 41 | 42 | Argo will deploy several pods in this namespace, including a server to access the Argo CD web console where users can check the status of any applications monitored by the Argo CD controller. 43 | 44 | ![argo-ui](images/argo-ui.png) 45 | 46 | Other pods include a Redis instance and an application controller. 47 | 48 | For detailed installation instructions regarding the Argo CD operator, click here: [Installation steps for Argo CD Operator](https://argocd-operator.readthedocs.io/en/latest/install/openshift/) 49 | 50 | ## Demo 51 | 52 | #### Service Accounts/Authentication 53 | Like other Kubernetes resources, Tekton and Argo use service accounts to authorize their activity. Creating resources within the cluster of course requires specific permissions, but we can also use service accounts combined with secrets for authentication purposes, such as automatically pushing code changes to GitHub as part of a CI/CD pipeline. For Tekton, service accounts can be specified in a `PipelineRun` under `spec.serviceAccountName`. Inspect this field in the `tekton/pipeline-run.yaml` file.

When a `PipelineRun` executes, the permissions granted to the service account, as well as any associated secrets, are used to execute the tasks defined in the pipeline. In this demo, we are using a service account called build-bot, which requires two secrets: one to authenticate with the Quay.io image repository, and one to authenticate with GitHub. Within the `tekton/auth` folder, create a file called `secrets.yaml`. Fill out the information in the angle brackets below, and then run `oc apply -f secrets.yaml` 54 | 55 | ``` 56 | apiVersion: v1 57 | kind: Secret 58 | metadata: 59 | name: basic-user-pass 60 | annotations: 61 | tekton.dev/docker-0: https://quay.io # Described below 62 | type: kubernetes.io/basic-auth 63 | stringData: 64 | username: 65 | password: 66 | --- 67 | apiVersion: v1 68 | kind: Secret 69 | metadata: 70 | name: basic-user-pass-2 71 | annotations: 72 | tekton.dev/git-0: https://github.com # Described below 73 | type: kubernetes.io/basic-auth 74 | stringData: 75 | username: 76 | password: 77 | ``` 78 | 79 | Tekton will take the specified credentials and convert them into a format sufficient for the application to consume. From the [OpenShift/TektonCD documentation](https://github.com/openshift/tektoncd-pipeline/blob/release-v0.11.3/docs/auth.md): 80 | 81 | >In their native form, these secrets are unsuitable for consumption by Git and Docker. For Git, they need to be turned into (some form of) `.gitconfig`. For Docker, they need to be turned into a `~/.docker/config.json` file. Also, while each of these supports has multiple credentials for multiple domains, those credentials typically need to be blended into a single canonical keyring. 82 | > 83 | >To solve this, before any `PipelineResources` are retrieved, all pods execute a credential initialization process that accesses each of its secrets and aggregates them into their respective files in `$HOME`. [...] 84 | > 85 | >Credential annotation keys must begin with `tekton.dev/docker-` or `tekton.dev/git-`, and the value describes the URL of the host with which to use the credential. 86 | 87 | For more information on ServiceAccount permissions and RBAC in Kubernetes, check out this link: [Using RBAC Authorization - Kubernetes](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#service-account-permissions) 88 | 89 | #### Persistent Storage 90 | Tekton provides a `Workspace` resource, which combined with a `persistentVolumeClaim` (PVC), enables the sharing of data from one `Task` to the next within a `Pipeline`. As an example, in this demo a `Workspace` backed by a PVC is used to pass source code from GitHub between a `Task` that builds a new application image and a subsequent `Task` that pushes modified source code with new manifest files to GitHub.

The details of the PVC will vary depending on how administrators configured the OpenShift environment. In some cases it may be necessary to specify the `storageClass` required for the PVC to bind to an available volume. 91 | 92 | ``` 93 | apiVersion: v1 94 | kind: PersistentVolumeClaim 95 | metadata: 96 | name: joel-ci-cd 97 | spec: 98 | #storageClassName: rook-cephfs 99 | accessModes: 100 | - ReadWriteOnce 101 | resources: 102 | requests: 103 | storage: 1Gi 104 | ``` 105 | The `PipelineRun` resource specifies the `Workspace` required for the `Pipeline` to execute successfully. 106 | 107 | #### Tasks, Pipelines, and PipelineRuns 108 | 109 | The `Pipeline` in this repository consists of three `Tasks`. The first, `git-clone`, clones code from a GitHub repository and stores it in a `Workspace`. The second, `build-service`, builds a new image and pushes it to an image repository. Finally, `canary-rollout` creates and pushes new manifest files to GitHub, which include a Kubernetes `Deployment` specifying the new image to use, as well as Istio resources to enable a canary rollout of the new code. 110 | 111 | The last step in this demo configuration uses Argo CD to deploy the newly created manifest files from GitHub as workloads in the cluster. As mentioned above, the manifests include routing rules for the Istio control plane to split traffic between previous versions of the microservice, with 10% sent to the new version. The Istio resources include a `DestinationRule` and `VirtualService`: 112 | 113 | ``` 114 | add rules here 115 | ``` 116 | 117 | To initiate a `Pipeline` we create a `PipelineRun`: 118 | ``` 119 | oc create -f pipeline-run.yaml 120 | ``` 121 | By passing parameters to the `PipelineRun`, we can specify which microservice to we intend to be built and deployed to the cluster. Parameters can be specified in the `PipelineRun` yaml file directly or using the command line. In this example `PipelineRun` we specify the location of the source code and revision to use, the builder image and image repository to use to build and push the new image, as well as the microservice to deploy and the tag to apply to the new version of the image. 122 | 123 | ``` 124 | params: 125 | - name: GIT_URL 126 | value: https://github.com/jkapl/ci-cd-istio-tekton 127 | - name: BUILDER_IMAGE 128 | value: https://quay.io/buildah/stable:v1.14.0 129 | - name: REVISION 130 | value: master 131 | - name: SERVICE_NAME 132 | value: productpage 133 | - name: IMAGE_REPOSITORY 134 | value: quay.io/jkap 135 | - name: SERVICE_VERSION 136 | value: v2 137 | ``` 138 | 139 | ## References 140 | 141 | [Bookinfo application demo](https://github.com/tnscorcoran/openshift-servicemesh) -------------------------------------------------------------------------------- /src/ratings/ratings.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Istio Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | var http = require('http') 16 | var dispatcher = require('httpdispatcher') 17 | 18 | var port = parseInt(process.argv[2]) 19 | 20 | var userAddedRatings = [] // used to demonstrate POST functionality 21 | 22 | var unavailable = false 23 | var healthy = true 24 | 25 | if (process.env.SERVICE_VERSION === 'v-unavailable') { 26 | // make the service unavailable once in 60 seconds 27 | setInterval(function () { 28 | unavailable = !unavailable 29 | }, 60000); 30 | } 31 | 32 | if (process.env.SERVICE_VERSION === 'v-unhealthy') { 33 | // make the service unavailable once in 15 minutes for 15 minutes. 34 | // 15 minutes is chosen since the Kubernetes's exponential back-off is reset after 10 minutes 35 | // of successful execution 36 | // see https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#restart-policy 37 | // Kiali shows the last 10 or 30 minutes, so to show the error rate of 50%, 38 | // it will be required to run the service for 30 minutes, 15 minutes of each state (healthy/unhealthy) 39 | setInterval(function () { 40 | healthy = !healthy 41 | unavailable = !unavailable 42 | }, 900000); 43 | } 44 | 45 | /** 46 | * We default to using mongodb, if DB_TYPE is not set to mysql. 47 | */ 48 | if (process.env.SERVICE_VERSION === 'v2') { 49 | if (process.env.DB_TYPE === 'mysql') { 50 | var mysql = require('mysql') 51 | var hostName = process.env.MYSQL_DB_HOST 52 | var portNumber = process.env.MYSQL_DB_PORT 53 | var username = process.env.MYSQL_DB_USER 54 | var password = process.env.MYSQL_DB_PASSWORD 55 | } else { 56 | var MongoClient = require('mongodb').MongoClient 57 | var url = process.env.MONGO_DB_URL 58 | } 59 | } 60 | 61 | dispatcher.onPost(/^\/ratings\/[0-9]*/, function (req, res) { 62 | var productIdStr = req.url.split('/').pop() 63 | var productId = parseInt(productIdStr) 64 | var ratings = {} 65 | 66 | if (Number.isNaN(productId)) { 67 | res.writeHead(400, {'Content-type': 'application/json'}) 68 | res.end(JSON.stringify({error: 'please provide numeric product ID'})) 69 | return 70 | } 71 | 72 | try { 73 | ratings = JSON.parse(req.body) 74 | } catch (error) { 75 | res.writeHead(400, {'Content-type': 'application/json'}) 76 | res.end(JSON.stringify({error: 'please provide valid ratings JSON'})) 77 | return 78 | } 79 | 80 | if (process.env.SERVICE_VERSION === 'v2') { // the version that is backed by a database 81 | res.writeHead(501, {'Content-type': 'application/json'}) 82 | res.end(JSON.stringify({error: 'Post not implemented for database backed ratings'})) 83 | } else { // the version that holds ratings in-memory 84 | res.writeHead(200, {'Content-type': 'application/json'}) 85 | res.end(JSON.stringify(putLocalReviews(productId, ratings))) 86 | } 87 | }) 88 | 89 | dispatcher.onGet(/^\/ratings\/[0-9]*/, function (req, res) { 90 | var productIdStr = req.url.split('/').pop() 91 | var productId = parseInt(productIdStr) 92 | 93 | if (Number.isNaN(productId)) { 94 | res.writeHead(400, {'Content-type': 'application/json'}) 95 | res.end(JSON.stringify({error: 'please provide numeric product ID'})) 96 | } else if (process.env.SERVICE_VERSION === 'v2') { 97 | var firstRating = 0 98 | var secondRating = 0 99 | 100 | if (process.env.DB_TYPE === 'mysql') { 101 | var connection = mysql.createConnection({ 102 | host: hostName, 103 | port: portNumber, 104 | user: username, 105 | password: password, 106 | database: 'test' 107 | }) 108 | 109 | connection.connect(function(err) { 110 | if (err) { 111 | res.end(JSON.stringify({error: 'could not connect to ratings database'})) 112 | console.log(err) 113 | return 114 | } 115 | connection.query('SELECT Rating FROM ratings', function (err, results, fields) { 116 | if (err) { 117 | res.writeHead(500, {'Content-type': 'application/json'}) 118 | res.end(JSON.stringify({error: 'could not perform select'})) 119 | console.log(err) 120 | } else { 121 | if (results[0]) { 122 | firstRating = results[0].Rating 123 | } 124 | if (results[1]) { 125 | secondRating = results[1].Rating 126 | } 127 | var result = { 128 | id: productId, 129 | ratings: { 130 | Reviewer1: firstRating, 131 | Reviewer2: secondRating 132 | } 133 | } 134 | res.writeHead(200, {'Content-type': 'application/json'}) 135 | res.end(JSON.stringify(result)) 136 | } 137 | }) 138 | // close the connection 139 | connection.end() 140 | }) 141 | } else { 142 | MongoClient.connect(url, function (err, db) { 143 | if (err) { 144 | res.writeHead(500, {'Content-type': 'application/json'}) 145 | res.end(JSON.stringify({error: 'could not connect to ratings database'})) 146 | } else { 147 | db.collection('ratings').find({}).toArray(function (err, data) { 148 | if (err) { 149 | res.writeHead(500, {'Content-type': 'application/json'}) 150 | res.end(JSON.stringify({error: 'could not load ratings from database'})) 151 | } else { 152 | firstRating = data[0].rating 153 | secondRating = data[1].rating 154 | var result = { 155 | id: productId, 156 | ratings: { 157 | Reviewer1: firstRating, 158 | Reviewer2: secondRating 159 | } 160 | } 161 | res.writeHead(200, {'Content-type': 'application/json'}) 162 | res.end(JSON.stringify(result)) 163 | } 164 | // close DB once done: 165 | db.close() 166 | }) 167 | } 168 | }) 169 | } 170 | } else { 171 | if (process.env.SERVICE_VERSION === 'v-faulty') { 172 | // in half of the cases return error, 173 | // in another half proceed as usual 174 | var random = Math.random(); // returns [0,1] 175 | if (random <= 0.5) { 176 | getLocalReviewsServiceUnavailable(res) 177 | } else { 178 | getLocalReviewsSuccessful(res, productId) 179 | } 180 | } 181 | else if (process.env.SERVICE_VERSION === 'v-delayed') { 182 | // in half of the cases delay for 7 seconds, 183 | // in another half proceed as usual 184 | var random = Math.random(); // returns [0,1] 185 | if (random <= 0.5) { 186 | setTimeout(getLocalReviewsSuccessful, 7000, res, productId) 187 | } else { 188 | getLocalReviewsSuccessful(res, productId) 189 | } 190 | } 191 | else if (process.env.SERVICE_VERSION === 'v-unavailable' || process.env.SERVICE_VERSION === 'v-unhealthy') { 192 | if (unavailable) { 193 | getLocalReviewsServiceUnavailable(res) 194 | } else { 195 | getLocalReviewsSuccessful(res, productId) 196 | } 197 | } 198 | else { 199 | getLocalReviewsSuccessful(res, productId) 200 | } 201 | } 202 | }) 203 | 204 | dispatcher.onGet('/health', function (req, res) { 205 | if (healthy) { 206 | res.writeHead(200, {'Content-type': 'application/json'}) 207 | res.end(JSON.stringify({status: 'Ratings is healthy'})) 208 | } else { 209 | res.writeHead(500, {'Content-type': 'application/json'}) 210 | res.end(JSON.stringify({status: 'Ratings is not healthy'})) 211 | } 212 | }) 213 | 214 | function putLocalReviews (productId, ratings) { 215 | userAddedRatings[productId] = { 216 | id: productId, 217 | ratings: ratings 218 | } 219 | return getLocalReviews(productId) 220 | } 221 | 222 | function getLocalReviewsSuccessful(res, productId) { 223 | res.writeHead(200, {'Content-type': 'application/json'}) 224 | res.end(JSON.stringify(getLocalReviews(productId))) 225 | } 226 | 227 | function getLocalReviewsServiceUnavailable(res) { 228 | res.writeHead(503, {'Content-type': 'application/json'}) 229 | res.end(JSON.stringify({error: 'Service unavailable'})) 230 | } 231 | 232 | function getLocalReviews (productId) { 233 | if (typeof userAddedRatings[productId] !== 'undefined') { 234 | return userAddedRatings[productId] 235 | } 236 | return { 237 | id: productId, 238 | ratings: { 239 | 'Reviewer1': 1, 240 | 'Reviewer2': 2 241 | } 242 | } 243 | } 244 | 245 | function handleRequest (request, response) { 246 | try { 247 | console.log(request.method + ' ' + request.url) 248 | dispatcher.dispatch(request, response) 249 | } catch (err) { 250 | console.log(err) 251 | } 252 | } 253 | 254 | var server = http.createServer(handleRequest) 255 | 256 | server.listen(port, function () { 257 | console.log('Server listening on: http://0.0.0.0:%s', port) 258 | }) 259 | -------------------------------------------------------------------------------- /src/productpage/productpage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # Copyright 2017 Istio Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | from __future__ import print_function 19 | from flask_bootstrap import Bootstrap 20 | from flask import Flask, request, session, render_template, redirect, url_for 21 | from flask import _request_ctx_stack as stack 22 | from jaeger_client import Tracer, ConstSampler 23 | from jaeger_client.reporter import NullReporter 24 | from jaeger_client.codecs import B3Codec 25 | from opentracing.ext import tags 26 | from opentracing.propagation import Format 27 | from opentracing_instrumentation.request_context import get_current_span, span_in_context 28 | import simplejson as json 29 | import requests 30 | import sys 31 | from json2html import * 32 | import logging 33 | import requests 34 | import os 35 | import asyncio 36 | 37 | # These two lines enable debugging at httplib level (requests->urllib3->http.client) 38 | # You will see the REQUEST, including HEADERS and DATA, and RESPONSE with HEADERS but without DATA. 39 | # The only thing missing will be the response.body which is not logged. 40 | try: 41 | import http.client as http_client 42 | except ImportError: 43 | # Python 2 44 | import httplib as http_client 45 | http_client.HTTPConnection.debuglevel = 1 46 | 47 | app = Flask(__name__) 48 | logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) 49 | requests_log = logging.getLogger("requests.packages.urllib3") 50 | requests_log.setLevel(logging.DEBUG) 51 | requests_log.propagate = True 52 | app.logger.addHandler(logging.StreamHandler(sys.stdout)) 53 | app.logger.setLevel(logging.DEBUG) 54 | 55 | # Set the secret key to some random bytes. Keep this really secret! 56 | app.secret_key = b'_5#y2L"F4Q8z\n\xec]/' 57 | 58 | Bootstrap(app) 59 | 60 | servicesDomain = "" if (os.environ.get("SERVICES_DOMAIN") is None) else "." + os.environ.get("SERVICES_DOMAIN") 61 | detailsHostname = "details" if (os.environ.get("DETAILS_HOSTNAME") is None) else os.environ.get("DETAILS_HOSTNAME") 62 | ratingsHostname = "ratings" if (os.environ.get("RATINGS_HOSTNAME") is None) else os.environ.get("RATINGS_HOSTNAME") 63 | reviewsHostname = "reviews" if (os.environ.get("REVIEWS_HOSTNAME") is None) else os.environ.get("REVIEWS_HOSTNAME") 64 | 65 | flood_factor = 0 if (os.environ.get("FLOOD_FACTOR") is None) else int(os.environ.get("FLOOD_FACTOR")) 66 | 67 | details = { 68 | "name": "http://{0}{1}:9080".format(detailsHostname, servicesDomain), 69 | "endpoint": "details", 70 | "children": [] 71 | } 72 | 73 | ratings = { 74 | "name": "http://{0}{1}:9080".format(ratingsHostname, servicesDomain), 75 | "endpoint": "ratings", 76 | "children": [] 77 | } 78 | 79 | reviews = { 80 | "name": "http://{0}{1}:9080".format(reviewsHostname, servicesDomain), 81 | "endpoint": "reviews", 82 | "children": [ratings] 83 | } 84 | 85 | productpage = { 86 | "name": "http://{0}{1}:9080".format(detailsHostname, servicesDomain), 87 | "endpoint": "details", 88 | "children": [details, reviews] 89 | } 90 | 91 | service_dict = { 92 | "productpage": productpage, 93 | "details": details, 94 | "reviews": reviews, 95 | } 96 | 97 | # A note on distributed tracing: 98 | # 99 | # Although Istio proxies are able to automatically send spans, they need some 100 | # hints to tie together the entire trace. Applications need to propagate the 101 | # appropriate HTTP headers so that when the proxies send span information, the 102 | # spans can be correlated correctly into a single trace. 103 | # 104 | # To do this, an application needs to collect and propagate the following 105 | # headers from the incoming request to any outgoing requests: 106 | # 107 | # x-request-id 108 | # x-b3-traceid 109 | # x-b3-spanid 110 | # x-b3-parentspanid 111 | # x-b3-sampled 112 | # x-b3-flags 113 | # 114 | # This example code uses OpenTracing (http://opentracing.io/) to propagate 115 | # the 'b3' (zipkin) headers. Using OpenTracing for this is not a requirement. 116 | # Using OpenTracing allows you to add application-specific tracing later on, 117 | # but you can just manually forward the headers if you prefer. 118 | # 119 | # The OpenTracing example here is very basic. It only forwards headers. It is 120 | # intended as a reference to help people get started, eg how to create spans, 121 | # extract/inject context, etc. 122 | 123 | # A very basic OpenTracing tracer (with null reporter) 124 | tracer = Tracer( 125 | one_span_per_rpc=True, 126 | service_name='productpage', 127 | reporter=NullReporter(), 128 | sampler=ConstSampler(decision=True), 129 | extra_codecs={Format.HTTP_HEADERS: B3Codec()} 130 | ) 131 | 132 | 133 | def trace(): 134 | ''' 135 | Function decorator that creates opentracing span from incoming b3 headers 136 | ''' 137 | def decorator(f): 138 | def wrapper(*args, **kwargs): 139 | request = stack.top.request 140 | try: 141 | # Create a new span context, reading in values (traceid, 142 | # spanid, etc) from the incoming x-b3-*** headers. 143 | span_ctx = tracer.extract( 144 | Format.HTTP_HEADERS, 145 | dict(request.headers) 146 | ) 147 | # Note: this tag means that the span will *not* be 148 | # a child span. It will use the incoming traceid and 149 | # spanid. We do this to propagate the headers verbatim. 150 | rpc_tag = {tags.SPAN_KIND: tags.SPAN_KIND_RPC_SERVER} 151 | span = tracer.start_span( 152 | operation_name='op', child_of=span_ctx, tags=rpc_tag 153 | ) 154 | except Exception as e: 155 | # We failed to create a context, possibly due to no 156 | # incoming x-b3-*** headers. Start a fresh span. 157 | # Note: This is a fallback only, and will create fresh headers, 158 | # not propagate headers. 159 | span = tracer.start_span('op') 160 | with span_in_context(span): 161 | r = f(*args, **kwargs) 162 | return r 163 | wrapper.__name__ = f.__name__ 164 | return wrapper 165 | return decorator 166 | 167 | 168 | def getForwardHeaders(request): 169 | headers = {} 170 | 171 | # x-b3-*** headers can be populated using the opentracing span 172 | span = get_current_span() 173 | carrier = {} 174 | tracer.inject( 175 | span_context=span.context, 176 | format=Format.HTTP_HEADERS, 177 | carrier=carrier) 178 | 179 | headers.update(carrier) 180 | 181 | # We handle other (non x-b3-***) headers manually 182 | if 'user' in session: 183 | headers['end-user'] = session['user'] 184 | 185 | incoming_headers = ['x-request-id', 'x-datadog-trace-id', 'x-datadog-parent-id', 'x-datadog-sampled'] 186 | 187 | # Add user-agent to headers manually 188 | if 'user-agent' in request.headers: 189 | headers['user-agent'] = request.headers.get('user-agent') 190 | 191 | for ihdr in incoming_headers: 192 | val = request.headers.get(ihdr) 193 | if val is not None: 194 | headers[ihdr] = val 195 | # print "incoming: "+ihdr+":"+val 196 | 197 | return headers 198 | 199 | 200 | # The UI: 201 | @app.route('/') 202 | @app.route('/index.html') 203 | def index(): 204 | """ Display productpage with normal user and test user buttons""" 205 | global productpage 206 | 207 | table = json2html.convert(json=json.dumps(productpage), 208 | table_attributes="class=\"table table-condensed table-bordered table-hover\"") 209 | 210 | return render_template('index.html', serviceTable=table) 211 | 212 | 213 | @app.route('/health') 214 | def health(): 215 | return 'Product page is healthy' 216 | 217 | 218 | @app.route('/login', methods=['POST']) 219 | def login(): 220 | user = request.values.get('username') 221 | response = app.make_response(redirect(request.referrer)) 222 | session['user'] = user 223 | return response 224 | 225 | 226 | @app.route('/logout', methods=['GET']) 227 | def logout(): 228 | response = app.make_response(redirect(request.referrer)) 229 | session.pop('user', None) 230 | return response 231 | 232 | # a helper function for asyncio.gather, does not return a value 233 | 234 | 235 | async def getProductReviewsIgnoreResponse(product_id, headers): 236 | getProductReviews(product_id, headers) 237 | 238 | # flood reviews with unnecessary requests to demonstrate Istio rate limiting, asynchoronously 239 | 240 | 241 | async def floodReviewsAsynchronously(product_id, headers): 242 | # the response is disregarded 243 | await asyncio.gather(*(getProductReviewsIgnoreResponse(product_id, headers) for _ in range(flood_factor))) 244 | 245 | # flood reviews with unnecessary requests to demonstrate Istio rate limiting 246 | 247 | 248 | def floodReviews(product_id, headers): 249 | loop = asyncio.new_event_loop() 250 | loop.run_until_complete(floodReviewsAsynchronously(product_id, headers)) 251 | loop.close() 252 | 253 | 254 | @app.route('/productpage') 255 | @trace() 256 | def front(): 257 | product_id = 0 # TODO: replace default value 258 | headers = getForwardHeaders(request) 259 | user = session.get('user', '') 260 | product = getProduct(product_id) 261 | detailsStatus, details = getProductDetails(product_id, headers) 262 | 263 | if flood_factor > 0: 264 | floodReviews(product_id, headers) 265 | 266 | reviewsStatus, reviews = getProductReviews(product_id, headers) 267 | return render_template( 268 | 'productpage.html', 269 | detailsStatus=detailsStatus, 270 | reviewsStatus=reviewsStatus, 271 | product=product, 272 | details=details, 273 | reviews=reviews, 274 | user=user) 275 | 276 | 277 | # The API: 278 | @app.route('/api/v1/products') 279 | def productsRoute(): 280 | return json.dumps(getProducts()), 200, {'Content-Type': 'application/json'} 281 | 282 | 283 | @app.route('/api/v1/products/') 284 | @trace() 285 | def productRoute(product_id): 286 | headers = getForwardHeaders(request) 287 | status, details = getProductDetails(product_id, headers) 288 | return json.dumps(details), status, {'Content-Type': 'application/json'} 289 | 290 | 291 | @app.route('/api/v1/products//reviews') 292 | @trace() 293 | def reviewsRoute(product_id): 294 | headers = getForwardHeaders(request) 295 | status, reviews = getProductReviews(product_id, headers) 296 | return json.dumps(reviews), status, {'Content-Type': 'application/json'} 297 | 298 | 299 | @app.route('/api/v1/products//ratings') 300 | @trace() 301 | def ratingsRoute(product_id): 302 | headers = getForwardHeaders(request) 303 | status, ratings = getProductRatings(product_id, headers) 304 | return json.dumps(ratings), status, {'Content-Type': 'application/json'} 305 | 306 | 307 | # Data providers: 308 | def getProducts(): 309 | return [ 310 | { 311 | 'id': 0, 312 | 'title': 'The Comedy of Errors', 313 | 'descriptionHtml': 'Wikipedia Summary: The Comedy of Errors is one of William Shakespeare\'s early plays. It is his shortest and one of his most farcical comedies, with a major part of the humour coming from slapstick and mistaken identity, in addition to puns and word play.' 314 | } 315 | ] 316 | 317 | 318 | def getProduct(product_id): 319 | products = getProducts() 320 | if product_id + 1 > len(products): 321 | return None 322 | else: 323 | return products[product_id] 324 | 325 | 326 | def getProductDetails(product_id, headers): 327 | try: 328 | url = details['name'] + "/" + details['endpoint'] + "/" + str(product_id) 329 | res = requests.get(url, headers=headers, timeout=3.0) 330 | except BaseException: 331 | res = None 332 | if res and res.status_code == 200: 333 | return 200, res.json() 334 | else: 335 | status = res.status_code if res is not None and res.status_code else 500 336 | return status, {'error': 'Sorry, product details are currently unavailable for this book.'} 337 | 338 | 339 | def getProductReviews(product_id, headers): 340 | # Do not remove. Bug introduced explicitly for illustration in fault injection task 341 | # TODO: Figure out how to achieve the same effect using Envoy retries/timeouts 342 | for _ in range(2): 343 | try: 344 | url = reviews['name'] + "/" + reviews['endpoint'] + "/" + str(product_id) 345 | res = requests.get(url, headers=headers, timeout=3.0) 346 | except BaseException: 347 | res = None 348 | if res and res.status_code == 200: 349 | return 200, res.json() 350 | status = res.status_code if res is not None and res.status_code else 500 351 | return status, {'error': 'Sorry, product reviews are currently unavailable for this book.'} 352 | 353 | 354 | def getProductRatings(product_id, headers): 355 | try: 356 | url = ratings['name'] + "/" + ratings['endpoint'] + "/" + str(product_id) 357 | res = requests.get(url, headers=headers, timeout=3.0) 358 | except BaseException: 359 | res = None 360 | if res and res.status_code == 200: 361 | return 200, res.json() 362 | else: 363 | status = res.status_code if res is not None and res.status_code else 500 364 | return status, {'error': 'Sorry, product ratings are currently unavailable for this book.'} 365 | 366 | 367 | class Writer(object): 368 | def __init__(self, filename): 369 | self.file = open(filename, 'w') 370 | 371 | def write(self, data): 372 | self.file.write(data) 373 | 374 | def flush(self): 375 | self.file.flush() 376 | 377 | 378 | if __name__ == '__main__': 379 | if len(sys.argv) < 2: 380 | logging.error("usage: %s port" % (sys.argv[0])) 381 | sys.exit(-1) 382 | 383 | p = int(sys.argv[1]) 384 | logging.info("start at port %s" % (p)) 385 | app.run(host='::', port=p, debug=True, threaded=True) 386 | -------------------------------------------------------------------------------- /src/productpage/static/bootstrap/css/bootstrap-theme.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.5 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */.btn-danger,.btn-default,.btn-info,.btn-primary,.btn-success,.btn-warning{text-shadow:0 -1px 0 rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075)}.btn-danger.active,.btn-danger:active,.btn-default.active,.btn-default:active,.btn-info.active,.btn-info:active,.btn-primary.active,.btn-primary:active,.btn-success.active,.btn-success:active,.btn-warning.active,.btn-warning:active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-danger.disabled,.btn-danger[disabled],.btn-default.disabled,.btn-default[disabled],.btn-info.disabled,.btn-info[disabled],.btn-primary.disabled,.btn-primary[disabled],.btn-success.disabled,.btn-success[disabled],.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-danger,fieldset[disabled] .btn-default,fieldset[disabled] .btn-info,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-success,fieldset[disabled] .btn-warning{-webkit-box-shadow:none;box-shadow:none}.btn-danger .badge,.btn-default .badge,.btn-info .badge,.btn-primary .badge,.btn-success .badge,.btn-warning .badge{text-shadow:none}.btn.active,.btn:active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-o-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e0e0e0));background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#dbdbdb;border-color:#ccc}.btn-default:focus,.btn-default:hover{background-color:#e0e0e0;background-position:0 -15px}.btn-default.active,.btn-default:active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-default.disabled,.btn-default.disabled.active,.btn-default.disabled.focus,.btn-default.disabled:active,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled],.btn-default[disabled].active,.btn-default[disabled].focus,.btn-default[disabled]:active,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default,fieldset[disabled] .btn-default.active,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:active,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#e0e0e0;background-image:none}.btn-primary{background-image:-webkit-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-o-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#265a88));background-image:linear-gradient(to bottom,#337ab7 0,#265a88 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#245580}.btn-primary:focus,.btn-primary:hover{background-color:#265a88;background-position:0 -15px}.btn-primary.active,.btn-primary:active{background-color:#265a88;border-color:#245580}.btn-primary.disabled,.btn-primary.disabled.active,.btn-primary.disabled.focus,.btn-primary.disabled:active,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled],.btn-primary[disabled].active,.btn-primary[disabled].focus,.btn-primary[disabled]:active,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-primary.active,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:active,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#265a88;background-image:none}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#419641));background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:focus,.btn-success:hover{background-color:#419641;background-position:0 -15px}.btn-success.active,.btn-success:active{background-color:#419641;border-color:#3e8f3e}.btn-success.disabled,.btn-success.disabled.active,.btn-success.disabled.focus,.btn-success.disabled:active,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled],.btn-success[disabled].active,.btn-success[disabled].focus,.btn-success[disabled]:active,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success,fieldset[disabled] .btn-success.active,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:active,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#419641;background-image:none}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#2aabd2));background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:focus,.btn-info:hover{background-color:#2aabd2;background-position:0 -15px}.btn-info.active,.btn-info:active{background-color:#2aabd2;border-color:#28a4c9}.btn-info.disabled,.btn-info.disabled.active,.btn-info.disabled.focus,.btn-info.disabled:active,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled],.btn-info[disabled].active,.btn-info[disabled].focus,.btn-info[disabled]:active,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info,fieldset[disabled] .btn-info.active,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:active,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#2aabd2;background-image:none}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#eb9316));background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:focus,.btn-warning:hover{background-color:#eb9316;background-position:0 -15px}.btn-warning.active,.btn-warning:active{background-color:#eb9316;border-color:#e38d13}.btn-warning.disabled,.btn-warning.disabled.active,.btn-warning.disabled.focus,.btn-warning.disabled:active,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled],.btn-warning[disabled].active,.btn-warning[disabled].focus,.btn-warning[disabled]:active,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning,fieldset[disabled] .btn-warning.active,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:active,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#eb9316;background-image:none}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c12e2a));background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:focus,.btn-danger:hover{background-color:#c12e2a;background-position:0 -15px}.btn-danger.active,.btn-danger:active{background-color:#c12e2a;border-color:#b92c28}.btn-danger.disabled,.btn-danger.disabled.active,.btn-danger.disabled.focus,.btn-danger.disabled:active,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled],.btn-danger[disabled].active,.btn-danger[disabled].focus,.btn-danger[disabled]:active,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger,fieldset[disabled] .btn-danger.active,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:active,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#c12e2a;background-image:none}.img-thumbnail,.thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{background-color:#e8e8e8;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{background-color:#2e6da4;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-o-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#f8f8f8));background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075)}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-o-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dbdbdb),to(#e2e2e2));background-image:linear-gradient(to bottom,#dbdbdb 0,#e2e2e2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.075);box-shadow:inset 0 3px 9px rgba(0,0,0,.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-o-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#3c3c3c),to(#222));background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-o-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#080808),to(#0f0f0f));background-image:linear-gradient(to bottom,#080808 0,#0f0f0f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.25);box-shadow:inset 0 3px 9px rgba(0,0,0,.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,.25)}.navbar-fixed-bottom,.navbar-fixed-top,.navbar-static-top{border-radius:0}@media (max-width:767px){.navbar .navbar-nav .open .dropdown-menu>.active>a,.navbar .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}}.alert{text-shadow:0 1px 0 rgba(255,255,255,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#c8e5bc));background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);background-repeat:repeat-x;border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#b9def0));background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);background-repeat:repeat-x;border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#f8efc0));background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);background-repeat:repeat-x;border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-o-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#e7c3c3));background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);background-repeat:repeat-x;border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f5f5f5));background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x}.progress-bar{background-image:-webkit-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-o-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#286090));background-image:linear-gradient(to bottom,#337ab7 0,#286090 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);background-repeat:repeat-x}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#449d44));background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);background-repeat:repeat-x}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#31b0d5));background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);background-repeat:repeat-x}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#ec971f));background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);background-repeat:repeat-x}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c9302c));background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);background-repeat:repeat-x}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{text-shadow:0 -1px 0 #286090;background-image:-webkit-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2b669a));background-image:linear-gradient(to bottom,#337ab7 0,#2b669a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);background-repeat:repeat-x;border-color:#2b669a}.list-group-item.active .badge,.list-group-item.active:focus .badge,.list-group-item.active:hover .badge{text-shadow:none}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.05);box-shadow:0 1px 2px rgba(0,0,0,.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#d0e9c6));background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);background-repeat:repeat-x}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#c4e3f3));background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);background-repeat:repeat-x}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#faf2cc));background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);background-repeat:repeat-x}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-o-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#ebcccc));background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);background-repeat:repeat-x}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#e8e8e8),to(#f5f5f5));background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x;border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1)} -------------------------------------------------------------------------------- /src/productpage/static/bootstrap/css/bootstrap-theme.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.5 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | .btn-default, 7 | .btn-primary, 8 | .btn-success, 9 | .btn-info, 10 | .btn-warning, 11 | .btn-danger { 12 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .2); 13 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); 14 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); 15 | } 16 | .btn-default:active, 17 | .btn-primary:active, 18 | .btn-success:active, 19 | .btn-info:active, 20 | .btn-warning:active, 21 | .btn-danger:active, 22 | .btn-default.active, 23 | .btn-primary.active, 24 | .btn-success.active, 25 | .btn-info.active, 26 | .btn-warning.active, 27 | .btn-danger.active { 28 | -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); 29 | box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); 30 | } 31 | .btn-default.disabled, 32 | .btn-primary.disabled, 33 | .btn-success.disabled, 34 | .btn-info.disabled, 35 | .btn-warning.disabled, 36 | .btn-danger.disabled, 37 | .btn-default[disabled], 38 | .btn-primary[disabled], 39 | .btn-success[disabled], 40 | .btn-info[disabled], 41 | .btn-warning[disabled], 42 | .btn-danger[disabled], 43 | fieldset[disabled] .btn-default, 44 | fieldset[disabled] .btn-primary, 45 | fieldset[disabled] .btn-success, 46 | fieldset[disabled] .btn-info, 47 | fieldset[disabled] .btn-warning, 48 | fieldset[disabled] .btn-danger { 49 | -webkit-box-shadow: none; 50 | box-shadow: none; 51 | } 52 | .btn-default .badge, 53 | .btn-primary .badge, 54 | .btn-success .badge, 55 | .btn-info .badge, 56 | .btn-warning .badge, 57 | .btn-danger .badge { 58 | text-shadow: none; 59 | } 60 | .btn:active, 61 | .btn.active { 62 | background-image: none; 63 | } 64 | .btn-default { 65 | text-shadow: 0 1px 0 #fff; 66 | background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%); 67 | background-image: -o-linear-gradient(top, #fff 0%, #e0e0e0 100%); 68 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#e0e0e0)); 69 | background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%); 70 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0); 71 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 72 | background-repeat: repeat-x; 73 | border-color: #dbdbdb; 74 | border-color: #ccc; 75 | } 76 | .btn-default:hover, 77 | .btn-default:focus { 78 | background-color: #e0e0e0; 79 | background-position: 0 -15px; 80 | } 81 | .btn-default:active, 82 | .btn-default.active { 83 | background-color: #e0e0e0; 84 | border-color: #dbdbdb; 85 | } 86 | .btn-default.disabled, 87 | .btn-default[disabled], 88 | fieldset[disabled] .btn-default, 89 | .btn-default.disabled:hover, 90 | .btn-default[disabled]:hover, 91 | fieldset[disabled] .btn-default:hover, 92 | .btn-default.disabled:focus, 93 | .btn-default[disabled]:focus, 94 | fieldset[disabled] .btn-default:focus, 95 | .btn-default.disabled.focus, 96 | .btn-default[disabled].focus, 97 | fieldset[disabled] .btn-default.focus, 98 | .btn-default.disabled:active, 99 | .btn-default[disabled]:active, 100 | fieldset[disabled] .btn-default:active, 101 | .btn-default.disabled.active, 102 | .btn-default[disabled].active, 103 | fieldset[disabled] .btn-default.active { 104 | background-color: #e0e0e0; 105 | background-image: none; 106 | } 107 | .btn-primary { 108 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%); 109 | background-image: -o-linear-gradient(top, #337ab7 0%, #265a88 100%); 110 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#265a88)); 111 | background-image: linear-gradient(to bottom, #337ab7 0%, #265a88 100%); 112 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0); 113 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 114 | background-repeat: repeat-x; 115 | border-color: #245580; 116 | } 117 | .btn-primary:hover, 118 | .btn-primary:focus { 119 | background-color: #265a88; 120 | background-position: 0 -15px; 121 | } 122 | .btn-primary:active, 123 | .btn-primary.active { 124 | background-color: #265a88; 125 | border-color: #245580; 126 | } 127 | .btn-primary.disabled, 128 | .btn-primary[disabled], 129 | fieldset[disabled] .btn-primary, 130 | .btn-primary.disabled:hover, 131 | .btn-primary[disabled]:hover, 132 | fieldset[disabled] .btn-primary:hover, 133 | .btn-primary.disabled:focus, 134 | .btn-primary[disabled]:focus, 135 | fieldset[disabled] .btn-primary:focus, 136 | .btn-primary.disabled.focus, 137 | .btn-primary[disabled].focus, 138 | fieldset[disabled] .btn-primary.focus, 139 | .btn-primary.disabled:active, 140 | .btn-primary[disabled]:active, 141 | fieldset[disabled] .btn-primary:active, 142 | .btn-primary.disabled.active, 143 | .btn-primary[disabled].active, 144 | fieldset[disabled] .btn-primary.active { 145 | background-color: #265a88; 146 | background-image: none; 147 | } 148 | .btn-success { 149 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%); 150 | background-image: -o-linear-gradient(top, #5cb85c 0%, #419641 100%); 151 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#419641)); 152 | background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%); 153 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0); 154 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 155 | background-repeat: repeat-x; 156 | border-color: #3e8f3e; 157 | } 158 | .btn-success:hover, 159 | .btn-success:focus { 160 | background-color: #419641; 161 | background-position: 0 -15px; 162 | } 163 | .btn-success:active, 164 | .btn-success.active { 165 | background-color: #419641; 166 | border-color: #3e8f3e; 167 | } 168 | .btn-success.disabled, 169 | .btn-success[disabled], 170 | fieldset[disabled] .btn-success, 171 | .btn-success.disabled:hover, 172 | .btn-success[disabled]:hover, 173 | fieldset[disabled] .btn-success:hover, 174 | .btn-success.disabled:focus, 175 | .btn-success[disabled]:focus, 176 | fieldset[disabled] .btn-success:focus, 177 | .btn-success.disabled.focus, 178 | .btn-success[disabled].focus, 179 | fieldset[disabled] .btn-success.focus, 180 | .btn-success.disabled:active, 181 | .btn-success[disabled]:active, 182 | fieldset[disabled] .btn-success:active, 183 | .btn-success.disabled.active, 184 | .btn-success[disabled].active, 185 | fieldset[disabled] .btn-success.active { 186 | background-color: #419641; 187 | background-image: none; 188 | } 189 | .btn-info { 190 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); 191 | background-image: -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); 192 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#2aabd2)); 193 | background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%); 194 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0); 195 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 196 | background-repeat: repeat-x; 197 | border-color: #28a4c9; 198 | } 199 | .btn-info:hover, 200 | .btn-info:focus { 201 | background-color: #2aabd2; 202 | background-position: 0 -15px; 203 | } 204 | .btn-info:active, 205 | .btn-info.active { 206 | background-color: #2aabd2; 207 | border-color: #28a4c9; 208 | } 209 | .btn-info.disabled, 210 | .btn-info[disabled], 211 | fieldset[disabled] .btn-info, 212 | .btn-info.disabled:hover, 213 | .btn-info[disabled]:hover, 214 | fieldset[disabled] .btn-info:hover, 215 | .btn-info.disabled:focus, 216 | .btn-info[disabled]:focus, 217 | fieldset[disabled] .btn-info:focus, 218 | .btn-info.disabled.focus, 219 | .btn-info[disabled].focus, 220 | fieldset[disabled] .btn-info.focus, 221 | .btn-info.disabled:active, 222 | .btn-info[disabled]:active, 223 | fieldset[disabled] .btn-info:active, 224 | .btn-info.disabled.active, 225 | .btn-info[disabled].active, 226 | fieldset[disabled] .btn-info.active { 227 | background-color: #2aabd2; 228 | background-image: none; 229 | } 230 | .btn-warning { 231 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); 232 | background-image: -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); 233 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#eb9316)); 234 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%); 235 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0); 236 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 237 | background-repeat: repeat-x; 238 | border-color: #e38d13; 239 | } 240 | .btn-warning:hover, 241 | .btn-warning:focus { 242 | background-color: #eb9316; 243 | background-position: 0 -15px; 244 | } 245 | .btn-warning:active, 246 | .btn-warning.active { 247 | background-color: #eb9316; 248 | border-color: #e38d13; 249 | } 250 | .btn-warning.disabled, 251 | .btn-warning[disabled], 252 | fieldset[disabled] .btn-warning, 253 | .btn-warning.disabled:hover, 254 | .btn-warning[disabled]:hover, 255 | fieldset[disabled] .btn-warning:hover, 256 | .btn-warning.disabled:focus, 257 | .btn-warning[disabled]:focus, 258 | fieldset[disabled] .btn-warning:focus, 259 | .btn-warning.disabled.focus, 260 | .btn-warning[disabled].focus, 261 | fieldset[disabled] .btn-warning.focus, 262 | .btn-warning.disabled:active, 263 | .btn-warning[disabled]:active, 264 | fieldset[disabled] .btn-warning:active, 265 | .btn-warning.disabled.active, 266 | .btn-warning[disabled].active, 267 | fieldset[disabled] .btn-warning.active { 268 | background-color: #eb9316; 269 | background-image: none; 270 | } 271 | .btn-danger { 272 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%); 273 | background-image: -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%); 274 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c12e2a)); 275 | background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%); 276 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0); 277 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 278 | background-repeat: repeat-x; 279 | border-color: #b92c28; 280 | } 281 | .btn-danger:hover, 282 | .btn-danger:focus { 283 | background-color: #c12e2a; 284 | background-position: 0 -15px; 285 | } 286 | .btn-danger:active, 287 | .btn-danger.active { 288 | background-color: #c12e2a; 289 | border-color: #b92c28; 290 | } 291 | .btn-danger.disabled, 292 | .btn-danger[disabled], 293 | fieldset[disabled] .btn-danger, 294 | .btn-danger.disabled:hover, 295 | .btn-danger[disabled]:hover, 296 | fieldset[disabled] .btn-danger:hover, 297 | .btn-danger.disabled:focus, 298 | .btn-danger[disabled]:focus, 299 | fieldset[disabled] .btn-danger:focus, 300 | .btn-danger.disabled.focus, 301 | .btn-danger[disabled].focus, 302 | fieldset[disabled] .btn-danger.focus, 303 | .btn-danger.disabled:active, 304 | .btn-danger[disabled]:active, 305 | fieldset[disabled] .btn-danger:active, 306 | .btn-danger.disabled.active, 307 | .btn-danger[disabled].active, 308 | fieldset[disabled] .btn-danger.active { 309 | background-color: #c12e2a; 310 | background-image: none; 311 | } 312 | .thumbnail, 313 | .img-thumbnail { 314 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 315 | box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 316 | } 317 | .dropdown-menu > li > a:hover, 318 | .dropdown-menu > li > a:focus { 319 | background-color: #e8e8e8; 320 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 321 | background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 322 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); 323 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); 324 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); 325 | background-repeat: repeat-x; 326 | } 327 | .dropdown-menu > .active > a, 328 | .dropdown-menu > .active > a:hover, 329 | .dropdown-menu > .active > a:focus { 330 | background-color: #2e6da4; 331 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 332 | background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 333 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); 334 | background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); 335 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); 336 | background-repeat: repeat-x; 337 | } 338 | .navbar-default { 339 | background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%); 340 | background-image: -o-linear-gradient(top, #fff 0%, #f8f8f8 100%); 341 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#f8f8f8)); 342 | background-image: linear-gradient(to bottom, #fff 0%, #f8f8f8 100%); 343 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0); 344 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 345 | background-repeat: repeat-x; 346 | border-radius: 4px; 347 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); 348 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); 349 | } 350 | .navbar-default .navbar-nav > .open > a, 351 | .navbar-default .navbar-nav > .active > a { 352 | background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%); 353 | background-image: -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%); 354 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dbdbdb), to(#e2e2e2)); 355 | background-image: linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%); 356 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0); 357 | background-repeat: repeat-x; 358 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); 359 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); 360 | } 361 | .navbar-brand, 362 | .navbar-nav > li > a { 363 | text-shadow: 0 1px 0 rgba(255, 255, 255, .25); 364 | } 365 | .navbar-inverse { 366 | background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%); 367 | background-image: -o-linear-gradient(top, #3c3c3c 0%, #222 100%); 368 | background-image: -webkit-gradient(linear, left top, left bottom, from(#3c3c3c), to(#222)); 369 | background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%); 370 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0); 371 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 372 | background-repeat: repeat-x; 373 | border-radius: 4px; 374 | } 375 | .navbar-inverse .navbar-nav > .open > a, 376 | .navbar-inverse .navbar-nav > .active > a { 377 | background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%); 378 | background-image: -o-linear-gradient(top, #080808 0%, #0f0f0f 100%); 379 | background-image: -webkit-gradient(linear, left top, left bottom, from(#080808), to(#0f0f0f)); 380 | background-image: linear-gradient(to bottom, #080808 0%, #0f0f0f 100%); 381 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0); 382 | background-repeat: repeat-x; 383 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); 384 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); 385 | } 386 | .navbar-inverse .navbar-brand, 387 | .navbar-inverse .navbar-nav > li > a { 388 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .25); 389 | } 390 | .navbar-static-top, 391 | .navbar-fixed-top, 392 | .navbar-fixed-bottom { 393 | border-radius: 0; 394 | } 395 | @media (max-width: 767px) { 396 | .navbar .navbar-nav .open .dropdown-menu > .active > a, 397 | .navbar .navbar-nav .open .dropdown-menu > .active > a:hover, 398 | .navbar .navbar-nav .open .dropdown-menu > .active > a:focus { 399 | color: #fff; 400 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 401 | background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 402 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); 403 | background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); 404 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); 405 | background-repeat: repeat-x; 406 | } 407 | } 408 | .alert { 409 | text-shadow: 0 1px 0 rgba(255, 255, 255, .2); 410 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); 411 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); 412 | } 413 | .alert-success { 414 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); 415 | background-image: -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); 416 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#c8e5bc)); 417 | background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%); 418 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0); 419 | background-repeat: repeat-x; 420 | border-color: #b2dba1; 421 | } 422 | .alert-info { 423 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%); 424 | background-image: -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%); 425 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#b9def0)); 426 | background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%); 427 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0); 428 | background-repeat: repeat-x; 429 | border-color: #9acfea; 430 | } 431 | .alert-warning { 432 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); 433 | background-image: -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); 434 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#f8efc0)); 435 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%); 436 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0); 437 | background-repeat: repeat-x; 438 | border-color: #f5e79e; 439 | } 440 | .alert-danger { 441 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); 442 | background-image: -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); 443 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#e7c3c3)); 444 | background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%); 445 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0); 446 | background-repeat: repeat-x; 447 | border-color: #dca7a7; 448 | } 449 | .progress { 450 | background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); 451 | background-image: -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); 452 | background-image: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#f5f5f5)); 453 | background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%); 454 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0); 455 | background-repeat: repeat-x; 456 | } 457 | .progress-bar { 458 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #286090 100%); 459 | background-image: -o-linear-gradient(top, #337ab7 0%, #286090 100%); 460 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#286090)); 461 | background-image: linear-gradient(to bottom, #337ab7 0%, #286090 100%); 462 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0); 463 | background-repeat: repeat-x; 464 | } 465 | .progress-bar-success { 466 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%); 467 | background-image: -o-linear-gradient(top, #5cb85c 0%, #449d44 100%); 468 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#449d44)); 469 | background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%); 470 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0); 471 | background-repeat: repeat-x; 472 | } 473 | .progress-bar-info { 474 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); 475 | background-image: -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); 476 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#31b0d5)); 477 | background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%); 478 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0); 479 | background-repeat: repeat-x; 480 | } 481 | .progress-bar-warning { 482 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); 483 | background-image: -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); 484 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#ec971f)); 485 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%); 486 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0); 487 | background-repeat: repeat-x; 488 | } 489 | .progress-bar-danger { 490 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%); 491 | background-image: -o-linear-gradient(top, #d9534f 0%, #c9302c 100%); 492 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c9302c)); 493 | background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%); 494 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0); 495 | background-repeat: repeat-x; 496 | } 497 | .progress-bar-striped { 498 | background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); 499 | background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); 500 | background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); 501 | } 502 | .list-group { 503 | border-radius: 4px; 504 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 505 | box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 506 | } 507 | .list-group-item.active, 508 | .list-group-item.active:hover, 509 | .list-group-item.active:focus { 510 | text-shadow: 0 -1px 0 #286090; 511 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2b669a 100%); 512 | background-image: -o-linear-gradient(top, #337ab7 0%, #2b669a 100%); 513 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2b669a)); 514 | background-image: linear-gradient(to bottom, #337ab7 0%, #2b669a 100%); 515 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0); 516 | background-repeat: repeat-x; 517 | border-color: #2b669a; 518 | } 519 | .list-group-item.active .badge, 520 | .list-group-item.active:hover .badge, 521 | .list-group-item.active:focus .badge { 522 | text-shadow: none; 523 | } 524 | .panel { 525 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05); 526 | box-shadow: 0 1px 2px rgba(0, 0, 0, .05); 527 | } 528 | .panel-default > .panel-heading { 529 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 530 | background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 531 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); 532 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); 533 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); 534 | background-repeat: repeat-x; 535 | } 536 | .panel-primary > .panel-heading { 537 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 538 | background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 539 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); 540 | background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); 541 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); 542 | background-repeat: repeat-x; 543 | } 544 | .panel-success > .panel-heading { 545 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); 546 | background-image: -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); 547 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#d0e9c6)); 548 | background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%); 549 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0); 550 | background-repeat: repeat-x; 551 | } 552 | .panel-info > .panel-heading { 553 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); 554 | background-image: -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); 555 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#c4e3f3)); 556 | background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%); 557 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0); 558 | background-repeat: repeat-x; 559 | } 560 | .panel-warning > .panel-heading { 561 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); 562 | background-image: -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); 563 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#faf2cc)); 564 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%); 565 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0); 566 | background-repeat: repeat-x; 567 | } 568 | .panel-danger > .panel-heading { 569 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%); 570 | background-image: -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%); 571 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#ebcccc)); 572 | background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%); 573 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0); 574 | background-repeat: repeat-x; 575 | } 576 | .well { 577 | background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); 578 | background-image: -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); 579 | background-image: -webkit-gradient(linear, left top, left bottom, from(#e8e8e8), to(#f5f5f5)); 580 | background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%); 581 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0); 582 | background-repeat: repeat-x; 583 | border-color: #dcdcdc; 584 | -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); 585 | box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); 586 | } 587 | /*# sourceMappingURL=bootstrap-theme.css.map */ 588 | -------------------------------------------------------------------------------- /src/productpage/static/bootstrap/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.5 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under the MIT license 5 | */ 6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher")}(jQuery),+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){return a(b.target).is(this)?b.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.5",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a(f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.5",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")?(c.prop("checked")&&(a=!1),b.find(".active").removeClass("active"),this.$element.addClass("active")):"checkbox"==c.prop("type")&&(c.prop("checked")!==this.$element.hasClass("active")&&(a=!1),this.$element.toggleClass("active")),c.prop("checked",this.$element.hasClass("active")),a&&c.trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active")),this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target);d.hasClass("btn")||(d=d.closest(".btn")),b.call(d,"toggle"),a(c.target).is('input[type="radio"]')||a(c.target).is('input[type="checkbox"]')||c.preventDefault()}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=null,this.sliding=null,this.interval=null,this.$active=null,this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.5",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){if(!/input|textarea/i.test(a.target.tagName)){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()}},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c=this.getItemIndex(b),d="prev"==a&&0===c||"next"==a&&c==this.$items.length-1;if(d&&!this.options.wrap)return b;var e="prev"==a?-1:1,f=(c+e)%this.$items.length;return this.$items.eq(f)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(".item.active"));return a>this.$items.length-1||0>a?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?"next":"prev",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){return this.sliding?void 0:this.slide("next")},c.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},c.prototype.slide=function(b,d){var e=this.$element.find(".item.active"),f=d||this.getItemForDirection(b,e),g=this.interval,h="next"==b?"left":"right",i=this;if(f.hasClass("active"))return this.sliding=!1;var j=f[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:h});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(f)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:h});return a.support.transition&&this.$element.hasClass("slide")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one("bsTransitionEnd",function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger(m)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}};a(document).on("click.bs.carousel.data-api","[data-slide]",e).on("click.bs.carousel.data-api","[data-slide-to]",e),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){var c,d=b.attr("data-target")||(c=b.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data("bs.collapse"),f=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b);!e&&f.toggle&&/show|hide/.test(b)&&(f.toggle=!1),e||c.data("bs.collapse",e=new d(this,f)),"string"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a('[data-toggle="collapse"][href="#'+b.id+'"],[data-toggle="collapse"][data-target="#'+b.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION="3.3.5",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0},d.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b,e=this.$parent&&this.$parent.children(".panel").children(".in, .collapsing");if(!(e&&e.length&&(b=e.data("bs.collapse"),b&&b.transitioning))){var f=a.Event("show.bs.collapse");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,"hide"),b||e.data("bs.collapse",null));var g=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[g](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var h=function(){this.$element.removeClass("collapsing").addClass("collapse in")[g](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return h.call(this);var i=a.camelCase(["scroll",g].join("-"));this.$element.one("bsTransitionEnd",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass("in");a.attr("aria-expanded",c),b.toggleClass("collapsed",!c).attr("aria-expanded",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(d){var e=a(this);e.attr("data-target")||d.preventDefault();var f=b(e),g=f.data("bs.collapse"),h=g?"toggle":e.data();c.call(f,h)})}(jQuery),+function(a){"use strict";function b(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function c(c){c&&3===c.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=b(d),f={relatedTarget:this};e.hasClass("open")&&(c&&"click"==c.type&&/input|textarea/i.test(c.target.tagName)&&a.contains(e[0],c.target)||(e.trigger(c=a.Event("hide.bs.dropdown",f)),c.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger("hidden.bs.dropdown",f))))}))}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.3.5",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=b(e),g=f.hasClass("open");if(c(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a(document.createElement("div")).addClass("dropdown-backdrop").insertAfter(a(this)).on("click",c);var h={relatedTarget:this};if(f.trigger(d=a.Event("show.bs.dropdown",h)),d.isDefaultPrevented())return;e.trigger("focus").attr("aria-expanded","true"),f.toggleClass("open").trigger("shown.bs.dropdown",h)}return!1}},g.prototype.keydown=function(c){if(/(38|40|27|32)/.test(c.which)&&!/input|textarea/i.test(c.target.tagName)){var d=a(this);if(c.preventDefault(),c.stopPropagation(),!d.is(".disabled, :disabled")){var e=b(d),g=e.hasClass("open");if(!g&&27!=c.which||g&&27==c.which)return 27==c.which&&e.find(f).trigger("focus"),d.trigger("click");var h=" li:not(.disabled):visible a",i=e.find(".dropdown-menu"+h);if(i.length){var j=i.index(c.target);38==c.which&&j>0&&j--,40==c.which&&jdocument.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&a?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!a?this.scrollbarWidth:""})},c.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},c.prototype.checkScrollbar=function(){var a=window.innerWidth;if(!a){var b=document.documentElement.getBoundingClientRect();a=b.right-Math.abs(b.left)}this.bodyIsOverflowing=document.body.clientWidth
',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},c.prototype.init=function(b,c,d){if(this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.$viewport=this.options.viewport&&a(a.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},c.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},c.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusin"==b.type?"focus":"hover"]=!0),c.tip().hasClass("in")||"in"==c.hoverState?void(c.hoverState="in"):(clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show())},c.prototype.isInStateTrue=function(){for(var a in this.inState)if(this.inState[a])return!0;return!1},c.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusout"==b.type?"focus":"hover"]=!1),c.isInStateTrue()?void 0:(clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide())},c.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);var d=a.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(b.isDefaultPrevented()||!d)return;var e=this,f=this.tip(),g=this.getUID(this.type);this.setContent(),f.attr("id",g),this.$element.attr("aria-describedby",g),this.options.animation&&f.addClass("fade");var h="function"==typeof this.options.placement?this.options.placement.call(this,f[0],this.$element[0]):this.options.placement,i=/\s?auto?\s?/i,j=i.test(h);j&&(h=h.replace(i,"")||"top"),f.detach().css({top:0,left:0,display:"block"}).addClass(h).data("bs."+this.type,this),this.options.container?f.appendTo(this.options.container):f.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var k=this.getPosition(),l=f[0].offsetWidth,m=f[0].offsetHeight;if(j){var n=h,o=this.getPosition(this.$viewport);h="bottom"==h&&k.bottom+m>o.bottom?"top":"top"==h&&k.top-mo.width?"left":"left"==h&&k.left-lg.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;jg.right&&(e.left=g.left+g.width-k)}return e},c.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},c.prototype.getUID=function(a){do a+=~~(1e6*Math.random());while(document.getElementById(a));return a},c.prototype.tip=function(){if(!this.$tip&&(this.$tip=a(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},c.prototype.enable=function(){this.enabled=!0},c.prototype.disable=function(){this.enabled=!1},c.prototype.toggleEnabled=function(){this.enabled=!this.enabled},c.prototype.toggle=function(b){var c=this;b&&(c=a(b.currentTarget).data("bs."+this.type),c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c))),b?(c.inState.click=!c.inState.click,c.isInStateTrue()?c.enter(c):c.leave(c)):c.tip().hasClass("in")?c.leave(c):c.enter(c)},c.prototype.destroy=function(){var a=this;clearTimeout(this.timeout),this.hide(function(){a.$element.off("."+a.type).removeData("bs."+a.type),a.$tip&&a.$tip.detach(),a.$tip=null,a.$arrow=null,a.$viewport=null})};var d=a.fn.tooltip;a.fn.tooltip=b,a.fn.tooltip.Constructor=c,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=d,this}}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof b&&b;(e||!/destroy|hide/.test(b))&&(e||d.data("bs.popover",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");c.VERSION="3.3.5",c.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),c.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),c.prototype.constructor=c,c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},c.prototype.hasContent=function(){return this.getTitle()||this.getContent()},c.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var d=a.fn.popover;a.fn.popover=b,a.fn.popover.Constructor=c,a.fn.popover.noConflict=function(){return a.fn.popover=d,this}}(jQuery),+function(a){"use strict";function b(c,d){this.$body=a(document.body),this.$scrollElement=a(a(c).is(document.body)?window:c),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",a.proxy(this.process,this)),this.refresh(),this.process()}function c(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})}b.VERSION="3.3.5",b.DEFAULTS={offset:10},b.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},b.prototype.refresh=function(){var b=this,c="offset",d=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),a.isWindow(this.$scrollElement[0])||(c="position",d=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var b=a(this),e=b.data("target")||b.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[c]().top+d,e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){b.offsets.push(this[0]),b.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.getScrollHeight(),d=this.options.offset+c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(this.scrollHeight!=c&&this.refresh(),b>=d)return g!=(a=f[f.length-1])&&this.activate(a);if(g&&b=e[a]&&(void 0===e[a+1]||b .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),b.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),h?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu").length&&b.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),e&&e()}var g=d.find("> .active"),h=e&&a.support.transition&&(g.length&&g.hasClass("fade")||!!d.find("> .fade").length);g.length&&h?g.one("bsTransitionEnd",f).emulateTransitionEnd(c.TRANSITION_DURATION):f(),g.removeClass("in")};var d=a.fn.tab;a.fn.tab=b,a.fn.tab.Constructor=c,a.fn.tab.noConflict=function(){return a.fn.tab=d,this};var e=function(c){c.preventDefault(),b.call(a(this),"show")};a(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',e).on("click.bs.tab.data-api",'[data-toggle="pill"]',e)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof b&&b;e||d.data("bs.affix",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.options=a.extend({},c.DEFAULTS,d),this.$target=a(this.options.target).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(b),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};c.VERSION="3.3.5",c.RESET="affix affix-top affix-bottom",c.DEFAULTS={offset:0,target:window},c.prototype.getState=function(a,b,c,d){var e=this.$target.scrollTop(),f=this.$element.offset(),g=this.$target.height();if(null!=c&&"top"==this.affixed)return c>e?"top":!1;if("bottom"==this.affixed)return null!=c?e+this.unpin<=f.top?!1:"bottom":a-d>=e+g?!1:"bottom";var h=null==this.affixed,i=h?e:f.top,j=h?g:b;return null!=c&&c>=e?"top":null!=d&&i+j>=a-d?"bottom":!1},c.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(c.RESET).addClass("affix");var a=this.$target.scrollTop(),b=this.$element.offset();return this.pinnedOffset=b.top-a},c.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},c.prototype.checkPosition=function(){if(this.$element.is(":visible")){var b=this.$element.height(),d=this.options.offset,e=d.top,f=d.bottom,g=Math.max(a(document).height(),a(document.body).height());"object"!=typeof d&&(f=e=d),"function"==typeof e&&(e=d.top(this.$element)),"function"==typeof f&&(f=d.bottom(this.$element));var h=this.getState(g,b,e,f);if(this.affixed!=h){null!=this.unpin&&this.$element.css("top","");var i="affix"+(h?"-"+h:""),j=a.Event(i+".bs.affix");if(this.$element.trigger(j),j.isDefaultPrevented())return;this.affixed=h,this.unpin="bottom"==h?this.getPinnedOffset():null,this.$element.removeClass(c.RESET).addClass(i).trigger(i.replace("affix","affixed")+".bs.affix")}"bottom"==h&&this.$element.offset({top:g-b-f})}};var d=a.fn.affix;a.fn.affix=b,a.fn.affix.Constructor=c,a.fn.affix.noConflict=function(){return a.fn.affix=d,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var c=a(this),d=c.data();d.offset=d.offset||{},null!=d.offsetBottom&&(d.offset.bottom=d.offsetBottom),null!=d.offsetTop&&(d.offset.top=d.offsetTop),b.call(c,d)})})}(jQuery); --------------------------------------------------------------------------------