├── glide.yaml ├── .gitignore ├── static ├── sprite.png ├── sprite2.png ├── favicon-16x16.png ├── favicon-32x32.png ├── game.css ├── game.js └── game2.js ├── glide.lock ├── charts └── croc-hunter │ ├── Chart.yaml │ ├── templates │ ├── _helpers.tpl │ ├── tests │ │ └── croc-hunter-tests.yaml │ ├── pdb.yaml │ ├── croc-hunter-ingress.yaml │ └── croc-hunter.yaml │ ├── .helmignore │ ├── values.yaml │ └── README.md ├── README.md ├── Dockerfile ├── Jenkinsfile.json ├── LICENSE ├── Makefile ├── jenkins-values.yaml ├── croc-hunter.go ├── DEMO.md └── Jenkinsfile /glide.yaml: -------------------------------------------------------------------------------- 1 | package: github.com/lachie83/croc-hunter 2 | import: [] 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/.DS_Store 2 | vendor/ 3 | *.tgz 4 | levo-charts 5 | croc-hunter -------------------------------------------------------------------------------- /static/sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lachie83/croc-hunter/HEAD/static/sprite.png -------------------------------------------------------------------------------- /static/sprite2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lachie83/croc-hunter/HEAD/static/sprite2.png -------------------------------------------------------------------------------- /static/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lachie83/croc-hunter/HEAD/static/favicon-16x16.png -------------------------------------------------------------------------------- /static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lachie83/croc-hunter/HEAD/static/favicon-32x32.png -------------------------------------------------------------------------------- /glide.lock: -------------------------------------------------------------------------------- 1 | hash: 9ea2c8da196b68a2959378e2c1fa65e270c6e08aed138b50bc7845431d16a574 2 | updated: 2016-09-08T00:30:45.638313477-07:00 3 | imports: [] 4 | testImports: [] 5 | -------------------------------------------------------------------------------- /charts/croc-hunter/Chart.yaml: -------------------------------------------------------------------------------- 1 | name: croc-hunter 2 | home: https://github.com/lachie83/croc-hunter 3 | version: 0.3.1 4 | description: Live out your dream hunting Crocs 5 | sources: 6 | - https://github.com/lachie83/croc-hunter 7 | maintainers: 8 | - name: Lachlan Evenson 9 | email: lachlan.evenson@gmail.com 10 | -------------------------------------------------------------------------------- /charts/croc-hunter/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | 3 | {{/* 4 | Create a default fully qualified app name. 5 | We truncate at 24 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 6 | */}} 7 | {{- define "fullname" -}} 8 | {{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" -}} 9 | {{- end -}} 10 | -------------------------------------------------------------------------------- /charts/croc-hunter/templates/tests/croc-hunter-tests.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: "{{ template "fullname" . }}-web-test" 5 | annotations: 6 | "helm.sh/hook": test-success 7 | spec: 8 | containers: 9 | - name: {{ template "fullname" . }}-web-test 10 | image: busybox 11 | command: ["wget", "--spider", "{{ template "fullname" . }}:{{.Values.servicePort}}"] 12 | restartPolicy: Never -------------------------------------------------------------------------------- /static/game.css: -------------------------------------------------------------------------------- 1 | body{background:#222;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:1em}#canvasBg{display:block;background:#fff;margin:100px auto 0}#canvasEnemy,#canvasHud,#canvasJet{display:block;margin:-500px auto 0}.section{margin-top:20px}.name{border-radius:4px;padding:6px 10px;color:#fff}.name.server{background:#29abe2}.name.visitor{background:#36D446}.title{text-transform:uppercase;color:#aaa}.details{text-align:center;color:#fff} -------------------------------------------------------------------------------- /charts/croc-hunter/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Croc Hunter - The game! 2 | 3 | For those that have dreamt to hunt crocs 4 | 5 | # Usage 6 | Basic go webserver to demonstrate example CI/CD pipeline using Kubernetes 7 | 8 | # Deploy using Jenkins Chart and Helm 9 | [![Demo Pipeline](https://img.youtube.com/vi/NVoln4HdZOY/0.jpg)](https://youtu.be/NVoln4HdZOY "Demo Pipeline") 10 | 11 | # How to setup the Jenkins infrastructure 12 | [![Jenkins Setup](https://img.youtube.com/vi/eMOzF_xAm7w/0.jpg)](https://youtu.be/eMOzF_xAm7w "Jenkins Setup") 13 | * See DEMO.md for steps 14 | -------------------------------------------------------------------------------- /charts/croc-hunter/templates/pdb.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.pdb.enabled }} 2 | apiVersion: policy/v1beta1 3 | kind: PodDisruptionBudget 4 | metadata: 5 | name: {{ template "fullname" . }} 6 | annotations: 7 | labels: 8 | heritage: {{.Release.Service | quote }} 9 | release: {{.Release.Name | quote }} 10 | chart: "{{.Chart.Name}}-{{.Chart.Version}}" 11 | component: "{{.Release.Name}}-{{.Values.component}}" 12 | spec: 13 | selector: 14 | matchLabels: 15 | component: "{{.Release.Name}}-{{.Values.component}}" 16 | minAvailable: {{.Values.pdb.minAvailable}} 17 | {{- end }} -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.8-alpine3.6 2 | 3 | MAINTAINER Lachlan Evenson 4 | 5 | ARG VCS_REF 6 | ARG BUILD_DATE 7 | 8 | # Metadata 9 | LABEL org.label-schema.vcs-ref=$VCS_REF \ 10 | org.label-schema.vcs-url="https://github.com/lachie83/croc-hunter" \ 11 | org.label-schema.build-date=$BUILD_DATE \ 12 | org.label-schema.docker.dockerfile="/Dockerfile" 13 | 14 | COPY . /go/src/github.com/lachie83/croc-hunter 15 | COPY static/ static/ 16 | 17 | ENV GIT_SHA $VCS_REF 18 | ENV GOPATH /go 19 | RUN cd $GOPATH/src/github.com/lachie83/croc-hunter && go install -v . 20 | 21 | CMD ["croc-hunter"] 22 | 23 | EXPOSE 8080 24 | 25 | -------------------------------------------------------------------------------- /Jenkinsfile.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": { 3 | "name": "croc-hunter", 4 | "replicas": "3", 5 | "cpu": "10m", 6 | "memory": "128Mi", 7 | "test": true, 8 | "hostname": "croc-hunter.acs.az.estrado.io" 9 | }, 10 | "container_repo": { 11 | "host": "quay.io", 12 | "master_acct": "lachie83", 13 | "alt_acct": "lachie83", 14 | "jenkins_creds_id": "quay_creds", 15 | "repo": "croc-hunter", 16 | "dockeremail": ".", 17 | "dockerfile": "./", 18 | "image_scanning": false 19 | }, 20 | "pipeline": { 21 | "enabled": true, 22 | "debug": false, 23 | "library": { 24 | "branch": "dev" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /charts/croc-hunter/templates/croc-hunter-ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled }} 2 | apiVersion: extensions/v1beta1 3 | kind: Ingress 4 | metadata: 5 | {{- if .Values.ingress.annotations }} 6 | annotations: 7 | {{ toYaml .Values.ingress.annotations | indent 4 }} 8 | {{- end }} 9 | name: {{ template "fullname" . }} 10 | spec: 11 | rules: 12 | - host: {{ .Values.ingress.hostname | quote }} 13 | http: 14 | paths: 15 | - path: / 16 | backend: 17 | serviceName: {{ template "fullname" . }} 18 | servicePort: {{ .Values.servicePort }} 19 | {{- if .Values.ingress.tls }} 20 | tls: 21 | - secretName: {{ .Values.ingress.hostname | quote }} 22 | hosts: 23 | - {{ .Values.ingress.hostname | quote }} 24 | {{- end -}} 25 | {{- end }} 26 | -------------------------------------------------------------------------------- /charts/croc-hunter/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for croc-hunter. 2 | # This is a YAML-formatted file. 3 | # Declare name/value pairs to be passed into your templates. 4 | # name: value 5 | 6 | serviceType: ClusterIP 7 | servicePort: 80 8 | containerPort: 8080 9 | component: "croc-hunter" 10 | replicas: 3 11 | image: "quay.io/lachie83/croc-hunter" 12 | imageTag: "latest" 13 | imagePullPolicy: "Always" 14 | ## If you have a private registry you specify a secret to use 15 | #imagePullSecrets: 16 | cpu: "10m" 17 | memory: "128Mi" 18 | ## Ingress settings 19 | ingress: 20 | enabled: true 21 | hostname: croc-hunter.acs.az.estrado.io 22 | annotations: 23 | kubernetes.io/ingress.class: nginx 24 | kubernetes.io/tls-acme: "true" 25 | tls: true 26 | ## PodDisruptionBudget 27 | pdb: 28 | enabled: false 29 | minAvailable: 2 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Lachlan Evenson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | default: docker_build 2 | 3 | DOCKER_IMAGE ?= quay.io/lachie83/croc-hunter 4 | BUILD_NUMBER ?= `git rev-parse --short HEAD` 5 | VCS_REF ?= `git rev-parse --short HEAD` 6 | 7 | .PHONY: docker_build 8 | docker_build: 9 | @docker build \ 10 | --build-arg VCS_REF=$(VCS_REF) \ 11 | --build-arg BUILD_DATE=`date -u +"%Y-%m-%dT%H:%M:%SZ"` \ 12 | -t $(DOCKER_IMAGE):$(BUILD_NUMBER) . 13 | 14 | .PHONY: docker_push 15 | docker_push: 16 | # Push to DockerHub 17 | docker tag $(DOCKER_IMAGE):$(BUILD_NUMBER) $(DOCKER_IMAGE):latest 18 | docker push $(DOCKER_IMAGE):$(BUILD_NUMBER) 19 | docker push $(DOCKER_IMAGE):latest 20 | 21 | # go option 22 | GO ?= go 23 | PKG := $(shell glide novendor) 24 | TAGS := 25 | TESTS := . 26 | TESTFLAGS := 27 | LDFLAGS := 28 | GOFLAGS := 29 | BINDIR := $(CURDIR)/bin 30 | 31 | .PHONY: all 32 | all: build 33 | 34 | .PHONY: build 35 | build: 36 | GOBIN=$(BINDIR) $(GO) build $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)' 37 | 38 | HAS_GLIDE := $(shell command -v glide;) 39 | HAS_GIT := $(shell command -v git;) 40 | 41 | .PHONY: bootstrap 42 | bootstrap: 43 | ifndef HAS_GLIDE 44 | go get -u github.com/Masterminds/glide 45 | endif 46 | ifndef HAS_GIT 47 | $(error You must install Git) 48 | endif 49 | glide install 50 | -------------------------------------------------------------------------------- /jenkins-values.yaml: -------------------------------------------------------------------------------- 1 | # Includes complete Jenkins configuration in order to run croc-hunter pipeline 2 | # To install on your own cluster, run: 3 | # helm --namespace jenkins --name jenkins -f ./jenkins-values.yaml install stable/jenkins 4 | 5 | Master: 6 | ImageTag: "2.74" 7 | Memory: "512Mi" 8 | HostName: jenkins.acs.az.estrado.io 9 | ServiceType: ClusterIP 10 | InstallPlugins: 11 | - kubernetes:0.12 12 | - workflow-aggregator:2.5 13 | - credentials-binding:1.13 14 | - git:3.5.1 15 | - pipeline-github-lib:1.0 16 | - ghprb:1.39.0 17 | - blueocean:1.1.7 18 | 19 | ScriptApproval: 20 | - "method groovy.json.JsonSlurperClassic parseText java.lang.String" 21 | - "new groovy.json.JsonSlurperClassic" 22 | - "staticMethod org.codehaus.groovy.runtime.DefaultGroovyMethods leftShift java.util.Map java.util.Map" 23 | - "staticMethod org.codehaus.groovy.runtime.DefaultGroovyMethods split java.lang.String" 24 | - "method java.util.Collection toArray" 25 | - "staticMethod org.kohsuke.groovy.sandbox.impl.Checker checkedCall java.lang.Object boolean boolean java.lang.String java.lang.Object[]" 26 | - "staticMethod org.kohsuke.groovy.sandbox.impl.Checker checkedGetProperty java.lang.Object boolean boolean java.lang.Object" 27 | 28 | Ingress: 29 | Annotations: 30 | kubernetes.io/ingress.class: nginx 31 | kubernetes.io/tls-acme: "true" 32 | 33 | TLS: 34 | - secretName: jenkins.acs.az.estrado.io 35 | hosts: 36 | - jenkins.acs.az.estrado.io 37 | 38 | Agent: 39 | Enabled: false 40 | -------------------------------------------------------------------------------- /charts/croc-hunter/templates/croc-hunter.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ template "fullname" . }} 5 | labels: 6 | heritage: {{.Release.Service | quote }} 7 | release: {{.Release.Name | quote }} 8 | chart: "{{.Chart.Name}}-{{.Chart.Version}}" 9 | component: "{{.Release.Name}}-{{.Values.component}}" 10 | spec: 11 | ports: 12 | - port: {{.Values.servicePort}} 13 | targetPort: {{.Values.containerPort}} 14 | selector: 15 | component: "{{.Release.Name}}-{{.Values.component}}" 16 | type: {{ default "ClusterIP" .Values.serviceType | quote }} 17 | 18 | --- 19 | apiVersion: extensions/v1beta1 20 | kind: Deployment 21 | metadata: 22 | name: {{ template "fullname" . }} 23 | labels: 24 | heritage: {{.Release.Service | quote }} 25 | release: {{.Release.Name | quote }} 26 | chart: "{{.Chart.Name}}-{{.Chart.Version}}" 27 | component: "{{.Release.Name}}-{{.Values.component}}" 28 | spec: 29 | replicas: {{ default 1 .Values.replicas }} 30 | strategy: 31 | type: RollingUpdate 32 | selector: 33 | matchLabels: 34 | component: "{{.Release.Name}}-{{.Values.component}}" 35 | template: 36 | metadata: 37 | labels: 38 | heritage: {{.Release.Service | quote }} 39 | release: {{.Release.Name | quote }} 40 | chart: "{{.Chart.Name}}-{{.Chart.Version}}" 41 | component: "{{.Release.Name}}-{{.Values.component}}" 42 | spec: 43 | {{- if .Values.imagePullSecrets }} 44 | imagePullSecrets: 45 | - name: {{ .Values.imagePullSecrets }} 46 | {{- end }} 47 | containers: 48 | - name: {{ template "fullname" . }} 49 | image: "{{.Values.image}}:{{.Values.imageTag}}" 50 | imagePullPolicy: "{{.Values.imagePullPolicy}}" 51 | ports: 52 | - name: http 53 | containerPort: {{.Values.containerPort}} 54 | resources: 55 | requests: 56 | cpu: "{{.Values.cpu}}" 57 | memory: "{{.Values.memory}}" 58 | env: 59 | - name: WORKFLOW_RELEASE 60 | value: {{.Release.Name | quote }} 61 | livenessProbe: 62 | httpGet: 63 | path: /healthz 64 | port: http 65 | readinessProbe: 66 | httpGet: 67 | path: /healthz 68 | port: http 69 | -------------------------------------------------------------------------------- /croc-hunter.go: -------------------------------------------------------------------------------- 1 | // The infamous "croc-hunter" game as featured at many a demo 2 | package main 3 | 4 | import ( 5 | "flag" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "os" 10 | ) 11 | 12 | func main() { 13 | httpListenAddr := flag.String("port", "8080", "HTTP Listen address.") 14 | 15 | flag.Parse() 16 | 17 | log.Println("Starting server...") 18 | 19 | // point / at the handler function 20 | http.HandleFunc("/", handler) 21 | 22 | // serve static content from /static 23 | http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static/")))) 24 | 25 | log.Println("Server started. Listening on port " + *httpListenAddr) 26 | log.Fatal(http.ListenAndServe(":"+*httpListenAddr, nil)) 27 | } 28 | 29 | const ( 30 | html = ` 31 | 32 | 33 | 34 | Croc Hunter 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 | Hostname: %s
47 | Release: %s
48 | Commit: %s
49 | Powered By: %s
50 |
51 | 52 | 53 | ` 54 | ) 55 | 56 | func handler(w http.ResponseWriter, r *http.Request) { 57 | 58 | if r.URL.Path == "/healthz" { 59 | w.WriteHeader(http.StatusOK) 60 | return 61 | } 62 | 63 | hostname, err := os.Hostname() 64 | if err != nil { 65 | log.Fatalf("could not get hostname: %s", err) 66 | } 67 | 68 | release := os.Getenv("WORKFLOW_RELEASE") 69 | commit := os.Getenv("GIT_SHA") 70 | powered := os.Getenv("POWERED_BY") 71 | 72 | if release == "" { 73 | release = "unknown" 74 | } 75 | if commit == "" { 76 | commit = "not present" 77 | } 78 | if powered == "" { 79 | powered = "deis" 80 | } 81 | 82 | fmt.Fprintf(w, html, hostname, release, commit, powered) 83 | } 84 | -------------------------------------------------------------------------------- /charts/croc-hunter/README.md: -------------------------------------------------------------------------------- 1 | # Croc Hunter Helm Chart 2 | 3 | Inspired be the groundbreaking game feature at OpenStack Tokyo Summit 4 | 5 | ## Chart Details 6 | This chart will do the following by default: 7 | 8 | * 3 x croc-hunter instances with port 8080 exposed on an external LoadBalancer 9 | * All using Kubernetes Deployments 10 | 11 | 12 | ## Get this chart 13 | 14 | Download the latest release of the chart from the [releases](../../../releases) page. 15 | 16 | Alternatively, clone the repo if you wish to use the development snapshot: 17 | 18 | ```bash 19 | $ git clone https://github.com/lachie83/croc-hunter/charts 20 | ``` 21 | 22 | ## Chart signing 23 | 24 | The chart is signed using Helm provenance and integrity. 25 | * https://github.com/kubernetes/helm/blob/master/docs/provenance.md 26 | 27 | ## Installing the Chart 28 | 29 | To install the chart with the release name `my-release`: 30 | 31 | ```bash 32 | $ helm repo add levo-charts https://storage.googleapis.com/levo-charts 33 | $ helm install --verify levo-charts/croc-hunter-0.2.0.tgz 34 | ``` 35 | 36 | ## Configuration 37 | 38 | The following tables lists the configurable parameters of the Spark chart and their default values. 39 | 40 | ### Croc-hunter 41 | 42 | | Parameter | Description | Default | 43 | |-----------------------|----------------------------------|----------------------------------------------------------| 44 | | `Name` | app name | `croc-hunter` | 45 | | `Image` | Container image name | `quay.io/lachie83/croc-hunter` | 46 | | `ImageTag` | Container image tag | `latest` | 47 | | `ImagePullPolicy` | Container pull policy | `Always` | 48 | | `Replicas` | k8s deployment replicas | `3` | 49 | | `Component` | k8s selector key | `croc-hunter` | 50 | | `Cpu` | container requested cpu | `10m` | 51 | | `Memory` | container requested memory | `128Mi` | 52 | | `ServiceType` | k8s service type | `LoadBalancer` | 53 | | `ServicePort` | k8s service port | `80` | 54 | | `ContainerPort` | Container listening port | `8080` | 55 | 56 | Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`. 57 | 58 | Alternatively, a YAML file that specifies the values for the parameters can be provided while installing the chart. For example, 59 | 60 | ```bash 61 | $ helm install --name my-release -f values.yaml --verify levo-charts/croc-hunter-0.2.0.tgz 62 | ``` 63 | 64 | > **Tip**: You can use the default [values.yaml](values.yaml) 65 | -------------------------------------------------------------------------------- /DEMO.md: -------------------------------------------------------------------------------- 1 | # Demo Walkthrough 2 | 3 | ## Acknowledgements 4 | Continuation of the awesome work by everett-toews. 5 | * https://gist.github.com/everett-toews/ed56adcfd525ce65b178d2e5a5eb06aa 6 | 7 | ## Watch Demo 8 | 9 | https://www.youtube.com/watch?v=eMOzF_xAm7w 10 | 11 | # Prerequisites 12 | kubectl access to a Kubernetes 1.4+ cluster 13 | 14 | # Install Helm (Mac OS) 15 | 16 | ``` 17 | brew install kubernetes-helm 18 | helm init 19 | helm repo update 20 | ``` 21 | 22 | ## Fork repo 23 | ``` 24 | https://github.com/lachie83/croc-hunter#fork-destination-box 25 | ``` 26 | 27 | ## Install Kube Lego chart 28 | ``` 29 | helm install stable/kube-lego --set config.LEGO_EMAIL=,config.LEGO_URL=https://acme-v01.api.letsencrypt.org/directory 30 | ``` 31 | 32 | ## Install Nginx ingress chart 33 | ``` 34 | helm install stable/nginx-ingress 35 | 36 | Follow the notes from helm status to determine the external IP of the nginx-ingress service 37 | ``` 38 | 39 | ## Add a DNS entry with your provider and point it do the external IP 40 | ``` 41 | blah.test.com in A 42 | 43 | or *.test.com in A 44 | 45 | ``` 46 | 47 | 48 | ## Update jenkins.values.yaml 49 | ``` 50 | Find and replace `jenkins.acs.az.estrado.io` with the DNS name provisioned above 51 | 52 | helm --namespace jenkins --name jenkins -f ./jenkins-values.yaml install stable/jenkins 53 | 54 | watch kubectl get svc --namespace jenkins # wait for external ip 55 | export JENKINS_IP=$(kubectl get svc jenkins-jenkins --namespace jenkins --template "{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}") 56 | export JENKINS_URL=http://${JENKINS_IP}:8080 57 | 58 | kubectl get pods --namespace jenkins # wait for running 59 | open ${JENKINS_URL}/login 60 | 61 | printf $(kubectl get secret --namespace jenkins jenkins-jenkins -o jsonpath="{.data.jenkins-admin-password}" | base64 --decode) | pbcopy 62 | ``` 63 | 64 | ## Add credentials for private container registry (optional) 65 | ``` 66 | kubectl create secret docker-registry croc-hunter-secrets --docker-server=$DOCKER_SERVER --docker-username=$DOCKER_USERNAME --docker-password=$DOCKER_PASSWORD --docker-email=$DOCKER_EMAIL --namespace=croc-hunter 67 | ``` 68 | Reference to the secret name must also be added to the chart values.yaml or set on install. 69 | 70 | ## Login and configure Jenkins and setup pipeline 71 | ``` 72 | # username: admin 73 | # password: 74 | 75 | If you're not using quay you can configure this to alternate locations in Jenkinsfile.json 76 | # Credentials > Jenkins > Global credentials > Add Credentials 77 | # Username: lachie83 78 | # Password: *** 79 | # ID: quay_creds 80 | # Description: https://quay.io/user/lachie83 81 | 82 | # Open Blue Ocean 83 | # Create a new Pipeline 84 | # Where do you store your code? 85 | # GitHub 86 | # Connect to Github 87 | # Create an access key here 88 | # Token description: kubernetes-jenkins 89 | # Generate token > Copy Token > Paste back in Jenkins 90 | # Which organization does the repository belong to? 91 | # lachie83 92 | # Create a single Pipeline or discover all Pipelines? 93 | # New pipeline 94 | # Choose a repository 95 | # croc-hunter 96 | # Create Pipeline 97 | ``` 98 | 99 | ## Watch Jenkins build agents run 100 | ``` 101 | kubectl get pods --namespace jenkins 102 | ``` 103 | 104 | ## Update Org to build PRs 105 | ``` 106 | # Classic Jenkins 107 | # lachie83 (GitHub org) 108 | # Configure 109 | # Advanced 110 | # Build origin PRs (merged with base branch) 111 | # Save 112 | ``` 113 | 114 | 115 | ## Setup Webhook in Github 116 | ``` 117 | printf ${JENKINS_URL}/github-webhook/ | pbcopy 118 | 119 | # https://github.com/lachie83/croc-hunter/settings/hooks 120 | # Add webhook 121 | # Payload URL: 122 | # Which events would you like to trigger this webhook? 123 | # Send me everything. 124 | # Add webhook 125 | ``` 126 | 127 | ## Update croc-hunter ingress records 128 | ``` 129 | Update croc-hunter.acs.az.estrado.io in charts/croc-hunter/values.yaml 130 | 131 | Configured DNS A record to point to the Nginx Ingress IP 132 | Once master branch is pushed it should be available at that name 133 | ``` 134 | 135 | 136 | ## Pushing Game update 137 | ``` 138 | git checkout dev 139 | sed -i "" "s/game\.js/game2\.js/g" croc-hunter.go 140 | git commit -am "Game 2" 141 | git push 142 | ``` 143 | 144 | ### Building and releasing 145 | ``` 146 | open ${JENKINS_URL}/blue/organizations/jenkins/lachie83%2Fcroc-hunter/activity/ 147 | 148 | # dev branch builds 149 | 150 | open https://github.com/lachie83/croc-hunter 151 | 152 | # PR from dev to master 153 | # PR builds 154 | # merge the PR 155 | # master builds and deploys new version 156 | ``` 157 | -------------------------------------------------------------------------------- /static/game.js: -------------------------------------------------------------------------------- 1 | var canvasBg=document.getElementById("canvasBg");var contextBg=canvasBg.getContext("2d");var canvasJet=document.getElementById("canvasJet");var contextJet=canvasJet.getContext("2d");var canvasEnemy=document.getElementById("canvasEnemy");var contextEnemy=canvasEnemy.getContext("2d");var canvasHud=document.getElementById("canvasHud");var contextHud=canvasHud.getContext("2d");contextHud.fillStyle="hsla(0, 0%, 0%, 0.5)";contextHud.font="bold 20px Arial";var jet1=new Jet;var btnPlay=new Button(265,535,220,335);var gameWidth=canvasBg.width;var gameHeight=canvasBg.height;var mouseX=0;var mouseY=0;var isPlaying=false;var requestAnimFrame=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(callback){window.setTimeout(callback,1e3/60)};var enemies=[];var imgSprite=new Image;imgSprite.src="/static/sprite.png";imgSprite.addEventListener("load",init,false);var bgDrawX1=0;var bgDrawX2=1600;function moveBg(){bgDrawX1-=5;bgDrawX2-=5;if(bgDrawX1<=-1600)bgDrawX1=1600;if(bgDrawX2<=-1600)bgDrawX2=1600;drawBg()}function init(){spawnEnemy(5);drawMenu();document.addEventListener("click",mouseClicked,false)}function playGame(){drawBg();startLoop();updateHud();document.addEventListener("keydown",checkKeyDown,false);document.addEventListener("keyup",checkKeyUp,false)}function spawnEnemy(numSpawns){for(var i=0;i0){this.drawY-=this.speed}if(this.isRightKey&&this.rightX0){this.drawX-=this.speed}};Jet.prototype.drawAllBullets=function(){for(var i=0;i=0)this.bullets[i].draw();if(this.bullets[i].explosion.hasHit)this.bullets[i].explosion.draw()}};Jet.prototype.checkShooting=function(){if(this.isSpaceBar&&!this.isShooting){this.isShooting=true;this.bullets[this.currentBullet].fire(this.noseX,this.noseY);this.currentBullet++;if(this.currentBullet>=this.bullets.length)this.currentBullet=0}else if(!this.isSpaceBar){this.isShooting=false}};Jet.prototype.updateScore=function(points){this.score+=points;updateHud()};function clearContextJet(){contextJet.clearRect(0,0,gameWidth,gameHeight)}function Bullet(j){this.srcX=100;this.srcY=500;this.drawX=-20;this.drawY=0;this.width=18;this.height=4;this.speed=3;this.explosion=new Explosion;this.jet=j}Bullet.prototype.draw=function(){this.drawX+=this.speed;contextJet.drawImage(imgSprite,this.srcX,this.srcY,this.width,this.height,this.drawX,this.drawY,this.width,this.height);this.checkHitEnemy();if(this.drawX>gameWidth)this.recycle()};Bullet.prototype.recycle=function(){this.drawX=-20};Bullet.prototype.fire=function(noseX,noseY){this.drawX=noseX;this.drawY=noseY};Bullet.prototype.checkHitEnemy=function(){for(var i=0;i=enemies[i].drawX&&this.drawX<=enemies[i].drawX+enemies[i].width&&this.drawY>=enemies[i].drawY&&this.drawY<=enemies[i].drawY+enemies[i].height){this.explosion.drawX=enemies[i].drawX-this.explosion.width/2;this.explosion.drawY=enemies[i].drawY;this.explosion.hasHit=true;this.recycle();enemies[i].recycleEnemy();this.jet.updateScore(enemies[i].rewardPoints)}}};function Explosion(){this.srcX=742;this.srcY=495;this.drawX=0;this.drawY=0;this.width=60;this.height=55;this.currentFrame=0;this.totalFrames=10;this.hasHit=false}Explosion.prototype.draw=function(){if(this.currentFrame<=this.totalFrames){contextJet.drawImage(imgSprite,this.srcX,this.srcY,this.width,this.height,this.drawX,this.drawY,this.width,this.height);this.currentFrame++}else{this.hasHit=false;this.currentFrame=0}};function Enemy(){this.srcX=0;this.srcY=644;this.width=97;this.height=51;this.speed=2;this.drawX=Math.floor(Math.random()*1e3)+gameWidth;this.drawY=Math.floor(Math.random()*gameHeight)-this.height;this.rewardPoints=5}Enemy.prototype.draw=function(){this.drawX-=this.speed;contextEnemy.drawImage(imgSprite,this.srcX,this.srcY,this.width,this.height,this.drawX,this.drawY,this.width,this.height);this.checkEscaped()};Enemy.prototype.checkEscaped=function(){if(this.drawX+this.width<=0){this.recycleEnemy()}};Enemy.prototype.recycleEnemy=function(){this.drawX=Math.floor(Math.random()*1e3)+gameWidth;this.drawY=Math.floor(Math.random()*gameHeight)};function clearContextEnemy(){contextEnemy.clearRect(0,0,gameWidth,gameHeight)}function Button(xL,xR,yT,yB){this.xLeft=xL;this.xRight=xR;this.yTop=yT;this.yBottom=yB}Button.prototype.checkClicked=function(){return this.xLeft<=mouseX&&mouseX<=this.xRight&&this.yTop<=mouseY&&mouseY<=this.yBottom};function mouseClicked(e){mouseX=e.pageX-canvasBg.offsetLeft;mouseY=e.pageY-canvasBg.offsetTop;if(!isPlaying)if(btnPlay.checkClicked())playGame()}function checkKeyDown(e){var keyId=e.keyCode||e.which;if(keyId==38||keyId==87){jet1.isUpKey=true;e.preventDefault()}if(keyId==39||keyId==68){jet1.isRightKey=true;e.preventDefault()}if(keyId==40||keyId==83){jet1.isDownKey=true;e.preventDefault()}if(keyId==37||keyId==65){jet1.isLeftKey=true;e.preventDefault()}if(keyId==32){jet1.isSpaceBar=true;e.preventDefault()}}function checkKeyUp(e){var keyId=e.keyCode||e.which;if(keyId==38||keyId==87){jet1.isUpKey=false;e.preventDefault()}if(keyId==39||keyId==68){jet1.isRightKey=false;e.preventDefault()}if(keyId==40||keyId==83){jet1.isDownKey=false;e.preventDefault()}if(keyId==37||keyId==65){jet1.isLeftKey=false;e.preventDefault()}if(keyId==32){jet1.isSpaceBar=false;e.preventDefault()}} 2 | -------------------------------------------------------------------------------- /static/game2.js: -------------------------------------------------------------------------------- 1 | var canvasBg=document.getElementById("canvasBg");var contextBg=canvasBg.getContext("2d");var canvasJet=document.getElementById("canvasJet");var contextJet=canvasJet.getContext("2d");var canvasEnemy=document.getElementById("canvasEnemy");var contextEnemy=canvasEnemy.getContext("2d");var canvasHud=document.getElementById("canvasHud");var contextHud=canvasHud.getContext("2d");contextHud.fillStyle="hsla(0, 0%, 0%, 0.5)";contextHud.font="bold 20px Arial";var jet1=new Jet;var btnPlay=new Button(265,535,220,335);var gameWidth=canvasBg.width;var gameHeight=canvasBg.height;var mouseX=0;var mouseY=0;var isPlaying=false;var requestAnimFrame=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(callback){window.setTimeout(callback,1e3/60)};var enemies=[];var imgSprite=new Image;imgSprite.src="/static/sprite2.png";imgSprite.addEventListener("load",init,false);var bgDrawX1=0;var bgDrawX2=1600;function moveBg(){bgDrawX1-=5;bgDrawX2-=5;if(bgDrawX1<=-1600)bgDrawX1=1600;if(bgDrawX2<=-1600)bgDrawX2=1600;drawBg()}function init(){spawnEnemy(5);drawMenu();document.addEventListener("click",mouseClicked,false)}function playGame(){drawBg();startLoop();updateHud();document.addEventListener("keydown",checkKeyDown,false);document.addEventListener("keyup",checkKeyUp,false)}function spawnEnemy(numSpawns){for(var i=0;i0){this.drawY-=this.speed}if(this.isRightKey&&this.rightX0){this.drawX-=this.speed}};Jet.prototype.drawAllBullets=function(){for(var i=0;i=0)this.bullets[i].draw();if(this.bullets[i].explosion.hasHit)this.bullets[i].explosion.draw()}};Jet.prototype.checkShooting=function(){if(this.isSpaceBar&&!this.isShooting){this.isShooting=true;this.bullets[this.currentBullet].fire(this.noseX,this.noseY);this.currentBullet++;if(this.currentBullet>=this.bullets.length)this.currentBullet=0}else if(!this.isSpaceBar){this.isShooting=false}};Jet.prototype.updateScore=function(points){this.score+=points;updateHud()};function clearContextJet(){contextJet.clearRect(0,0,gameWidth,gameHeight)}function Bullet(j){this.srcX=176;this.srcY=501;this.drawX=-20;this.drawY=0;this.width=48;this.height=20;this.speed=3;this.explosion=new Explosion;this.jet=j}Bullet.prototype.draw=function(){this.drawX+=this.speed;contextJet.drawImage(imgSprite,this.srcX,this.srcY,this.width,this.height,this.drawX,this.drawY,this.width,this.height);this.checkHitEnemy();if(this.drawX>gameWidth)this.recycle()};Bullet.prototype.recycle=function(){this.drawX=-20};Bullet.prototype.fire=function(noseX,noseY){this.drawX=noseX;this.drawY=noseY};Bullet.prototype.checkHitEnemy=function(){for(var i=0;i=enemies[i].drawX&&this.drawX<=enemies[i].drawX+enemies[i].width&&this.drawY>=enemies[i].drawY&&this.drawY<=enemies[i].drawY+enemies[i].height){this.explosion.drawX=enemies[i].drawX-this.explosion.width/2;this.explosion.drawY=enemies[i].drawY;this.explosion.hasHit=true;this.recycle();enemies[i].recycleEnemy();this.jet.updateScore(enemies[i].rewardPoints)}}};function Explosion(){this.srcX=720;this.srcY=510;this.drawX=0;this.drawY=0;this.width=129;this.height=61;this.currentFrame=0;this.totalFrames=10;this.hasHit=false}Explosion.prototype.draw=function(){if(this.currentFrame<=this.totalFrames){contextJet.drawImage(imgSprite,this.srcX,this.srcY,this.width,this.height,this.drawX,this.drawY,this.width,this.height);this.currentFrame++}else{this.hasHit=false;this.currentFrame=0}};function Enemy(){this.srcX=0;this.srcY=644;this.width=97;this.height=51;this.speed=2;this.drawX=Math.floor(Math.random()*1e3)+gameWidth;this.drawY=Math.floor(Math.random()*gameHeight)-this.height;this.rewardPoints=5}Enemy.prototype.draw=function(){this.drawX-=this.speed;contextEnemy.drawImage(imgSprite,this.srcX,this.srcY,this.width,this.height,this.drawX,this.drawY,this.width,this.height);this.checkEscaped()};Enemy.prototype.checkEscaped=function(){if(this.drawX+this.width<=0){this.recycleEnemy()}};Enemy.prototype.recycleEnemy=function(){this.drawX=Math.floor(Math.random()*1e3)+gameWidth;this.drawY=Math.floor(Math.random()*gameHeight)};function clearContextEnemy(){contextEnemy.clearRect(0,0,gameWidth,gameHeight)}function Button(xL,xR,yT,yB){this.xLeft=xL;this.xRight=xR;this.yTop=yT;this.yBottom=yB}Button.prototype.checkClicked=function(){return this.xLeft<=mouseX&&mouseX<=this.xRight&&this.yTop<=mouseY&&mouseY<=this.yBottom};function mouseClicked(e){mouseX=e.pageX-canvasBg.offsetLeft;mouseY=e.pageY-canvasBg.offsetTop;if(!isPlaying)if(btnPlay.checkClicked())playGame()}function checkKeyDown(e){var keyId=e.keyCode||e.which;if(keyId==38||keyId==87){jet1.isUpKey=true;e.preventDefault()}if(keyId==39||keyId==68){jet1.isRightKey=true;e.preventDefault()}if(keyId==40||keyId==83){jet1.isDownKey=true;e.preventDefault()}if(keyId==37||keyId==65){jet1.isLeftKey=true;e.preventDefault()}if(keyId==32){jet1.isSpaceBar=true;e.preventDefault()}}function checkKeyUp(e){var keyId=e.keyCode||e.which;if(keyId==38||keyId==87){jet1.isUpKey=false;e.preventDefault()}if(keyId==39||keyId==68){jet1.isRightKey=false;e.preventDefault()}if(keyId==40||keyId==83){jet1.isDownKey=false;e.preventDefault()}if(keyId==37||keyId==65){jet1.isLeftKey=false;e.preventDefault()}if(keyId==32){jet1.isSpaceBar=false;e.preventDefault()}} 2 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/groovy 2 | 3 | // load pipeline functions 4 | // Requires pipeline-github-lib plugin to load library from github 5 | 6 | @Library('github.com/lachie83/jenkins-pipeline@dev') 7 | 8 | def pipeline = new io.estrado.Pipeline() 9 | 10 | podTemplate(label: 'jenkins-pipeline', containers: [ 11 | containerTemplate(name: 'jnlp', image: 'lachlanevenson/jnlp-slave:3.10-1-alpine', args: '${computer.jnlpmac} ${computer.name}', workingDir: '/home/jenkins', resourceRequestCpu: '200m', resourceLimitCpu: '300m', resourceRequestMemory: '256Mi', resourceLimitMemory: '512Mi'), 12 | containerTemplate(name: 'docker', image: 'docker:1.12.6', command: 'cat', ttyEnabled: true), 13 | containerTemplate(name: 'golang', image: 'golang:1.8.3', command: 'cat', ttyEnabled: true), 14 | containerTemplate(name: 'helm', image: 'lachlanevenson/k8s-helm:v2.6.0', command: 'cat', ttyEnabled: true), 15 | containerTemplate(name: 'kubectl', image: 'lachlanevenson/k8s-kubectl:v1.4.8', command: 'cat', ttyEnabled: true) 16 | ], 17 | volumes:[ 18 | hostPathVolume(mountPath: '/var/run/docker.sock', hostPath: '/var/run/docker.sock'), 19 | ]){ 20 | 21 | node ('jenkins-pipeline') { 22 | 23 | def pwd = pwd() 24 | def chart_dir = "${pwd}/charts/croc-hunter" 25 | 26 | checkout scm 27 | 28 | // read in required jenkins workflow config values 29 | def inputFile = readFile('Jenkinsfile.json') 30 | def config = new groovy.json.JsonSlurperClassic().parseText(inputFile) 31 | println "pipeline config ==> ${config}" 32 | 33 | // continue only if pipeline enabled 34 | if (!config.pipeline.enabled) { 35 | println "pipeline disabled" 36 | return 37 | } 38 | 39 | // set additional git envvars for image tagging 40 | pipeline.gitEnvVars() 41 | 42 | // If pipeline debugging enabled 43 | if (config.pipeline.debug) { 44 | println "DEBUG ENABLED" 45 | sh "env | sort" 46 | 47 | println "Runing kubectl/helm tests" 48 | container('kubectl') { 49 | pipeline.kubectlTest() 50 | } 51 | container('helm') { 52 | pipeline.helmConfig() 53 | } 54 | } 55 | 56 | def acct = pipeline.getContainerRepoAcct(config) 57 | 58 | // tag image with version, and branch-commit_id 59 | def image_tags_map = pipeline.getContainerTags(config) 60 | 61 | // compile tag list 62 | def image_tags_list = pipeline.getMapValues(image_tags_map) 63 | 64 | stage ('compile and test') { 65 | 66 | container('golang') { 67 | sh "go test -v -race ./..." 68 | sh "make bootstrap build" 69 | } 70 | } 71 | 72 | stage ('test deployment') { 73 | 74 | container('helm') { 75 | 76 | // run helm chart linter 77 | pipeline.helmLint(chart_dir) 78 | 79 | // run dry-run helm chart installation 80 | pipeline.helmDeploy( 81 | dry_run : true, 82 | name : config.app.name, 83 | namespace : config.app.name, 84 | chart_dir : chart_dir, 85 | set : [ 86 | "imageTag": image_tags_list.get(0), 87 | "replicas": config.app.replicas, 88 | "cpu": config.app.cpu, 89 | "memory": config.app.memory, 90 | "ingress.hostname": config.app.hostname, 91 | ] 92 | ) 93 | 94 | } 95 | } 96 | 97 | stage ('publish container') { 98 | 99 | container('docker') { 100 | 101 | // perform docker login to container registry as the docker-pipeline-plugin doesn't work with the next auth json format 102 | withCredentials([[$class : 'UsernamePasswordMultiBinding', credentialsId: config.container_repo.jenkins_creds_id, 103 | usernameVariable: 'USERNAME', passwordVariable: 'PASSWORD']]) { 104 | sh "docker login -u ${env.USERNAME} -p ${env.PASSWORD} ${config.container_repo.host}" 105 | } 106 | 107 | // build and publish container 108 | pipeline.containerBuildPub( 109 | dockerfile: config.container_repo.dockerfile, 110 | host : config.container_repo.host, 111 | acct : acct, 112 | repo : config.container_repo.repo, 113 | tags : image_tags_list, 114 | auth_id : config.container_repo.jenkins_creds_id, 115 | image_scanning: config.container_repo.image_scanning 116 | ) 117 | 118 | // anchore image scanning configuration 119 | println "Add container image tags to anchore scanning list" 120 | 121 | def tag = image_tags_list.get(0) 122 | def imageLine = "${config.container_repo.host}/${acct}/${config.container_repo.repo}:${tag}" + ' ' + env.WORKSPACE + '/Dockerfile' 123 | writeFile file: 'anchore_images', text: imageLine 124 | anchore name: 'anchore_images', inputQueries: [[query: 'list-packages all'], [query: 'list-files all'], [query: 'cve-scan all'], [query: 'show-pkg-diffs base']] 125 | 126 | } 127 | 128 | } 129 | 130 | if (env.BRANCH_NAME =~ "PR-*" ) { 131 | stage ('deploy to k8s') { 132 | container('helm') { 133 | // Deploy using Helm chart 134 | pipeline.helmDeploy( 135 | dry_run : false, 136 | name : env.BRANCH_NAME.toLowerCase(), 137 | namespace : env.BRANCH_NAME.toLowerCase(), 138 | chart_dir : chart_dir, 139 | set : [ 140 | "imageTag": image_tags_list.get(0), 141 | "replicas": config.app.replicas, 142 | "cpu": config.app.cpu, 143 | "memory": config.app.memory, 144 | "ingress.hostname": config.app.hostname, 145 | ] 146 | ) 147 | 148 | // Run helm tests 149 | if (config.app.test) { 150 | pipeline.helmTest( 151 | name : env.BRANCH_NAME.toLowerCase() 152 | ) 153 | } 154 | 155 | // delete test deployment 156 | pipeline.helmDelete( 157 | name : env.BRANCH_NAME.toLowerCase() 158 | ) 159 | } 160 | } 161 | } 162 | 163 | // deploy only the master branch 164 | if (env.BRANCH_NAME == 'master') { 165 | stage ('deploy to k8s') { 166 | container('helm') { 167 | // Deploy using Helm chart 168 | pipeline.helmDeploy( 169 | dry_run : false, 170 | name : config.app.name, 171 | namespace : config.app.name, 172 | chart_dir : chart_dir, 173 | set : [ 174 | "imageTag": image_tags_list.get(0), 175 | "replicas": config.app.replicas, 176 | "cpu": config.app.cpu, 177 | "memory": config.app.memory, 178 | "ingress.hostname": config.app.hostname, 179 | ] 180 | ) 181 | 182 | // Run helm tests 183 | if (config.app.test) { 184 | pipeline.helmTest( 185 | name : config.app.name 186 | ) 187 | } 188 | } 189 | } 190 | } 191 | } 192 | } 193 | --------------------------------------------------------------------------------