Skip to content

eumel8/oro-demoapp

Repository files navigation

OTC RDS Operator Demoapp

This demo is part of OTC RDS Operator and deploy on a Kubernetes cluster and will use the following feature:

  • Webapplication with UI
  • MySQL backend (RDS OTC)
  • Storage (requires default StorageClass in cluster)

Usage

Ensure existing OTC VPC,Subnet, and SecurityGroup in demoapp-rds.yaml

and deploy manifest:

kubectl apply -f demoapp-rds.yaml

deploy manifest:

kubectl apply -f demoapp.yaml

If the cluster supports Ingress, adjust demoapp-ingress.yaml (e.g. hostname) and deploy manifest:

kubectl apply -f demoapp-ingress.yaml

On Ingress endpoint in your browser should appear Golang Mysql Curd Example

Deep Dive

Go:

import (
	// ...
	"k8s.io/client-go/rest"
	rdsv1alpha1clientset "github.com/eumel8/otc-rds-operator/pkg/rds/v1alpha1/apis/clientset/versioned"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

Import Kubernetes Go Rest Client, OTC RDS API clientset, and Kubernetes API Metadata

	// ...
	for {
		log.Println("LOOP: get rds " + rdsname)
	
	rds, err := rdsclientset.McspsV1alpha1().Rdss(namespace).Get(context.TODO(), rdsname, metav1.GetOptions{})
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			log.Println("KRDS: " + err.Error())
			return nil, err
		}
		if rds.Status.Status == "ACTIVE" {
			log.Println("DB ACTIVE")
			for _, i := range *rds.Spec.Users {
				dbUser = i.Name
				dbPass = i.Password
				break
			}
			dbDriver := "mysql"
			dbHost := rds.Status.Ip
			dbPort := rds.Spec.Port
			dbName := rds.Spec.Databases[0]
			log.Println("DB CONNECT " + dbHost)
			db, err = sql.Open(dbDriver, dbUser+":"+dbPass+"@tcp("+dbHost+":"+dbPort+")/"+dbName)
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				log.Println("DBCONN: " + err.Error())
				return nil, err
			}
		}
		// ...
		time.Sleep(5 * time.Second)
	}

After in-cluster authentication on K8S API, create rdsclientset, search for specific API and fetch, ip-address of RDS instance, username/password of app user and try to connect

func Index(w http.ResponseWriter, r *http.Request) {
	db, err := dbConn(w)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		log.Println("DB: " + err.Error())
		return
	}
	selDB, err := db.Query("SELECT * FROM employee ORDER BY id DESC")
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		log.Println("INDEX: " + err.Error())
		return
	}
	emp := Employee{}
	res := []Employee{}
	for selDB.Next() {
		var id int
		var name, city, photo string
		err = selDB.Scan(&id, &name, &city, &photo)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			log.Println("INDEX 2: " + err.Error())
			return
		}
		emp.Id = id
		emp.Name = name
		emp.City = city
		if photo != "none" {
			f, err := os.Open(dataDir + "/" + photo)
			if err != nil {
				// http.Error(w, err.Error(), http.StatusInternalServerError)
				log.Println("INDEX : photoload " + err.Error())
				// return
			} else {
				img, _, err := image.Decode(f)
				sane := resize.Resize(100, 100, img, resize.Bilinear)
				var buff bytes.Buffer
				png.Encode(&buff, sane)

				encodedString := base64.StdEncoding.EncodeToString(buff.Bytes())
				emp.Photo = encodedString
				if err != nil {
					http.Error(w, err.Error(), http.StatusInternalServerError)
					log.Println("INDEX : photodecode" + err.Error())
					return
				}
			}
			defer f.Close()
		} else {
			emp.Photo = "iVBORw0KGgoAAAANSUhEUgAAAJoAAAB/CAYAAAAXdtsmAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAFiUAABYlAUlSJPAAAAFdSURBVHhe7dKxAYAwDMCw0P9/Boa+EE/S4gf8vL+BZecWVhmNhNFIGI2E0UgYjYTRSBiNhNFIGI2E0UgYjYTRSBiNhNFIGI2E0UgYjYTRSBiNhNFIGI2E0UgYjYTRSBiNhNFIGI2E0UgYjYTRSBiNhNFIGI2E0UgYjYTRSBiNhNFIGI2E0UgYjYTRSBiNhNFIGI2E0UgYjYTRSBiNhNFIGI2E0UgYjYTRSBiNhNFIGI2E0UgYjYTRSBiNhNFIGI2E0UgYjYTRSBiNhNFIGI2E0UgYjYTRSBiNhNFIGI2E0UgYjYTRSBiNhNFIGI2E0UgYjYTRSBiNhNFIGI2E0UgYjYTRSBiNhNFIGI2E0UgYjYTRSBiNhNFIGI2E0UgYjYTRSBiNhNFIGI2E0UgYjYTRSBiNhNFIGI2E0UgYjYTRSBiNhNFIGI2E0UgYjYTRSBiNhNFIGI2E0UgYjcDMB+WSBPrvm9bgAAAAAElFTkSuQmCC"
		}
		res = append(res, emp)
	}
	tmpl.ExecuteTemplate(w, "Index", res)
	defer db.Close()

Using Go text/template package to provide HTML views for the web app. Form data are stored in MySQL database. The user can upload a file, which is stored on specific data dir.

func main() {
	log.Println("Server started on: :8080")
	http.HandleFunc("/", Index)
	http.HandleFunc("/show", Show)
	http.HandleFunc("/new", New)
	http.HandleFunc("/edit", Edit)
	http.HandleFunc("/insert", Insert)
	http.HandleFunc("/update", Update)
	http.HandleFunc("/delete", Delete)
	http.ListenAndServe(":8080", nil)
}

Routing of http handler to different function. Service web app on port :8080

---
apiVersion: apps/v1
kind: StatefulSet
spec:
  template:
    spec:
      containers:
      - image: ghcr.io/eumel8/demoapp:1.0.0
        name: demoapp
        ports:
        - containerPort: 8080
        env:
        - name: MYSQL_NAME
          valueFrom:
            fieldRef:
              fieldPath: spec.serviceAccountName
        - name: MYSQL_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        - name: DATA_DIR
          value: "/data" # external pvc
        volumeMounts:
        - mountPath: /data
          name: demoapp-volume
      serviceAccountName: demoapp
      volumes:
      - name: demoapp-volume
        persistentVolumeClaim:
          claimName: demoapp-volume

Use K8S metadata for MYSQL_NAME and MYSQL_NAMESPACE, Attach external PVC to the web app container, and expose containerPort 8080

FROM mtr.devops.telekom.de/mcsps/golang:1.18 as builder

WORKDIR /app
ADD . /app

RUN go mod download && go mod tidy && go build -o main main.go

FROM gcr.io/distroless/base
LABEL org.opencontainers.image.authors="f.kloeker@telekom.de"
LABEL version="1.0.0"
LABEL description="Create DemoApp for OTC RDS Operator"

WORKDIR /

USER nonroot:nonroot
COPY --from=builder --chown=nonroot:nonroot /app/main /
COPY --from=builder --chown=nonroot:nonroot /app/kodata /var/run/ko

ENV KO_DATA_PATH=/var/run/ko
EXPOSE 8080
ENTRYPOINT ["/main"]

A Dockerfile to compile to Go binary and copy the result in a distroless images

name: Build
# https://github.com/marketplace/actions/cosign-installer
on:
  push:
    branches:
      - "**"
  pull_request:
    branches: [ master ]
  release:
    types: [created]
    
env:
  IMAGE_NAME: oro-demoapp/demoapp
  
jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - name: Setup Go
      uses: actions/setup-go@v2
      with:
        go-version: 1.18

    - name: Checkout Repo
      uses: actions/checkout@v2

    - name: Setup ko
      uses: imjasonh/setup-ko@v0.6

    - name: Install Cosign
      uses: sigstore/cosign-installer@main

    - name: Log in to ghcr registry
      run: |
        echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
        echo "${{ secrets.GITHUB_TOKEN }}" | ko login ghcr.io -u ${{ github.actor }} --password-stdin

    - name: Build the image with ko
      run: |
        ko build -t ko --sbom none --bare
      env:
       KO_DOCKER_REPO: ghcr.io/${{ github.repository_owner }}/oro-demoapp/demoapp

    - name: Build the image with docker
      run: docker build . --file Dockerfile --tag $IMAGE_NAME --label "runnumber=${GITHUB_RUN_ID}"

    - name: Push & Sign image
      run: |
        IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME
        # Change all uppercase to lowercase
        IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
        # Strip git ref prefix from version
        VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
        # Strip "v" prefix from tag name
        [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
        # Use Docker `latest` tag convention
        [ "$VERSION" == "master" ] && VERSION=latest
        echo IMAGE_ID=$IMAGE_ID
        echo VERSION=$VERSION
        echo "{{ github.ref.type }}"
        docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
        docker push $IMAGE_ID:$VERSION
        cosign sign --key env://COSIGN_KEY $IMAGE_ID:$VERSION
        cosign sign --key env://COSIGN_KEY ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME:ko
      env:
        COSIGN_KEY: ${{secrets.COSIGN_KEY}}
        COSIGN_PASSWORD: ${{secrets.COSIGN_PASSWORD}}

    - name: Push & Sign image release
      if: github.ref.type == 'tag'
      run: |
        IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME
        IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
        VERSION=${GITHUB_REF_NAME}
        docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
        docker push $IMAGE_ID:$VERSION
        cosign sign --key env://COSIGN_KEY $IMAGE_ID:$VERSION
      env:
        COSIGN_KEY: ${{secrets.COSIGN_KEY}}
        COSIGN_PASSWORD: ${{secrets.COSIGN_PASSWORD}}

Github Action build the image with docker and with ko which don't need a Dockerfile definition. Both images are signed with cosign to verify with the pubkey

Using the Github Registry

Credits

Frank Kloeker f.kloeker@telekom.de

Life is for sharing. If you have an issue with the code or want to improve it, feel free to open an issue or an pull request.

Go Crud Example is adapted from www.golangprograms.com