├── .gitignore ├── Dockerfile ├── Makefile ├── README.md ├── deployment-template.json ├── pkg ├── v1 │ └── types.go └── website-controller.go └── service-template.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | MAINTAINER marko.luksa@gmail.com 3 | ADD website-controller / 4 | ADD deployment-template.json / 5 | ADD service-template.json / 6 | CMD ["/website-controller"] 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | CGO_ENABLED=0 GOOS=linux go build -o website-controller -a pkg/website-controller.go 3 | 4 | image: build 5 | docker build -t luksa/website-controller . 6 | 7 | push: image 8 | docker push luksa/website-controller:latest 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # k8s-website-controller 2 | A barely working example of a Kubernetes controller, which watches the Kubernetes API server for `website` objects and runs an Nginx webserver for each of them. 3 | 4 | NOTE: This is not the correct way to create a Kubernetes controller, as it simply performs a watch request in a loop. This will never work properly, because some watch events will be lost. 5 | 6 | -------------------------------------------------------------------------------- /deployment-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "extensions/v1beta1", 3 | "kind": "Deployment", 4 | "metadata": { 5 | "name": "[NAME]", 6 | "labels": { 7 | "webserver": "[NAME]" 8 | } 9 | }, 10 | "spec": { 11 | "replicas": 1, 12 | "template": { 13 | "metadata": { 14 | "name": "[NAME]", 15 | "labels": { 16 | "webserver": "[NAME]" 17 | } 18 | }, 19 | "spec": { 20 | "containers": [ 21 | { 22 | "image": "nginx:alpine", 23 | "name": "main", 24 | "volumeMounts": [ 25 | { 26 | "name": "html", 27 | "mountPath": "/usr/share/nginx/html", 28 | "readOnly": true 29 | } 30 | ], 31 | "ports": [ 32 | { 33 | "containerPort": 80, 34 | "protocol": "TCP" 35 | } 36 | ] 37 | }, 38 | { 39 | "image": "openweb/git-sync", 40 | "name": "git-sync", 41 | "env": [ 42 | { 43 | "name": "GIT_SYNC_REPO", 44 | "value": "[GIT-REPO]" 45 | }, 46 | { 47 | "name": "GIT_SYNC_DEST", 48 | "value": "/gitrepo" 49 | }, 50 | { 51 | "name": "GIT_SYNC_BRANCH", 52 | "value": "master" 53 | }, 54 | { 55 | "name": "GIT_SYNC_REV", 56 | "value": "FETCH_HEAD" 57 | }, 58 | { 59 | "name": "GIT_SYNC_WAIT", 60 | "value": "10" 61 | } 62 | ], 63 | "volumeMounts": [ 64 | { 65 | "name": "html", 66 | "mountPath": "/gitrepo" 67 | } 68 | ] 69 | } 70 | ], 71 | "volumes": [ 72 | { 73 | "name": "html", 74 | "emptyDir": { 75 | "medium": "" 76 | } 77 | } 78 | ] 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /pkg/v1/types.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | type Metadata struct { 4 | Name string 5 | Namespace string 6 | } 7 | 8 | type WebsiteSpec struct { 9 | GitRepo string 10 | } 11 | 12 | type Website struct { 13 | Metadata Metadata 14 | Spec WebsiteSpec 15 | } 16 | 17 | type WebsiteWatchEvent struct { 18 | Type string 19 | Object Website 20 | } 21 | -------------------------------------------------------------------------------- /pkg/website-controller.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "encoding/json" 7 | "io" 8 | "log" 9 | "github.com/luksa/website-controller/pkg/v1" 10 | "io/ioutil" 11 | "strings" 12 | ) 13 | 14 | func main() { 15 | log.Println("website-controller started.") 16 | for { 17 | resp, err := http.Get("http://localhost:8001/apis/extensions.example.com/v1/websites?watch=true") 18 | if err != nil { 19 | panic(err) 20 | } 21 | defer resp.Body.Close() 22 | 23 | decoder := json.NewDecoder(resp.Body) 24 | for { 25 | var event v1.WebsiteWatchEvent 26 | if err := decoder.Decode(&event); err == io.EOF { 27 | break 28 | } else if err != nil { 29 | log.Fatal(err) 30 | } 31 | 32 | log.Printf("Received watch event: %s: %s: %s\n", event.Type, event.Object.Metadata.Name, event.Object.Spec.GitRepo) 33 | 34 | if event.Type == "ADDED" { 35 | createWebsite(event.Object) 36 | } else if event.Type == "DELETED" { 37 | deleteWebsite(event.Object) 38 | } 39 | } 40 | } 41 | 42 | } 43 | 44 | func createWebsite(website v1.Website) { 45 | createResource(website, "api/v1", "services", "service-template.json") 46 | createResource(website, "apis/extensions/v1beta1", "deployments", "deployment-template.json") 47 | } 48 | 49 | func deleteWebsite(website v1.Website) { 50 | deleteResource(website, "api/v1", "services", getName(website)); 51 | deleteResource(website, "apis/extensions/v1beta1", "deployments", getName(website)); 52 | } 53 | 54 | func createResource(webserver v1.Website, apiGroup string, kind string, filename string) { 55 | log.Printf("Creating %s with name %s in namespace %s", kind, getName(webserver), webserver.Metadata.Namespace) 56 | templateBytes, err := ioutil.ReadFile(filename) 57 | if err != nil { 58 | log.Fatal(err) 59 | } 60 | template := strings.Replace(string(templateBytes), "[NAME]", getName(webserver), -1) 61 | template = strings.Replace(template, "[GIT-REPO]", webserver.Spec.GitRepo, -1) 62 | 63 | resp, err := http.Post(fmt.Sprintf("http://localhost:8001/%s/namespaces/%s/%s/", apiGroup, webserver.Metadata.Namespace, kind), "application/json", strings.NewReader(template)) 64 | if err != nil { 65 | log.Fatal(err) 66 | } 67 | log.Println("response Status:", resp.Status) 68 | } 69 | 70 | func deleteResource(webserver v1.Website, apiGroup string, kind string, name string) { 71 | log.Printf("Deleting %s with name %s in namespace %s", kind, name, webserver.Metadata.Namespace) 72 | req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("http://localhost:8001/%s/namespaces/%s/%s/%s", apiGroup, webserver.Metadata.Namespace, kind, name), nil) 73 | if err != nil { 74 | log.Fatal(err) 75 | return 76 | } 77 | resp, err := http.DefaultClient.Do(req) 78 | if err != nil { 79 | log.Fatal(err) 80 | return 81 | } 82 | log.Println("response Status:", resp.Status) 83 | 84 | } 85 | 86 | func getName(website v1.Website) string { 87 | return website.Metadata.Name + "-website"; 88 | } 89 | -------------------------------------------------------------------------------- /service-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "v1", 3 | "kind": "Service", 4 | "metadata": { 5 | "labels": { 6 | "webserver": "[NAME]" 7 | }, 8 | "name": "[NAME]" 9 | }, 10 | "spec": { 11 | "type": "NodePort", 12 | "ports": [ 13 | { 14 | "port": 80, 15 | "protocol": "TCP", 16 | "targetPort": 80 17 | } 18 | ], 19 | "selector": { 20 | "webserver": "[NAME]" 21 | } 22 | } 23 | } 24 | --------------------------------------------------------------------------------