diff --git a/ch06/visitors-helm/templates/auth.yaml b/ch06/visitors-helm/templates/auth.yaml new file mode 100644 index 0000000..d2b7d25 --- /dev/null +++ b/ch06/visitors-helm/templates/auth.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Secret +metadata: + name: mysql-auth +type: Opaque +stringData: + username: visitors-user + password: visitors-pass diff --git a/ch06/visitors-helm/templates/backend-deployment.yaml b/ch06/visitors-helm/templates/backend-deployment.yaml index d3c760e..52540ea 100644 --- a/ch06/visitors-helm/templates/backend-deployment.yaml +++ b/ch06/visitors-helm/templates/backend-deployment.yaml @@ -26,3 +26,13 @@ spec: value: visitors - name: MYSQL_SERVICE_HOST value: mysql-service + - name: MYSQL_USERNAME + valueFrom: + secretKeyRef: + name: mysql-auth + key: username + - name: MYSQL_PASSWORD + valueFrom: + secretKeyRef: + name: mysql-auth + key: password diff --git a/ch06/visitors-helm/templates/mysql-deployment.yaml b/ch06/visitors-helm/templates/mysql-deployment.yaml index e4d80c9..002cac9 100644 --- a/ch06/visitors-helm/templates/mysql-deployment.yaml +++ b/ch06/visitors-helm/templates/mysql-deployment.yaml @@ -28,6 +28,12 @@ spec: - name: MYSQL_DATABASE value: visitors - name: MYSQL_USER - value: visitors + valueFrom: + secretKeyRef: + name: mysql-auth + key: username - name: MYSQL_PASSWORD - value: visitors \ No newline at end of file + valueFrom: + secretKeyRef: + name: mysql-auth + key: password diff --git a/ch07/visitors-operator/.gitignore b/ch07/visitors-operator/.gitignore new file mode 100644 index 0000000..7c50470 --- /dev/null +++ b/ch07/visitors-operator/.gitignore @@ -0,0 +1,77 @@ +# Temporary Build Files +build/_output +build/_test +# Created by https://www.gitignore.io/api/go,vim,emacs,visualstudiocode +### Emacs ### +# -*- mode: gitignore; -*- +*~ +\#*\# +/.emacs.desktop +/.emacs.desktop.lock +*.elc +auto-save-list +tramp +.\#* +# Org-mode +.org-id-locations +*_archive +# flymake-mode +*_flymake.* +# eshell files +/eshell/history +/eshell/lastdir +# elpa packages +/elpa/ +# reftex files +*.rel +# AUCTeX auto folder +/auto/ +# cask packages +.cask/ +dist/ +# Flycheck +flycheck_*.el +# server auth directory +/server/ +# projectiles files +.projectile +projectile-bookmarks.eld +# directory configuration +.dir-locals.el +# saveplace +places +# url cache +url/cache/ +# cedet +ede-projects.el +# smex +smex-items +# company-statistics +company-statistics-cache.el +# anaconda-mode +anaconda-mode/ +### Go ### +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +# Test binary, build with 'go test -c' +*.test +# Output of the go coverage tool, specifically when used with LiteIDE +*.out +### Vim ### +# swap +.sw[a-p] +.*.sw[a-p] +# session +Session.vim +# temporary +.netrwhist +# auto-generated tag files +tags +### VisualStudioCode ### +.vscode/* +.history +# End of https://www.gitignore.io/api/go,vim,emacs,visualstudiocode diff --git a/ch07/visitors-operator/build/Dockerfile b/ch07/visitors-operator/build/Dockerfile new file mode 100644 index 0000000..5f151c6 --- /dev/null +++ b/ch07/visitors-operator/build/Dockerfile @@ -0,0 +1,15 @@ +FROM registry.access.redhat.com/ubi7/ubi-minimal:latest + +ENV OPERATOR=/usr/local/bin/visitors-operator \ + USER_UID=1001 \ + USER_NAME=visitors-operator + +# install operator binary +COPY build/_output/bin/visitors-operator ${OPERATOR} + +COPY build/bin /usr/local/bin +RUN /usr/local/bin/user_setup + +ENTRYPOINT ["/usr/local/bin/entrypoint"] + +USER ${USER_UID} diff --git a/ch07/visitors-operator/build/bin/entrypoint b/ch07/visitors-operator/build/bin/entrypoint new file mode 100755 index 0000000..5fcb4df --- /dev/null +++ b/ch07/visitors-operator/build/bin/entrypoint @@ -0,0 +1,12 @@ +#!/bin/sh -e + +# This is documented here: +# https://docs.openshift.com/container-platform/3.11/creating_images/guidelines.html#openshift-specific-guidelines + +if ! whoami &>/dev/null; then + if [ -w /etc/passwd ]; then + echo "${USER_NAME:-visitors-operator}:x:$(id -u):$(id -g):${USER_NAME:-visitors-operator} user:${HOME}:/sbin/nologin" >> /etc/passwd + fi +fi + +exec ${OPERATOR} $@ diff --git a/ch07/visitors-operator/build/bin/user_setup b/ch07/visitors-operator/build/bin/user_setup new file mode 100755 index 0000000..1e36064 --- /dev/null +++ b/ch07/visitors-operator/build/bin/user_setup @@ -0,0 +1,13 @@ +#!/bin/sh +set -x + +# ensure $HOME exists and is accessible by group 0 (we don't know what the runtime UID will be) +mkdir -p ${HOME} +chown ${USER_UID}:0 ${HOME} +chmod ug+rwx ${HOME} + +# runtime user will need to be able to self-insert in /etc/passwd +chmod g+rw /etc/passwd + +# no need for this script to remain in the image after running +rm $0 diff --git a/ch07/visitors-operator/cmd/manager/main.go b/ch07/visitors-operator/cmd/manager/main.go new file mode 100644 index 0000000..ca06fa3 --- /dev/null +++ b/ch07/visitors-operator/cmd/manager/main.go @@ -0,0 +1,125 @@ +package main + +import ( + "context" + "flag" + "fmt" + "os" + "runtime" + + // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) + _ "k8s.io/client-go/plugin/pkg/client/auth" + + "github.com/jdob/visitors-operator/pkg/apis" + "github.com/jdob/visitors-operator/pkg/controller" + + "github.com/operator-framework/operator-sdk/pkg/k8sutil" + "github.com/operator-framework/operator-sdk/pkg/leader" + "github.com/operator-framework/operator-sdk/pkg/log/zap" + "github.com/operator-framework/operator-sdk/pkg/metrics" + "github.com/operator-framework/operator-sdk/pkg/restmapper" + sdkVersion "github.com/operator-framework/operator-sdk/version" + "github.com/spf13/pflag" + "sigs.k8s.io/controller-runtime/pkg/client/config" + "sigs.k8s.io/controller-runtime/pkg/manager" + logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" + "sigs.k8s.io/controller-runtime/pkg/runtime/signals" +) + +// Change below variables to serve metrics on different host or port. +var ( + metricsHost = "0.0.0.0" + metricsPort int32 = 8383 +) +var log = logf.Log.WithName("cmd") + +func printVersion() { + log.Info(fmt.Sprintf("Go Version: %s", runtime.Version())) + log.Info(fmt.Sprintf("Go OS/Arch: %s/%s", runtime.GOOS, runtime.GOARCH)) + log.Info(fmt.Sprintf("Version of operator-sdk: %v", sdkVersion.Version)) +} + +func main() { + // Add the zap logger flag set to the CLI. The flag set must + // be added before calling pflag.Parse(). + pflag.CommandLine.AddFlagSet(zap.FlagSet()) + + // Add flags registered by imported packages (e.g. glog and + // controller-runtime) + pflag.CommandLine.AddGoFlagSet(flag.CommandLine) + + pflag.Parse() + + // Use a zap logr.Logger implementation. If none of the zap + // flags are configured (or if the zap flag set is not being + // used), this defaults to a production zap logger. + // + // The logger instantiated here can be changed to any logger + // implementing the logr.Logger interface. This logger will + // be propagated through the whole operator, generating + // uniform and structured logs. + logf.SetLogger(zap.Logger()) + + printVersion() + + namespace, err := k8sutil.GetWatchNamespace() + if err != nil { + log.Error(err, "Failed to get watch namespace") + os.Exit(1) + } + + // Get a config to talk to the apiserver + cfg, err := config.GetConfig() + if err != nil { + log.Error(err, "") + os.Exit(1) + } + + ctx := context.TODO() + + // Become the leader before proceeding + err = leader.Become(ctx, "visitors-operator-lock") + if err != nil { + log.Error(err, "") + os.Exit(1) + } + + // Create a new Cmd to provide shared dependencies and start components + mgr, err := manager.New(cfg, manager.Options{ + Namespace: namespace, + MapperProvider: restmapper.NewDynamicRESTMapper, + MetricsBindAddress: fmt.Sprintf("%s:%d", metricsHost, metricsPort), + }) + if err != nil { + log.Error(err, "") + os.Exit(1) + } + + log.Info("Registering Components.") + + // Setup Scheme for all resources + if err := apis.AddToScheme(mgr.GetScheme()); err != nil { + log.Error(err, "") + os.Exit(1) + } + + // Setup all Controllers + if err := controller.AddToManager(mgr); err != nil { + log.Error(err, "") + os.Exit(1) + } + + // Create Service object to expose the metrics port. + _, err = metrics.ExposeMetricsPort(ctx, metricsPort) + if err != nil { + log.Info(err.Error()) + } + + log.Info("Starting the Cmd.") + + // Start the Cmd + if err := mgr.Start(signals.SetupSignalHandler()); err != nil { + log.Error(err, "Manager exited non-zero") + os.Exit(1) + } +} diff --git a/ch07/visitors-operator/deploy/crds/example_v1_visitorsapp_cr.yaml b/ch07/visitors-operator/deploy/crds/example_v1_visitorsapp_cr.yaml new file mode 100644 index 0000000..29df91c --- /dev/null +++ b/ch07/visitors-operator/deploy/crds/example_v1_visitorsapp_cr.yaml @@ -0,0 +1,7 @@ +apiVersion: example.com/v1 +kind: VisitorsApp +metadata: + name: ex +spec: + size: 1 + title: "Custom Dashboard Title" diff --git a/ch07/visitors-operator/deploy/crds/example_v1_visitorsapp_crd.yaml b/ch07/visitors-operator/deploy/crds/example_v1_visitorsapp_crd.yaml new file mode 100644 index 0000000..0a4d18b --- /dev/null +++ b/ch07/visitors-operator/deploy/crds/example_v1_visitorsapp_crd.yaml @@ -0,0 +1,50 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: visitorsapps.example.com +spec: + group: example.com + names: + kind: VisitorsApp + listKind: VisitorsAppList + plural: visitorsapps + singular: visitorsapp + scope: Namespaced + subresources: + status: {} + validation: + openAPIV3Schema: + properties: + apiVersion: + 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' + type: string + kind: + 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' + type: string + metadata: + type: object + spec: + type: object + properties: + size: + type: integer + title: + type: string + required: + - size + status: + type: object + properties: + backendImage: + type: string + frontendImage: + type: string + version: v1 + versions: + - name: v1 + served: true + storage: true diff --git a/ch07/visitors-operator/deploy/operator.yaml b/ch07/visitors-operator/deploy/operator.yaml new file mode 100644 index 0000000..b3f6786 --- /dev/null +++ b/ch07/visitors-operator/deploy/operator.yaml @@ -0,0 +1,33 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: visitors-operator +spec: + replicas: 1 + selector: + matchLabels: + name: visitors-operator + template: + metadata: + labels: + name: visitors-operator + spec: + serviceAccountName: visitors-operator + containers: + - name: visitors-operator + # Replace this with the built image name + image: REPLACE_IMAGE + command: + - visitors-operator + imagePullPolicy: Always + env: + - name: WATCH_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: OPERATOR_NAME + value: "visitors-operator" diff --git a/ch07/visitors-operator/deploy/role.yaml b/ch07/visitors-operator/deploy/role.yaml new file mode 100644 index 0000000..0360c85 --- /dev/null +++ b/ch07/visitors-operator/deploy/role.yaml @@ -0,0 +1,48 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + creationTimestamp: null + name: visitors-operator +rules: +- apiGroups: + - "" + resources: + - pods + - services + - endpoints + - persistentvolumeclaims + - events + - configmaps + - secrets + verbs: + - '*' +- apiGroups: + - apps + resources: + - deployments + - daemonsets + - replicasets + - statefulsets + verbs: + - '*' +- apiGroups: + - monitoring.coreos.com + resources: + - servicemonitors + verbs: + - get + - create +- apiGroups: + - apps + resourceNames: + - visitors-operator + resources: + - deployments/finalizers + verbs: + - update +- apiGroups: + - example.com + resources: + - '*' + verbs: + - '*' diff --git a/ch07/visitors-operator/deploy/role_binding.yaml b/ch07/visitors-operator/deploy/role_binding.yaml new file mode 100644 index 0000000..3ff0137 --- /dev/null +++ b/ch07/visitors-operator/deploy/role_binding.yaml @@ -0,0 +1,11 @@ +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: visitors-operator +subjects: +- kind: ServiceAccount + name: visitors-operator +roleRef: + kind: Role + name: visitors-operator + apiGroup: rbac.authorization.k8s.io diff --git a/ch07/visitors-operator/deploy/service_account.yaml b/ch07/visitors-operator/deploy/service_account.yaml new file mode 100644 index 0000000..6283872 --- /dev/null +++ b/ch07/visitors-operator/deploy/service_account.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: visitors-operator diff --git a/ch07/visitors-operator/pkg/apis/addtoscheme_example_v1.go b/ch07/visitors-operator/pkg/apis/addtoscheme_example_v1.go new file mode 100644 index 0000000..22d4ed0 --- /dev/null +++ b/ch07/visitors-operator/pkg/apis/addtoscheme_example_v1.go @@ -0,0 +1,10 @@ +package apis + +import ( + v1 "github.com/jdob/visitors-operator/pkg/apis/example/v1" +) + +func init() { + // Register the types with the Scheme so the components can map objects to GroupVersionKinds and back + AddToSchemes = append(AddToSchemes, v1.SchemeBuilder.AddToScheme) +} diff --git a/ch07/visitors-operator/pkg/apis/apis.go b/ch07/visitors-operator/pkg/apis/apis.go new file mode 100644 index 0000000..07dc961 --- /dev/null +++ b/ch07/visitors-operator/pkg/apis/apis.go @@ -0,0 +1,13 @@ +package apis + +import ( + "k8s.io/apimachinery/pkg/runtime" +) + +// AddToSchemes may be used to add all resources defined in the project to a Scheme +var AddToSchemes runtime.SchemeBuilder + +// AddToScheme adds all Resources to the Scheme +func AddToScheme(s *runtime.Scheme) error { + return AddToSchemes.AddToScheme(s) +} diff --git a/ch07/visitors-operator/pkg/apis/example/v1/doc.go b/ch07/visitors-operator/pkg/apis/example/v1/doc.go new file mode 100644 index 0000000..357b749 --- /dev/null +++ b/ch07/visitors-operator/pkg/apis/example/v1/doc.go @@ -0,0 +1,4 @@ +// Package v1 contains API Schema definitions for the example v1 API group +// +k8s:deepcopy-gen=package,register +// +groupName=example.com +package v1 diff --git a/ch07/visitors-operator/pkg/apis/example/v1/register.go b/ch07/visitors-operator/pkg/apis/example/v1/register.go new file mode 100644 index 0000000..2eacc38 --- /dev/null +++ b/ch07/visitors-operator/pkg/apis/example/v1/register.go @@ -0,0 +1,19 @@ +// NOTE: Boilerplate only. Ignore this file. + +// Package v1 contains API Schema definitions for the example v1 API group +// +k8s:deepcopy-gen=package,register +// +groupName=example.com +package v1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/runtime/scheme" +) + +var ( + // SchemeGroupVersion is group version used to register these objects + SchemeGroupVersion = schema.GroupVersion{Group: "example.com", Version: "v1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion} +) diff --git a/ch07/visitors-operator/pkg/apis/example/v1/visitorsapp_types.go b/ch07/visitors-operator/pkg/apis/example/v1/visitorsapp_types.go new file mode 100644 index 0000000..3412c7d --- /dev/null +++ b/ch07/visitors-operator/pkg/apis/example/v1/visitorsapp_types.go @@ -0,0 +1,56 @@ +package v1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// VisitorsAppSpec defines the desired state of VisitorsApp +// +k8s:openapi-gen=true +type VisitorsAppSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file + // Add custom validation using kubebuilder tags: https://book.kubebuilder.io/beyond_basics/generating_crd.html + + Size int32 `json:"size"` + Title string `json:"title"` +} + +// VisitorsAppStatus defines the observed state of VisitorsApp +// +k8s:openapi-gen=true +type VisitorsAppStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file + // Add custom validation using kubebuilder tags: https://book.kubebuilder.io/beyond_basics/generating_crd.html + + BackendImage string `json:"backendImage"` + FrontendImage string `json:"frontendImage"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// VisitorsApp is the Schema for the visitorsapps API +// +k8s:openapi-gen=true +// +kubebuilder:subresource:status +type VisitorsApp struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec VisitorsAppSpec `json:"spec,omitempty"` + Status VisitorsAppStatus `json:"status,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// VisitorsAppList contains a list of VisitorsApp +type VisitorsAppList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []VisitorsApp `json:"items"` +} + +func init() { + SchemeBuilder.Register(&VisitorsApp{}, &VisitorsAppList{}) +} diff --git a/ch07/visitors-operator/pkg/apis/example/v1/zz_generated.deepcopy.go b/ch07/visitors-operator/pkg/apis/example/v1/zz_generated.deepcopy.go new file mode 100644 index 0000000..37c8bea --- /dev/null +++ b/ch07/visitors-operator/pkg/apis/example/v1/zz_generated.deepcopy.go @@ -0,0 +1,102 @@ +// +build !ignore_autogenerated + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VisitorsApp) DeepCopyInto(out *VisitorsApp) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + out.Status = in.Status + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VisitorsApp. +func (in *VisitorsApp) DeepCopy() *VisitorsApp { + if in == nil { + return nil + } + out := new(VisitorsApp) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VisitorsApp) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VisitorsAppList) DeepCopyInto(out *VisitorsAppList) { + *out = *in + out.TypeMeta = in.TypeMeta + out.ListMeta = in.ListMeta + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]VisitorsApp, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VisitorsAppList. +func (in *VisitorsAppList) DeepCopy() *VisitorsAppList { + if in == nil { + return nil + } + out := new(VisitorsAppList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VisitorsAppList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VisitorsAppSpec) DeepCopyInto(out *VisitorsAppSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VisitorsAppSpec. +func (in *VisitorsAppSpec) DeepCopy() *VisitorsAppSpec { + if in == nil { + return nil + } + out := new(VisitorsAppSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VisitorsAppStatus) DeepCopyInto(out *VisitorsAppStatus) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VisitorsAppStatus. +func (in *VisitorsAppStatus) DeepCopy() *VisitorsAppStatus { + if in == nil { + return nil + } + out := new(VisitorsAppStatus) + in.DeepCopyInto(out) + return out +} diff --git a/ch07/visitors-operator/pkg/apis/example/v1/zz_generated.openapi.go b/ch07/visitors-operator/pkg/apis/example/v1/zz_generated.openapi.go new file mode 100644 index 0000000..19a83b7 --- /dev/null +++ b/ch07/visitors-operator/pkg/apis/example/v1/zz_generated.openapi.go @@ -0,0 +1,87 @@ +// +build !ignore_autogenerated + +// Code generated by openapi-gen. DO NOT EDIT. + +// This file was autogenerated by openapi-gen. Do not edit it manually! + +package v1 + +import ( + spec "github.com/go-openapi/spec" + common "k8s.io/kube-openapi/pkg/common" +) + +func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { + return map[string]common.OpenAPIDefinition{ + "github.com/jdob/visitors-operator/pkg/apis/example/v1.VisitorsApp": schema_pkg_apis_example_v1_VisitorsApp(ref), + "github.com/jdob/visitors-operator/pkg/apis/example/v1.VisitorsAppSpec": schema_pkg_apis_example_v1_VisitorsAppSpec(ref), + "github.com/jdob/visitors-operator/pkg/apis/example/v1.VisitorsAppStatus": schema_pkg_apis_example_v1_VisitorsAppStatus(ref), + } +} + +func schema_pkg_apis_example_v1_VisitorsApp(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "VisitorsApp is the Schema for the visitorsapps API", + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + 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", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + 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", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/jdob/visitors-operator/pkg/apis/example/v1.VisitorsAppSpec"), + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/jdob/visitors-operator/pkg/apis/example/v1.VisitorsAppStatus"), + }, + }, + }, + }, + }, + Dependencies: []string{ + "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"}, + } +} + +func schema_pkg_apis_example_v1_VisitorsAppSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "VisitorsAppSpec defines the desired state of VisitorsApp", + Properties: map[string]spec.Schema{}, + }, + }, + Dependencies: []string{}, + } +} + +func schema_pkg_apis_example_v1_VisitorsAppStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "VisitorsAppStatus defines the observed state of VisitorsApp", + Properties: map[string]spec.Schema{}, + }, + }, + Dependencies: []string{}, + } +} diff --git a/ch07/visitors-operator/pkg/controller/add_visitorsapp.go b/ch07/visitors-operator/pkg/controller/add_visitorsapp.go new file mode 100644 index 0000000..9ce2127 --- /dev/null +++ b/ch07/visitors-operator/pkg/controller/add_visitorsapp.go @@ -0,0 +1,10 @@ +package controller + +import ( + "github.com/jdob/visitors-operator/pkg/controller/visitorsapp" +) + +func init() { + // AddToManagerFuncs is a list of functions to create controllers and add them to a manager. + AddToManagerFuncs = append(AddToManagerFuncs, visitorsapp.Add) +} diff --git a/ch07/visitors-operator/pkg/controller/controller.go b/ch07/visitors-operator/pkg/controller/controller.go new file mode 100644 index 0000000..7c069f3 --- /dev/null +++ b/ch07/visitors-operator/pkg/controller/controller.go @@ -0,0 +1,18 @@ +package controller + +import ( + "sigs.k8s.io/controller-runtime/pkg/manager" +) + +// AddToManagerFuncs is a list of functions to add all Controllers to the Manager +var AddToManagerFuncs []func(manager.Manager) error + +// AddToManager adds all Controllers to the Manager +func AddToManager(m manager.Manager) error { + for _, f := range AddToManagerFuncs { + if err := f(m); err != nil { + return err + } + } + return nil +} diff --git a/ch07/visitors-operator/pkg/controller/visitorsapp/backend.go b/ch07/visitors-operator/pkg/controller/visitorsapp/backend.go new file mode 100644 index 0000000..c7aaffe --- /dev/null +++ b/ch07/visitors-operator/pkg/controller/visitorsapp/backend.go @@ -0,0 +1,132 @@ +package visitorsapp + +import ( + "context" + "time" + + examplev1 "github.com/jdob/visitors-operator/pkg/apis/example/v1" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +const backendPort = 8000 +const backendServicePort = 30685 +const backendImage = "jdob/visitors-service:latest" + +func backendDeploymentName(v *examplev1.VisitorsApp) string { + return v.Name + "-backend" +} + +func backendServiceName(v *examplev1.VisitorsApp) string { + return v.Name + "-backend-service" +} + +func (r *ReconcileVisitorsApp) backendDeployment(v *examplev1.VisitorsApp) *appsv1.Deployment { + labels := labels(v, "backend") + size := v.Spec.Size + + dep := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: backendDeploymentName(v), + Namespace: v.Namespace, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: &size, + Selector: &metav1.LabelSelector{ + MatchLabels: labels, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Image: backendImage, + ImagePullPolicy: corev1.PullAlways, + Name: "visitors-service", + Ports: []corev1.ContainerPort{{ + ContainerPort: backendPort, + Name: "visitors", + }}, + Env: []corev1.EnvVar{ + { + Name: "MYSQL_DATABASE", + Value: "visitors", + }, + { + Name: "MYSQL_SERVICE_HOST", + Value: mysqlServiceName(), + }, + }, + }}, + }, + }, + }, + } + + controllerutil.SetControllerReference(v, dep, r.scheme) + return dep +} + +func (r *ReconcileVisitorsApp) backendService(v *examplev1.VisitorsApp) *corev1.Service { + labels := labels(v, "backend") + + s := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: backendServiceName(v), + Namespace: v.Namespace, + }, + Spec: corev1.ServiceSpec{ + Selector: labels, + Ports: []corev1.ServicePort{{ + Protocol: corev1.ProtocolTCP, + Port: backendPort, + TargetPort: intstr.FromInt(backendPort), + NodePort: 30685, + }}, + Type: corev1.ServiceTypeNodePort, + }, + } + + controllerutil.SetControllerReference(v, s, r.scheme) + return s +} + +func (r *ReconcileVisitorsApp) updateBackendStatus(v *examplev1.VisitorsApp) (error) { + v.Status.BackendImage = backendImage + err := r.client.Status().Update(context.TODO(), v) + return err +} + +func (r *ReconcileVisitorsApp) handleBackendChanges(v *examplev1.VisitorsApp) (*reconcile.Result, error) { + found := &appsv1.Deployment{} + err := r.client.Get(context.TODO(), types.NamespacedName{ + Name: backendDeploymentName(v), + Namespace: v.Namespace, + }, found) + if err != nil { + // The deployment may not have been created yet, so requeue + return &reconcile.Result{RequeueAfter:5 * time.Second}, err + } + + size := v.Spec.Size + + if size != *found.Spec.Replicas { + found.Spec.Replicas = &size + err = r.client.Update(context.TODO(), found) + if err != nil { + log.Error(err, "Failed to update Deployment.", "Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name) + return &reconcile.Result{}, err + } + // Spec updated - return and requeue + return &reconcile.Result{Requeue: true}, nil + } + + return nil, nil +} \ No newline at end of file diff --git a/ch07/visitors-operator/pkg/controller/visitorsapp/common.go b/ch07/visitors-operator/pkg/controller/visitorsapp/common.go new file mode 100644 index 0000000..ba7efd2 --- /dev/null +++ b/ch07/visitors-operator/pkg/controller/visitorsapp/common.go @@ -0,0 +1,89 @@ +package visitorsapp + +import ( + "context" + + examplev1 "github.com/jdob/visitors-operator/pkg/apis/example/v1" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +func (r *ReconcileVisitorsApp) ensureDeployment(request reconcile.Request, + instance *examplev1.VisitorsApp, + name string, + dep *appsv1.Deployment, +) (*reconcile.Result, error) { + + // See if deployment already exists and create if it doesn't + found := &appsv1.Deployment{} + err := r.client.Get(context.TODO(), types.NamespacedName{ + Name: name, + Namespace: instance.Namespace, + }, found) + if err != nil && errors.IsNotFound(err) { + + // Create the deployment + log.Info("Creating a new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name) + err = r.client.Create(context.TODO(), dep) + + if err != nil { + // Deployment failed + log.Error(err, "Failed to create new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name) + return &reconcile.Result{}, err + } else { + // Deployment was successful + return nil, nil + } + } else if err != nil { + // Error that isn't due to the deployment not existing + log.Error(err, "Failed to get Deployment") + return &reconcile.Result{}, err + } + + return nil, nil +} + +func (r *ReconcileVisitorsApp) ensureService(request reconcile.Request, + instance *examplev1.VisitorsApp, + name string, + s *corev1.Service, +) (*reconcile.Result, error) { + found := &corev1.Service{} + err := r.client.Get(context.TODO(), types.NamespacedName{ + Name: name, + Namespace: instance.Namespace, + }, found) + if err != nil && errors.IsNotFound(err) { + + // Create the service + log.Info("Creating a new Service", "Service.Namespace", s.Namespace, "Service.Name", s.Name) + err = r.client.Create(context.TODO(), s) + + if err != nil { + // Creation failed + log.Error(err, "Failed to create new Service", "Service.Namespace", s.Namespace, "Service.Name", s.Name) + return &reconcile.Result{}, err + } else { + // Creation was successful + return nil, nil + } + } else if err != nil { + // Error that isn't due to the service not existing + log.Error(err, "Failed to get Service") + return &reconcile.Result{}, err + } + + return nil, nil +} + +func labels(v *examplev1.VisitorsApp, tier string) map[string]string { + return map[string]string{ + "app": "visitors", + "visitorssite_cr": v.Name, + "tier": tier, + } +} diff --git a/ch07/visitors-operator/pkg/controller/visitorsapp/frontend.go b/ch07/visitors-operator/pkg/controller/visitorsapp/frontend.go new file mode 100644 index 0000000..ee05a39 --- /dev/null +++ b/ch07/visitors-operator/pkg/controller/visitorsapp/frontend.go @@ -0,0 +1,134 @@ +package visitorsapp + +import ( + "context" + "time" + + examplev1 "github.com/jdob/visitors-operator/pkg/apis/example/v1" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +const frontendPort = 3000 +const frontendServicePort = 30686 +const frontendImage = "jdob/visitors-webui:latest" + +func frontendDeploymentName(v *examplev1.VisitorsApp) string { + return v.Name + "-frontend" +} + +func frontendServiceName(v *examplev1.VisitorsApp) string { + return v.Name + "-frontend-service" +} + +func (r *ReconcileVisitorsApp) frontendDeployment(v *examplev1.VisitorsApp) *appsv1.Deployment { + labels := labels(v, "frontend") + size := int32(1) + + // If the header was specified, add it as an env variable + env := []corev1.EnvVar{} + if v.Spec.Title != "" { + env = append(env, corev1.EnvVar{ + Name: "REACT_APP_TITLE", + Value: v.Spec.Title, + }) + } + + dep := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: frontendDeploymentName(v), + Namespace: v.Namespace, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: &size, + Selector: &metav1.LabelSelector{ + MatchLabels: labels, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Image: frontendImage, + Name: "visitors-webui", + Ports: []corev1.ContainerPort{{ + ContainerPort: frontendPort, + Name: "visitors", + }}, + Env: env, + }}, + }, + }, + }, + } + + controllerutil.SetControllerReference(v, dep, r.scheme) + return dep +} + +func (r *ReconcileVisitorsApp) frontendService(v *examplev1.VisitorsApp) *corev1.Service { + labels := labels(v, "frontend") + + s := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: frontendServiceName(v), + Namespace: v.Namespace, + }, + Spec: corev1.ServiceSpec{ + Selector: labels, + Ports: []corev1.ServicePort{{ + Protocol: corev1.ProtocolTCP, + Port: frontendPort, + TargetPort: intstr.FromInt(frontendPort), + NodePort: frontendServicePort, + }}, + Type: corev1.ServiceTypeNodePort, + }, + } + + log.Info("Service Spec", "Service.Name", s.ObjectMeta.Name) + + controllerutil.SetControllerReference(v, s, r.scheme) + return s +} + +func (r *ReconcileVisitorsApp) updateFrontendStatus(v *examplev1.VisitorsApp) (error) { + v.Status.FrontendImage = frontendImage + err := r.client.Status().Update(context.TODO(), v) + return err +} + +func (r *ReconcileVisitorsApp) handleFrontendChanges(v *examplev1.VisitorsApp) (*reconcile.Result, error) { + found := &appsv1.Deployment{} + err := r.client.Get(context.TODO(), types.NamespacedName{ + Name: frontendDeploymentName(v), + Namespace: v.Namespace, + }, found) + if err != nil { + // The deployment may not have been created yet, so requeue + return &reconcile.Result{RequeueAfter:5 * time.Second}, err + } + + title := v.Spec.Title + existing := (*found).Spec.Template.Spec.Containers[0].Env[0].Value + + if title != existing { + (*found).Spec.Template.Spec.Containers[0].Env[0].Value = title + err = r.client.Update(context.TODO(), found) + if err != nil { + log.Error(err, "Failed to update Deployment.", "Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name) + return &reconcile.Result{}, err + } + // Spec updated - return and requeue + return &reconcile.Result{Requeue: true}, nil + } + + return nil, nil +} \ No newline at end of file diff --git a/ch07/visitors-operator/pkg/controller/visitorsapp/mysql.go b/ch07/visitors-operator/pkg/controller/visitorsapp/mysql.go new file mode 100644 index 0000000..eca4b76 --- /dev/null +++ b/ch07/visitors-operator/pkg/controller/visitorsapp/mysql.go @@ -0,0 +1,139 @@ +package visitorsapp + +import ( + "context" + "time" + + examplev1 "github.com/jdob/visitors-operator/pkg/apis/example/v1" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +func mysqlDeploymentName() string { + return "mysql" +} + +func mysqlServiceName() string { + return "mysql-service" +} + +func (r *ReconcileVisitorsApp) secret(v *examplev1.VisitorsApp) *corev1.Secret { + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: v.Namespace, + }, + Type: "Opaque", + StringData: map[string]string{ + "username": "visitors-user", + "password": "visitors-pass", + } + } + controllerutil.SetControllerReference(v, secret, r.scheme) + return secret +} + +func (r *ReconcileVisitorsApp) mysqlDeployment(v *examplev1.VisitorsApp) *appsv1.Deployment { + labels := labels(v, "mysql") + size := int32(1) + + dep := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: mysqlDeploymentName(), + Namespace: v.Namespace, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: &size, + Selector: &metav1.LabelSelector{ + MatchLabels: labels, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Image: "mysql:5.7", + Name: "visitors-mysql", + Ports: []corev1.ContainerPort{{ + ContainerPort: 3306, + Name: "mysql", + }}, + Env: []corev1.EnvVar{ + { + Name: "MYSQL_ROOT_PASSWORD", + Value: "password", + }, + { + Name: "MYSQL_DATABASE", + Value: "visitors", + }, + { + Name: "MYSQL_USER", + Value: "visitors", + }, + { + Name: "MYSQL_PASSWORD", + Value: "visitors", + }, + }, + }}, + }, + }, + }, + } + + controllerutil.SetControllerReference(v, dep, r.scheme) + return dep +} + +func (r *ReconcileVisitorsApp) mysqlService(v *examplev1.VisitorsApp) *corev1.Service { + labels := labels(v, "mysql") + + s := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: mysqlServiceName(), + Namespace: v.Namespace, + }, + Spec: corev1.ServiceSpec{ + Selector: labels, + Ports: []corev1.ServicePort{{ + Port: 3306, + }}, + ClusterIP: "None", + }, + } + + controllerutil.SetControllerReference(v, s, r.scheme) + return s +} + +// Blocks until the MySQL deployment has finished +func (r *ReconcileVisitorsApp) waitForMysql(v *examplev1.VisitorsApp) (error) { + deployment := &appsv1.Deployment{} + err := wait.Poll(1*time.Second, 1*time.Minute, + func() (done bool, err error) { + err = r.client.Get(context.TODO(), types.NamespacedName{ + Name: mysqlDeploymentName(), + Namespace: v.Namespace, + }, deployment) + if err != nil { + log.Error(err, "Deployment mysql not found") + return false, nil + } + + if deployment.Status.ReadyReplicas == 1 { + log.Info("MySQL ready replica count met") + return true, nil + } + + log.Info("Waiting for MySQL to start") + return false, nil + }, + ) + return err +} \ No newline at end of file diff --git a/ch07/visitors-operator/pkg/controller/visitorsapp/visitorsapp_controller.go b/ch07/visitors-operator/pkg/controller/visitorsapp/visitorsapp_controller.go new file mode 100644 index 0000000..4acc3dd --- /dev/null +++ b/ch07/visitors-operator/pkg/controller/visitorsapp/visitorsapp_controller.go @@ -0,0 +1,196 @@ +package visitorsapp + +import ( + "context" + + examplev1 "github.com/jdob/visitors-operator/pkg/apis/example/v1" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" + "sigs.k8s.io/controller-runtime/pkg/source" +) + +var log = logf.Log.WithName("controller_visitorsapp") + +/** +* USER ACTION REQUIRED: This is a scaffold file intended for the user to modify with their own Controller +* business logic. Delete these comments after modifying this file.* + */ + +// Add creates a new VisitorsApp Controller and adds it to the Manager. The Manager will set fields on the Controller +// and Start it when the Manager is Started. +func Add(mgr manager.Manager) error { + return add(mgr, newReconciler(mgr)) +} + +// newReconciler returns a new reconcile.Reconciler +func newReconciler(mgr manager.Manager) reconcile.Reconciler { + return &ReconcileVisitorsApp{client: mgr.GetClient(), scheme: mgr.GetScheme()} +} + +// add adds a new Controller to mgr with r as the reconcile.Reconciler +func add(mgr manager.Manager, r reconcile.Reconciler) error { + // Create a new controller + c, err := controller.New("visitorsapp-controller", mgr, controller.Options{Reconciler: r}) + if err != nil { + return err + } + + // Watch for changes to primary resource VisitorsApp + err = c.Watch(&source.Kind{Type: &examplev1.VisitorsApp{}}, &handler.EnqueueRequestForObject{}) + if err != nil { + return err + } + + // TODO(user): Modify this to be the types you create that are owned by the primary resource + // Watch for changes to secondary resource Pods and requeue the owner VisitorsApp + err = c.Watch(&source.Kind{Type: &appsv1.Deployment{}}, &handler.EnqueueRequestForOwner{ + IsController: true, + OwnerType: &examplev1.VisitorsApp{}, + }) + if err != nil { + return err + } + + err = c.Watch(&source.Kind{Type: &corev1.Service{}}, &handler.EnqueueRequestForOwner{ + IsController: true, + OwnerType: &examplev1.VisitorsApp{}, + }) + if err != nil { + return err + } + + return nil +} + +// blank assignment to verify that ReconcileVisitorsApp implements reconcile.Reconciler +var _ reconcile.Reconciler = &ReconcileVisitorsApp{} + +// ReconcileVisitorsApp reconciles a VisitorsApp object +type ReconcileVisitorsApp struct { + // This client, initialized using mgr.Client() above, is a split client + // that reads objects from the cache and writes to the apiserver + client client.Client + scheme *runtime.Scheme +} + +// Reconcile reads that state of the cluster for a VisitorsApp object and makes changes based on the state read +// and what is in the VisitorsApp.Spec +// TODO(user): Modify this Reconcile function to implement your Controller logic. This example creates +// a Pod as an example +// Note: +// The Controller will requeue the Request to be processed again if the returned error is non-nil or +// Result.Requeue is true, otherwise upon completion it will remove the work from the queue. +func (r *ReconcileVisitorsApp) Reconcile(request reconcile.Request) (reconcile.Result, error) { + reqLogger := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name) + reqLogger.Info("Reconciling VisitorsApp") + + // Fetch the VisitorsApp instance + v := &examplev1.VisitorsApp{} + err := r.client.Get(context.TODO(), request.NamespacedName, v) + if err != nil { + if errors.IsNotFound(err) { + // Request object not found, could have been deleted after reconcile request. + // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. + // Return and don't requeue + return reconcile.Result{}, nil + } + // Error reading the object - requeue the request. + return reconcile.Result{}, err + } + + var result *reconcile.Result + + // == MySQL ========== + result, err = r.ensureDeployment( + request, + v, + "mysql", + r.mysqlDeployment(v)) + if result != nil { + return *result, err + } + + result, err = r.ensureService( + request, + v, + "mysql", + r.mysqlService(v)) + if result != nil { + return *result, err + } + r.waitForMysql(v) + + // == Visitors Backend ========== + result, err = r.ensureDeployment( + request, + v, + backendDeploymentName(v), + r.backendDeployment(v)) + if result != nil { + return *result, err + } + + result, err = r.ensureService( + request, + v, + backendServiceName(v), + r.backendService(v)) + if result != nil { + return *result, err + } + + err = r.updateBackendStatus(v) + if err != nil { + // Requeue the request + return reconcile.Result{}, err + } + + result, err = r.handleBackendChanges(v) + if result != nil { + return *result, err + } + + // == Visitors Frontend ========== + result, err = r.ensureDeployment( + request, + v, + frontendDeploymentName(v), + r.frontendDeployment(v)) + if result != nil { + return *result, err + } + + result, err = r.ensureService( + request, + v, + frontendServiceName(v), + r.frontendService(v), + ) + if result != nil { + return *result, err + } + + err = r.updateFrontendStatus(v) + if err != nil { + // Requeue the request + return reconcile.Result{}, err + } + + result, err = r.handleFrontendChanges(v) + if result != nil { + return *result, err + } + + // == Finish ========== + // Everything went fine, don't requeue + return reconcile.Result{}, nil +} diff --git a/ch07/visitors-operator/version/version.go b/ch07/visitors-operator/version/version.go new file mode 100644 index 0000000..e3e130b --- /dev/null +++ b/ch07/visitors-operator/version/version.go @@ -0,0 +1,5 @@ +package version + +var ( + Version = "0.0.1" +)