diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f2d4151 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +.git +.idea +*.iml +.editorconfig +build-harness +.build-harness diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..da7face --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +# Override for Makefile +[{Makefile, makefile, GNUmakefile}] +indent_style = tab +indent_size = 4 + +[Makefile.*] +indent_style = tab +indent_size = 4 + +[*.yaml] +indent_style = spaces +indent_size = 2 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9123714 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +release/* +.build-harness +build-harness/ +.idea +*.iml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..943f92a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,32 @@ +FROM golang:alpine3.8 AS builder + +# Copy source into builder +ADD . /src + +# Build the app +RUN cd /src && \ + go build -o example-app + +# Build the final image +FROM alpine:3.8 as final + +# Install the cloudposse alpine repository +ADD https://apk.cloudposse.com/ops@cloudposse.com.rsa.pub /etc/apk/keys/ +RUN echo "@cloudposse https://apk.cloudposse.com/3.8/vendor" >> /etc/apk/repositories +RUN apk add --update bash variant@cloudposse + +# Expose port of the app +EXPOSE 8080 + +# Set the runtime working directory +WORKDIR /app + +# Copy the helmfile deployment configuration +COPY deploy/ /deploy/ +COPY public/ /app/public/ + +# Install the app +COPY --from=builder /src/example-app /app/ + +# Define the entrypoint +ENTRYPOINT ["./example-app"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7e753ed --- /dev/null +++ b/Makefile @@ -0,0 +1,12 @@ +DOCKER_IMAGE_NAME ?= example-app +SHELL = /bin/bash + +PATH:=$(PATH):$(GOPATH)/bin + +-include $(shell curl -sSL -o .build-harness "https://git.io/build-harness"; echo .build-harness) + +build: go/build + @exit 0 + +run: + docker run -it -p 8080:8080 --rm $(DOCKER_IMAGE_NAME) diff --git a/README.md b/README.md index 199308b..6bea3b9 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,5 @@ # example-app -Example application for CI/CD demonstrations + +Example application for Codefresh CI/CD demonstrations. + +Contact diff --git a/codefresh/build.yaml b/codefresh/build.yaml new file mode 100644 index 0000000..f2bb7d5 --- /dev/null +++ b/codefresh/build.yaml @@ -0,0 +1,32 @@ +version: '1.0' + +stages: + - Prepare + - Build + - Push + +steps: + main_clone: + title: "Clone repository" + type: git-clone + stage: Prepare + description: "Initialize" + repo: ${{CF_REPO_OWNER}}/${{CF_REPO_NAME}} + git: CF-default + revision: ${{CF_REVISION}} + + build_image: + title: Build image + stage: Build + type: build + description: Build image + image_name: ${{CF_REPO_NAME}} + dockerfile: Dockerfile + + push_image_commit: + title: Push image with commit tag + stage: Push + type: push + candidate: ${{build_image}} + tags: + - "${{CF_REVISION}}" \ No newline at end of file diff --git a/codefresh/deploy.yaml b/codefresh/deploy.yaml new file mode 100644 index 0000000..eb2137d --- /dev/null +++ b/codefresh/deploy.yaml @@ -0,0 +1,70 @@ +version: '1.0' + +stages: + - Prepare + - Deploy + +steps: + main_clone: + title: "Create Context" + stage: Prepare + image: alpine + commands: + - cf_export NAMESPACE=${{STAGE}} + - cf_export IMAGE_NAME=${{CF_DOCKER_REPO_URL}}/${{CF_REPO_NAME}} + - cf_export IMAGE_TAG=${{CF_RELEASE_TAG}} + + set_github_deployment_status_to_pending: + title: Set GitHub deployment status to "pending" + stage: Deploy + image: cloudposse/github-status-updater + environment: + - GITHUB_ACTION=update_state + - GITHUB_TOKEN=${{GITHUB_TOKEN}} + - GITHUB_OWNER=${{CF_REPO_OWNER}} + - GITHUB_REPO=${{CF_REPO_NAME}} + - GITHUB_REF=${{CF_REVISION}} + - GITHUB_CONTEXT=${{STAGE}}-environment + - GITHUB_STATE=pending + - GITHUB_DESCRIPTION=Deploying changes to ${{NAMESPACE}} namespace + - GITHUB_TARGET_URL=${{ATLANTIS_ATLANTIS_URL}} + when: + condition: + all: + githubNotificationsEnabled: "'${{GITHUB_NOTIFICATIONS_ENABLED}}' == 'true'" + + deploy_helmfile: + title: "Deploy with helmfile" + stage: "Deploy" + image: "${{CF_DOCKER_REPO_URL}}/${{CF_REPO_NAME}}:${{CF_REVISION}}" + working_directory: /deploy/ + commands: + # Announce the release version + - "echo 'Preparing to deploy version ${{CF_RELEASE_TAG}}'" + # Fetch the build-harness + - "curl -sSL -o Makefile https://git.io/build-harness" + # Initialize the build-harness + - "make init" + # Install or upgrade tiller + - "make helm/toolbox/upsert" + # Deploy chart to cluster using helmfile + - "helmfile --namespace ${{NAMESPACE}} --selector color=blue sync" + + set_github_deployment_status_to_success: + title: Set GitHub deployment status to "success" + stage: "Deploy" + image: cloudposse/github-status-updater + environment: + - GITHUB_ACTION=update_state + - GITHUB_TOKEN=${{GITHUB_TOKEN}} + - GITHUB_OWNER=${{CF_REPO_OWNER}} + - GITHUB_REPO=${{CF_REPO_NAME}} + - GITHUB_REF=${{CF_REVISION}} + - GITHUB_CONTEXT=${{STAGE}}-environment + - GITHUB_STATE=success + - GITHUB_DESCRIPTION=Deployed to ${{NAMESPACE}} namespace + - GITHUB_TARGET_URL=${{ATLANTIS_ATLANTIS_URL}} + when: + condition: + all: + githubNotificationsEnabled: "'${{GITHUB_NOTIFICATIONS_ENABLED}}' == 'true'" \ No newline at end of file diff --git a/codefresh/release.yaml b/codefresh/release.yaml new file mode 100644 index 0000000..07af5a5 --- /dev/null +++ b/codefresh/release.yaml @@ -0,0 +1,42 @@ +version: '1.0' + +stages: + - Prepare + - Promote + - Deploy + +steps: + main_clone: + title: "Create Context" + stage: "Prepare" + image: alpine + commands: + # Extract the postfix of a semver (e.g. 0.0.0-stage => stage) + - cf_export STAGE=$(echo ${{CF_RELEASE_TAG}} | sed -E 's/^[^-]+-?//') + + pull_image_sha: + title: Pull image with commit sha + stage: "Promote" + image: "${{CF_DOCKER_REPO_URL}}/${{CF_REPO_NAME}}:${{CF_REVISION}}" + commands: + - "true" + + push_image_tag: + title: Push image with release tag + stage: "Promote" + type: push + image_name: ${{CF_REPO_NAME}} + candidate: "${{CF_DOCKER_REPO_URL}}/${{CF_REPO_NAME}}:${{CF_REVISION}}" + tags: + - "${{CF_RELEASE_TAG}}" + + deploy: + title: Deploy Release + stage: "Deploy" + image: 'codefresh/cli:latest' + commands: + - codefresh run ${{CF_REPO_OWNER}}/${{CF_REPO_NAME}}/deploy-${{STAGE}} -d -b=${{CF_BRANCH}} -v CF_RELEASE_TAG=${{CF_RELEASE_TAG}} -v CF_PRERELEASE_FLAG=${{CF_PRERELEASE_FLAG}} -v STAGE=${{STAGE}} + when: + condition: + all: + stageDefined: "'${{STAGE}}' != ''" \ No newline at end of file diff --git a/deploy/ctl b/deploy/ctl new file mode 100755 index 0000000..f32fdc0 --- /dev/null +++ b/deploy/ctl @@ -0,0 +1,78 @@ +#!/usr/bin/env variant +# vim:set ft=yaml + +parameters: +- name: config + default: .color + description: "Config file that keeps track of the current color" + +- name: namespace + default: "default" + description: "Kubernetes namespace" + +tasks: + + # https://istio.io/docs/tasks/traffic-management/ingress/#determining-the-ingress-ip-and-ports-when-using-an-external-load-balancer + istio-ingress: + description: Output the FQHN of the Istio Ingress Gateway + script: | + kubectl --namespace istio-system get service istio-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' + + # https://istio.io/docs/setup/kubernetes/quick-start/ + istio-injection: + description: Enable Istio Sidecar Injection for a namespace + script: | + kubectl label namespace {{ get "namespace" }} istio-injection=enabled --overwrite=true + + use-context: + description: "Configure kube-context" + script: | + kubectl config use-context ${KUBE_CONTEXT} + + deps: + description: "Install alpine dependencies" + script: | + ! which apk >/dev/null || apk add --update curl make bash git kubectl@cloudposse helm@cloudposse helmfile@cloudposse + helm init --client-only + + color: + description: "Lookup the current color" + script: | + config=${CF_VOLUME_PATH:-.}/{{ get "config" }} + if [ -f ${config} ]; then + cat ${config} + else + echo "blue" | tee ${config} + fi + + blue-green: + description: "Trigger a blue-green deployment" + parameters: + - name: color + type: string + description: "Selected color to deploy" + + - name: blue + type: string + default: green + description: "Flip blue color to this color" + + - name: green + type: string + default: blue + description: "Flip green color to this color" + + steps: + - task: deps + - task: use-context + - task: istio-injection + - script: | + config=${CF_VOLUME_PATH:-.}/{{ get "config" }} + cur_color={{ get "color" }} + new_color={{ get "color" | get }} + echo "Config file is ${config}" + echo "Updating [${KUBE_CONTEXT}] context {{ get "namespace" }} namespace: $cur_color => $new_color" + export COLOR=$new_color + # Deploy the app and update istio virtual service + helmfile --namespace {{ get "namespace" }} sync + echo "$new_color" > ${config} diff --git a/deploy/helmfile.yaml b/deploy/helmfile.yaml new file mode 100644 index 0000000..21de18c --- /dev/null +++ b/deploy/helmfile.yaml @@ -0,0 +1,4 @@ +# Ordered list of releases. +helmfiles: + - "releases/app.yaml" + - "releases/istio.yaml" diff --git a/deploy/releases/app.yaml b/deploy/releases/app.yaml new file mode 100644 index 0000000..cdbba95 --- /dev/null +++ b/deploy/releases/app.yaml @@ -0,0 +1,108 @@ +repositories: +# Cloud Posse incubator repo of helm charts +- name: "cloudposse-incubator" + url: "https://charts.cloudposse.com/incubator/" + +releases: +# +# References: +# - https://github.com/cloudposse/charts/blob/master/incubator/monochart +# +- name: 'example-{{ requiredEnv "COLOR" }}' + labels: + color: "{{ requiredEnv "COLOR" }}" + chart: "cloudposse-incubator/monochart" + version: "0.7.0" + wait: true + force: true + recreatePods: false + values: + - fullnameOverride: "example-{{ requiredEnv "COLOR" }}" + image: + repository: '{{ env "IMAGE_NAME" | default "cloudposse/example-app" }}' + tag: '{{ env "IMAGE_TAG" | default "0.1.0" }}' + pullPolicy: Always + pullSecrets: + - "pull-secret" + replicaCount: 5 + # Deployment configuration + deployment: + enabled: true + strategy: + type: "RollingUpdate" + rollingUpdate: + maxUnavailable: 2 + revisionHistoryLimit: 10 + + # Configuration Settings + configMaps: + default: + enabled: true + env: + # Color to deploy + #COLOR: "#0099ff" # Blue + #COLOR: "#009900" # Green + COLOR: "{{ env "COLOR" }}" + + # Service endpoint + service: + enabled: true + type: ClusterIP + ports: + default: + internal: 8080 + external: 80 + + ingress: + default: + enabled: true + labels: + dns: "route53" + annotations: + kubernetes.io/ingress.class: nginx + kubernetes.io/tls-acme: "true" + external-dns.alpha.kubernetes.io/target: '{{ requiredEnv "NGINX_INGRESS_HOSTNAME" }}' + external-dns.alpha.kubernetes.io/ttl: "60" + hosts: + ### Required: APP_HOSTNAME; + '{{ requiredEnv "COLOR" }}.{{ env "APP_HOSTNAME" }}': / + tls: + ### Optional: ATLANTIS_TLS_SECRET_NAME; + - secretName: '{{ env "APP_TLS_SECRET_NAME" | default (requiredEnv "COLOR" | printf "example-%s-server-tls") }}' + hosts: + ### Required: APP_HOSTNAME; + - '{{ requiredEnv "COLOR" }}.{{ env "APP_HOSTNAME" }}' + + probes: + # Probe that ensures service is healthy + livenessProbe: + httpGet: + path: /healthz + port: default + scheme: HTTP + periodSeconds: 3 + initialDelaySeconds: 3 + timeoutSeconds: 3 + successThreshold: 1 + failureThreshold: 2 + scheme: HTTP + + # Probe that ensures service has started + readinessProbe: + httpGet: + path: /healthz + port: default + scheme: HTTP + periodSeconds: 3 + initialDelaySeconds: 3 + timeoutSeconds: 3 + successThreshold: 1 + failureThreshold: 2 + + resources: + requests: + memory: 10Mi + cpu: 100m + limits: + memory: 10Mi + cpu: 100m diff --git a/deploy/releases/istio.yaml b/deploy/releases/istio.yaml new file mode 100644 index 0000000..4d4a266 --- /dev/null +++ b/deploy/releases/istio.yaml @@ -0,0 +1,53 @@ +repositories: +# Kubernetes incubator repo of helm charts +- name: "kubernetes-incubator" + url: "https://kubernetes-charts-incubator.storage.googleapis.com" + +releases: +# +# References: +# - https://github.com/helm/charts/tree/master/incubator/raw +# +- name: 'example-istio' + chart: "kubernetes-incubator/raw" + version: "0.1.0" + wait: true + force: true + recreatePods: false + values: + - resources: + + # Istio Gateway + - apiVersion: networking.istio.io/v1alpha3 + kind: Gateway + metadata: + name: example-app + spec: + selector: + # use istio default controller + istio: ingressgateway + servers: + - port: + number: 80 + name: http + protocol: HTTP + hosts: + - "{{ env "APP_HOSTNAME" }}" + + # Istio Virtual Service + - apiVersion: networking.istio.io/v1alpha3 + kind: VirtualService + metadata: + name: example-app + spec: + hosts: + - "{{ env "APP_HOSTNAME" }}" + gateways: + - example-app + http: + - route: + - destination: + host: example-{{ env "COLOR" }} + port: + number: 80 + weight: 100 diff --git a/main.go b/main.go new file mode 100644 index 0000000..fc45dd1 --- /dev/null +++ b/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "fmt" + "io/ioutil" + "net/http" + "os" +) + +func main() { + c := os.Getenv("COLOR") + if len(c) == 0 { + c = "cyan" + } + count := 0 + + // Healthcheck endpoint + http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "OK") + }) + + // Take one for the team + boom, _ := ioutil.ReadFile("public/boom.html") + http.HandleFunc("/boom", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, string(boom)) + fmt.Printf("Goodbye\n") + //os.Exit(0) + }) + + // Dashboard + dashboard, _ := ioutil.ReadFile("public/dashboard.html") + http.HandleFunc("/dashboard", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, string(dashboard)) + fmt.Printf("GET %s\n", r.URL.Path) + }) + + // Default + index, _ := ioutil.ReadFile("public/index.html") + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + count += 1 + fmt.Fprintf(w, string(index), c, count) + fmt.Printf("GET %s\n", r.URL.Path) + }) + + http.ListenAndServe(":8080", nil) +} diff --git a/public/boom.html b/public/boom.html new file mode 100644 index 0000000..0ddae86 --- /dev/null +++ b/public/boom.html @@ -0,0 +1,18 @@ + + + + + + BOOM!!! + diff --git a/public/dashboard.html b/public/dashboard.html new file mode 100644 index 0000000..8378337 --- /dev/null +++ b/public/dashboard.html @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..f187446 --- /dev/null +++ b/public/index.html @@ -0,0 +1,20 @@ + + + + + + %v +