diff --git a/.gitignore b/.gitignore index 25d7f824547b35d46f6f77ac75112d9ad3de545b..f9ff4393f402abf8e6d91d199a488fa41c891d0b 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ # dependencies /node_modules +# Helm release files for testing. +/values.yaml + # testing /coverage diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..c571ace79910e0a9b00a2544d71e1726d27b3d01 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,5 @@ +FROM nginx:stable-alpine + +COPY build /usr/share/nginx/html/ +#COPY docker/nginx.conf /etc/nginx/conf.d/default.conf + diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000000000000000000000000000000000000..bae5c12201652475668302d47d27e2933e94302a --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,163 @@ +#!/usr/bin/env groovy + +@Library('nynja-common') _ + +pipeline { + environment { + SLACK_CHANNEL = "#nynja-devops-feed" + NAMESPACE = "admin-console" + APP_NAME = "admin-console-service" + IMAGE_NAME = "eu.gcr.io/nynja-ci-201610/${NAMESPACE}/${APP_NAME}" + IMAGE_BUILD_TAG = "$BRANCH_NAME-$BUILD_NUMBER" + HELM_CHART_NAME = "admin-console" + DEV_BRANCH = "dev" + } + agent { + kubernetes(builders.multi([ + "nodejs":"node:10.14.1", + "helm":"lachlanevenson/k8s-helm:v2.9.1" + ])) + } + options { + skipDefaultCheckout() + buildDiscarder(logRotator(numToKeepStr: '15')) + } + stages { + stage('Checkout') { + steps { + container('nodejs') { + script { + def vars = checkout scm + vars.each { k,v -> env.setProperty(k, v) } + } + slackSend channel: SLACK_CHANNEL, message: slackStartMsg() + slackSend channel: SLACK_CHANNEL, message: "", attachments: slackBuildInfo() + } + } + } + /* + stage('Build PR') { + when { + branch 'PR-*' + } + stages { + stage('Build') { + steps { + echo 'build & test' + dockerBuildAndPushToRegistry "${NAMESPACE}/${APP_NAME}", [IMAGE_BUILD_TAG] + } + } + stage('Deploy preview') { + steps { + echo 'build & test' + } + } + } + } + */ + stage('Build Dev') { + when { + branch env.DEV_BRANCH + } + stages { + stage('Build') { + steps { + container('nodejs') { + sh 'npm install' + sh 'npm run build:prod' + dockerBuildAndPushToRegistry "${NAMESPACE}/${APP_NAME}", [IMAGE_BUILD_TAG] + } + } + } + stage("Helm chart") { + steps { + container('helm') { + helmBuildAndPushToRegistry HELM_CHART_NAME + } + } + } + stage('Deploy preview') { + steps { + deployHelmTo "dev", NAMESPACE + } + } + } + post { + success { + container('nodejs') { slackSend channel: SLACK_CHANNEL, message: slackEndMsg(), color: 'good' } + } + failure { + container('nodejs') { slackSend channel: SLACK_CHANNEL, message: slackEndMsg(), color: 'danger' } + } + } + } + stage('Build Release') { + when { + branch 'master' + } + stages { + stage("Build") { + steps { + container('nodejs') { + sh 'npm install' + sh 'npm run build:prod' + dockerBuildAndPushToRegistry "${NAMESPACE}/${APP_NAME}", [IMAGE_BUILD_TAG] + } + } + } + stage("Helm chart") { + steps { + container('helm') { + helmBuildAndPushToRegistry HELM_CHART_NAME + } + } + } + stage("Approval: Deploy to staging ?") { + steps { + slackSend channel: SLACK_CHANNEL, message: "$APP_NAME: build #$BUILD_NUMBER ready to deploy to `STAGING`, approval required: $BUILD_URL (24h)" + + timeout(time: 24, unit: 'HOURS') { input 'Deploy to staging ?' } + } + post { failure { echo 'Deploy aborted for build #...' }} + } + stage("Deploy to staging") { + steps { + slackSend channel: SLACK_CHANNEL, message: "$APP_NAME: deploying build #$BUILD_NUMBER to `STAGING`" + deployHelmTo "staging", NAMESPACE + } + } + stage("Approval: Deploy to production ?") { + steps { + slackSend channel: SLACK_CHANNEL, message: "$APP_NAME: build #$BUILD_NUMBER ready to deploy to `PRODUCTION`, approval required: $BUILD_URL (24h)" + + timeout(time: 7, unit: 'DAYS') { input 'Deploy to production ?' } + } + post { failure { echo 'Deploy aborted for build #...' }} + } + stage('Tagging release') { + steps { + container('nodejs') { + // Updating the "latest tag" + dockerTagLatestAndPushToRegistry "${NAMESPACE}/${APP_NAME}", IMAGE_BUILD_TAG + } + } + } + /* + stage('Deploy release to canary') { + steps { + slackSend channel: SLACK_CHANNEL, message: "$APP_NAME: deploying build #$BUILD_NUMBER to `PRODUCTION` (canary)" + echo "deploy to canary" + } + } + */ + stage("Deploy to production") { + steps { + slackSend channel: SLACK_CHANNEL, message: "$APP_NAME: deploying build #$BUILD_NUMBER to `PRODUCTION`" + + deployHelmTo "prod", NAMESPACE + } + } + } + } + } +} diff --git a/charts/admin-console/.helmignore b/charts/admin-console/.helmignore new file mode 100644 index 0000000000000000000000000000000000000000..f0c13194444163d1cba5c67d9e79231a62bc8f44 --- /dev/null +++ b/charts/admin-console/.helmignore @@ -0,0 +1,21 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/charts/admin-console/Chart.yaml b/charts/admin-console/Chart.yaml new file mode 100644 index 0000000000000000000000000000000000000000..2ba852c6e603abb63cf6f7951b42ead253a425b2 --- /dev/null +++ b/charts/admin-console/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +appVersion: "1.0" +description: Deployment of the nynja adming console. +name: admin-console +version: 0.1.0 diff --git a/charts/admin-console/templates/00-label.yaml b/charts/admin-console/templates/00-label.yaml new file mode 100644 index 0000000000000000000000000000000000000000..dd5d17afc13ed355afb9d48f5d41bb161f685ccc --- /dev/null +++ b/charts/admin-console/templates/00-label.yaml @@ -0,0 +1,32 @@ +# This hook depends on helm creating the target namespace if it doesn't exist +# before the hook is called. This is the case on Helm v2.9.1 +apiVersion: batch/v1 +kind: Job +metadata: + name: enable-istio-injection-{{ .Release.Namespace }} + namespace: kube-system + labels: + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} + app.kubernetes.io/managed-by: {{.Release.Service | quote }} + app.kubernetes.io/instance: {{.Release.Name | quote }} + helm.sh/chart: "{{.Chart.Name}}-{{.Chart.Version}}" + annotations: + helm.sh/hook: pre-install + helm.sh/hook-delete-policy: hook-before-creation,hook-succeeded +spec: + template: + spec: + containers: + - name: labeler + image: gcr.io/google_containers/hyperkube:v1.9.7 + command: + - kubectl + - label + - --overwrite + - ns + - {{ .Release.Namespace }} + - istio-injection=enabled + restartPolicy: Never + # use tiller service account since it should have permissions to do namespace labeling + serviceAccountName: tiller diff --git a/charts/admin-console/templates/_helpers.tpl b/charts/admin-console/templates/_helpers.tpl new file mode 100644 index 0000000000000000000000000000000000000000..6501da33a90125cc75b4502f113dc378016921a8 --- /dev/null +++ b/charts/admin-console/templates/_helpers.tpl @@ -0,0 +1,32 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "admin-console.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "admin-console.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "admin-console.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} diff --git a/charts/admin-console/templates/deployment.yaml b/charts/admin-console/templates/deployment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..ce48e4b5f5b3dbaf95474c08dd9ce317752d7439 --- /dev/null +++ b/charts/admin-console/templates/deployment.yaml @@ -0,0 +1,62 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: {{ template "admin-console.fullname" . }} + labels: + app: {{ template "admin-console.name" . }} + chart: {{ template "admin-console.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app: {{ template "admin-console.name" . }} + release: {{ .Release.Name }} + template: + metadata: + annotations: + sidecar.istio.io/inject: "true" + labels: + app: {{ template "admin-console.name" . }} + release: {{ .Release.Name }} + spec: + containers: + - name: nginx-admin + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 80 + readinessProbe: + httpGet: + path: / + port: http + successThreshold: 1 + failureThreshold: 10 + initialDelaySeconds: 20 + periodSeconds: 3 + timeoutSeconds: 5 + livenessProbe: + httpGet: + path: / + port: http + successThreshold: 1 + failureThreshold: 10 + initialDelaySeconds: 10 + periodSeconds: 3 + timeoutSeconds: 5 + resources: +{{ toYaml .Values.resources | indent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} diff --git a/charts/admin-console/templates/service.yaml b/charts/admin-console/templates/service.yaml new file mode 100644 index 0000000000000000000000000000000000000000..dca83f18fdc807da86d2044c8f33fa6a614799c8 --- /dev/null +++ b/charts/admin-console/templates/service.yaml @@ -0,0 +1,18 @@ +kind: Service +apiVersion: v1 +metadata: + name: {{ template "admin-console.fullname" . }} + labels: + app: {{ template "admin-console.name" . }} + chart: {{ template "admin-console.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + selector: + app: {{ template "admin-console.name" . }} + release: {{ .Release.Name }} + ports: + - protocol: TCP + port: 80 + targetPort: 80 + name: http diff --git a/charts/admin-console/templates/virtualservice.yaml b/charts/admin-console/templates/virtualservice.yaml new file mode 100644 index 0000000000000000000000000000000000000000..cafac237e47dc1ef869375870dd50063f8f62bf8 --- /dev/null +++ b/charts/admin-console/templates/virtualservice.yaml @@ -0,0 +1,40 @@ +apiVersion: networking.istio.io/v1alpha3 +kind: VirtualService +metadata: + name: {{ template "admin-console.fullname" . }} + labels: + app: {{ template "admin-console.name" . }} + chart: {{ template "admin-console.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + gateways: + {{- range .Values.gateway.selector }} + - {{ . }} + {{- end }} + hosts: + {{- range .Values.gateway.hosts }} + - {{ . }} + {{- end }} + http: + - route: + - destination: + host: {{ template "admin-console.fullname" . }} + port: + number: 80 + corsPolicy: + allowOrigin: + {{- range .Values.corsPolicy.allowOrigin }} + - {{ . }} + {{- end }} + allowMethods: + {{- range .Values.corsPolicy.allowMethods}} + - {{ . }} + {{- end }} + allowCredentials: {{ .Values.corsPolicy.allowCredentials }} + allowHeaders: + {{- range .Values.corsPolicy.allowHeaders }} + - {{ . }} + {{- end }} + maxAge: {{ .Values.corsPolicy.maxAge }} + diff --git a/charts/admin-console/values.yaml b/charts/admin-console/values.yaml new file mode 100644 index 0000000000000000000000000000000000000000..094c1a115715b92f25aadcef3c5e063b0ecc01b5 --- /dev/null +++ b/charts/admin-console/values.yaml @@ -0,0 +1,34 @@ + +replicaCount: 1 + +auth: + # Whether of not basic auth should be enabled to access the website + basic: + enabled: true + # the .htpasswd file content, sealed for the target environment (cluster + namespace) + sealedSecret: + +image: + repository: eu.gcr.io/nynja-ci-201610/admin-console/admin-console + tag: stable + pullPolicy: IfNotPresent + +gateway: + selector: + - api-gateway.default.svc.cluster.local + hosts: + - admin-console.dev-eu.nynja.net + +resources: + limits: + cpu: 100m + memory: 200Mi + requests: + cpu: 50m + memory: 100Mi + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000000000000000000000000000000000000..b2c96d778d4af91eebc8586df54dde645187384a --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,82 @@ +server { + listen 80; + server_name _; + + charset utf-8; + #access_log /var/log/nginx/host.access.log main; + + # GZIP settings + gzip on; + gzip_comp_level 5; + gzip_disable "msie6"; + gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/javascript; + + location / { + root /usr/share/nginx/html; + index index.html; + try_files $uri /index.html; + + set $auth_type "Restricted"; + # Handle join links (ex. join.nynja.net/123) + set $join_link 0; + + if ( $host ~ ^join\.(?.+)$ ) { + set $join_link 1; + set $auth_type "off"; + } + if ($request_uri ~ ^/.well-known/.*$ ) { + set $join_link 0; + set $auth_type "off"; + } + if ($join_link = 1) { + return 302 https://web.$domain/join$request_uri; + } + + auth_basic $auth_type; + auth_basic_user_file /usr/share/nginx/html/.htpasswd; + } + + location /status { + auth_basic off; + return 200; + } + + # Webpack debug proxy (temporary) + #location /debug { + # proxy_set_header X-Forwarded-For $remote_addr; + # proxy_set_header Host $http_host; + # proxy_pass "http://127.0.0.1:8080"; + #} + + #error_page 404 /404.html; + + # redirect server error pages to the static page /50x.html + # + #error_page 500 502 503 504 /50x.html; + #location = /50x.html { + # root /usr/share/nginx/html; + #} + + # proxy the PHP scripts to Apache listening on 127.0.0.1:80 + # + #location ~ \.php$ { + # proxy_pass http://127.0.0.1; + #} + + # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 + # + #location ~ \.php$ { + # root html; + # fastcgi_pass 127.0.0.1:9000; + # fastcgi_index index.php; + # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; + # include fastcgi_params; + #} + + # deny access to .htaccess files, if Apache's document root + # concurs with nginx's one + # + location ~ /\.ht { + deny all; + } +} diff --git a/releases/dev/admin-console.yaml b/releases/dev/admin-console.yaml new file mode 100644 index 0000000000000000000000000000000000000000..fa80e3b8ccb933612d812c24cf494de0e742cee8 --- /dev/null +++ b/releases/dev/admin-console.yaml @@ -0,0 +1,55 @@ +kind: HelmRelease +metadata: + name: admin-console + namespace: content +spec: + chart: + name: admin-console + values: + replicaCount: 1 + + image: + repository: ${IMAGE_NAME} + tag: ${IMAGE_BUILD_TAG} + + gateway: + selector: + - api-gateway.default.svc.cluster.local + hosts: + - admin-console.dev-eu.nynja.net + + resources: + limits: + cpu: 1 + memory: 1500Mi + requests: + cpu: 500m + memory: 1000Mi + + # ports: + # containerPort: + # http: 8001 + + # CORS policy + corsPolicy: + allowOrigin: + - http://localhost:3000 + - https://localhost + - https://localhost/grpc/ + - http://10.191.224.180:3000 + - https://localhost:8080 + - https://127.0.0.1:8080 + - https://web.dev-eu.nynja.net + - https://web.staging.nynja.net + - https://web.nynja.net + allowMethods: + - POST + - GET + - OPTIONS + allowCredentials: false + allowHeaders: + - content-type + - x-grpc-web + - authorization + maxAge: "600s" +