├── .gitignore ├── ch03 ├── etcd-cluster-cr.yaml ├── etcd-operator-crd.yaml ├── etcd-operator-deployment.yaml ├── etcd-operator-role.yaml ├── etcd-operator-rolebinding.yaml └── etcd-operator-sa.yaml ├── ch05 ├── backend.yaml ├── database.yaml └── frontend.yaml ├── ch06 ├── ansible │ ├── playbook.yml │ └── visitors │ │ ├── README.md │ │ ├── defaults │ │ └── main.yml │ │ ├── handlers │ │ └── main.yml │ │ ├── meta │ │ └── main.yml │ │ ├── tasks │ │ ├── backend.yml │ │ ├── database.yml │ │ ├── frontend.yml │ │ └── main.yml │ │ ├── tests │ │ ├── inventory │ │ └── test.yml │ │ └── vars │ │ └── main.yml └── visitors-helm │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ ├── _helpers.tpl │ ├── auth.yaml │ ├── backend-deployment.yaml │ ├── backend-service.yaml │ ├── frontend-deployment.yaml │ ├── frontend-service.yaml │ ├── mysql-deployment.yaml │ └── mysql-service.yaml │ └── values.yaml ├── ch07 └── visitors-operator │ ├── .gitignore │ ├── build │ ├── Dockerfile │ └── bin │ │ ├── entrypoint │ │ └── user_setup │ ├── cmd │ └── manager │ │ └── main.go │ ├── deploy │ ├── crds │ │ ├── example_v1_visitorsapp_cr.yaml │ │ └── example_v1_visitorsapp_crd.yaml │ ├── operator.yaml │ ├── role.yaml │ ├── role_binding.yaml │ └── service_account.yaml │ ├── pkg │ ├── apis │ │ ├── addtoscheme_example_v1.go │ │ ├── apis.go │ │ └── example │ │ │ └── v1 │ │ │ ├── doc.go │ │ │ ├── register.go │ │ │ ├── visitorsapp_types.go │ │ │ ├── zz_generated.deepcopy.go │ │ │ └── zz_generated.openapi.go │ └── controller │ │ ├── add_visitorsapp.go │ │ ├── controller.go │ │ └── visitorsapp │ │ ├── backend.go │ │ ├── common.go │ │ ├── frontend.go │ │ ├── mysql.go │ │ └── visitorsapp_controller.go │ └── version │ └── version.go └── ch08 ├── bundle ├── example_v1_visitorsapp_crd.yaml ├── visitors-operator.package.yaml └── visitors-operator.v1.0.0.clusterserviceversion.yaml ├── get-quay-token └── testing ├── example_v1_visitorsapp_cr.yaml ├── operator-group.yaml ├── operator-source.yaml └── subscription.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | -------------------------------------------------------------------------------- /ch03/etcd-cluster-cr.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: etcd.database.coreos.com/v1beta2 2 | kind: EtcdCluster 3 | metadata: 4 | name: example-etcd-cluster 5 | spec: 6 | size: 3 7 | version: 3.1.10 8 | -------------------------------------------------------------------------------- /ch03/etcd-operator-crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdclusters.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdCluster 9 | listKind: EtcdClusterList 10 | plural: etcdclusters 11 | shortNames: 12 | - etcdclus 13 | - etcd 14 | singular: etcdcluster 15 | scope: Namespaced 16 | versions: 17 | - name: v1beta2 18 | served: true 19 | storage: true 20 | schema: 21 | openAPIV3Schema: 22 | type: object 23 | properties: 24 | spec: 25 | type: object 26 | properties: 27 | size: 28 | type: integer 29 | version: 30 | type: string 31 | -------------------------------------------------------------------------------- /ch03/etcd-operator-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: etcd-operator 5 | spec: 6 | selector: 7 | matchLabels: 8 | app: etcd-operator 9 | replicas: 1 10 | template: 11 | metadata: 12 | labels: 13 | app: etcd-operator 14 | spec: 15 | containers: 16 | - name: etcd-operator 17 | image: quay.io/coreos/etcd-operator:v0.9.4 18 | command: 19 | - etcd-operator 20 | - --create-crd=false 21 | env: 22 | - name: MY_POD_NAMESPACE 23 | valueFrom: 24 | fieldRef: 25 | fieldPath: metadata.namespace 26 | - name: MY_POD_NAME 27 | valueFrom: 28 | fieldRef: 29 | fieldPath: metadata.name 30 | imagePullPolicy: IfNotPresent 31 | serviceAccountName: etcd-operator-sa 32 | -------------------------------------------------------------------------------- /ch03/etcd-operator-role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: Role 3 | metadata: 4 | name: etcd-operator-role 5 | rules: 6 | - apiGroups: 7 | - etcd.database.coreos.com 8 | resources: 9 | - etcdclusters 10 | - etcdbackups 11 | - etcdrestores 12 | verbs: 13 | - '*' 14 | - apiGroups: 15 | - "" 16 | resources: 17 | - pods 18 | - services 19 | - endpoints 20 | - persistentvolumeclaims 21 | - events 22 | verbs: 23 | - '*' 24 | - apiGroups: 25 | - apps 26 | resources: 27 | - deployments 28 | verbs: 29 | - '*' 30 | - apiGroups: 31 | - "" 32 | resources: 33 | - secrets 34 | verbs: 35 | - get 36 | -------------------------------------------------------------------------------- /ch03/etcd-operator-rolebinding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: etcd-operator-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: Role 8 | name: etcd-operator-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: etcd-operator-sa 12 | namespace: default 13 | -------------------------------------------------------------------------------- /ch03/etcd-operator-sa.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: etcd-operator-sa 5 | -------------------------------------------------------------------------------- /ch05/backend.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: visitors-backend 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: visitors 11 | tier: backend 12 | template: 13 | metadata: 14 | labels: 15 | app: visitors 16 | tier: backend 17 | spec: 18 | containers: 19 | - name: visitors-backend 20 | image: "jdob/visitors-service:1.0.0" 21 | imagePullPolicy: Always 22 | ports: 23 | - name: visitors 24 | containerPort: 8000 25 | env: 26 | - name: MYSQL_DATABASE 27 | value: visitors_db 28 | - name: MYSQL_SERVICE_HOST 29 | value: mysql-service 30 | - name: MYSQL_USERNAME 31 | valueFrom: 32 | secretKeyRef: 33 | name: mysql-auth 34 | key: username 35 | - name: MYSQL_PASSWORD 36 | valueFrom: 37 | secretKeyRef: 38 | name: mysql-auth 39 | key: password 40 | --- 41 | apiVersion: v1 42 | kind: Service 43 | metadata: 44 | name: visitors-backend-service 45 | labels: 46 | app: visitors 47 | tier: backend 48 | spec: 49 | type: NodePort 50 | ports: 51 | - port: 8000 52 | targetPort: 8000 53 | nodePort: 30685 54 | protocol: TCP 55 | selector: 56 | app: visitors 57 | tier: backend 58 | -------------------------------------------------------------------------------- /ch05/database.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: mysql-auth 6 | type: Opaque 7 | stringData: 8 | username: visitors-user 9 | password: visitors-pass 10 | --- 11 | apiVersion: apps/v1 12 | kind: Deployment 13 | metadata: 14 | name: mysql 15 | spec: 16 | replicas: 1 17 | selector: 18 | matchLabels: 19 | app: visitors 20 | tier: mysql 21 | template: 22 | metadata: 23 | labels: 24 | app: visitors 25 | tier: mysql 26 | spec: 27 | containers: 28 | - name: visitors-mysql 29 | image: "mysql:5.7" 30 | imagePullPolicy: Always 31 | ports: 32 | - name: mysql 33 | containerPort: 3306 34 | protocol: TCP 35 | env: 36 | - name: MYSQL_ROOT_PASSWORD 37 | value: password 38 | - name: MYSQL_DATABASE 39 | value: visitors_db 40 | - name: MYSQL_USER 41 | valueFrom: 42 | secretKeyRef: 43 | name: mysql-auth 44 | key: username 45 | - name: MYSQL_PASSWORD 46 | valueFrom: 47 | secretKeyRef: 48 | name: mysql-auth 49 | key: password 50 | --- 51 | apiVersion: v1 52 | kind: Service 53 | metadata: 54 | name: mysql-service 55 | labels: 56 | app: visitors 57 | tier: mysql 58 | spec: 59 | clusterIP: None 60 | ports: 61 | - port: 3306 62 | selector: 63 | app: visitors 64 | tier: mysql -------------------------------------------------------------------------------- /ch05/frontend.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: visitors-frontend 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: visitors 11 | tier: frontend 12 | template: 13 | metadata: 14 | labels: 15 | app: visitors 16 | tier: frontend 17 | spec: 18 | containers: 19 | - name: visitors-frontend 20 | image: "jdob/visitors-webui:1.0.0" 21 | imagePullPolicy: Always 22 | ports: 23 | - name: visitors 24 | containerPort: 3000 25 | env: 26 | - name: REACT_APP_TITLE 27 | value: "Visitors Dashboard" 28 | --- 29 | apiVersion: v1 30 | kind: Service 31 | metadata: 32 | name: visitors-frontend-service 33 | labels: 34 | app: visitors 35 | tier: frontend 36 | spec: 37 | type: NodePort 38 | ports: 39 | - port: 3000 40 | targetPort: 3000 41 | nodePort: 30686 42 | protocol: TCP 43 | selector: 44 | app: visitors 45 | tier: frontend 46 | -------------------------------------------------------------------------------- /ch06/ansible/playbook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install the Visitors Site 3 | hosts: localhost 4 | roles: 5 | - visitors -------------------------------------------------------------------------------- /ch06/ansible/visitors/README.md: -------------------------------------------------------------------------------- 1 | Role Name 2 | ========= 3 | 4 | A brief description of the role goes here. 5 | 6 | Requirements 7 | ------------ 8 | 9 | Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required. 10 | 11 | Role Variables 12 | -------------- 13 | 14 | A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well. 15 | 16 | Dependencies 17 | ------------ 18 | 19 | A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles. 20 | 21 | Example Playbook 22 | ---------------- 23 | 24 | Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too: 25 | 26 | - hosts: servers 27 | roles: 28 | - { role: username.rolename, x: 42 } 29 | 30 | License 31 | ------- 32 | 33 | BSD 34 | 35 | Author Information 36 | ------------------ 37 | 38 | An optional section for the role authors to include contact information, or a website (HTML is not allowed). 39 | -------------------------------------------------------------------------------- /ch06/ansible/visitors/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | namespace: default 3 | size: 1 4 | title: Ansible Deployed Visitors Site -------------------------------------------------------------------------------- /ch06/ansible/visitors/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # handlers file for visitors-ansible -------------------------------------------------------------------------------- /ch06/ansible/visitors/meta/main.yml: -------------------------------------------------------------------------------- 1 | galaxy_info: 2 | author: your name 3 | description: your description 4 | company: your company (optional) 5 | 6 | # If the issue tracker for your role is not on github, uncomment the 7 | # next line and provide a value 8 | # issue_tracker_url: http://example.com/issue/tracker 9 | 10 | # Some suggested licenses: 11 | # - BSD (default) 12 | # - MIT 13 | # - GPLv2 14 | # - GPLv3 15 | # - Apache 16 | # - CC-BY 17 | license: license (GPLv2, CC-BY, etc) 18 | 19 | min_ansible_version: 1.2 20 | 21 | # If this a Container Enabled role, provide the minimum Ansible Container version. 22 | # min_ansible_container_version: 23 | 24 | # Optionally specify the branch Galaxy will use when accessing the GitHub 25 | # repo for this role. During role install, if no tags are available, 26 | # Galaxy will use this branch. During import Galaxy will access files on 27 | # this branch. If Travis integration is configured, only notifications for this 28 | # branch will be accepted. Otherwise, in all cases, the repo's default branch 29 | # (usually master) will be used. 30 | #github_branch: 31 | 32 | # 33 | # platforms is a list of platforms, and each platform has a name and a list of versions. 34 | # 35 | # platforms: 36 | # - name: Fedora 37 | # versions: 38 | # - all 39 | # - 25 40 | # - name: SomePlatform 41 | # versions: 42 | # - all 43 | # - 1.0 44 | # - 7 45 | # - 99.99 46 | 47 | galaxy_tags: [] 48 | # List tags for your role here, one per line. A tag is a keyword that describes 49 | # and categorizes the role. Users find roles by searching for tags. Be sure to 50 | # remove the '[]' above, if you add tags to this list. 51 | # 52 | # NOTE: A tag is limited to a single word comprised of alphanumeric characters. 53 | # Maximum 20 tags per role. 54 | 55 | dependencies: [] 56 | # List your role dependencies here, one per line. Be sure to remove the '[]' above, 57 | # if you add dependencies to this list. -------------------------------------------------------------------------------- /ch06/ansible/visitors/tasks/backend.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: deploy backend 3 | k8s: 4 | definition: 5 | kind: Deployment 6 | apiVersion: apps/v1 7 | metadata: 8 | name: visitors-backend 9 | namespace: "{{ namespace }}" 10 | spec: 11 | replicas: "{{ size }}" 12 | selector: 13 | matchLabels: 14 | app: visitors 15 | tier: backend 16 | template: 17 | metadata: 18 | labels: 19 | app: visitors 20 | tier: backend 21 | spec: 22 | containers: 23 | - name: visitors-backend 24 | image: "jdob/visitors-service:1.0.0" 25 | imagePullPolicy: Always 26 | ports: 27 | - name: visitors 28 | containerPort: 8000 29 | env: 30 | - name: MYSQL_DATABASE 31 | value: "{{ db_name }}" 32 | - name: MYSQL_SERVICE_HOST 33 | value: "{{ db_service }}" 34 | - name: MYSQL_USERNAME 35 | valueFrom: 36 | secretKeyRef: 37 | name: mysql-auth 38 | key: username 39 | - name: MYSQL_PASSWORD 40 | valueFrom: 41 | secretKeyRef: 42 | name: mysql-auth 43 | key: password 44 | - name: create backend service 45 | k8s: 46 | definition: 47 | apiVersion: v1 48 | kind: Service 49 | metadata: 50 | name: visitors-backend-service 51 | namespace: "{{ namespace }}" 52 | labels: 53 | app: visitors 54 | tier: backend 55 | spec: 56 | type: NodePort 57 | ports: 58 | - port: 8000 59 | targetPort: 8000 60 | nodePort: 30685 61 | protocol: TCP 62 | selector: 63 | app: visitors 64 | tier: backend 65 | -------------------------------------------------------------------------------- /ch06/ansible/visitors/tasks/database.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: create database authentication secret 3 | k8s: 4 | definition: 5 | kind: Secret 6 | apiVersion: v1 7 | metadata: 8 | name: mysql-auth 9 | namespace: "{{ namespace }}" 10 | type: Opaque 11 | stringData: 12 | username: "{{ db_user }}" 13 | password: "{{ db_pass }}" 14 | - name: deploy mysql 15 | k8s: 16 | definition: 17 | kind: Deployment 18 | apiVersion: apps/v1 19 | metadata: 20 | name: mysql 21 | namespace: "{{ namespace }}" 22 | spec: 23 | replicas: 1 24 | selector: 25 | matchLabels: 26 | app: visitors 27 | tier: mysql 28 | template: 29 | metadata: 30 | labels: 31 | app: visitors 32 | tier: mysql 33 | spec: 34 | containers: 35 | - name: visitors-mysql 36 | image: "mysql:5.7" 37 | ports: 38 | - name: mysql 39 | containerPort: 3306 40 | protocol: TCP 41 | env: 42 | - name: MYSQL_ROOT_PASSWORD 43 | value: password 44 | - name: MYSQL_DATABASE 45 | value: "{{ db_name }}" 46 | - name: MYSQL_USER 47 | valueFrom: 48 | secretKeyRef: 49 | name: mysql-auth 50 | key: username 51 | - name: MYSQL_PASSWORD 52 | valueFrom: 53 | secretKeyRef: 54 | name: mysql-auth 55 | key: password 56 | - name: create mysql service 57 | k8s: 58 | definition: 59 | kind: Service 60 | metadata: 61 | name: "{{ db_service }}" 62 | namespace: "{{ namespace }}" 63 | labels: 64 | app: visitors 65 | tier: mysql 66 | spec: 67 | clusterIP: None 68 | ports: 69 | - port: 3306 70 | selector: 71 | app: visitors 72 | tier: mysql 73 | -------------------------------------------------------------------------------- /ch06/ansible/visitors/tasks/frontend.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: deploy frontend 3 | k8s: 4 | definition: 5 | kind: Deployment 6 | apiVersion: apps/v1 7 | metadata: 8 | name: visitors-frontend 9 | namespace: "{{ namespace }}" 10 | spec: 11 | replicas: 1 12 | selector: 13 | matchLabels: 14 | app: visitors 15 | tier: frontend 16 | template: 17 | metadata: 18 | labels: 19 | app: visitors 20 | tier: frontend 21 | spec: 22 | containers: 23 | - name: visitors-frontend 24 | image: "jdob/visitors-webui:1.0.0" 25 | imagePullPolicy: Always 26 | ports: 27 | - name: visitors 28 | containerPort: 3000 29 | env: 30 | - name: REACT_APP_TITLE 31 | value: "{{ title }}" 32 | - name: create frontend service 33 | k8s: 34 | definition: 35 | kind: Service 36 | apiVersion: v1 37 | metadata: 38 | name: visitors-frontend-service 39 | namespace: "{{ namespace }}" 40 | labels: 41 | app: visitors 42 | tier: frontend 43 | spec: 44 | type: NodePort 45 | ports: 46 | - port: 3000 47 | targetPort: 3000 48 | nodePort: 30686 49 | protocol: TCP 50 | selector: 51 | app: visitors 52 | tier: frontend 53 | -------------------------------------------------------------------------------- /ch06/ansible/visitors/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - include: database.yml 3 | - include: backend.yml 4 | - include: frontend.yml -------------------------------------------------------------------------------- /ch06/ansible/visitors/tests/inventory: -------------------------------------------------------------------------------- 1 | localhost 2 | 3 | -------------------------------------------------------------------------------- /ch06/ansible/visitors/tests/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: localhost 3 | remote_user: root 4 | roles: 5 | - visitors -------------------------------------------------------------------------------- /ch06/ansible/visitors/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | db_name: visitors 3 | db_user: visitors 4 | db_pass: visitors 5 | db_service: mysql-service -------------------------------------------------------------------------------- /ch06/visitors-helm/.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 | .vscode/ 23 | -------------------------------------------------------------------------------- /ch06/visitors-helm/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | appVersion: "1.0" 3 | description: A Helm chart for Kubernetes 4 | name: visitors-helm 5 | version: 0.1.0 6 | -------------------------------------------------------------------------------- /ch06/visitors-helm/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "visitors-helm.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "visitors-helm.fullname" -}} 15 | {{- if .Values.fullnameOverride -}} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 17 | {{- else -}} 18 | {{- $name := default .Chart.Name .Values.nameOverride -}} 19 | {{- if contains $name .Release.Name -}} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 21 | {{- else -}} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 23 | {{- end -}} 24 | {{- end -}} 25 | {{- end -}} 26 | 27 | {{/* 28 | Create chart name and version as used by the chart label. 29 | */}} 30 | {{- define "visitors-helm.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 32 | {{- end -}} 33 | 34 | {{/* 35 | Common labels 36 | */}} 37 | {{- define "visitors-helm.labels" -}} 38 | app.kubernetes.io/name: {{ include "visitors-helm.name" . }} 39 | helm.sh/chart: {{ include "visitors-helm.chart" . }} 40 | app.kubernetes.io/instance: {{ .Release.Name }} 41 | {{- if .Chart.AppVersion }} 42 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 43 | {{- end }} 44 | app.kubernetes.io/managed-by: {{ .Release.Service }} 45 | {{- end -}} 46 | -------------------------------------------------------------------------------- /ch06/visitors-helm/templates/auth.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: mysql-auth 5 | type: Opaque 6 | stringData: 7 | username: visitors-user 8 | password: visitors-pass 9 | -------------------------------------------------------------------------------- /ch06/visitors-helm/templates/backend-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: visitors-backend 5 | spec: 6 | replicas: {{ .Values.backend.size }} 7 | selector: 8 | matchLabels: 9 | app: visitors 10 | tier: backend 11 | template: 12 | metadata: 13 | labels: 14 | app: visitors 15 | tier: backend 16 | spec: 17 | containers: 18 | - name: visitors-backend 19 | image: "jdob/visitors-service:1.0.0" 20 | imagePullPolicy: Always 21 | ports: 22 | - name: visitors 23 | containerPort: 8000 24 | env: 25 | - name: MYSQL_DATABASE 26 | value: visitors 27 | - name: MYSQL_SERVICE_HOST 28 | value: mysql-service 29 | - name: MYSQL_USERNAME 30 | valueFrom: 31 | secretKeyRef: 32 | name: mysql-auth 33 | key: username 34 | - name: MYSQL_PASSWORD 35 | valueFrom: 36 | secretKeyRef: 37 | name: mysql-auth 38 | key: password 39 | -------------------------------------------------------------------------------- /ch06/visitors-helm/templates/backend-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: visitors-backend-service 5 | labels: 6 | app: visitors 7 | tier: backend 8 | spec: 9 | type: NodePort 10 | ports: 11 | - port: 8000 12 | targetPort: 8000 13 | nodePort: 30685 14 | protocol: TCP 15 | selector: 16 | app: visitors 17 | tier: backend -------------------------------------------------------------------------------- /ch06/visitors-helm/templates/frontend-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: visitors-frontend 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: visitors 10 | tier: frontend 11 | template: 12 | metadata: 13 | labels: 14 | app: visitors 15 | tier: frontend 16 | spec: 17 | containers: 18 | - name: visitors-frontend 19 | image: "jdob/visitors-webui:1.0.0" 20 | imagePullPolicy: Always 21 | ports: 22 | - name: visitors 23 | containerPort: 3000 24 | env: 25 | - name: REACT_APP_TITLE 26 | value: {{ .Values.frontend.title }} 27 | -------------------------------------------------------------------------------- /ch06/visitors-helm/templates/frontend-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: visitors-frontend-service 5 | labels: 6 | app: visitors 7 | tier: frontend 8 | spec: 9 | type: NodePort 10 | ports: 11 | - port: 3000 12 | targetPort: 3000 13 | nodePort: 30686 14 | protocol: TCP 15 | selector: 16 | app: visitors 17 | tier: frontend 18 | -------------------------------------------------------------------------------- /ch06/visitors-helm/templates/mysql-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: mysql 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: visitors 10 | tier: mysql 11 | template: 12 | metadata: 13 | labels: 14 | app: visitors 15 | tier: mysql 16 | spec: 17 | containers: 18 | - name: visitors-mysql 19 | image: "mysql:5.7" 20 | imagePullPolicy: Always 21 | ports: 22 | - name: mysql 23 | containerPort: 3306 24 | protocol: TCP 25 | env: 26 | - name: MYSQL_ROOT_PASSWORD 27 | value: password 28 | - name: MYSQL_DATABASE 29 | value: visitors 30 | - name: MYSQL_USER 31 | valueFrom: 32 | secretKeyRef: 33 | name: mysql-auth 34 | key: username 35 | - name: MYSQL_PASSWORD 36 | valueFrom: 37 | secretKeyRef: 38 | name: mysql-auth 39 | key: password 40 | -------------------------------------------------------------------------------- /ch06/visitors-helm/templates/mysql-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: mysql-service 5 | labels: 6 | app: visitors 7 | tier: mysql 8 | spec: 9 | clusterIP: None 10 | ports: 11 | - port: 3306 12 | selector: 13 | app: visitors 14 | tier: mysql -------------------------------------------------------------------------------- /ch06/visitors-helm/values.yaml: -------------------------------------------------------------------------------- 1 | backend: 2 | size: 1 3 | 4 | frontend: 5 | title: Helm Installed Visitors Site 6 | -------------------------------------------------------------------------------- /ch07/visitors-operator/.gitignore: -------------------------------------------------------------------------------- 1 | # Temporary Build Files 2 | build/_output 3 | build/_test 4 | # Created by https://www.gitignore.io/api/go,vim,emacs,visualstudiocode 5 | ### Emacs ### 6 | # -*- mode: gitignore; -*- 7 | *~ 8 | \#*\# 9 | /.emacs.desktop 10 | /.emacs.desktop.lock 11 | *.elc 12 | auto-save-list 13 | tramp 14 | .\#* 15 | # Org-mode 16 | .org-id-locations 17 | *_archive 18 | # flymake-mode 19 | *_flymake.* 20 | # eshell files 21 | /eshell/history 22 | /eshell/lastdir 23 | # elpa packages 24 | /elpa/ 25 | # reftex files 26 | *.rel 27 | # AUCTeX auto folder 28 | /auto/ 29 | # cask packages 30 | .cask/ 31 | dist/ 32 | # Flycheck 33 | flycheck_*.el 34 | # server auth directory 35 | /server/ 36 | # projectiles files 37 | .projectile 38 | projectile-bookmarks.eld 39 | # directory configuration 40 | .dir-locals.el 41 | # saveplace 42 | places 43 | # url cache 44 | url/cache/ 45 | # cedet 46 | ede-projects.el 47 | # smex 48 | smex-items 49 | # company-statistics 50 | company-statistics-cache.el 51 | # anaconda-mode 52 | anaconda-mode/ 53 | ### Go ### 54 | # Binaries for programs and plugins 55 | *.exe 56 | *.exe~ 57 | *.dll 58 | *.so 59 | *.dylib 60 | # Test binary, build with 'go test -c' 61 | *.test 62 | # Output of the go coverage tool, specifically when used with LiteIDE 63 | *.out 64 | ### Vim ### 65 | # swap 66 | .sw[a-p] 67 | .*.sw[a-p] 68 | # session 69 | Session.vim 70 | # temporary 71 | .netrwhist 72 | # auto-generated tag files 73 | tags 74 | ### VisualStudioCode ### 75 | .vscode/* 76 | .history 77 | # End of https://www.gitignore.io/api/go,vim,emacs,visualstudiocode 78 | -------------------------------------------------------------------------------- /ch07/visitors-operator/build/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM registry.access.redhat.com/ubi7/ubi-minimal:latest 2 | 3 | ENV OPERATOR=/usr/local/bin/visitors-operator \ 4 | USER_UID=1001 \ 5 | USER_NAME=visitors-operator 6 | 7 | # install operator binary 8 | COPY build/_output/bin/visitors-operator ${OPERATOR} 9 | 10 | COPY build/bin /usr/local/bin 11 | RUN /usr/local/bin/user_setup 12 | 13 | ENTRYPOINT ["/usr/local/bin/entrypoint"] 14 | 15 | USER ${USER_UID} 16 | -------------------------------------------------------------------------------- /ch07/visitors-operator/build/bin/entrypoint: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | # This is documented here: 4 | # https://docs.openshift.com/container-platform/3.11/creating_images/guidelines.html#openshift-specific-guidelines 5 | 6 | if ! whoami &>/dev/null; then 7 | if [ -w /etc/passwd ]; then 8 | echo "${USER_NAME:-visitors-operator}:x:$(id -u):$(id -g):${USER_NAME:-visitors-operator} user:${HOME}:/sbin/nologin" >> /etc/passwd 9 | fi 10 | fi 11 | 12 | exec ${OPERATOR} $@ 13 | -------------------------------------------------------------------------------- /ch07/visitors-operator/build/bin/user_setup: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -x 3 | 4 | # ensure $HOME exists and is accessible by group 0 (we don't know what the runtime UID will be) 5 | mkdir -p ${HOME} 6 | chown ${USER_UID}:0 ${HOME} 7 | chmod ug+rwx ${HOME} 8 | 9 | # runtime user will need to be able to self-insert in /etc/passwd 10 | chmod g+rw /etc/passwd 11 | 12 | # no need for this script to remain in the image after running 13 | rm $0 14 | -------------------------------------------------------------------------------- /ch07/visitors-operator/cmd/manager/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "runtime" 9 | 10 | // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) 11 | _ "k8s.io/client-go/plugin/pkg/client/auth" 12 | 13 | "github.com/jdob/visitors-operator/pkg/apis" 14 | "github.com/jdob/visitors-operator/pkg/controller" 15 | 16 | "github.com/operator-framework/operator-sdk/pkg/k8sutil" 17 | "github.com/operator-framework/operator-sdk/pkg/leader" 18 | "github.com/operator-framework/operator-sdk/pkg/log/zap" 19 | "github.com/operator-framework/operator-sdk/pkg/metrics" 20 | "github.com/operator-framework/operator-sdk/pkg/restmapper" 21 | sdkVersion "github.com/operator-framework/operator-sdk/version" 22 | "github.com/spf13/pflag" 23 | "sigs.k8s.io/controller-runtime/pkg/client/config" 24 | "sigs.k8s.io/controller-runtime/pkg/manager" 25 | logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" 26 | "sigs.k8s.io/controller-runtime/pkg/runtime/signals" 27 | ) 28 | 29 | // Change below variables to serve metrics on different host or port. 30 | var ( 31 | metricsHost = "0.0.0.0" 32 | metricsPort int32 = 8383 33 | ) 34 | var log = logf.Log.WithName("cmd") 35 | 36 | func printVersion() { 37 | log.Info(fmt.Sprintf("Go Version: %s", runtime.Version())) 38 | log.Info(fmt.Sprintf("Go OS/Arch: %s/%s", runtime.GOOS, runtime.GOARCH)) 39 | log.Info(fmt.Sprintf("Version of operator-sdk: %v", sdkVersion.Version)) 40 | } 41 | 42 | func main() { 43 | // Add the zap logger flag set to the CLI. The flag set must 44 | // be added before calling pflag.Parse(). 45 | pflag.CommandLine.AddFlagSet(zap.FlagSet()) 46 | 47 | // Add flags registered by imported packages (e.g. glog and 48 | // controller-runtime) 49 | pflag.CommandLine.AddGoFlagSet(flag.CommandLine) 50 | 51 | pflag.Parse() 52 | 53 | // Use a zap logr.Logger implementation. If none of the zap 54 | // flags are configured (or if the zap flag set is not being 55 | // used), this defaults to a production zap logger. 56 | // 57 | // The logger instantiated here can be changed to any logger 58 | // implementing the logr.Logger interface. This logger will 59 | // be propagated through the whole operator, generating 60 | // uniform and structured logs. 61 | logf.SetLogger(zap.Logger()) 62 | 63 | printVersion() 64 | 65 | namespace, err := k8sutil.GetWatchNamespace() 66 | if err != nil { 67 | log.Error(err, "Failed to get watch namespace") 68 | os.Exit(1) 69 | } 70 | 71 | // Get a config to talk to the apiserver 72 | cfg, err := config.GetConfig() 73 | if err != nil { 74 | log.Error(err, "") 75 | os.Exit(1) 76 | } 77 | 78 | ctx := context.TODO() 79 | 80 | // Become the leader before proceeding 81 | err = leader.Become(ctx, "visitors-operator-lock") 82 | if err != nil { 83 | log.Error(err, "") 84 | os.Exit(1) 85 | } 86 | 87 | // Create a new Cmd to provide shared dependencies and start components 88 | mgr, err := manager.New(cfg, manager.Options{ 89 | Namespace: namespace, 90 | MapperProvider: restmapper.NewDynamicRESTMapper, 91 | MetricsBindAddress: fmt.Sprintf("%s:%d", metricsHost, metricsPort), 92 | }) 93 | if err != nil { 94 | log.Error(err, "") 95 | os.Exit(1) 96 | } 97 | 98 | log.Info("Registering Components.") 99 | 100 | // Setup Scheme for all resources 101 | if err := apis.AddToScheme(mgr.GetScheme()); err != nil { 102 | log.Error(err, "") 103 | os.Exit(1) 104 | } 105 | 106 | // Setup all Controllers 107 | if err := controller.AddToManager(mgr); err != nil { 108 | log.Error(err, "") 109 | os.Exit(1) 110 | } 111 | 112 | // Create Service object to expose the metrics port. 113 | _, err = metrics.ExposeMetricsPort(ctx, metricsPort) 114 | if err != nil { 115 | log.Info(err.Error()) 116 | } 117 | 118 | log.Info("Starting the Cmd.") 119 | 120 | // Start the Cmd 121 | if err := mgr.Start(signals.SetupSignalHandler()); err != nil { 122 | log.Error(err, "Manager exited non-zero") 123 | os.Exit(1) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /ch07/visitors-operator/deploy/crds/example_v1_visitorsapp_cr.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: example.com/v1 2 | kind: VisitorsApp 3 | metadata: 4 | name: ex 5 | spec: 6 | size: 1 7 | title: "Custom Dashboard Title" 8 | -------------------------------------------------------------------------------- /ch07/visitors-operator/deploy/crds/example_v1_visitorsapp_crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: visitorsapps.example.com 5 | spec: 6 | group: example.com 7 | names: 8 | kind: VisitorsApp 9 | listKind: VisitorsAppList 10 | plural: visitorsapps 11 | singular: visitorsapp 12 | scope: Namespaced 13 | subresources: 14 | status: {} 15 | validation: 16 | openAPIV3Schema: 17 | properties: 18 | apiVersion: 19 | description: 'APIVersion defines the versioned schema of this representation 20 | of an object. Servers should convert recognized schemas to the latest 21 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#resources' 22 | type: string 23 | kind: 24 | description: 'Kind is a string value representing the REST resource this 25 | object represents. Servers may infer this from the endpoint the client 26 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds' 27 | type: string 28 | metadata: 29 | type: object 30 | spec: 31 | type: object 32 | properties: 33 | size: 34 | type: integer 35 | title: 36 | type: string 37 | required: 38 | - size 39 | status: 40 | type: object 41 | properties: 42 | backendImage: 43 | type: string 44 | frontendImage: 45 | type: string 46 | version: v1 47 | versions: 48 | - name: v1 49 | served: true 50 | storage: true 51 | -------------------------------------------------------------------------------- /ch07/visitors-operator/deploy/operator.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: visitors-operator 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | name: visitors-operator 10 | template: 11 | metadata: 12 | labels: 13 | name: visitors-operator 14 | spec: 15 | serviceAccountName: visitors-operator 16 | containers: 17 | - name: visitors-operator 18 | # Replace this with the built image name 19 | image: REPLACE_IMAGE 20 | command: 21 | - visitors-operator 22 | imagePullPolicy: Always 23 | env: 24 | - name: WATCH_NAMESPACE 25 | valueFrom: 26 | fieldRef: 27 | fieldPath: metadata.namespace 28 | - name: POD_NAME 29 | valueFrom: 30 | fieldRef: 31 | fieldPath: metadata.name 32 | - name: OPERATOR_NAME 33 | value: "visitors-operator" 34 | -------------------------------------------------------------------------------- /ch07/visitors-operator/deploy/role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: Role 3 | metadata: 4 | creationTimestamp: null 5 | name: visitors-operator 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - pods 11 | - services 12 | - endpoints 13 | - persistentvolumeclaims 14 | - events 15 | - configmaps 16 | - secrets 17 | verbs: 18 | - '*' 19 | - apiGroups: 20 | - apps 21 | resources: 22 | - deployments 23 | - daemonsets 24 | - replicasets 25 | - statefulsets 26 | verbs: 27 | - '*' 28 | - apiGroups: 29 | - monitoring.coreos.com 30 | resources: 31 | - servicemonitors 32 | verbs: 33 | - get 34 | - create 35 | - apiGroups: 36 | - apps 37 | resourceNames: 38 | - visitors-operator 39 | resources: 40 | - deployments/finalizers 41 | verbs: 42 | - update 43 | - apiGroups: 44 | - example.com 45 | resources: 46 | - '*' 47 | verbs: 48 | - '*' 49 | -------------------------------------------------------------------------------- /ch07/visitors-operator/deploy/role_binding.yaml: -------------------------------------------------------------------------------- 1 | kind: RoleBinding 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | metadata: 4 | name: visitors-operator 5 | subjects: 6 | - kind: ServiceAccount 7 | name: visitors-operator 8 | roleRef: 9 | kind: Role 10 | name: visitors-operator 11 | apiGroup: rbac.authorization.k8s.io 12 | -------------------------------------------------------------------------------- /ch07/visitors-operator/deploy/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: visitors-operator 5 | -------------------------------------------------------------------------------- /ch07/visitors-operator/pkg/apis/addtoscheme_example_v1.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | v1 "github.com/jdob/visitors-operator/pkg/apis/example/v1" 5 | ) 6 | 7 | func init() { 8 | // Register the types with the Scheme so the components can map objects to GroupVersionKinds and back 9 | AddToSchemes = append(AddToSchemes, v1.SchemeBuilder.AddToScheme) 10 | } 11 | -------------------------------------------------------------------------------- /ch07/visitors-operator/pkg/apis/apis.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "k8s.io/apimachinery/pkg/runtime" 5 | ) 6 | 7 | // AddToSchemes may be used to add all resources defined in the project to a Scheme 8 | var AddToSchemes runtime.SchemeBuilder 9 | 10 | // AddToScheme adds all Resources to the Scheme 11 | func AddToScheme(s *runtime.Scheme) error { 12 | return AddToSchemes.AddToScheme(s) 13 | } 14 | -------------------------------------------------------------------------------- /ch07/visitors-operator/pkg/apis/example/v1/doc.go: -------------------------------------------------------------------------------- 1 | // Package v1 contains API Schema definitions for the example v1 API group 2 | // +k8s:deepcopy-gen=package,register 3 | // +groupName=example.com 4 | package v1 5 | -------------------------------------------------------------------------------- /ch07/visitors-operator/pkg/apis/example/v1/register.go: -------------------------------------------------------------------------------- 1 | // NOTE: Boilerplate only. Ignore this file. 2 | 3 | // Package v1 contains API Schema definitions for the example v1 API group 4 | // +k8s:deepcopy-gen=package,register 5 | // +groupName=example.com 6 | package v1 7 | 8 | import ( 9 | "k8s.io/apimachinery/pkg/runtime/schema" 10 | "sigs.k8s.io/controller-runtime/pkg/runtime/scheme" 11 | ) 12 | 13 | var ( 14 | // SchemeGroupVersion is group version used to register these objects 15 | SchemeGroupVersion = schema.GroupVersion{Group: "example.com", Version: "v1"} 16 | 17 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 18 | SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion} 19 | ) 20 | -------------------------------------------------------------------------------- /ch07/visitors-operator/pkg/apis/example/v1/visitorsapp_types.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | ) 6 | 7 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 8 | // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. 9 | 10 | // VisitorsAppSpec defines the desired state of VisitorsApp 11 | // +k8s:openapi-gen=true 12 | type VisitorsAppSpec struct { 13 | // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster 14 | // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file 15 | // Add custom validation using kubebuilder tags: https://book.kubebuilder.io/beyond_basics/generating_crd.html 16 | 17 | Size int32 `json:"size"` 18 | Title string `json:"title"` 19 | } 20 | 21 | // VisitorsAppStatus defines the observed state of VisitorsApp 22 | // +k8s:openapi-gen=true 23 | type VisitorsAppStatus struct { 24 | // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster 25 | // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file 26 | // Add custom validation using kubebuilder tags: https://book.kubebuilder.io/beyond_basics/generating_crd.html 27 | 28 | BackendImage string `json:"backendImage"` 29 | FrontendImage string `json:"frontendImage"` 30 | } 31 | 32 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 33 | 34 | // VisitorsApp is the Schema for the visitorsapps API 35 | // +k8s:openapi-gen=true 36 | // +kubebuilder:subresource:status 37 | type VisitorsApp struct { 38 | metav1.TypeMeta `json:",inline"` 39 | metav1.ObjectMeta `json:"metadata,omitempty"` 40 | 41 | Spec VisitorsAppSpec `json:"spec,omitempty"` 42 | Status VisitorsAppStatus `json:"status,omitempty"` 43 | } 44 | 45 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 46 | 47 | // VisitorsAppList contains a list of VisitorsApp 48 | type VisitorsAppList struct { 49 | metav1.TypeMeta `json:",inline"` 50 | metav1.ListMeta `json:"metadata,omitempty"` 51 | Items []VisitorsApp `json:"items"` 52 | } 53 | 54 | func init() { 55 | SchemeBuilder.Register(&VisitorsApp{}, &VisitorsAppList{}) 56 | } 57 | -------------------------------------------------------------------------------- /ch07/visitors-operator/pkg/apis/example/v1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | // +build !ignore_autogenerated 2 | 3 | // Code generated by deepcopy-gen. DO NOT EDIT. 4 | 5 | package v1 6 | 7 | import ( 8 | runtime "k8s.io/apimachinery/pkg/runtime" 9 | ) 10 | 11 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 12 | func (in *VisitorsApp) DeepCopyInto(out *VisitorsApp) { 13 | *out = *in 14 | out.TypeMeta = in.TypeMeta 15 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 16 | out.Spec = in.Spec 17 | out.Status = in.Status 18 | return 19 | } 20 | 21 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VisitorsApp. 22 | func (in *VisitorsApp) DeepCopy() *VisitorsApp { 23 | if in == nil { 24 | return nil 25 | } 26 | out := new(VisitorsApp) 27 | in.DeepCopyInto(out) 28 | return out 29 | } 30 | 31 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 32 | func (in *VisitorsApp) DeepCopyObject() runtime.Object { 33 | if c := in.DeepCopy(); c != nil { 34 | return c 35 | } 36 | return nil 37 | } 38 | 39 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 40 | func (in *VisitorsAppList) DeepCopyInto(out *VisitorsAppList) { 41 | *out = *in 42 | out.TypeMeta = in.TypeMeta 43 | out.ListMeta = in.ListMeta 44 | if in.Items != nil { 45 | in, out := &in.Items, &out.Items 46 | *out = make([]VisitorsApp, len(*in)) 47 | for i := range *in { 48 | (*in)[i].DeepCopyInto(&(*out)[i]) 49 | } 50 | } 51 | return 52 | } 53 | 54 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VisitorsAppList. 55 | func (in *VisitorsAppList) DeepCopy() *VisitorsAppList { 56 | if in == nil { 57 | return nil 58 | } 59 | out := new(VisitorsAppList) 60 | in.DeepCopyInto(out) 61 | return out 62 | } 63 | 64 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 65 | func (in *VisitorsAppList) DeepCopyObject() runtime.Object { 66 | if c := in.DeepCopy(); c != nil { 67 | return c 68 | } 69 | return nil 70 | } 71 | 72 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 73 | func (in *VisitorsAppSpec) DeepCopyInto(out *VisitorsAppSpec) { 74 | *out = *in 75 | return 76 | } 77 | 78 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VisitorsAppSpec. 79 | func (in *VisitorsAppSpec) DeepCopy() *VisitorsAppSpec { 80 | if in == nil { 81 | return nil 82 | } 83 | out := new(VisitorsAppSpec) 84 | in.DeepCopyInto(out) 85 | return out 86 | } 87 | 88 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 89 | func (in *VisitorsAppStatus) DeepCopyInto(out *VisitorsAppStatus) { 90 | *out = *in 91 | return 92 | } 93 | 94 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VisitorsAppStatus. 95 | func (in *VisitorsAppStatus) DeepCopy() *VisitorsAppStatus { 96 | if in == nil { 97 | return nil 98 | } 99 | out := new(VisitorsAppStatus) 100 | in.DeepCopyInto(out) 101 | return out 102 | } 103 | -------------------------------------------------------------------------------- /ch07/visitors-operator/pkg/apis/example/v1/zz_generated.openapi.go: -------------------------------------------------------------------------------- 1 | // +build !ignore_autogenerated 2 | 3 | // Code generated by openapi-gen. DO NOT EDIT. 4 | 5 | // This file was autogenerated by openapi-gen. Do not edit it manually! 6 | 7 | package v1 8 | 9 | import ( 10 | spec "github.com/go-openapi/spec" 11 | common "k8s.io/kube-openapi/pkg/common" 12 | ) 13 | 14 | func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { 15 | return map[string]common.OpenAPIDefinition{ 16 | "github.com/jdob/visitors-operator/pkg/apis/example/v1.VisitorsApp": schema_pkg_apis_example_v1_VisitorsApp(ref), 17 | "github.com/jdob/visitors-operator/pkg/apis/example/v1.VisitorsAppSpec": schema_pkg_apis_example_v1_VisitorsAppSpec(ref), 18 | "github.com/jdob/visitors-operator/pkg/apis/example/v1.VisitorsAppStatus": schema_pkg_apis_example_v1_VisitorsAppStatus(ref), 19 | } 20 | } 21 | 22 | func schema_pkg_apis_example_v1_VisitorsApp(ref common.ReferenceCallback) common.OpenAPIDefinition { 23 | return common.OpenAPIDefinition{ 24 | Schema: spec.Schema{ 25 | SchemaProps: spec.SchemaProps{ 26 | Description: "VisitorsApp is the Schema for the visitorsapps API", 27 | Properties: map[string]spec.Schema{ 28 | "kind": { 29 | SchemaProps: spec.SchemaProps{ 30 | Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds", 31 | Type: []string{"string"}, 32 | Format: "", 33 | }, 34 | }, 35 | "apiVersion": { 36 | SchemaProps: spec.SchemaProps{ 37 | Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#resources", 38 | Type: []string{"string"}, 39 | Format: "", 40 | }, 41 | }, 42 | "metadata": { 43 | SchemaProps: spec.SchemaProps{ 44 | Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), 45 | }, 46 | }, 47 | "spec": { 48 | SchemaProps: spec.SchemaProps{ 49 | Ref: ref("github.com/jdob/visitors-operator/pkg/apis/example/v1.VisitorsAppSpec"), 50 | }, 51 | }, 52 | "status": { 53 | SchemaProps: spec.SchemaProps{ 54 | Ref: ref("github.com/jdob/visitors-operator/pkg/apis/example/v1.VisitorsAppStatus"), 55 | }, 56 | }, 57 | }, 58 | }, 59 | }, 60 | Dependencies: []string{ 61 | "github.com/jdob/visitors-operator/pkg/apis/example/v1.VisitorsAppSpec", "github.com/jdob/visitors-operator/pkg/apis/example/v1.VisitorsAppStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, 62 | } 63 | } 64 | 65 | func schema_pkg_apis_example_v1_VisitorsAppSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { 66 | return common.OpenAPIDefinition{ 67 | Schema: spec.Schema{ 68 | SchemaProps: spec.SchemaProps{ 69 | Description: "VisitorsAppSpec defines the desired state of VisitorsApp", 70 | Properties: map[string]spec.Schema{}, 71 | }, 72 | }, 73 | Dependencies: []string{}, 74 | } 75 | } 76 | 77 | func schema_pkg_apis_example_v1_VisitorsAppStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { 78 | return common.OpenAPIDefinition{ 79 | Schema: spec.Schema{ 80 | SchemaProps: spec.SchemaProps{ 81 | Description: "VisitorsAppStatus defines the observed state of VisitorsApp", 82 | Properties: map[string]spec.Schema{}, 83 | }, 84 | }, 85 | Dependencies: []string{}, 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /ch07/visitors-operator/pkg/controller/add_visitorsapp.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/jdob/visitors-operator/pkg/controller/visitorsapp" 5 | ) 6 | 7 | func init() { 8 | // AddToManagerFuncs is a list of functions to create controllers and add them to a manager. 9 | AddToManagerFuncs = append(AddToManagerFuncs, visitorsapp.Add) 10 | } 11 | -------------------------------------------------------------------------------- /ch07/visitors-operator/pkg/controller/controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "sigs.k8s.io/controller-runtime/pkg/manager" 5 | ) 6 | 7 | // AddToManagerFuncs is a list of functions to add all Controllers to the Manager 8 | var AddToManagerFuncs []func(manager.Manager) error 9 | 10 | // AddToManager adds all Controllers to the Manager 11 | func AddToManager(m manager.Manager) error { 12 | for _, f := range AddToManagerFuncs { 13 | if err := f(m); err != nil { 14 | return err 15 | } 16 | } 17 | return nil 18 | } 19 | -------------------------------------------------------------------------------- /ch07/visitors-operator/pkg/controller/visitorsapp/backend.go: -------------------------------------------------------------------------------- 1 | package visitorsapp 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | examplev1 "github.com/jdob/visitors-operator/pkg/apis/example/v1" 8 | 9 | appsv1 "k8s.io/api/apps/v1" 10 | corev1 "k8s.io/api/core/v1" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | "k8s.io/apimachinery/pkg/types" 13 | "k8s.io/apimachinery/pkg/util/intstr" 14 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 15 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 16 | ) 17 | 18 | const backendPort = 8000 19 | const backendServicePort = 30685 20 | const backendImage = "jdob/visitors-service:1.0.0" 21 | 22 | func backendDeploymentName(v *examplev1.VisitorsApp) string { 23 | return v.Name + "-backend" 24 | } 25 | 26 | func backendServiceName(v *examplev1.VisitorsApp) string { 27 | return v.Name + "-backend-service" 28 | } 29 | 30 | func (r *ReconcileVisitorsApp) backendDeployment(v *examplev1.VisitorsApp) *appsv1.Deployment { 31 | labels := labels(v, "backend") 32 | size := v.Spec.Size 33 | 34 | userSecret := &corev1.EnvVarSource{ 35 | SecretKeyRef: &corev1.SecretKeySelector{ 36 | LocalObjectReference: corev1.LocalObjectReference{Name: mysqlAuthName()}, 37 | Key: "username", 38 | }, 39 | } 40 | 41 | passwordSecret := &corev1.EnvVarSource{ 42 | SecretKeyRef: &corev1.SecretKeySelector{ 43 | LocalObjectReference: corev1.LocalObjectReference{Name: mysqlAuthName()}, 44 | Key: "password", 45 | }, 46 | } 47 | 48 | dep := &appsv1.Deployment{ 49 | ObjectMeta: metav1.ObjectMeta{ 50 | Name: backendDeploymentName(v), 51 | Namespace: v.Namespace, 52 | }, 53 | Spec: appsv1.DeploymentSpec{ 54 | Replicas: &size, 55 | Selector: &metav1.LabelSelector{ 56 | MatchLabels: labels, 57 | }, 58 | Template: corev1.PodTemplateSpec{ 59 | ObjectMeta: metav1.ObjectMeta{ 60 | Labels: labels, 61 | }, 62 | Spec: corev1.PodSpec{ 63 | Containers: []corev1.Container{{ 64 | Image: backendImage, 65 | ImagePullPolicy: corev1.PullAlways, 66 | Name: "visitors-service", 67 | Ports: []corev1.ContainerPort{{ 68 | ContainerPort: backendPort, 69 | Name: "visitors", 70 | }}, 71 | Env: []corev1.EnvVar{ 72 | { 73 | Name: "MYSQL_DATABASE", 74 | Value: "visitors", 75 | }, 76 | { 77 | Name: "MYSQL_SERVICE_HOST", 78 | Value: mysqlServiceName(), 79 | }, 80 | { 81 | Name: "MYSQL_USERNAME", 82 | ValueFrom: userSecret, 83 | }, 84 | { 85 | Name: "MYSQL_PASSWORD", 86 | ValueFrom: passwordSecret, 87 | }, 88 | }, 89 | }}, 90 | }, 91 | }, 92 | }, 93 | } 94 | 95 | controllerutil.SetControllerReference(v, dep, r.scheme) 96 | return dep 97 | } 98 | 99 | func (r *ReconcileVisitorsApp) backendService(v *examplev1.VisitorsApp) *corev1.Service { 100 | labels := labels(v, "backend") 101 | 102 | s := &corev1.Service{ 103 | ObjectMeta: metav1.ObjectMeta{ 104 | Name: backendServiceName(v), 105 | Namespace: v.Namespace, 106 | }, 107 | Spec: corev1.ServiceSpec{ 108 | Selector: labels, 109 | Ports: []corev1.ServicePort{{ 110 | Protocol: corev1.ProtocolTCP, 111 | Port: backendPort, 112 | TargetPort: intstr.FromInt(backendPort), 113 | NodePort: 30685, 114 | }}, 115 | Type: corev1.ServiceTypeNodePort, 116 | }, 117 | } 118 | 119 | controllerutil.SetControllerReference(v, s, r.scheme) 120 | return s 121 | } 122 | 123 | func (r *ReconcileVisitorsApp) updateBackendStatus(v *examplev1.VisitorsApp) (error) { 124 | v.Status.BackendImage = backendImage 125 | err := r.client.Status().Update(context.TODO(), v) 126 | return err 127 | } 128 | 129 | func (r *ReconcileVisitorsApp) handleBackendChanges(v *examplev1.VisitorsApp) (*reconcile.Result, error) { 130 | found := &appsv1.Deployment{} 131 | err := r.client.Get(context.TODO(), types.NamespacedName{ 132 | Name: backendDeploymentName(v), 133 | Namespace: v.Namespace, 134 | }, found) 135 | if err != nil { 136 | // The deployment may not have been created yet, so requeue 137 | return &reconcile.Result{RequeueAfter:5 * time.Second}, err 138 | } 139 | 140 | size := v.Spec.Size 141 | 142 | if size != *found.Spec.Replicas { 143 | found.Spec.Replicas = &size 144 | err = r.client.Update(context.TODO(), found) 145 | if err != nil { 146 | log.Error(err, "Failed to update Deployment.", "Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name) 147 | return &reconcile.Result{}, err 148 | } 149 | // Spec updated - return and requeue 150 | return &reconcile.Result{Requeue: true}, nil 151 | } 152 | 153 | return nil, nil 154 | } -------------------------------------------------------------------------------- /ch07/visitors-operator/pkg/controller/visitorsapp/common.go: -------------------------------------------------------------------------------- 1 | package visitorsapp 2 | 3 | import ( 4 | "context" 5 | 6 | examplev1 "github.com/jdob/visitors-operator/pkg/apis/example/v1" 7 | 8 | appsv1 "k8s.io/api/apps/v1" 9 | corev1 "k8s.io/api/core/v1" 10 | "k8s.io/apimachinery/pkg/api/errors" 11 | "k8s.io/apimachinery/pkg/types" 12 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 13 | ) 14 | 15 | func (r *ReconcileVisitorsApp) ensureDeployment(request reconcile.Request, 16 | instance *examplev1.VisitorsApp, 17 | dep *appsv1.Deployment, 18 | ) (*reconcile.Result, error) { 19 | 20 | // See if deployment already exists and create if it doesn't 21 | found := &appsv1.Deployment{} 22 | err := r.client.Get(context.TODO(), types.NamespacedName{ 23 | Name: dep.Name, 24 | Namespace: instance.Namespace, 25 | }, found) 26 | if err != nil && errors.IsNotFound(err) { 27 | 28 | // Create the deployment 29 | log.Info("Creating a new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name) 30 | err = r.client.Create(context.TODO(), dep) 31 | 32 | if err != nil { 33 | // Deployment failed 34 | log.Error(err, "Failed to create new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name) 35 | return &reconcile.Result{}, err 36 | } else { 37 | // Deployment was successful 38 | return nil, nil 39 | } 40 | } else if err != nil { 41 | // Error that isn't due to the deployment not existing 42 | log.Error(err, "Failed to get Deployment") 43 | return &reconcile.Result{}, err 44 | } 45 | 46 | return nil, nil 47 | } 48 | 49 | func (r *ReconcileVisitorsApp) ensureService(request reconcile.Request, 50 | instance *examplev1.VisitorsApp, 51 | s *corev1.Service, 52 | ) (*reconcile.Result, error) { 53 | found := &corev1.Service{} 54 | err := r.client.Get(context.TODO(), types.NamespacedName{ 55 | Name: s.Name, 56 | Namespace: instance.Namespace, 57 | }, found) 58 | if err != nil && errors.IsNotFound(err) { 59 | 60 | // Create the service 61 | log.Info("Creating a new Service", "Service.Namespace", s.Namespace, "Service.Name", s.Name) 62 | err = r.client.Create(context.TODO(), s) 63 | 64 | if err != nil { 65 | // Creation failed 66 | log.Error(err, "Failed to create new Service", "Service.Namespace", s.Namespace, "Service.Name", s.Name) 67 | return &reconcile.Result{}, err 68 | } else { 69 | // Creation was successful 70 | return nil, nil 71 | } 72 | } else if err != nil { 73 | // Error that isn't due to the service not existing 74 | log.Error(err, "Failed to get Service") 75 | return &reconcile.Result{}, err 76 | } 77 | 78 | return nil, nil 79 | } 80 | 81 | func (r *ReconcileVisitorsApp) ensureSecret(request reconcile.Request, 82 | instance *examplev1.VisitorsApp, 83 | s *corev1.Secret, 84 | ) (*reconcile.Result, error) { 85 | found := &corev1.Secret{} 86 | err := r.client.Get(context.TODO(), types.NamespacedName{ 87 | Name: s.Name, 88 | Namespace: instance.Namespace, 89 | }, found) 90 | if err != nil && errors.IsNotFound(err) { 91 | // Create the secret 92 | log.Info("Creating a new secret", "Secret.Namespace", s.Namespace, "Secret.Name", s.Name) 93 | err = r.client.Create(context.TODO(), s) 94 | 95 | if err != nil { 96 | // Creation failed 97 | log.Error(err, "Failed to create new Secret", "Secret.Namespace", s.Namespace, "Secret.Name", s.Name) 98 | return &reconcile.Result{}, err 99 | } else { 100 | // Creation was successful 101 | return nil, nil 102 | } 103 | } else if err != nil { 104 | // Error that isn't due to the secret not existing 105 | log.Error(err, "Failed to get Secret") 106 | return &reconcile.Result{}, err 107 | } 108 | 109 | return nil, nil 110 | } 111 | 112 | func labels(v *examplev1.VisitorsApp, tier string) map[string]string { 113 | return map[string]string{ 114 | "app": "visitors", 115 | "visitorssite_cr": v.Name, 116 | "tier": tier, 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /ch07/visitors-operator/pkg/controller/visitorsapp/frontend.go: -------------------------------------------------------------------------------- 1 | package visitorsapp 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | examplev1 "github.com/jdob/visitors-operator/pkg/apis/example/v1" 8 | 9 | appsv1 "k8s.io/api/apps/v1" 10 | corev1 "k8s.io/api/core/v1" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | "k8s.io/apimachinery/pkg/types" 13 | "k8s.io/apimachinery/pkg/util/intstr" 14 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 15 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 16 | ) 17 | 18 | const frontendPort = 3000 19 | const frontendServicePort = 30686 20 | const frontendImage = "jdob/visitors-webui:1.0.0" 21 | 22 | func frontendDeploymentName(v *examplev1.VisitorsApp) string { 23 | return v.Name + "-frontend" 24 | } 25 | 26 | func frontendServiceName(v *examplev1.VisitorsApp) string { 27 | return v.Name + "-frontend-service" 28 | } 29 | 30 | func (r *ReconcileVisitorsApp) frontendDeployment(v *examplev1.VisitorsApp) *appsv1.Deployment { 31 | labels := labels(v, "frontend") 32 | size := int32(1) 33 | 34 | // If the header was specified, add it as an env variable 35 | env := []corev1.EnvVar{} 36 | if v.Spec.Title != "" { 37 | env = append(env, corev1.EnvVar{ 38 | Name: "REACT_APP_TITLE", 39 | Value: v.Spec.Title, 40 | }) 41 | } 42 | 43 | dep := &appsv1.Deployment{ 44 | ObjectMeta: metav1.ObjectMeta{ 45 | Name: frontendDeploymentName(v), 46 | Namespace: v.Namespace, 47 | }, 48 | Spec: appsv1.DeploymentSpec{ 49 | Replicas: &size, 50 | Selector: &metav1.LabelSelector{ 51 | MatchLabels: labels, 52 | }, 53 | Template: corev1.PodTemplateSpec{ 54 | ObjectMeta: metav1.ObjectMeta{ 55 | Labels: labels, 56 | }, 57 | Spec: corev1.PodSpec{ 58 | Containers: []corev1.Container{{ 59 | Image: frontendImage, 60 | Name: "visitors-webui", 61 | Ports: []corev1.ContainerPort{{ 62 | ContainerPort: frontendPort, 63 | Name: "visitors", 64 | }}, 65 | Env: env, 66 | }}, 67 | }, 68 | }, 69 | }, 70 | } 71 | 72 | controllerutil.SetControllerReference(v, dep, r.scheme) 73 | return dep 74 | } 75 | 76 | func (r *ReconcileVisitorsApp) frontendService(v *examplev1.VisitorsApp) *corev1.Service { 77 | labels := labels(v, "frontend") 78 | 79 | s := &corev1.Service{ 80 | ObjectMeta: metav1.ObjectMeta{ 81 | Name: frontendServiceName(v), 82 | Namespace: v.Namespace, 83 | }, 84 | Spec: corev1.ServiceSpec{ 85 | Selector: labels, 86 | Ports: []corev1.ServicePort{{ 87 | Protocol: corev1.ProtocolTCP, 88 | Port: frontendPort, 89 | TargetPort: intstr.FromInt(frontendPort), 90 | NodePort: frontendServicePort, 91 | }}, 92 | Type: corev1.ServiceTypeNodePort, 93 | }, 94 | } 95 | 96 | log.Info("Service Spec", "Service.Name", s.ObjectMeta.Name) 97 | 98 | controllerutil.SetControllerReference(v, s, r.scheme) 99 | return s 100 | } 101 | 102 | func (r *ReconcileVisitorsApp) updateFrontendStatus(v *examplev1.VisitorsApp) (error) { 103 | v.Status.FrontendImage = frontendImage 104 | err := r.client.Status().Update(context.TODO(), v) 105 | return err 106 | } 107 | 108 | func (r *ReconcileVisitorsApp) handleFrontendChanges(v *examplev1.VisitorsApp) (*reconcile.Result, error) { 109 | found := &appsv1.Deployment{} 110 | err := r.client.Get(context.TODO(), types.NamespacedName{ 111 | Name: frontendDeploymentName(v), 112 | Namespace: v.Namespace, 113 | }, found) 114 | if err != nil { 115 | // The deployment may not have been created yet, so requeue 116 | return &reconcile.Result{RequeueAfter:5 * time.Second}, err 117 | } 118 | 119 | title := v.Spec.Title 120 | existing := (*found).Spec.Template.Spec.Containers[0].Env[0].Value 121 | 122 | if title != existing { 123 | (*found).Spec.Template.Spec.Containers[0].Env[0].Value = title 124 | err = r.client.Update(context.TODO(), found) 125 | if err != nil { 126 | log.Error(err, "Failed to update Deployment.", "Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name) 127 | return &reconcile.Result{}, err 128 | } 129 | // Spec updated - return and requeue 130 | return &reconcile.Result{Requeue: true}, nil 131 | } 132 | 133 | return nil, nil 134 | } -------------------------------------------------------------------------------- /ch07/visitors-operator/pkg/controller/visitorsapp/mysql.go: -------------------------------------------------------------------------------- 1 | package visitorsapp 2 | 3 | import ( 4 | "context" 5 | 6 | examplev1 "github.com/jdob/visitors-operator/pkg/apis/example/v1" 7 | 8 | appsv1 "k8s.io/api/apps/v1" 9 | corev1 "k8s.io/api/core/v1" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/apimachinery/pkg/types" 12 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 13 | ) 14 | 15 | func mysqlDeploymentName() string { 16 | return "mysql" 17 | } 18 | 19 | func mysqlServiceName() string { 20 | return "mysql-service" 21 | } 22 | 23 | func mysqlAuthName() string { 24 | return "mysql-auth" 25 | } 26 | 27 | func (r *ReconcileVisitorsApp) mysqlAuthSecret(v *examplev1.VisitorsApp) *corev1.Secret { 28 | secret := &corev1.Secret{ 29 | ObjectMeta: metav1.ObjectMeta{ 30 | Name: mysqlAuthName(), 31 | Namespace: v.Namespace, 32 | }, 33 | Type: "Opaque", 34 | StringData: map[string]string{ 35 | "username": "visitors-user", 36 | "password": "visitors-pass", 37 | }, 38 | } 39 | controllerutil.SetControllerReference(v, secret, r.scheme) 40 | return secret 41 | } 42 | 43 | func (r *ReconcileVisitorsApp) mysqlDeployment(v *examplev1.VisitorsApp) *appsv1.Deployment { 44 | labels := labels(v, "mysql") 45 | size := int32(1) 46 | 47 | userSecret := &corev1.EnvVarSource{ 48 | SecretKeyRef: &corev1.SecretKeySelector{ 49 | LocalObjectReference: corev1.LocalObjectReference{Name: mysqlAuthName()}, 50 | Key: "username", 51 | }, 52 | } 53 | 54 | passwordSecret := &corev1.EnvVarSource{ 55 | SecretKeyRef: &corev1.SecretKeySelector{ 56 | LocalObjectReference: corev1.LocalObjectReference{Name: mysqlAuthName()}, 57 | Key: "password", 58 | }, 59 | } 60 | 61 | dep := &appsv1.Deployment{ 62 | ObjectMeta: metav1.ObjectMeta{ 63 | Name: mysqlDeploymentName(), 64 | Namespace: v.Namespace, 65 | }, 66 | Spec: appsv1.DeploymentSpec{ 67 | Replicas: &size, 68 | Selector: &metav1.LabelSelector{ 69 | MatchLabels: labels, 70 | }, 71 | Template: corev1.PodTemplateSpec{ 72 | ObjectMeta: metav1.ObjectMeta{ 73 | Labels: labels, 74 | }, 75 | Spec: corev1.PodSpec{ 76 | Containers: []corev1.Container{{ 77 | Image: "mysql:5.7", 78 | Name: "visitors-mysql", 79 | Ports: []corev1.ContainerPort{{ 80 | ContainerPort: 3306, 81 | Name: "mysql", 82 | }}, 83 | Env: []corev1.EnvVar{ 84 | { 85 | Name: "MYSQL_ROOT_PASSWORD", 86 | Value: "password", 87 | }, 88 | { 89 | Name: "MYSQL_DATABASE", 90 | Value: "visitors", 91 | }, 92 | { 93 | Name: "MYSQL_USER", 94 | ValueFrom: userSecret, 95 | }, 96 | { 97 | Name: "MYSQL_PASSWORD", 98 | ValueFrom: passwordSecret, 99 | }, 100 | }, 101 | }}, 102 | }, 103 | }, 104 | }, 105 | } 106 | 107 | controllerutil.SetControllerReference(v, dep, r.scheme) 108 | return dep 109 | } 110 | 111 | func (r *ReconcileVisitorsApp) mysqlService(v *examplev1.VisitorsApp) *corev1.Service { 112 | labels := labels(v, "mysql") 113 | 114 | s := &corev1.Service{ 115 | ObjectMeta: metav1.ObjectMeta{ 116 | Name: mysqlServiceName(), 117 | Namespace: v.Namespace, 118 | }, 119 | Spec: corev1.ServiceSpec{ 120 | Selector: labels, 121 | Ports: []corev1.ServicePort{{ 122 | Port: 3306, 123 | }}, 124 | ClusterIP: "None", 125 | }, 126 | } 127 | 128 | controllerutil.SetControllerReference(v, s, r.scheme) 129 | return s 130 | } 131 | 132 | // Returns whether or not the MySQL deployment is running 133 | func (r *ReconcileVisitorsApp) isMysqlUp(v *examplev1.VisitorsApp) (bool) { 134 | deployment := &appsv1.Deployment{} 135 | 136 | err := r.client.Get(context.TODO(), types.NamespacedName{ 137 | Name: mysqlDeploymentName(), 138 | Namespace: v.Namespace, 139 | }, deployment) 140 | 141 | if err != nil { 142 | log.Error(err, "Deployment mysql not found") 143 | return false 144 | } 145 | 146 | if deployment.Status.ReadyReplicas == 1 { 147 | return true 148 | } 149 | 150 | return false 151 | } 152 | -------------------------------------------------------------------------------- /ch07/visitors-operator/pkg/controller/visitorsapp/visitorsapp_controller.go: -------------------------------------------------------------------------------- 1 | package visitorsapp 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | examplev1 "github.com/jdob/visitors-operator/pkg/apis/example/v1" 9 | 10 | appsv1 "k8s.io/api/apps/v1" 11 | corev1 "k8s.io/api/core/v1" 12 | "k8s.io/apimachinery/pkg/api/errors" 13 | "k8s.io/apimachinery/pkg/runtime" 14 | "sigs.k8s.io/controller-runtime/pkg/client" 15 | "sigs.k8s.io/controller-runtime/pkg/controller" 16 | "sigs.k8s.io/controller-runtime/pkg/handler" 17 | "sigs.k8s.io/controller-runtime/pkg/manager" 18 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 19 | logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" 20 | "sigs.k8s.io/controller-runtime/pkg/source" 21 | ) 22 | 23 | var log = logf.Log.WithName("controller_visitorsapp") 24 | 25 | /** 26 | * USER ACTION REQUIRED: This is a scaffold file intended for the user to modify with their own Controller 27 | * business logic. Delete these comments after modifying this file.* 28 | */ 29 | 30 | // Add creates a new VisitorsApp Controller and adds it to the Manager. The Manager will set fields on the Controller 31 | // and Start it when the Manager is Started. 32 | func Add(mgr manager.Manager) error { 33 | return add(mgr, newReconciler(mgr)) 34 | } 35 | 36 | // newReconciler returns a new reconcile.Reconciler 37 | func newReconciler(mgr manager.Manager) reconcile.Reconciler { 38 | return &ReconcileVisitorsApp{client: mgr.GetClient(), scheme: mgr.GetScheme()} 39 | } 40 | 41 | // add adds a new Controller to mgr with r as the reconcile.Reconciler 42 | func add(mgr manager.Manager, r reconcile.Reconciler) error { 43 | // Create a new controller 44 | c, err := controller.New("visitorsapp-controller", mgr, controller.Options{Reconciler: r}) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | // Watch for changes to primary resource VisitorsApp 50 | err = c.Watch(&source.Kind{Type: &examplev1.VisitorsApp{}}, &handler.EnqueueRequestForObject{}) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | // TODO(user): Modify this to be the types you create that are owned by the primary resource 56 | // Watch for changes to secondary resource Pods and requeue the owner VisitorsApp 57 | err = c.Watch(&source.Kind{Type: &appsv1.Deployment{}}, &handler.EnqueueRequestForOwner{ 58 | IsController: true, 59 | OwnerType: &examplev1.VisitorsApp{}, 60 | }) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | err = c.Watch(&source.Kind{Type: &corev1.Service{}}, &handler.EnqueueRequestForOwner{ 66 | IsController: true, 67 | OwnerType: &examplev1.VisitorsApp{}, 68 | }) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | return nil 74 | } 75 | 76 | // blank assignment to verify that ReconcileVisitorsApp implements reconcile.Reconciler 77 | var _ reconcile.Reconciler = &ReconcileVisitorsApp{} 78 | 79 | // ReconcileVisitorsApp reconciles a VisitorsApp object 80 | type ReconcileVisitorsApp struct { 81 | // This client, initialized using mgr.Client() above, is a split client 82 | // that reads objects from the cache and writes to the apiserver 83 | client client.Client 84 | scheme *runtime.Scheme 85 | } 86 | 87 | // Reconcile reads that state of the cluster for a VisitorsApp object and makes changes based on the state read 88 | // and what is in the VisitorsApp.Spec 89 | // TODO(user): Modify this Reconcile function to implement your Controller logic. This example creates 90 | // a Pod as an example 91 | // Note: 92 | // The Controller will requeue the Request to be processed again if the returned error is non-nil or 93 | // Result.Requeue is true, otherwise upon completion it will remove the work from the queue. 94 | func (r *ReconcileVisitorsApp) Reconcile(request reconcile.Request) (reconcile.Result, error) { 95 | reqLogger := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name) 96 | reqLogger.Info("Reconciling VisitorsApp") 97 | 98 | // Fetch the VisitorsApp instance 99 | v := &examplev1.VisitorsApp{} 100 | err := r.client.Get(context.TODO(), request.NamespacedName, v) 101 | if err != nil { 102 | if errors.IsNotFound(err) { 103 | // Request object not found, could have been deleted after reconcile request. 104 | // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. 105 | // Return and don't requeue 106 | return reconcile.Result{}, nil 107 | } 108 | // Error reading the object - requeue the request. 109 | return reconcile.Result{}, err 110 | } 111 | 112 | var result *reconcile.Result 113 | 114 | // == MySQL ========== 115 | result, err = r.ensureSecret(request, v, r.mysqlAuthSecret(v)) 116 | if result != nil { 117 | return *result, err 118 | } 119 | 120 | result, err = r.ensureDeployment(request, v, r.mysqlDeployment(v)) 121 | if result != nil { 122 | return *result, err 123 | } 124 | 125 | result, err = r.ensureService(request, v, r.mysqlService(v)) 126 | if result != nil { 127 | return *result, err 128 | } 129 | 130 | mysqlRunning := r.isMysqlUp(v) 131 | 132 | if !mysqlRunning { 133 | // If MySQL isn't running yet, requeue the reconcile 134 | // to run again after a delay 135 | delay := time.Second*time.Duration(5) 136 | 137 | log.Info(fmt.Sprintf("MySQL isn't running, waiting for %s", delay)) 138 | return reconcile.Result{RequeueAfter: delay}, nil 139 | } 140 | 141 | // == Visitors Backend ========== 142 | result, err = r.ensureDeployment(request, v, r.backendDeployment(v)) 143 | if result != nil { 144 | return *result, err 145 | } 146 | 147 | result, err = r.ensureService(request, v, r.backendService(v)) 148 | if result != nil { 149 | return *result, err 150 | } 151 | 152 | err = r.updateBackendStatus(v) 153 | if err != nil { 154 | // Requeue the request if the status could not be updated 155 | return reconcile.Result{}, err 156 | } 157 | 158 | result, err = r.handleBackendChanges(v) 159 | if result != nil { 160 | return *result, err 161 | } 162 | 163 | // == Visitors Frontend ========== 164 | result, err = r.ensureDeployment(request, v, r.frontendDeployment(v)) 165 | if result != nil { 166 | return *result, err 167 | } 168 | 169 | result, err = r.ensureService(request, v, r.frontendService(v), 170 | ) 171 | if result != nil { 172 | return *result, err 173 | } 174 | 175 | err = r.updateFrontendStatus(v) 176 | if err != nil { 177 | // Requeue the request 178 | return reconcile.Result{}, err 179 | } 180 | 181 | result, err = r.handleFrontendChanges(v) 182 | if result != nil { 183 | return *result, err 184 | } 185 | 186 | // == Finish ========== 187 | // Everything went fine, don't requeue 188 | return reconcile.Result{}, nil 189 | } 190 | -------------------------------------------------------------------------------- /ch07/visitors-operator/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | var ( 4 | Version = "0.0.1" 5 | ) 6 | -------------------------------------------------------------------------------- /ch08/bundle/example_v1_visitorsapp_crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: visitorsapps.example.com 5 | spec: 6 | group: example.com 7 | names: 8 | kind: VisitorsApp 9 | listKind: VisitorsAppList 10 | plural: visitorsapps 11 | singular: visitorsapp 12 | scope: Namespaced 13 | subresources: 14 | status: {} 15 | validation: 16 | openAPIV3Schema: 17 | properties: 18 | apiVersion: 19 | description: 'APIVersion defines the versioned schema of this representation 20 | of an object. Servers should convert recognized schemas to the latest 21 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#resources' 22 | type: string 23 | kind: 24 | description: 'Kind is a string value representing the REST resource this 25 | object represents. Servers may infer this from the endpoint the client 26 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds' 27 | type: string 28 | metadata: 29 | type: object 30 | spec: 31 | type: object 32 | properties: 33 | size: 34 | type: integer 35 | title: 36 | type: string 37 | required: 38 | - size 39 | status: 40 | type: object 41 | properties: 42 | backendImage: 43 | type: string 44 | frontendImage: 45 | type: string 46 | version: v1 47 | versions: 48 | - name: v1 49 | served: true 50 | storage: true 51 | -------------------------------------------------------------------------------- /ch08/bundle/visitors-operator.package.yaml: -------------------------------------------------------------------------------- 1 | packageName: visitors-operator 2 | channels: 3 | - name: stable 4 | currentCSV: visitors-operator.v1.0.0 5 | defaultChannel: stable 6 | -------------------------------------------------------------------------------- /ch08/bundle/visitors-operator.v1.0.0.clusterserviceversion.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: operators.coreos.com/v1alpha1 2 | kind: ClusterServiceVersion 3 | metadata: 4 | annotations: 5 | alm-examples: '[{"apiVersion":"example.com/v1","kind":"VisitorsApp","metadata":{"name":"ex"},"spec":{"size":1,"title":"Custom 6 | Dashboard Title"}}]' 7 | capabilities: Basic Install 8 | name: visitors-operator.v1.0.0 9 | namespace: placeholder 10 | spec: 11 | apiservicedefinitions: {} 12 | customresourcedefinitions: 13 | owned: 14 | - kind: VisitorsApp 15 | name: visitorsapps.example.com 16 | version: v1 17 | displayName: Visitors Site Application 18 | description: Full stack for the Visitors Site 19 | resources: 20 | - kind: Service 21 | version: v1 22 | - kind: Deployment 23 | version: v1 24 | specDescriptors: 25 | - displayName: Size 26 | description: The number of backend pods to create 27 | path: size 28 | - displayName: Title 29 | description: Title to display on the web UI dashboard 30 | path: title 31 | statusDescriptors: 32 | - displayName: Backend Image 33 | description: Full name and version of the image used to create the backend service 34 | path: backendImage 35 | - displayName: Frontend Image 36 | description: Full name and version of the image used to create the frontend service 37 | path: frontendImage 38 | description: Operator for installing the Visitors Site application 39 | displayName: Visitors Operator 40 | install: 41 | spec: 42 | deployments: 43 | - name: visitors-operator 44 | spec: 45 | replicas: 1 46 | selector: 47 | matchLabels: 48 | name: visitors-operator 49 | strategy: {} 50 | template: 51 | metadata: 52 | labels: 53 | name: visitors-operator 54 | spec: 55 | containers: 56 | - command: 57 | - visitors-operator 58 | env: 59 | - name: WATCH_NAMESPACE 60 | valueFrom: 61 | fieldRef: 62 | fieldPath: metadata.namespace 63 | - name: POD_NAME 64 | valueFrom: 65 | fieldRef: 66 | fieldPath: metadata.name 67 | - name: OPERATOR_NAME 68 | value: visitors-operator 69 | image: jdob/visitors-operator:1.0.0 70 | imagePullPolicy: Always 71 | name: visitors-operator 72 | resources: {} 73 | serviceAccountName: visitors-operator 74 | permissions: 75 | - rules: 76 | - apiGroups: 77 | - "" 78 | resources: 79 | - pods 80 | - services 81 | - endpoints 82 | - persistentvolumeclaims 83 | - events 84 | - configmaps 85 | - secrets 86 | verbs: 87 | - '*' 88 | - apiGroups: 89 | - apps 90 | resources: 91 | - deployments 92 | - daemonsets 93 | - replicasets 94 | - statefulsets 95 | verbs: 96 | - '*' 97 | - apiGroups: 98 | - monitoring.coreos.com 99 | resources: 100 | - servicemonitors 101 | verbs: 102 | - get 103 | - create 104 | - apiGroups: 105 | - apps 106 | resourceNames: 107 | - visitors-operator 108 | resources: 109 | - deployments/finalizers 110 | verbs: 111 | - update 112 | - apiGroups: 113 | - example.com 114 | resources: 115 | - '*' 116 | verbs: 117 | - '*' 118 | serviceAccountName: visitors-operator 119 | strategy: deployment 120 | installModes: 121 | - supported: true 122 | type: OwnNamespace 123 | - supported: true 124 | type: SingleNamespace 125 | - supported: false 126 | type: MultiNamespace 127 | - supported: true 128 | type: AllNamespaces 129 | maturity: alpha 130 | provider: {} 131 | version: 1.0.0 132 | -------------------------------------------------------------------------------- /ch08/get-quay-token: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | echo -n "Username: " 6 | read USERNAME 7 | echo -n "Password: " 8 | read -s PASSWORD 9 | echo 10 | 11 | TOKEN_JSON=$(curl -s -H "Content-Type: application/json" -XPOST https://quay.io/cnr/api/v1/users/login -d ' 12 | { 13 | "user": { 14 | "username": "'"${USERNAME}"'", 15 | "password": "'"${PASSWORD}"'" 16 | } 17 | }') 18 | 19 | TOKEN=`echo $TOKEN_JSON | awk '{split($0,a,"\""); print a[4]}'` 20 | 21 | echo "" 22 | echo "========================================" 23 | echo "Auth Token is: ${TOKEN}" 24 | echo "The following command will assign the token to the expected variable:" 25 | echo " export QUAY_TOKEN=\"${TOKEN}\"" 26 | echo "========================================" 27 | -------------------------------------------------------------------------------- /ch08/testing/example_v1_visitorsapp_cr.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: example.com/v1 2 | kind: VisitorsApp 3 | metadata: 4 | name: ex 5 | spec: 6 | size: 1 7 | title: "Custom Dashboard Title" 8 | -------------------------------------------------------------------------------- /ch08/testing/operator-group.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: operators.coreos.com/v1alpha2 2 | kind: OperatorGroup 3 | metadata: 4 | name: book-operatorgroup 5 | namespace: marketplace 6 | spec: 7 | targetNamespaces: 8 | - marketplace 9 | -------------------------------------------------------------------------------- /ch08/testing/operator-source.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: operators.coreos.com/v1 2 | kind: OperatorSource 3 | metadata: 4 | name: jdob-operators 5 | namespace: marketplace 6 | spec: 7 | type: appregistry 8 | endpoint: https://quay.io/cnr 9 | registryNamespace: jdob 10 | -------------------------------------------------------------------------------- /ch08/testing/subscription.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: operators.coreos.com/v1alpha1 2 | kind: Subscription 3 | metadata: 4 | name: book-sub 5 | namespace: marketplace 6 | spec: 7 | channel: stable 8 | name: visitors-operator 9 | source: jdob-operators 10 | sourceNamespace: marketplace 11 | --------------------------------------------------------------------------------