├── integration ├── helm │ ├── ingress-nginx │ │ ├── values.yaml │ │ ├── .helmignore │ │ ├── Chart.yaml │ │ └── templates │ │ │ ├── _helpers.tpl │ │ │ └── deploy.yaml │ └── prometheus-rbac │ │ ├── values.yaml │ │ ├── templates │ │ ├── clusterrolebinding-prometheus.yaml │ │ ├── clusterrole-prometheus.yaml │ │ └── _helpers.tpl │ │ ├── .helmignore │ │ └── Chart.yaml ├── cluster-kind.yaml └── kube-prometheus-stack-values.yaml ├── ci.Dockerfile.dockerignore ├── test ├── externalIP-patch.yaml ├── keepalivedgroup.yaml ├── keepalivedgroup2.yaml ├── test-service.yaml ├── test-servicemultiple.yaml └── test-service-g2.yaml ├── config ├── manifests │ └── kustomization.yaml ├── prometheus │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ ├── role.yaml │ ├── rolebinding.yaml │ └── monitor.yaml ├── certmanager │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ └── certificate.yaml ├── local-development │ ├── tilt │ │ ├── ca-injection.yaml │ │ ├── service-account.yaml │ │ ├── readme.md │ │ ├── secret-injection.yaml │ │ ├── env-replace-image.yaml │ │ ├── replace-image.yaml │ │ └── kustomization.yaml │ └── kustomization.yaml ├── helmchart │ ├── cert-manager-ca-injection.yaml │ ├── Chart.yaml.tpl │ ├── .helmignore │ ├── kustomization.yaml │ ├── templates │ │ ├── certificate.yaml │ │ ├── _helpers.tpl │ │ └── manager.yaml │ └── values.yaml.tpl ├── scorecard │ ├── bases │ │ └── config.yaml │ ├── patches │ │ ├── basic.config.yaml │ │ └── olm.config.yaml │ └── kustomization.yaml ├── rbac │ ├── auth_proxy_client_clusterrole.yaml │ ├── role_binding.yaml │ ├── auth_proxy_role_binding.yaml │ ├── auth_proxy_role.yaml │ ├── leader_election_role_binding.yaml │ ├── auth_proxy_service.yaml │ ├── kustomization.yaml │ ├── keepalivedgroup_viewer_role.yaml │ ├── keepalivedgroup_editor_role.yaml │ ├── leader_election_role.yaml │ └── role.yaml ├── samples │ ├── kustomization.yaml │ └── redhatcop_v1alpha1_keepalivedgroup.yaml ├── community-operators │ └── ci.yaml ├── manager │ ├── controller_manager_config.yaml │ ├── kustomization.yaml │ └── manager.yaml ├── crd │ ├── patches │ │ ├── cainjection_in_keepalivedgroups.yaml │ │ └── webhook_in_keepalivedgroups.yaml │ ├── kustomizeconfig.yaml │ ├── kustomization.yaml │ └── bases │ │ └── redhatcop.redhat.io_keepalivedgroups.yaml ├── operatorhub │ └── operator.yaml ├── default │ ├── manager_config_patch.yaml │ ├── manager_auth_proxy_patch.yaml │ └── kustomization.yaml ├── docker │ └── notify.sh └── templates │ └── keepalived-template.yaml ├── media └── keepalived-operator.png ├── .dockerignore ├── renovate.json ├── .github └── workflows │ ├── pr.yaml │ └── push.yaml ├── .gitignore ├── PROJECT ├── hack └── boilerplate.go.txt ├── ci.Dockerfile ├── Tiltfile ├── Dockerfile ├── api └── v1alpha1 │ ├── groupversion_info.go │ ├── keepalivedgroup_types.go │ └── zz_generated.deepcopy.go ├── controllers ├── suite_test.go └── keepalivedgroup_controller.go ├── Ingress-how-to.md ├── main.go ├── go.mod ├── LICENSE ├── Makefile └── README.md /integration/helm/ingress-nginx/values.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /integration/helm/prometheus-rbac/values.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ci.Dockerfile.dockerignore: -------------------------------------------------------------------------------- 1 | api/ 2 | bundle/ 3 | controllers/ 4 | examples/ 5 | hack/ 6 | test/ 7 | -------------------------------------------------------------------------------- /test/externalIP-patch.yaml: -------------------------------------------------------------------------------- 1 | spec: 2 | externalIP: 3 | autoAssignCIDRs: 4 | - "${CIDR}" -------------------------------------------------------------------------------- /config/manifests/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - ../default 3 | - ../samples 4 | - ../scorecard 5 | -------------------------------------------------------------------------------- /config/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - monitor.yaml 3 | 4 | configurations: 5 | - kustomizeconfig.yaml 6 | -------------------------------------------------------------------------------- /config/certmanager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - certificate.yaml 3 | 4 | configurations: 5 | - kustomizeconfig.yaml 6 | -------------------------------------------------------------------------------- /media/keepalived-operator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-cop/keepalived-operator/HEAD/media/keepalived-operator.png -------------------------------------------------------------------------------- /config/local-development/tilt/ca-injection.yaml: -------------------------------------------------------------------------------- 1 | - op: add 2 | path: /metadata/annotations 3 | value: 4 | service.beta.openshift.io/inject-cabundle: "true" -------------------------------------------------------------------------------- /config/local-development/tilt/service-account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | -------------------------------------------------------------------------------- /config/local-development/tilt/readme.md: -------------------------------------------------------------------------------- 1 | remove namespace 2 | 3 | annotation in webhook service -> webhook-server-cert 4 | annotation in mutating and validating webhooks -------------------------------------------------------------------------------- /config/prometheus/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | varReference: 3 | - path: spec/endpoints/tlsConfig/serverName 4 | kind: ServiceMonitor 5 | - path: roleRef/name 6 | kind: RoleBinding -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Ignore all files which are not go type 3 | !**/*.go 4 | !**/*.mod 5 | !**/*.sum 6 | -------------------------------------------------------------------------------- /config/local-development/tilt/secret-injection.yaml: -------------------------------------------------------------------------------- 1 | - op: add 2 | path: /metadata/annotations 3 | value: 4 | service.alpha.openshift.io/serving-cert-secret-name: webhook-server-cert -------------------------------------------------------------------------------- /config/helmchart/cert-manager-ca-injection.yaml: -------------------------------------------------------------------------------- 1 | - op: add 2 | path: /metadata/annotations 3 | value: 4 | cert-manager.io/inject-ca-from: "{{ .Release.Namespace }}/webhook-server-cert" -------------------------------------------------------------------------------- /config/scorecard/bases/config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: scorecard.operatorframework.io/v1alpha3 2 | kind: Configuration 3 | metadata: 4 | name: config 5 | stages: 6 | - parallel: true 7 | tests: [] 8 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_client_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: metrics-reader 5 | rules: 6 | - nonResourceURLs: ["/metrics"] 7 | verbs: ["get"] 8 | -------------------------------------------------------------------------------- /config/samples/kustomization.yaml: -------------------------------------------------------------------------------- 1 | ## Append samples you want in your CSV to this file as resources ## 2 | resources: 3 | - redhatcop_v1alpha1_keepalivedgroup.yaml 4 | # +kubebuilder:scaffold:manifestskustomizesamples 5 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:best-practices", 5 | "schedule:earlyMondays" 6 | ], 7 | "postUpdateOptions": ["gomodTidy"] 8 | } -------------------------------------------------------------------------------- /test/keepalivedgroup.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: redhatcop.redhat.io/v1alpha1 2 | kind: KeepalivedGroup 3 | metadata: 4 | name: keepalivedgroup-test 5 | spec: 6 | interface: ens3 7 | nodeSelector: 8 | node-role.kubernetes.io/worker: "" -------------------------------------------------------------------------------- /config/community-operators/ci.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Use `replaces-mode` or `semver-mode`. Once you switch to `semver-mode`, there is no easy way back. 3 | updateGraph: semver-mode 4 | addReviewers: true 5 | reviewers: 6 | - raffaelespazzoli -------------------------------------------------------------------------------- /test/keepalivedgroup2.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: redhatcop.redhat.io/v1alpha1 2 | kind: KeepalivedGroup 3 | metadata: 4 | name: keepalivedgroup-test2 5 | spec: 6 | interface: ens3 7 | nodeSelector: 8 | node-role.kubernetes.io/worker: "" -------------------------------------------------------------------------------- /test/test-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: kubernetes 5 | spec: 6 | ports: 7 | - name: https 8 | port: 443 9 | protocol: TCP 10 | targetPort: 6443 11 | type: LoadBalancer -------------------------------------------------------------------------------- /config/local-development/tilt/env-replace-image.yaml: -------------------------------------------------------------------------------- 1 | - op: replace 2 | path: /spec/template/spec/containers/1/image 3 | value: 4 | quay.io/$repo/keepalived-operator:latest 5 | - op: add 6 | path: /spec/template/spec/containers/1/args/- 7 | value: 8 | --zap-devel=true -------------------------------------------------------------------------------- /config/local-development/tilt/replace-image.yaml: -------------------------------------------------------------------------------- 1 | - op: replace 2 | path: /spec/template/spec/containers/1/image 3 | value: 4 | quay.io/keepalived-operator/keepalived-operator:latest 5 | - op: add 6 | path: /spec/template/spec/containers/1/args/- 7 | value: 8 | --zap-devel=true -------------------------------------------------------------------------------- /config/scorecard/patches/basic.config.yaml: -------------------------------------------------------------------------------- 1 | - op: add 2 | path: /stages/0/tests/- 3 | value: 4 | entrypoint: 5 | - scorecard-test 6 | - basic-check-spec 7 | image: quay.io/operator-framework/scorecard-test:v1.5.0 8 | labels: 9 | suite: basic 10 | test: basic-check-spec-test 11 | -------------------------------------------------------------------------------- /config/manager/controller_manager_config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: controller-runtime.sigs.k8s.io/v1alpha1 2 | kind: ControllerManagerConfig 3 | health: 4 | healthProbeBindAddress: :8081 5 | metrics: 6 | bindAddress: 127.0.0.1:8080 7 | webhook: 8 | port: 9443 9 | leaderElection: 10 | leaderElect: true 11 | resourceName: 03846a46.redhat.io 12 | -------------------------------------------------------------------------------- /config/prometheus/role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: Role 3 | metadata: 4 | name: prometheus-k8s 5 | namespace: system 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - endpoints 11 | - pods 12 | - services 13 | verbs: 14 | - get 15 | - list 16 | - watch 17 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: manager-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: manager-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: proxy-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: proxy-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: proxy-role 5 | rules: 6 | - apiGroups: ["authentication.k8s.io"] 7 | resources: 8 | - tokenreviews 9 | verbs: ["create"] 10 | - apiGroups: ["authorization.k8s.io"] 11 | resources: 12 | - subjectaccessreviews 13 | verbs: ["create"] 14 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_keepalivedgroups.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 7 | name: keepalivedgroups.redhatcop.redhat.io 8 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: leader-election-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: Role 8 | name: leader-election-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/helmchart/Chart.yaml.tpl: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | name: keepalived-operator 3 | version: ${version} 4 | appVersion: ${version} 5 | description: Helm chart that deploys keepalived-operator 6 | keywords: 7 | - volume 8 | - storage 9 | - csi 10 | - expansion 11 | - monitoring 12 | sources: 13 | - https://github.com/redhat-cop/keepalived-operator 14 | engine: gotpl -------------------------------------------------------------------------------- /config/prometheus/rolebinding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: prometheus-k8s 5 | namespace: system 6 | roleRef: 7 | apiGroup: rbac.authorization.k8s.io 8 | kind: Role 9 | name: $(ROLE_NAME) 10 | subjects: 11 | - kind: ServiceAccount 12 | name: prometheus-k8s 13 | namespace: openshift-monitoring 14 | -------------------------------------------------------------------------------- /config/samples/redhatcop_v1alpha1_keepalivedgroup.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: redhatcop.redhat.io/v1alpha1 2 | kind: KeepalivedGroup 3 | metadata: 4 | name: keepalivedgroup-workers 5 | spec: 6 | image: registry.redhat.io/openshift4/ose-keepalived-ipfailover 7 | interface: ens3 8 | nodeSelector: 9 | node-role.kubernetes.io/loadbalancer: "" 10 | blacklistRouterIDs: 11 | - 1 12 | - 2 13 | -------------------------------------------------------------------------------- /integration/helm/prometheus-rbac/templates/clusterrolebinding-prometheus.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: prometheus 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: prometheus 9 | subjects: 10 | - kind: ServiceAccount 11 | name: kube-prometheus-stack-prometheus 12 | namespace: {{ .Release.Namespace }} 13 | -------------------------------------------------------------------------------- /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manager.yaml 3 | 4 | generatorOptions: 5 | disableNameSuffixHash: true 6 | 7 | configMapGenerator: 8 | - files: 9 | - controller_manager_config.yaml 10 | name: manager-config 11 | apiVersion: kustomize.config.k8s.io/v1beta1 12 | kind: Kustomization 13 | images: 14 | - name: controller 15 | newName: quay.io/raffaelespazzoli/keepalived-operator 16 | newTag: latest 17 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | operator: keepalived-operator 6 | annotations: 7 | service.alpha.openshift.io/serving-cert-secret-name: keepalived-operator-certs 8 | name: controller-manager-metrics 9 | namespace: system 10 | spec: 11 | ports: 12 | - name: https 13 | port: 8443 14 | targetPort: https 15 | selector: 16 | operator: keepalived-operator 17 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - role.yaml 3 | - role_binding.yaml 4 | - leader_election_role.yaml 5 | - leader_election_role_binding.yaml 6 | # Comment the following 4 lines if you want to disable 7 | # the auth proxy (https://github.com/brancz/kube-rbac-proxy) 8 | # which protects your /metrics endpoint. 9 | - auth_proxy_service.yaml 10 | - auth_proxy_role.yaml 11 | - auth_proxy_role_binding.yaml 12 | - auth_proxy_client_clusterrole.yaml 13 | -------------------------------------------------------------------------------- /integration/cluster-kind.yaml: -------------------------------------------------------------------------------- 1 | kind: Cluster 2 | apiVersion: kind.x-k8s.io/v1alpha4 3 | nodes: 4 | - role: control-plane 5 | kubeadmConfigPatches: 6 | - | 7 | kind: InitConfiguration 8 | nodeRegistration: 9 | kubeletExtraArgs: 10 | node-labels: "ingress-ready=true" 11 | extraPortMappings: 12 | - containerPort: 80 13 | hostPort: 8080 14 | protocol: TCP 15 | - containerPort: 443 16 | hostPort: 8443 17 | protocol: TCP -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_keepalivedgroups.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables a conversion webhook for the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: keepalivedgroups.redhatcop.redhat.io 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | -------------------------------------------------------------------------------- /config/helmchart/.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 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /config/operatorhub/operator.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: operators.coreos.com/v1alpha1 2 | kind: Subscription 3 | metadata: 4 | name: keepalived-operator 5 | spec: 6 | channel: alpha 7 | installPlanApproval: Automatic 8 | name: keepalived-operator 9 | source: community-operators 10 | sourceNamespace: openshift-marketplace 11 | --- 12 | apiVersion: operators.coreos.com/v1 13 | kind: OperatorGroup 14 | metadata: 15 | name: keepalived-operator 16 | spec: 17 | targetNamespaces: [] -------------------------------------------------------------------------------- /config/scorecard/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - bases/config.yaml 3 | patchesJson6902: 4 | - path: patches/basic.config.yaml 5 | target: 6 | group: scorecard.operatorframework.io 7 | version: v1alpha3 8 | kind: Configuration 9 | name: config 10 | - path: patches/olm.config.yaml 11 | target: 12 | group: scorecard.operatorframework.io 13 | version: v1alpha3 14 | kind: Configuration 15 | name: config 16 | # +kubebuilder:scaffold:patchesJson6902 17 | -------------------------------------------------------------------------------- /integration/helm/ingress-nginx/.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 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /config/certmanager/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This configuration is for teaching kustomize how to update name ref and var substitution 2 | nameReference: 3 | - kind: Issuer 4 | group: cert-manager.io 5 | fieldSpecs: 6 | - kind: Certificate 7 | group: cert-manager.io 8 | path: spec/issuerRef/name 9 | 10 | varReference: 11 | - kind: Certificate 12 | group: cert-manager.io 13 | path: spec/commonName 14 | - kind: Certificate 15 | group: cert-manager.io 16 | path: spec/dnsNames 17 | -------------------------------------------------------------------------------- /config/rbac/keepalivedgroup_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view keepalivedgroups. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: keepalivedgroup-viewer-role 6 | rules: 7 | - apiGroups: 8 | - redhatcop.redhat.io 9 | resources: 10 | - keepalivedgroups 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - redhatcop.redhat.io 17 | resources: 18 | - keepalivedgroups/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /integration/helm/prometheus-rbac/.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 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /.github/workflows/pr.yaml: -------------------------------------------------------------------------------- 1 | name: pull request 2 | on: 3 | pull_request: 4 | branches: 5 | - master 6 | - main 7 | 8 | jobs: 9 | shared-operator-workflow: 10 | name: shared-operator-workflow 11 | uses: redhat-cop/github-workflows-operators/.github/workflows/pr-operator.yml@111e0405debdca28ead7616868b14bdde2c79d57 # v1.0.6 12 | with: 13 | RUN_UNIT_TESTS: true 14 | RUN_INTEGRATION_TESTS: true 15 | RUN_HELMCHART_TEST: true 16 | GO_VERSION: "1.19" 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | bin 9 | testbin/* 10 | 11 | # Test binary, build with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Kubernetes Generated files - skip generated files, except for vendored files 18 | 19 | !vendor/**/zz_generated.* 20 | 21 | # editor and IDE paraphernalia 22 | .idea 23 | *.swp 24 | *.swo 25 | *~ 26 | 27 | bundle/ 28 | bundle.Dockerfile 29 | charts/ -------------------------------------------------------------------------------- /config/local-development/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Adds namespace to all resources. 2 | namespace: keepalived-operator-local 3 | 4 | # Value of this field is prepended to the 5 | # names of all resources, e.g. a deployment named 6 | # "wordpress" becomes "alices-wordpress". 7 | # Note that it should also match with the prefix (text before '-') of the namespace 8 | # field above. 9 | namePrefix: keepalived-operator- 10 | 11 | # Labels to add to all resources and selectors. 12 | #commonLabels: 13 | # someName: someValue 14 | 15 | bases: 16 | - ../rbac -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | domain: redhat.io 2 | layout: 3 | - go.kubebuilder.io/v3 4 | projectName: keepalived-operator 5 | repo: github.com/redhat-cop/keepalived-operator 6 | version: "3" 7 | resources: 8 | - api: 9 | crdVersion: v1 10 | namespaced: true 11 | group: redhat-cop 12 | domain: redhat.io 13 | controller: true 14 | kind: KeepalivedGroup 15 | version: v1alpha1 16 | path: github.com/redhat-cop/keepalived-operator/api/v1alpha1 17 | plugins: 18 | manifests.sdk.operatorframework.io/v2: {} 19 | scorecard.sdk.operatorframework.io/v2: {} 20 | -------------------------------------------------------------------------------- /config/rbac/keepalivedgroup_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit keepalivedgroups. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: keepalivedgroup-editor-role 6 | rules: 7 | - apiGroups: 8 | - redhatcop.redhat.io 9 | resources: 10 | - keepalivedgroups 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - redhatcop.redhat.io 21 | resources: 22 | - keepalivedgroups/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /config/default/manager_config_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: manager 11 | args: 12 | - "--config=controller_manager_config.yaml" 13 | volumeMounts: 14 | - name: manager-config 15 | mountPath: /controller_manager_config.yaml 16 | subPath: controller_manager_config.yaml 17 | volumes: 18 | - name: manager-config 19 | configMap: 20 | name: manager-config 21 | -------------------------------------------------------------------------------- /integration/helm/prometheus-rbac/templates/clusterrole-prometheus.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: prometheus 5 | rules: 6 | - apiGroups: [""] 7 | resources: 8 | - nodes 9 | - nodes/metrics 10 | - services 11 | - endpoints 12 | - pods 13 | verbs: ["get", "list", "watch"] 14 | - apiGroups: [""] 15 | resources: 16 | - configmaps 17 | verbs: ["get"] 18 | - apiGroups: 19 | - networking.k8s.io 20 | resources: 21 | - ingresses 22 | verbs: ["get", "list", "watch"] 23 | - nonResourceURLs: ["/metrics"] 24 | verbs: ["get"] 25 | -------------------------------------------------------------------------------- /config/crd/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This file is for teaching kustomize how to substitute name and namespace reference in CRD 2 | nameReference: 3 | - kind: Service 4 | version: v1 5 | fieldSpecs: 6 | - kind: CustomResourceDefinition 7 | version: v1 8 | group: apiextensions.k8s.io 9 | path: spec/conversion/webhook/clientConfig/service/name 10 | 11 | namespace: 12 | - kind: CustomResourceDefinition 13 | version: v1 14 | group: apiextensions.k8s.io 15 | path: spec/conversion/webhook/clientConfig/service/namespace 16 | create: false 17 | 18 | varReference: 19 | - path: metadata/annotations 20 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ -------------------------------------------------------------------------------- /config/helmchart/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Adds namespace to all resources. 2 | namespace: release-namespace 3 | 4 | bases: 5 | - ../local-development/tilt 6 | 7 | patchesJson6902: 8 | - target: 9 | group: admissionregistration.k8s.io 10 | version: v1 11 | kind: MutatingWebhookConfiguration 12 | name: keepalived-operator-mutating-webhook-configuration 13 | path: ./cert-manager-ca-injection.yaml 14 | - target: 15 | group: admissionregistration.k8s.io 16 | version: v1 17 | kind: ValidatingWebhookConfiguration 18 | name: keepalived-operator-validating-webhook-configuration 19 | path: ./cert-manager-ca-injection.yaml -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: leader-election-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - configmaps 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - create 16 | - update 17 | - patch 18 | - delete 19 | - apiGroups: 20 | - coordination.k8s.io 21 | resources: 22 | - leases 23 | verbs: 24 | - get 25 | - list 26 | - watch 27 | - create 28 | - update 29 | - patch 30 | - delete 31 | - apiGroups: 32 | - "" 33 | resources: 34 | - events 35 | verbs: 36 | - create 37 | - patch 38 | -------------------------------------------------------------------------------- /ci.Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM golang:1.18 as builder 3 | 4 | WORKDIR /workspace 5 | 6 | RUN go install github.com/gen2brain/keepalived_exporter@0.5.0 && \ 7 | cp ${GOPATH}/bin/keepalived_exporter ./ 8 | RUN go install github.com/rjeczalik/cmd/notify@1.0.3 && \ 9 | cp ${GOPATH}/bin/notify ./ 10 | 11 | FROM registry.access.redhat.com/ubi8/ubi 12 | WORKDIR / 13 | COPY --from=builder /workspace/notify /usr/local/bin 14 | COPY --from=builder /workspace/keepalived_exporter /usr/local/bin 15 | COPY bin/manager . 16 | COPY config/templates /templates 17 | COPY config/docker /usr/local/bin 18 | RUN yum -y install --disableplugin=subscription-manager kmod iproute && yum clean all 19 | USER 65532:65532 20 | 21 | ENTRYPOINT ["/manager"] 22 | -------------------------------------------------------------------------------- /.github/workflows/push.yaml: -------------------------------------------------------------------------------- 1 | name: push 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - master 7 | tags: 8 | - v* 9 | 10 | jobs: 11 | shared-operator-workflow: 12 | name: shared-operator-workflow 13 | uses: redhat-cop/github-workflows-operators/.github/workflows/release-operator.yml@111e0405debdca28ead7616868b14bdde2c79d57 # v1.0.6 14 | secrets: 15 | COMMUNITY_OPERATOR_PAT: ${{ secrets.COMMUNITY_OPERATOR_PAT }} 16 | REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} 17 | REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} 18 | with: 19 | PR_ACTOR: "raffaele.spazzoli@gmail.com" 20 | RUN_UNIT_TESTS: true 21 | RUN_INTEGRATION_TESTS: true 22 | RUN_HELMCHART_TEST: true 23 | GO_VERSION: "1.19" -------------------------------------------------------------------------------- /config/prometheus/monitor.yaml: -------------------------------------------------------------------------------- 1 | 2 | # Prometheus Monitor Service (Metrics) 3 | apiVersion: monitoring.coreos.com/v1 4 | kind: ServiceMonitor 5 | metadata: 6 | labels: 7 | operator: keepalived-operator 8 | name: controller-manager-metrics-monitor 9 | namespace: system 10 | spec: 11 | endpoints: 12 | - bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token 13 | interval: 30s 14 | port: https 15 | scheme: https 16 | tlsConfig: 17 | ca: 18 | secret: 19 | key: 'tls.crt' 20 | name: keepalived-operator-certs 21 | optional: false 22 | serverName: $(METRICS_SERVICE_NAME).$(METRICS_SERVICE_NAMESPACE).svc 23 | selector: 24 | matchLabels: 25 | operator: keepalived-operator 26 | -------------------------------------------------------------------------------- /test/test-servicemultiple.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: svc1 6 | annotations: 7 | keepalived-operator.redhat-cop.io/keepalivedgroup: keepalived-operator/keepalivedgroup-router 8 | spec: 9 | ports: 10 | - name: https 11 | port: 443 12 | protocol: TCP 13 | targetPort: 6443 14 | - name: http 15 | port: 80 16 | protocol: TCP 17 | targetPort: 8080 18 | type: LoadBalancer 19 | --- 20 | apiVersion: v1 21 | kind: Service 22 | metadata: 23 | name: svc2 24 | annotations: 25 | keepalived-operator.redhat-cop.io/keepalivedgroup: keepalived-operator/keepalivedgroup-router 26 | spec: 27 | externalIPs: 28 | - 192.168.131.130 29 | type: ClusterIP 30 | ports: 31 | - name: https 32 | port: 443 33 | protocol: TCP 34 | targetPort: 6443 35 | - name: http 36 | port: 80 37 | protocol: TCP 38 | targetPort: 8080 -------------------------------------------------------------------------------- /test/test-service-g2.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: svc3 6 | annotations: 7 | keepalived-operator.redhat-cop.io/keepalivedgroup: test-keepalived-operator/keepalivedgroup-test2 8 | spec: 9 | ports: 10 | - name: https 11 | port: 443 12 | protocol: TCP 13 | targetPort: 6443 14 | - name: http 15 | port: 80 16 | protocol: TCP 17 | targetPort: 8080 18 | type: LoadBalancer 19 | --- 20 | apiVersion: v1 21 | kind: Service 22 | metadata: 23 | name: svc4 24 | annotations: 25 | keepalived-operator.redhat-cop.io/keepalivedgroup: test-keepalived-operator/keepalivedgroup-test2 26 | spec: 27 | externalIPs: 28 | - 192.168.131.130 29 | type: ClusterIP 30 | ports: 31 | - name: https 32 | port: 443 33 | protocol: TCP 34 | targetPort: 6443 35 | - name: http 36 | port: 80 37 | protocol: TCP 38 | targetPort: 8080 -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # This kustomization.yaml is not intended to be run by itself, 2 | # since it depends on service name and namespace that are out of this kustomize package. 3 | # It should be run by config/default 4 | resources: 5 | - bases/redhatcop.redhat.io_keepalivedgroups.yaml 6 | # +kubebuilder:scaffold:crdkustomizeresource 7 | 8 | patchesStrategicMerge: 9 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 10 | # patches here are for enabling the conversion webhook for each CRD 11 | #- patches/webhook_in_keepalivedgroups.yaml 12 | # +kubebuilder:scaffold:crdkustomizewebhookpatch 13 | 14 | # [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix. 15 | # patches here are for enabling the CA injection for each CRD 16 | #- patches/cainjection_in_keepalivedgroups.yaml 17 | # +kubebuilder:scaffold:crdkustomizecainjectionpatch 18 | 19 | # the following config is for teaching kustomize how to do kustomization for CRDs. 20 | configurations: 21 | - kustomizeconfig.yaml 22 | -------------------------------------------------------------------------------- /config/certmanager/certificate.yaml: -------------------------------------------------------------------------------- 1 | # The following manifests contain a self-signed issuer CR and a certificate CR. 2 | # More document can be found at https://docs.cert-manager.io 3 | # WARNING: Targets CertManager v1.0. Check https://cert-manager.io/docs/installation/upgrading/ for breaking changes. 4 | apiVersion: cert-manager.io/v1 5 | kind: Issuer 6 | metadata: 7 | name: selfsigned-issuer 8 | namespace: system 9 | spec: 10 | selfSigned: {} 11 | --- 12 | apiVersion: cert-manager.io/v1 13 | kind: Certificate 14 | metadata: 15 | name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml 16 | namespace: system 17 | spec: 18 | # $(SERVICE_NAME) and $(SERVICE_NAMESPACE) will be substituted by kustomize 19 | dnsNames: 20 | - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc 21 | - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc.cluster.local 22 | issuerRef: 23 | kind: Issuer 24 | name: selfsigned-issuer 25 | secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize 26 | -------------------------------------------------------------------------------- /config/helmchart/templates/certificate.yaml: -------------------------------------------------------------------------------- 1 | {{ if .Values.enableCertManager }} 2 | apiVersion: cert-manager.io/v1 3 | kind: Issuer 4 | metadata: 5 | name: selfsigned-issuer 6 | spec: 7 | selfSigned: {} 8 | --- 9 | apiVersion: cert-manager.io/v1 10 | kind: Certificate 11 | metadata: 12 | name: serving-cert 13 | spec: 14 | dnsNames: 15 | - keepalived-operator-webhook-service.{{ .Release.Namespace }}.svc 16 | - keepalived-operator-webhook-service.{{ .Release.Namespace }}.svc.cluster.local 17 | issuerRef: 18 | kind: Issuer 19 | name: selfsigned-issuer 20 | secretName: webhook-server-cert 21 | --- 22 | apiVersion: cert-manager.io/v1 23 | kind: Certificate 24 | metadata: 25 | name: metrics-serving-cert 26 | spec: 27 | dnsNames: 28 | - keepalived-operator-controller-manager-metrics.{{ .Release.Namespace }}.svc 29 | - keepalived-operator-controller-manager-metrics.{{ .Release.Namespace }}.svc.cluster.local 30 | issuerRef: 31 | kind: Issuer 32 | name: selfsigned-issuer 33 | secretName: keepalived-operator-certs 34 | {{ end }} 35 | -------------------------------------------------------------------------------- /config/local-development/tilt/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Adds namespace to all resources. 2 | namespace: keepalived-operator 3 | 4 | # Labels to add to all resources and selectors. 5 | #commonLabels: 6 | # someName: someValue 7 | 8 | bases: 9 | - ../../default 10 | - ./service-account.yaml 11 | 12 | 13 | patchesJson6902: 14 | - target: 15 | group: admissionregistration.k8s.io 16 | version: v1 17 | kind: MutatingWebhookConfiguration 18 | name: keepalived-operator-mutating-webhook-configuration 19 | path: ./ca-injection.yaml 20 | - target: 21 | group: admissionregistration.k8s.io 22 | version: v1 23 | kind: ValidatingWebhookConfiguration 24 | name: keepalived-operator-validating-webhook-configuration 25 | path: ./ca-injection.yaml 26 | - target: 27 | group: "" 28 | version: v1 29 | kind: Service 30 | name: keepalived-operator-webhook-service 31 | path: ./secret-injection.yaml 32 | - target: 33 | group: apps 34 | version: v1 35 | kind: Deployment 36 | name: keepalived-operator-controller-manager 37 | path: ./replace-image.yaml -------------------------------------------------------------------------------- /config/helmchart/values.yaml.tpl: -------------------------------------------------------------------------------- 1 | # Default values for helm-try. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | 7 | image: 8 | repository: ${image_repo} 9 | pullPolicy: IfNotPresent 10 | # Overrides the image tag whose default is the chart appVersion. 11 | tag: ${version} 12 | 13 | imagePullSecrets: [] 14 | nameOverride: "" 15 | fullnameOverride: "" 16 | env: 17 | - name: KEEPALIVED_OPERATOR_IMAGE_NAME 18 | value: quay.io/redhat-cop/keepalived-operator:latest 19 | - name: KEEPALIVEDGROUP_TEMPLATE_FILE_NAME 20 | value: /templates/keepalived-template.yaml 21 | keepalivedTemplateFromConfigMap: "" #i.e. "keepalived-template" of an existing ConfigMap 22 | 23 | podAnnotations: {} 24 | 25 | resources: 26 | requests: 27 | cpu: 100m 28 | memory: 20Mi 29 | 30 | nodeSelector: {} 31 | 32 | tolerations: [] 33 | 34 | affinity: {} 35 | 36 | kube_rbac_proxy: 37 | image: 38 | repository: quay.io/redhat-cop/kube-rbac-proxy 39 | pullPolicy: IfNotPresent 40 | tag: v0.11.0 41 | resources: 42 | requests: 43 | cpu: 100m 44 | memory: 20Mi 45 | 46 | enableMonitoring: true -------------------------------------------------------------------------------- /config/docker/notify.sh: -------------------------------------------------------------------------------- 1 | ## $file contains the source file to be watched 2 | ## $dst_file contains the destination file to be created from the source file 3 | ## $reachip contains the IP to use for interface autodiscovery, or is empty if this behavior is disabled 4 | ## $pid contains the file with the PID to be notified with SIGHUP 5 | ## $create_config_only is set to true to launch the script in one-shot mode (no notification loop) 6 | 7 | function set_up_configs { 8 | cp $file $dst_file 9 | 10 | if [ -n "$reachip" ]; then 11 | IFACE=$(ip route get $reachip | grep -Po '(?<=(dev )).*(?= src| proto)') 12 | sed -i "s/interface.*$/interface $IFACE/g" $dst_file 13 | echo "autodicovered local interface that can reach $reachip to be $IFACE" 14 | fi 15 | } 16 | 17 | set -o nounset 18 | set -o errexit 19 | 20 | if [ "$create_config_only" = "true" ]; then 21 | set_up_configs 22 | exit 0 23 | fi 24 | 25 | HASH="" 26 | 27 | while true; do 28 | NEW_HASH=$(md5sum $(readlink -f $file)) 29 | if [ "$HASH" != "$NEW_HASH" ]; then 30 | HASH="$NEW_HASH" 31 | echo "[$(date +%s)] Trigger refresh" 32 | set_up_configs 33 | kill -SIGHUP $(cat $pid); 34 | echo "sent kill signal SIGHUP to $(cat $pid) with outcome $?" 35 | fi 36 | sleep 5 37 | done -------------------------------------------------------------------------------- /integration/helm/ingress-nginx/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: ingress-nginx 3 | description: A Helm chart for Kubernetes 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 1.1.1 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: "1.1.1" 25 | -------------------------------------------------------------------------------- /integration/helm/prometheus-rbac/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: prometheus-rbac 3 | description: A Helm chart for Kubernetes 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.1.0 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: "1.16.0" 25 | -------------------------------------------------------------------------------- /config/default/manager_auth_proxy_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch inject a sidecar container which is a HTTP proxy for the 2 | # controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: controller-manager 7 | namespace: system 8 | spec: 9 | template: 10 | spec: 11 | containers: 12 | - name: kube-rbac-proxy 13 | image: quay.io/redhat-cop/kube-rbac-proxy:v0.11.0 14 | args: 15 | - "--secure-listen-address=0.0.0.0:8443" 16 | - "--upstream=http://127.0.0.1:8080/" 17 | - "--logtostderr=true" 18 | - "--v=10" 19 | - "--tls-cert-file=/etc/certs/tls/tls.crt" 20 | - "--tls-private-key-file=/etc/certs/tls/tls.key" 21 | volumeMounts: 22 | - mountPath: /etc/certs/tls 23 | name: tls-cert 24 | ports: 25 | - containerPort: 8443 26 | name: https 27 | - name: manager 28 | args: 29 | - "--health-probe-bind-address=:8081" 30 | - "--metrics-bind-address=127.0.0.1:8080" 31 | - "--leader-elect" 32 | volumes: 33 | - name: tls-cert 34 | secret: 35 | defaultMode: 420 36 | secretName: keepalived-operator-certs 37 | -------------------------------------------------------------------------------- /Tiltfile: -------------------------------------------------------------------------------- 1 | # -*- mode: Python -*- 2 | 3 | compile_cmd = 'CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/manager main.go' 4 | image = 'quay.io/' + os.environ['repo'] + '/keepalived-operator' 5 | 6 | # Go Build 7 | local_resource( 8 | 'keepalived-operator-compile', 9 | compile_cmd, 10 | deps=['./main.go','./api','./controllers'] 11 | ) 12 | 13 | # Container Build 14 | custom_build( 15 | image, 16 | 'podman build -t $EXPECTED_REF --ignorefile ci.Dockerfile.dockerignore -f ./ci.Dockerfile . && podman push $EXPECTED_REF $EXPECTED_REF', 17 | entrypoint=['/manager'], 18 | deps=['./bin'], 19 | live_update=[ 20 | sync('./bin/manager',"/manager"), 21 | ], 22 | skips_local_docker=True, 23 | ) 24 | 25 | # Manifest Generation 26 | local_resource( 27 | 'keepalived-operator-manifests', 28 | 'make manifests', 29 | deps=['./bin'] 30 | ) 31 | 32 | allow_k8s_contexts(k8s_context()) 33 | 34 | # Local Dev 35 | watch_settings(ignore="./config/local-development/tilt/*") 36 | local('envsubst < ./config/local-development/tilt/env-replace-image.yaml > ./config/local-development/tilt/replace-image.yaml', echo_off=True) 37 | 38 | k8s_yaml(kustomize('./config/local-development/tilt')) 39 | k8s_resource('keepalived-operator-controller-manager', 40 | resource_deps=['keepalived-operator-compile', 'keepalived-operator-manifests']) -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM golang:1.18 as builder 3 | 4 | WORKDIR /workspace 5 | # Copy the Go Modules manifests 6 | COPY go.mod go.mod 7 | COPY go.sum go.sum 8 | # cache deps before building and copying source so that we don't need to re-download as much 9 | # and so that source changes don't invalidate our downloaded layer 10 | RUN go mod download 11 | 12 | # Copy the go source 13 | COPY main.go main.go 14 | COPY api/ api/ 15 | COPY controllers/ controllers/ 16 | 17 | # Build 18 | RUN CGO_ENABLED=0 GOOS=linux go build -a -o manager main.go 19 | RUN go install github.com/gen2brain/keepalived_exporter@0.5.0 && \ 20 | cp ${GOPATH}/bin/keepalived_exporter ./ 21 | RUN go install github.com/rjeczalik/cmd/notify@1.0.3 && \ 22 | cp ${GOPATH}/bin/notify ./ 23 | 24 | # Use distroless as minimal base image to package the manager binary 25 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 26 | FROM registry.access.redhat.com/ubi8/ubi 27 | WORKDIR / 28 | COPY --from=builder /workspace/manager . 29 | COPY --from=builder /workspace/notify /usr/local/bin 30 | COPY --from=builder /workspace/keepalived_exporter /usr/local/bin 31 | COPY config/templates /templates 32 | COPY config/docker /usr/local/bin 33 | RUN yum -y install --disableplugin=subscription-manager kmod iproute && yum clean all 34 | USER 65532:65532 35 | 36 | ENTRYPOINT ["/manager"] 37 | -------------------------------------------------------------------------------- /api/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1alpha1 contains API Schema definitions for the redhatcop v1alpha1 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=redhatcop.redhat.io 20 | package v1alpha1 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // GroupVersion is group version used to register these objects 29 | GroupVersion = schema.GroupVersion{Group: "redhatcop.redhat.io", Version: "v1alpha1"} 30 | 31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 33 | 34 | // AddToScheme adds the types in this group-version to the given scheme. 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | ) 37 | -------------------------------------------------------------------------------- /config/scorecard/patches/olm.config.yaml: -------------------------------------------------------------------------------- 1 | - op: add 2 | path: /stages/0/tests/- 3 | value: 4 | entrypoint: 5 | - scorecard-test 6 | - olm-bundle-validation 7 | image: quay.io/operator-framework/scorecard-test:v1.5.0 8 | labels: 9 | suite: olm 10 | test: olm-bundle-validation-test 11 | - op: add 12 | path: /stages/0/tests/- 13 | value: 14 | entrypoint: 15 | - scorecard-test 16 | - olm-crds-have-validation 17 | image: quay.io/operator-framework/scorecard-test:v1.5.0 18 | labels: 19 | suite: olm 20 | test: olm-crds-have-validation-test 21 | - op: add 22 | path: /stages/0/tests/- 23 | value: 24 | entrypoint: 25 | - scorecard-test 26 | - olm-crds-have-resources 27 | image: quay.io/operator-framework/scorecard-test:v1.5.0 28 | labels: 29 | suite: olm 30 | test: olm-crds-have-resources-test 31 | - op: add 32 | path: /stages/0/tests/- 33 | value: 34 | entrypoint: 35 | - scorecard-test 36 | - olm-spec-descriptors 37 | image: quay.io/operator-framework/scorecard-test:v1.5.0 38 | labels: 39 | suite: olm 40 | test: olm-spec-descriptors-test 41 | - op: add 42 | path: /stages/0/tests/- 43 | value: 44 | entrypoint: 45 | - scorecard-test 46 | - olm-status-descriptors 47 | image: quay.io/operator-framework/scorecard-test:v1.5.0 48 | labels: 49 | suite: olm 50 | test: olm-status-descriptors-test 51 | -------------------------------------------------------------------------------- /config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | openshift.io/cluster-monitoring: "true" 7 | name: system 8 | --- 9 | apiVersion: apps/v1 10 | kind: Deployment 11 | metadata: 12 | name: controller-manager 13 | namespace: system 14 | labels: 15 | operator: keepalived-operator 16 | spec: 17 | selector: 18 | matchLabels: 19 | operator: keepalived-operator 20 | replicas: 1 21 | template: 22 | metadata: 23 | labels: 24 | operator: keepalived-operator 25 | spec: 26 | serviceAccountName: controller-manager 27 | containers: 28 | - command: 29 | - /manager 30 | args: 31 | - --leader-elect 32 | image: controller:latest 33 | name: manager 34 | env: 35 | - name: KEEPALIVED_OPERATOR_IMAGE_NAME 36 | value: quay.io/redhat-cop/keepalived-operator:latest 37 | - name: KEEPALIVEDGROUP_TEMPLATE_FILE_NAME 38 | value: /templates/keepalived-template.yaml 39 | securityContext: 40 | allowPrivilegeEscalation: false 41 | livenessProbe: 42 | httpGet: 43 | path: /healthz 44 | port: 8081 45 | initialDelaySeconds: 15 46 | periodSeconds: 20 47 | readinessProbe: 48 | httpGet: 49 | path: /readyz 50 | port: 8081 51 | initialDelaySeconds: 5 52 | periodSeconds: 10 53 | resources: 54 | requests: 55 | cpu: 100m 56 | memory: 20Mi 57 | terminationGracePeriodSeconds: 10 58 | -------------------------------------------------------------------------------- /config/helmchart/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "keepalived-operator.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 "keepalived-operator.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 "keepalived-operator.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 32 | {{- end }} 33 | 34 | {{/* 35 | Common labels 36 | */}} 37 | {{- define "keepalived-operator.labels" -}} 38 | helm.sh/chart: {{ include "keepalived-operator.chart" . }} 39 | {{ include "keepalived-operator.selectorLabels" . }} 40 | {{- if .Chart.AppVersion }} 41 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 42 | {{- end }} 43 | app.kubernetes.io/managed-by: {{ .Release.Service }} 44 | {{- end }} 45 | 46 | {{/* 47 | Selector labels 48 | */}} 49 | {{- define "keepalived-operator.selectorLabels" -}} 50 | app.kubernetes.io/name: {{ include "keepalived-operator.name" . }} 51 | app.kubernetes.io/instance: {{ .Release.Name }} 52 | {{- end }} 53 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | creationTimestamp: null 6 | name: manager-role 7 | rules: 8 | - apiGroups: 9 | - "" 10 | resources: 11 | - configmaps 12 | verbs: 13 | - create 14 | - delete 15 | - get 16 | - list 17 | - patch 18 | - update 19 | - watch 20 | - apiGroups: 21 | - "" 22 | resources: 23 | - configmaps/finalizers 24 | verbs: 25 | - update 26 | - apiGroups: 27 | - "" 28 | resources: 29 | - endpoints 30 | - pods 31 | - secrets 32 | - services 33 | verbs: 34 | - get 35 | - list 36 | - watch 37 | - apiGroups: 38 | - "" 39 | resources: 40 | - events 41 | verbs: 42 | - create 43 | - get 44 | - list 45 | - patch 46 | - watch 47 | - apiGroups: 48 | - apps 49 | resources: 50 | - daemonsets 51 | - daemonsets/finalizers 52 | verbs: 53 | - create 54 | - delete 55 | - get 56 | - list 57 | - patch 58 | - update 59 | - watch 60 | - apiGroups: 61 | - monitoring.coreos.com 62 | resources: 63 | - podmonitors 64 | verbs: 65 | - create 66 | - delete 67 | - get 68 | - list 69 | - patch 70 | - update 71 | - watch 72 | - apiGroups: 73 | - monitoring.coreos.com 74 | resources: 75 | - podmonitors/finalizers 76 | verbs: 77 | - update 78 | - apiGroups: 79 | - rbac.authorization.k8s.io 80 | resources: 81 | - rolebindings 82 | - roles 83 | verbs: 84 | - create 85 | - delete 86 | - get 87 | - list 88 | - patch 89 | - update 90 | - watch 91 | - apiGroups: 92 | - redhatcop.redhat.io 93 | resources: 94 | - keepalivedgroups 95 | verbs: 96 | - create 97 | - delete 98 | - get 99 | - list 100 | - patch 101 | - update 102 | - watch 103 | - apiGroups: 104 | - redhatcop.redhat.io 105 | resources: 106 | - keepalivedgroups/finalizers 107 | verbs: 108 | - update 109 | - apiGroups: 110 | - redhatcop.redhat.io 111 | resources: 112 | - keepalivedgroups/status 113 | verbs: 114 | - get 115 | - patch 116 | - update 117 | -------------------------------------------------------------------------------- /integration/helm/ingress-nginx/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "ingress-nginx.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "ingress-nginx.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "ingress-nginx.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "ingress-nginx.labels" -}} 37 | helm.sh/chart: {{ include "ingress-nginx.chart" . }} 38 | {{ include "ingress-nginx.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "ingress-nginx.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "ingress-nginx.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "ingress-nginx.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "ingress-nginx.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /integration/helm/prometheus-rbac/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "prometheus-rbac.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "prometheus-rbac.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "prometheus-rbac.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "prometheus-rbac.labels" -}} 37 | helm.sh/chart: {{ include "prometheus-rbac.chart" . }} 38 | {{ include "prometheus-rbac.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "prometheus-rbac.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "prometheus-rbac.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "prometheus-rbac.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "prometheus-rbac.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /controllers/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers 18 | 19 | import ( 20 | "path/filepath" 21 | "testing" 22 | 23 | . "github.com/onsi/ginkgo" 24 | . "github.com/onsi/gomega" 25 | "k8s.io/client-go/kubernetes/scheme" 26 | "k8s.io/client-go/rest" 27 | "sigs.k8s.io/controller-runtime/pkg/client" 28 | "sigs.k8s.io/controller-runtime/pkg/envtest" 29 | "sigs.k8s.io/controller-runtime/pkg/envtest/printer" 30 | logf "sigs.k8s.io/controller-runtime/pkg/log" 31 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 32 | 33 | redhatcopv1alpha1 "github.com/redhat-cop/keepalived-operator/api/v1alpha1" 34 | // +kubebuilder:scaffold:imports 35 | ) 36 | 37 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 38 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 39 | 40 | var cfg *rest.Config 41 | var k8sClient client.Client 42 | var testEnv *envtest.Environment 43 | 44 | func TestAPIs(t *testing.T) { 45 | RegisterFailHandler(Fail) 46 | 47 | RunSpecsWithDefaultAndCustomReporters(t, 48 | "Controller Suite", 49 | []Reporter{printer.NewlineReporter{}}) 50 | } 51 | 52 | var _ = BeforeSuite(func() { 53 | logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) 54 | 55 | By("bootstrapping test environment") 56 | testEnv = &envtest.Environment{ 57 | CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, 58 | } 59 | 60 | cfg, err := testEnv.Start() 61 | Expect(err).NotTo(HaveOccurred()) 62 | Expect(cfg).NotTo(BeNil()) 63 | 64 | err = redhatcopv1alpha1.AddToScheme(scheme.Scheme) 65 | Expect(err).NotTo(HaveOccurred()) 66 | 67 | // +kubebuilder:scaffold:scheme 68 | 69 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) 70 | Expect(err).NotTo(HaveOccurred()) 71 | Expect(k8sClient).NotTo(BeNil()) 72 | 73 | }, 60) 74 | 75 | var _ = AfterSuite(func() { 76 | By("tearing down the test environment") 77 | err := testEnv.Stop() 78 | Expect(err).NotTo(HaveOccurred()) 79 | }) 80 | -------------------------------------------------------------------------------- /integration/kube-prometheus-stack-values.yaml: -------------------------------------------------------------------------------- 1 | # Taken from https://github.com/prometheus-community/helm-charts/blob/main/charts/kube-prometheus-stack/values.yaml 2 | 3 | ## Configuration for alertmanager 4 | ## ref: https://prometheus.io/docs/alerting/alertmanager/ 5 | ## 6 | alertmanager: 7 | 8 | ## Deploy alertmanager 9 | ## 10 | enabled: false 11 | 12 | ## Using default values from https://github.com/grafana/helm-charts/blob/main/charts/grafana/values.yaml 13 | ## 14 | grafana: 15 | enabled: false 16 | 17 | ## Component scraping kube state metrics 18 | ## 19 | kubeStateMetrics: 20 | enabled: false 21 | 22 | ## Deploy node exporter as a daemonset to all nodes 23 | ## 24 | nodeExporter: 25 | enabled: false 26 | 27 | ## Deploy a Prometheus instance 28 | ## 29 | prometheus: 30 | 31 | ## Settings affecting prometheusSpec 32 | ## ref: https://github.com/prometheus-operator/prometheus-operator/blob/main/Documentation/api.md#prometheusspec 33 | ## 34 | prometheusSpec: 35 | 36 | ## If true, a nil or {} value for prometheus.prometheusSpec.serviceMonitorSelector will cause the 37 | ## prometheus resource to be created with selectors based on values in the helm deployment, 38 | ## which will also match the servicemonitors created 39 | ## 40 | serviceMonitorSelectorNilUsesHelmValues: false 41 | 42 | ## Containers allows injecting additional containers. This is meant to allow adding an authentication proxy to a Prometheus pod. 43 | ## if using proxy extraContainer update targetPort with proxy container port 44 | 45 | ## The test-metrics sidecar will become ready when metrics are available. 46 | containers: 47 | - image: busybox:latest 48 | name: test-metrics 49 | command: 50 | - /bin/sh 51 | - -c 52 | - | 53 | while true; do 54 | out=$(wget -O - --post-data="query=controller_runtime_active_workers%7Bnamespace%3D%22keepalived-operator-local%22%7D%0A" localhost:9090/api/v1/query) 55 | if [[ -z "$(echo ${out})" || "$(echo $out | grep -F '{"status":"success","data":{"resultType":"vector","result":[]}}')" ]]; then 56 | echo "No Metrics yet..." 57 | echo "${out}" 58 | else 59 | echo "Metrics is working..." 60 | echo "${out}" > /tmp/ready 61 | cat /tmp/ready 62 | fi 63 | sleep 5s 64 | done 65 | readinessProbe: 66 | exec: 67 | command: 68 | - cat 69 | - /tmp/ready 70 | initialDelaySeconds: 5 71 | periodSeconds: 15 72 | failureThreshold: 30 73 | -------------------------------------------------------------------------------- /Ingress-how-to.md: -------------------------------------------------------------------------------- 1 | # Ingress How To 2 | 3 | This document explains how to configure the OpenShift ingress controller to take advantage of the keepalived-operator. 4 | 5 | ## Ingress configuration 6 | 7 | Assuming you have a properly installed keeapalived configuration, proceed as follows: 8 | 9 | Create an ingress controller: 10 | 11 | ```yaml 12 | apiVersion: operator.openshift.io/v1 13 | kind: IngressController 14 | metadata: 15 | name: my-keepalived-ingress 16 | namespace: openshift-ingress-operator 17 | spec: 18 | domain: myingress.mydomain 19 | replicas: 2 20 | endpointPublishingStrategy: 21 | type: Private 22 | ``` 23 | 24 | You can add any other field needed to your configuration, the important thing here is `endpointPublishingStrategy: Private`. 25 | This will create a set of pods in teh openshift-ingress namespace with prefix: `router-my-keepalived-ingress`. 26 | 27 | Create a load balancer service to server these pods: 28 | 29 | ```yaml 30 | kind: Service 31 | apiVersion: v1 32 | metadata: 33 | annotations: 34 | keepalived-operator.redhat-cop.io/keepalivedgroup: 35 | name: router-my-keepalived-ingress 36 | namespace: openshift-ingress 37 | spec: 38 | ports: 39 | - name: http 40 | protocol: TCP 41 | port: 80 42 | targetPort: http 43 | - name: https 44 | protocol: TCP 45 | port: 443 46 | targetPort: https 47 | selector: 48 | ingresscontroller.operator.openshift.io/deployment-ingresscontroller: my-keepalived-ingress 49 | type: LoadBalancer 50 | ``` 51 | 52 | At this point the keepalievd operator will provision a VIP and the routers are reachable there. 53 | 54 | If you need to control which IP the router needs to be serve on, use an external IPs: 55 | 56 | ```yaml 57 | kind: Service 58 | apiVersion: v1 59 | metadata: 60 | annotations: 61 | keepalived-operator.redhat-cop.io/keepalivedgroup: 62 | name: router-my-keepalived-ingress 63 | namespace: openshift-ingress 64 | spec: 65 | externalIPs: 66 | - 67 | ports: 68 | - name: http 69 | protocol: TCP 70 | port: 80 71 | targetPort: http 72 | - name: https 73 | protocol: TCP 74 | port: 443 75 | targetPort: https 76 | selector: 77 | ingresscontroller.operator.openshift.io/deployment-ingresscontroller: my-keepalived-ingress 78 | type: ClusterIP 79 | ``` 80 | 81 | At this point the only missing ingredient is to make sure that the DNS routes requests to `*.myingress.mydomain` are directed to the IP that was provisioned by the keepalived-operator or that was selected via the external IP. 82 | 83 | If you are using the [external-dns](https://github.com/kubernetes-sigs/external-dns) operator, you can easily automate this step by adding the following annotation to the service: `external-dns.alpha.kubernetes.io/hostname: *.myingress.mydomain`. 84 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Adds namespace to all resources. 2 | namespace: keepalived-operator 3 | 4 | # Value of this field is prepended to the 5 | # names of all resources, e.g. a deployment named 6 | # "wordpress" becomes "alices-wordpress". 7 | # Note that it should also match with the prefix (text before '-') of the namespace 8 | # field above. 9 | namePrefix: keepalived-operator- 10 | 11 | # Labels to add to all resources and selectors. 12 | #commonLabels: 13 | # someName: someValue 14 | 15 | bases: 16 | - ../crd 17 | - ../rbac 18 | - ../manager 19 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 20 | # crd/kustomization.yaml 21 | #- ../webhook 22 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. 23 | #- ../certmanager 24 | # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. 25 | - ../prometheus 26 | 27 | patchesStrategicMerge: 28 | # Protect the /metrics endpoint by putting it behind auth. 29 | # If you want your controller-manager to expose the /metrics 30 | # endpoint w/o any authn/z, please comment the following line. 31 | - manager_auth_proxy_patch.yaml 32 | 33 | # Mount the controller config file for loading manager configurations 34 | # through a ComponentConfig type 35 | #- manager_config_patch.yaml 36 | 37 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 38 | # crd/kustomization.yaml 39 | #- manager_webhook_patch.yaml 40 | 41 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 42 | # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. 43 | # 'CERTMANAGER' needs to be enabled to use ca injection 44 | #- webhookcainjection_patch.yaml 45 | 46 | # the following config is for teaching kustomize how to do var substitution 47 | vars: 48 | - name: METRICS_SERVICE_NAME 49 | objref: 50 | kind: Service 51 | version: v1 52 | name: controller-manager-metrics 53 | - name: METRICS_SERVICE_NAMESPACE 54 | objref: 55 | kind: Service 56 | version: v1 57 | name: controller-manager-metrics 58 | fieldref: 59 | fieldpath: metadata.namespace 60 | 61 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. 62 | #- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR 63 | # objref: 64 | # kind: Certificate 65 | # group: cert-manager.io 66 | # version: v1 67 | # name: serving-cert # this name should match the one in certificate.yaml 68 | # fieldref: 69 | # fieldpath: metadata.namespace 70 | #- name: CERTIFICATE_NAME 71 | # objref: 72 | # kind: Certificate 73 | # group: cert-manager.io 74 | # version: v1 75 | # name: serving-cert # this name should match the one in certificate.yaml 76 | #- name: SERVICE_NAMESPACE # namespace of the service 77 | # objref: 78 | # kind: Service 79 | # version: v1 80 | # name: webhook-service 81 | # fieldref: 82 | # fieldpath: metadata.namespace 83 | #- name: SERVICE_NAME 84 | # objref: 85 | # kind: Service 86 | # version: v1 87 | # name: webhook-service 88 | -------------------------------------------------------------------------------- /config/helmchart/templates/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "keepalived-operator.fullname" . }} 5 | labels: 6 | {{- include "keepalived-operator.labels" . | nindent 4 }} 7 | spec: 8 | selector: 9 | matchLabels: 10 | {{- include "keepalived-operator.selectorLabels" . | nindent 6 }} 11 | operator: keepalived-operator 12 | replicas: {{ .Values.replicaCount }} 13 | template: 14 | metadata: 15 | {{- with .Values.podAnnotations }} 16 | annotations: 17 | {{- toYaml . | nindent 8 }} 18 | {{- end }} 19 | labels: 20 | {{- include "keepalived-operator.selectorLabels" . | nindent 8 }} 21 | operator: keepalived-operator 22 | spec: 23 | serviceAccountName: controller-manager 24 | {{- with .Values.imagePullSecrets }} 25 | imagePullSecrets: 26 | {{- toYaml . | nindent 8 }} 27 | {{- end }} 28 | containers: 29 | - args: 30 | - --secure-listen-address=0.0.0.0:8443 31 | - --upstream=http://127.0.0.1:8080/ 32 | - --logtostderr=true 33 | - --tls-cert-file=/etc/certs/tls/tls.crt 34 | - --tls-private-key-file=/etc/certs/tls/tls.key 35 | - --v=0 36 | image: "{{ .Values.kube_rbac_proxy.image.repository }}:{{ .Values.kube_rbac_proxy.image.tag }}" 37 | name: kube-rbac-proxy 38 | ports: 39 | - containerPort: 8443 40 | name: https 41 | volumeMounts: 42 | - mountPath: /etc/certs/tls 43 | name: keepalived-operator-certs 44 | imagePullPolicy: {{ .Values.kube_rbac_proxy.image.pullPolicy }} 45 | {{- with .Values.env }} 46 | env: 47 | {{- toYaml . | nindent 8 }} 48 | {{- end }} 49 | resources: 50 | {{- toYaml .Values.kube_rbac_proxy.resources | nindent 10 }} 51 | - command: 52 | - /manager 53 | args: 54 | - --leader-elect 55 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 56 | imagePullPolicy: {{ .Values.image.pullPolicy }} 57 | {{- with .Values.env }} 58 | env: 59 | {{- toYaml . | nindent 8 }} 60 | {{- end }} 61 | {{- if .Values.keepalivedTemplateFromConfigMap }} 62 | - mountPath: /templates/ 63 | name: {{ .Values.keepalivedTemplateFromConfigMap }} 64 | {{- end }} 65 | name: {{ .Chart.Name }} 66 | resources: 67 | {{- toYaml .Values.resources | nindent 10 }} 68 | livenessProbe: 69 | httpGet: 70 | path: /healthz 71 | port: 8081 72 | initialDelaySeconds: 15 73 | periodSeconds: 20 74 | readinessProbe: 75 | httpGet: 76 | path: /readyz 77 | port: 8081 78 | initialDelaySeconds: 5 79 | periodSeconds: 10 80 | {{- with .Values.nodeSelector }} 81 | nodeSelector: 82 | {{- toYaml . | nindent 8 }} 83 | {{- end }} 84 | {{- with .Values.affinity }} 85 | affinity: 86 | {{- toYaml . | nindent 8 }} 87 | {{- end }} 88 | {{- with .Values.tolerations }} 89 | tolerations: 90 | {{- toYaml . | nindent 8 }} 91 | {{- end }} 92 | volumes: 93 | - name: keepalived-operator-certs 94 | secret: 95 | defaultMode: 420 96 | secretName: keepalived-operator-certs 97 | {{- if .Values.keepalivedTemplateFromConfigMap }} 98 | - configMap: 99 | defaultMode: 420 100 | name: {{ .Values.keepalivedTemplateFromConfigMap }} 101 | name: {{ .Values.keepalivedTemplateFromConfigMap }} 102 | {{- end }} 103 | 104 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "flag" 21 | "os" 22 | 23 | // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) 24 | // to ensure that exec-entrypoint and run can make use of them. 25 | _ "k8s.io/client-go/plugin/pkg/client/auth" 26 | 27 | "k8s.io/apimachinery/pkg/runtime" 28 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 29 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 30 | ctrl "sigs.k8s.io/controller-runtime" 31 | "sigs.k8s.io/controller-runtime/pkg/healthz" 32 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 33 | 34 | redhatcopv1alpha1 "github.com/redhat-cop/keepalived-operator/api/v1alpha1" 35 | "github.com/redhat-cop/keepalived-operator/controllers" 36 | "github.com/redhat-cop/operator-utils/pkg/util" 37 | // +kubebuilder:scaffold:imports 38 | ) 39 | 40 | var ( 41 | scheme = runtime.NewScheme() 42 | setupLog = ctrl.Log.WithName("setup") 43 | ) 44 | 45 | func init() { 46 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 47 | 48 | utilruntime.Must(redhatcopv1alpha1.AddToScheme(scheme)) 49 | // +kubebuilder:scaffold:scheme 50 | } 51 | 52 | func main() { 53 | var metricsAddr string 54 | var enableLeaderElection bool 55 | var probeAddr string 56 | flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") 57 | flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") 58 | flag.BoolVar(&enableLeaderElection, "leader-elect", false, 59 | "Enable leader election for controller manager. "+ 60 | "Enabling this will ensure there is only one active controller manager.") 61 | opts := zap.Options{ 62 | Development: true, 63 | } 64 | opts.BindFlags(flag.CommandLine) 65 | flag.Parse() 66 | 67 | ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) 68 | 69 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 70 | Scheme: scheme, 71 | MetricsBindAddress: metricsAddr, 72 | Port: 9443, 73 | HealthProbeBindAddress: probeAddr, 74 | LeaderElection: enableLeaderElection, 75 | LeaderElectionID: "03846a46.redhat.io", 76 | }) 77 | if err != nil { 78 | setupLog.Error(err, "unable to start manager") 79 | os.Exit(1) 80 | } 81 | 82 | keepalivedGroupReconciler := &controllers.KeepalivedGroupReconciler{ 83 | ReconcilerBase: util.NewReconcilerBase(mgr.GetClient(), mgr.GetScheme(), mgr.GetConfig(), mgr.GetEventRecorderFor("keepalived-controller"), mgr.GetAPIReader()), 84 | Log: ctrl.Log.WithName("controllers").WithName("KeepalivedGroup"), 85 | } 86 | 87 | if err = (keepalivedGroupReconciler).SetupWithManager(mgr); err != nil { 88 | setupLog.Error(err, "unable to create controller", "controller", "KeepalivedGroup") 89 | os.Exit(1) 90 | } 91 | // +kubebuilder:scaffold:builder 92 | 93 | if err := mgr.AddHealthzCheck("health", healthz.Ping); err != nil { 94 | setupLog.Error(err, "unable to set up health check") 95 | os.Exit(1) 96 | } 97 | if err := mgr.AddReadyzCheck("check", healthz.Ping); err != nil { 98 | setupLog.Error(err, "unable to set up ready check") 99 | os.Exit(1) 100 | } 101 | 102 | setupLog.Info("starting manager") 103 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 104 | setupLog.Error(err, "problem running manager") 105 | os.Exit(1) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /api/v1alpha1/keepalivedgroup_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | corev1 "k8s.io/api/core/v1" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | ) 23 | 24 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 25 | // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. 26 | 27 | // KeepalivedGroupSpec defines the desired state of KeepalivedGroup 28 | type KeepalivedGroupSpec struct { 29 | // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster 30 | // Important: Run "make" to regenerate code after modifying this file 31 | 32 | // +kubebuilder:validation:Optional 33 | // +mapType=granular 34 | NodeSelector map[string]string `json:"nodeSelector,omitempty"` 35 | 36 | // //+kubebuilder:validation:Optional 37 | // +kubebuilder:validation:Required 38 | // +kubebuilder:default:=registry.redhat.io/openshift4/ose-keepalived-ipfailover 39 | Image string `json:"image"` 40 | 41 | // +kubebuilder:validation:Required 42 | Interface string `json:"interface"` 43 | 44 | // +optional 45 | // +kubebuilder:validation:Format=ipv4 46 | InterfaceFromIP string `json:"interfaceFromIP"` 47 | 48 | // +optional 49 | PasswordAuth PasswordAuth `json:"passwordAuth,omitempty"` 50 | 51 | // +kubebuilder:validation:Optional 52 | // +mapType=granular 53 | VerbatimConfig map[string]string `json:"verbatimConfig,omitempty"` 54 | 55 | // +kubebuilder:validation:Optional 56 | // // +kubebuilder:validation:UniqueItems=true 57 | // +listType=set 58 | BlacklistRouterIDs []int `json:"blacklistRouterIDs,omitempty"` 59 | 60 | // +optional 61 | UnicastEnabled bool `json:"unicastEnabled,omitempty"` 62 | 63 | // +optional 64 | DaemonsetPodPriorityClassName string `json:"daemonsetPodPriorityClassName"` 65 | 66 | // +kubebuilder:validation:Optional 67 | // +mapType=granular 68 | DaemonsetPodAnnotations map[string]string `json:"daemonsetPodAnnotations,omitempty"` 69 | } 70 | 71 | // PasswordAuth references a Kubernetes secret to extract the password for VRRP authentication 72 | type PasswordAuth struct { 73 | // +required 74 | SecretRef corev1.LocalObjectReference `json:"secretRef"` 75 | 76 | // +optional 77 | // +kubebuilder:default:=password 78 | SecretKey string `json:"secretKey"` 79 | } 80 | 81 | // KeepalivedGroupStatus defines the observed state of KeepalivedGroup 82 | type KeepalivedGroupStatus struct { 83 | // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster 84 | // +patchMergeKey=type 85 | // +patchStrategy=merge 86 | // +listType=map 87 | // +listMapKey=type 88 | Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` 89 | 90 | // +mapType=granular 91 | RouterIDs map[string]int `json:"routerIDs,omitempty"` 92 | } 93 | 94 | func (m *KeepalivedGroup) GetConditions() []metav1.Condition { 95 | return m.Status.Conditions 96 | } 97 | 98 | func (m *KeepalivedGroup) SetConditions(conditions []metav1.Condition) { 99 | m.Status.Conditions = conditions 100 | } 101 | 102 | // +kubebuilder:object:root=true 103 | // +kubebuilder:subresource:status 104 | 105 | // KeepalivedGroup is the Schema for the keepalivedgroups API 106 | type KeepalivedGroup struct { 107 | metav1.TypeMeta `json:",inline"` 108 | metav1.ObjectMeta `json:"metadata,omitempty"` 109 | 110 | Spec KeepalivedGroupSpec `json:"spec,omitempty"` 111 | Status KeepalivedGroupStatus `json:"status,omitempty"` 112 | } 113 | 114 | // +kubebuilder:object:root=true 115 | 116 | // KeepalivedGroupList contains a list of KeepalivedGroup 117 | type KeepalivedGroupList struct { 118 | metav1.TypeMeta `json:",inline"` 119 | metav1.ListMeta `json:"metadata,omitempty"` 120 | Items []KeepalivedGroup `json:"items"` 121 | } 122 | 123 | func init() { 124 | SchemeBuilder.Register(&KeepalivedGroup{}, &KeepalivedGroupList{}) 125 | } 126 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redhat-cop/keepalived-operator 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/go-logr/logr v1.2.0 7 | github.com/onsi/ginkgo v1.16.5 8 | github.com/onsi/gomega v1.18.1 9 | github.com/redhat-cop/operator-utils v1.3.4 10 | github.com/scylladb/go-set v1.0.2 11 | k8s.io/api v0.24.2 12 | k8s.io/apimachinery v0.24.2 13 | k8s.io/client-go v0.24.2 14 | sigs.k8s.io/controller-runtime v0.12.2 15 | ) 16 | 17 | require ( 18 | cloud.google.com/go v0.81.0 // indirect 19 | github.com/Azure/go-autorest v14.2.0+incompatible // indirect 20 | github.com/Azure/go-autorest/autorest v0.11.18 // indirect 21 | github.com/Azure/go-autorest/autorest/adal v0.9.13 // indirect 22 | github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect 23 | github.com/Azure/go-autorest/logger v0.2.1 // indirect 24 | github.com/Azure/go-autorest/tracing v0.6.0 // indirect 25 | github.com/BurntSushi/toml v0.4.1 // indirect 26 | github.com/Masterminds/goutils v1.1.1 // indirect 27 | github.com/Masterminds/semver/v3 v3.1.1 // indirect 28 | github.com/Masterminds/sprig/v3 v3.2.2 // indirect 29 | github.com/PuerkitoBio/purell v1.1.1 // indirect 30 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect 31 | github.com/beorn7/perks v1.0.1 // indirect 32 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 33 | github.com/davecgh/go-spew v1.1.1 // indirect 34 | github.com/emicklei/go-restful v2.9.5+incompatible // indirect 35 | github.com/evanphx/json-patch v5.6.0+incompatible // indirect 36 | github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect 37 | github.com/fsnotify/fsnotify v1.5.1 // indirect 38 | github.com/go-logr/zapr v1.2.0 // indirect 39 | github.com/go-openapi/jsonpointer v0.19.5 // indirect 40 | github.com/go-openapi/jsonreference v0.19.5 // indirect 41 | github.com/go-openapi/swag v0.19.14 // indirect 42 | github.com/gogo/protobuf v1.3.2 // indirect 43 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 44 | github.com/golang/protobuf v1.5.2 // indirect 45 | github.com/google/gnostic v0.5.7-v3refs // indirect 46 | github.com/google/go-cmp v0.5.5 // indirect 47 | github.com/google/gofuzz v1.1.0 // indirect 48 | github.com/google/uuid v1.1.2 // indirect 49 | github.com/huandu/xstrings v1.3.1 // indirect 50 | github.com/imdario/mergo v0.3.12 // indirect 51 | github.com/josharian/intern v1.0.0 // indirect 52 | github.com/json-iterator/go v1.1.12 // indirect 53 | github.com/mailru/easyjson v0.7.6 // indirect 54 | github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect 55 | github.com/mitchellh/copystructure v1.0.0 // indirect 56 | github.com/mitchellh/reflectwalk v1.0.0 // indirect 57 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 58 | github.com/modern-go/reflect2 v1.0.2 // indirect 59 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 60 | github.com/nxadm/tail v1.4.8 // indirect 61 | github.com/pkg/errors v0.9.1 // indirect 62 | github.com/prometheus/client_golang v1.12.1 // indirect 63 | github.com/prometheus/client_model v0.2.0 // indirect 64 | github.com/prometheus/common v0.32.1 // indirect 65 | github.com/prometheus/procfs v0.7.3 // indirect 66 | github.com/shopspring/decimal v1.2.0 // indirect 67 | github.com/spf13/cast v1.3.1 // indirect 68 | github.com/spf13/pflag v1.0.5 // indirect 69 | go.uber.org/atomic v1.7.0 // indirect 70 | go.uber.org/multierr v1.6.0 // indirect 71 | go.uber.org/zap v1.19.1 // indirect 72 | golang.org/x/crypto v0.0.0-20220214200702-86341886e292 // indirect 73 | golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect 74 | golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect 75 | golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect 76 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect 77 | golang.org/x/text v0.3.7 // indirect 78 | golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect 79 | gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect 80 | google.golang.org/appengine v1.6.7 // indirect 81 | google.golang.org/protobuf v1.27.1 // indirect 82 | gopkg.in/inf.v0 v0.9.1 // indirect 83 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 84 | gopkg.in/yaml.v2 v2.4.0 // indirect 85 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 86 | k8s.io/apiextensions-apiserver v0.24.2 // indirect 87 | k8s.io/component-base v0.24.2 // indirect 88 | k8s.io/klog/v2 v2.60.1 // indirect 89 | k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42 // indirect 90 | k8s.io/kubectl v0.24.0 // indirect 91 | k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect 92 | sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect 93 | sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect 94 | sigs.k8s.io/yaml v1.3.0 // indirect 95 | ) 96 | -------------------------------------------------------------------------------- /api/v1alpha1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | // +build !ignore_autogenerated 3 | 4 | /* 5 | Copyright 2020. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | // Code generated by controller-gen. DO NOT EDIT. 21 | 22 | package v1alpha1 23 | 24 | import ( 25 | "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | runtime "k8s.io/apimachinery/pkg/runtime" 27 | ) 28 | 29 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 30 | func (in *KeepalivedGroup) DeepCopyInto(out *KeepalivedGroup) { 31 | *out = *in 32 | out.TypeMeta = in.TypeMeta 33 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 34 | in.Spec.DeepCopyInto(&out.Spec) 35 | in.Status.DeepCopyInto(&out.Status) 36 | } 37 | 38 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KeepalivedGroup. 39 | func (in *KeepalivedGroup) DeepCopy() *KeepalivedGroup { 40 | if in == nil { 41 | return nil 42 | } 43 | out := new(KeepalivedGroup) 44 | in.DeepCopyInto(out) 45 | return out 46 | } 47 | 48 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 49 | func (in *KeepalivedGroup) DeepCopyObject() runtime.Object { 50 | if c := in.DeepCopy(); c != nil { 51 | return c 52 | } 53 | return nil 54 | } 55 | 56 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 57 | func (in *KeepalivedGroupList) DeepCopyInto(out *KeepalivedGroupList) { 58 | *out = *in 59 | out.TypeMeta = in.TypeMeta 60 | in.ListMeta.DeepCopyInto(&out.ListMeta) 61 | if in.Items != nil { 62 | in, out := &in.Items, &out.Items 63 | *out = make([]KeepalivedGroup, len(*in)) 64 | for i := range *in { 65 | (*in)[i].DeepCopyInto(&(*out)[i]) 66 | } 67 | } 68 | } 69 | 70 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KeepalivedGroupList. 71 | func (in *KeepalivedGroupList) DeepCopy() *KeepalivedGroupList { 72 | if in == nil { 73 | return nil 74 | } 75 | out := new(KeepalivedGroupList) 76 | in.DeepCopyInto(out) 77 | return out 78 | } 79 | 80 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 81 | func (in *KeepalivedGroupList) DeepCopyObject() runtime.Object { 82 | if c := in.DeepCopy(); c != nil { 83 | return c 84 | } 85 | return nil 86 | } 87 | 88 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 89 | func (in *KeepalivedGroupSpec) DeepCopyInto(out *KeepalivedGroupSpec) { 90 | *out = *in 91 | if in.NodeSelector != nil { 92 | in, out := &in.NodeSelector, &out.NodeSelector 93 | *out = make(map[string]string, len(*in)) 94 | for key, val := range *in { 95 | (*out)[key] = val 96 | } 97 | } 98 | out.PasswordAuth = in.PasswordAuth 99 | if in.VerbatimConfig != nil { 100 | in, out := &in.VerbatimConfig, &out.VerbatimConfig 101 | *out = make(map[string]string, len(*in)) 102 | for key, val := range *in { 103 | (*out)[key] = val 104 | } 105 | } 106 | if in.BlacklistRouterIDs != nil { 107 | in, out := &in.BlacklistRouterIDs, &out.BlacklistRouterIDs 108 | *out = make([]int, len(*in)) 109 | copy(*out, *in) 110 | } 111 | if in.DaemonsetPodAnnotations != nil { 112 | in, out := &in.DaemonsetPodAnnotations, &out.DaemonsetPodAnnotations 113 | *out = make(map[string]string, len(*in)) 114 | for key, val := range *in { 115 | (*out)[key] = val 116 | } 117 | } 118 | } 119 | 120 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KeepalivedGroupSpec. 121 | func (in *KeepalivedGroupSpec) DeepCopy() *KeepalivedGroupSpec { 122 | if in == nil { 123 | return nil 124 | } 125 | out := new(KeepalivedGroupSpec) 126 | in.DeepCopyInto(out) 127 | return out 128 | } 129 | 130 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 131 | func (in *KeepalivedGroupStatus) DeepCopyInto(out *KeepalivedGroupStatus) { 132 | *out = *in 133 | if in.Conditions != nil { 134 | in, out := &in.Conditions, &out.Conditions 135 | *out = make([]v1.Condition, len(*in)) 136 | for i := range *in { 137 | (*in)[i].DeepCopyInto(&(*out)[i]) 138 | } 139 | } 140 | if in.RouterIDs != nil { 141 | in, out := &in.RouterIDs, &out.RouterIDs 142 | *out = make(map[string]int, len(*in)) 143 | for key, val := range *in { 144 | (*out)[key] = val 145 | } 146 | } 147 | } 148 | 149 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KeepalivedGroupStatus. 150 | func (in *KeepalivedGroupStatus) DeepCopy() *KeepalivedGroupStatus { 151 | if in == nil { 152 | return nil 153 | } 154 | out := new(KeepalivedGroupStatus) 155 | in.DeepCopyInto(out) 156 | return out 157 | } 158 | 159 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 160 | func (in *PasswordAuth) DeepCopyInto(out *PasswordAuth) { 161 | *out = *in 162 | out.SecretRef = in.SecretRef 163 | } 164 | 165 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PasswordAuth. 166 | func (in *PasswordAuth) DeepCopy() *PasswordAuth { 167 | if in == nil { 168 | return nil 169 | } 170 | out := new(PasswordAuth) 171 | in.DeepCopyInto(out) 172 | return out 173 | } 174 | -------------------------------------------------------------------------------- /config/crd/bases/redhatcop.redhat.io_keepalivedgroups.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.9.0 7 | creationTimestamp: null 8 | name: keepalivedgroups.redhatcop.redhat.io 9 | spec: 10 | group: redhatcop.redhat.io 11 | names: 12 | kind: KeepalivedGroup 13 | listKind: KeepalivedGroupList 14 | plural: keepalivedgroups 15 | singular: keepalivedgroup 16 | scope: Namespaced 17 | versions: 18 | - name: v1alpha1 19 | schema: 20 | openAPIV3Schema: 21 | description: KeepalivedGroup is the Schema for the keepalivedgroups API 22 | properties: 23 | apiVersion: 24 | description: 'APIVersion defines the versioned schema of this representation 25 | of an object. Servers should convert recognized schemas to the latest 26 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 27 | type: string 28 | kind: 29 | description: 'Kind is a string value representing the REST resource this 30 | object represents. Servers may infer this from the endpoint the client 31 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 32 | type: string 33 | metadata: 34 | type: object 35 | spec: 36 | description: KeepalivedGroupSpec defines the desired state of KeepalivedGroup 37 | properties: 38 | blacklistRouterIDs: 39 | description: // +kubebuilder:validation:UniqueItems=true 40 | items: 41 | type: integer 42 | type: array 43 | x-kubernetes-list-type: set 44 | daemonsetPodAnnotations: 45 | additionalProperties: 46 | type: string 47 | type: object 48 | x-kubernetes-map-type: granular 49 | daemonsetPodPriorityClassName: 50 | type: string 51 | image: 52 | default: registry.redhat.io/openshift4/ose-keepalived-ipfailover 53 | description: //+kubebuilder:validation:Optional 54 | type: string 55 | interface: 56 | type: string 57 | interfaceFromIP: 58 | format: ipv4 59 | type: string 60 | nodeSelector: 61 | additionalProperties: 62 | type: string 63 | type: object 64 | x-kubernetes-map-type: granular 65 | passwordAuth: 66 | description: PasswordAuth references a Kubernetes secret to extract 67 | the password for VRRP authentication 68 | properties: 69 | secretKey: 70 | default: password 71 | type: string 72 | secretRef: 73 | description: LocalObjectReference contains enough information 74 | to let you locate the referenced object inside the same namespace. 75 | properties: 76 | name: 77 | description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names 78 | TODO: Add other useful fields. apiVersion, kind, uid?' 79 | type: string 80 | type: object 81 | required: 82 | - secretRef 83 | type: object 84 | unicastEnabled: 85 | type: boolean 86 | verbatimConfig: 87 | additionalProperties: 88 | type: string 89 | type: object 90 | x-kubernetes-map-type: granular 91 | required: 92 | - image 93 | - interface 94 | type: object 95 | status: 96 | description: KeepalivedGroupStatus defines the observed state of KeepalivedGroup 97 | properties: 98 | conditions: 99 | description: INSERT ADDITIONAL STATUS FIELD - define observed state 100 | of cluster 101 | items: 102 | description: "Condition contains details for one aspect of the current 103 | state of this API Resource. --- This struct is intended for direct 104 | use as an array at the field path .status.conditions. For example, 105 | type FooStatus struct{ // Represents the observations of a foo's 106 | current state. // Known .status.conditions.type are: \"Available\", 107 | \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge 108 | // +listType=map // +listMapKey=type Conditions []metav1.Condition 109 | `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" 110 | protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" 111 | properties: 112 | lastTransitionTime: 113 | description: lastTransitionTime is the last time the condition 114 | transitioned from one status to another. This should be when 115 | the underlying condition changed. If that is not known, then 116 | using the time when the API field changed is acceptable. 117 | format: date-time 118 | type: string 119 | message: 120 | description: message is a human readable message indicating 121 | details about the transition. This may be an empty string. 122 | maxLength: 32768 123 | type: string 124 | observedGeneration: 125 | description: observedGeneration represents the .metadata.generation 126 | that the condition was set based upon. For instance, if .metadata.generation 127 | is currently 12, but the .status.conditions[x].observedGeneration 128 | is 9, the condition is out of date with respect to the current 129 | state of the instance. 130 | format: int64 131 | minimum: 0 132 | type: integer 133 | reason: 134 | description: reason contains a programmatic identifier indicating 135 | the reason for the condition's last transition. Producers 136 | of specific condition types may define expected values and 137 | meanings for this field, and whether the values are considered 138 | a guaranteed API. The value should be a CamelCase string. 139 | This field may not be empty. 140 | maxLength: 1024 141 | minLength: 1 142 | pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ 143 | type: string 144 | status: 145 | description: status of the condition, one of True, False, Unknown. 146 | enum: 147 | - "True" 148 | - "False" 149 | - Unknown 150 | type: string 151 | type: 152 | description: type of condition in CamelCase or in foo.example.com/CamelCase. 153 | --- Many .condition.type values are consistent across resources 154 | like Available, but because arbitrary conditions can be useful 155 | (see .node.status.conditions), the ability to deconflict is 156 | important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) 157 | maxLength: 316 158 | pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ 159 | type: string 160 | required: 161 | - lastTransitionTime 162 | - message 163 | - reason 164 | - status 165 | - type 166 | type: object 167 | type: array 168 | x-kubernetes-list-map-keys: 169 | - type 170 | x-kubernetes-list-type: map 171 | routerIDs: 172 | additionalProperties: 173 | type: integer 174 | type: object 175 | x-kubernetes-map-type: granular 176 | type: object 177 | type: object 178 | served: true 179 | storage: true 180 | subresources: 181 | status: {} 182 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /config/templates/keepalived-template.yaml: -------------------------------------------------------------------------------- 1 | # expected merge structure 2 | # .KeepAlivedGroup 3 | # .Services 4 | - apiVersion: apps/v1 5 | kind: DaemonSet 6 | metadata: 7 | name: {{ .KeepalivedGroup.ObjectMeta.Name }} 8 | namespace: {{ .KeepalivedGroup.ObjectMeta.Namespace }} 9 | spec: 10 | selector: 11 | matchLabels: 12 | keepalivedGroup: {{ .KeepalivedGroup.ObjectMeta.Name }} 13 | template: 14 | metadata: 15 | {{- with .KeepalivedGroup.Spec.DaemonsetPodAnnotations }} 16 | annotations: 17 | {{ range $index, $element := . }} 18 | {{ $index }}: {{ $element }} 19 | {{ end }} 20 | {{- end }} 21 | labels: 22 | keepalivedGroup: {{ .KeepalivedGroup.ObjectMeta.Name }} 23 | spec: 24 | {{- if .KeepalivedGroup.Spec.DaemonsetPodPriorityClassName }} 25 | priorityClassName: {{ .KeepalivedGroup.Spec.DaemonsetPodPriorityClassName }} 26 | {{- end }} 27 | tolerations: 28 | - operator: Exists 29 | nodeSelector: 30 | {{ range $index, $element := .KeepalivedGroup.Spec.NodeSelector }} 31 | {{ $index }}: {{ $element }} 32 | {{ end }} 33 | hostNetwork: true 34 | automountServiceAccountToken: false 35 | enableServiceLinks: false 36 | shareProcessNamespace: true 37 | initContainers: 38 | - name: config-setup 39 | image: {{ .Misc.image }} 40 | imagePullPolicy: Always 41 | command: 42 | - bash 43 | - -c 44 | - /usr/local/bin/notify.sh 45 | env: 46 | - name: file 47 | value: /etc/keepalived.d/src/keepalived.conf 48 | - name: dst_file 49 | value: /etc/keepalived.d/dst/keepalived.conf 50 | - name: reachip 51 | {{- if .KeepalivedGroup.Spec.InterfaceFromIP }} 52 | value: {{ .KeepalivedGroup.Spec.InterfaceFromIP }} 53 | {{- else }} 54 | value: "" 55 | {{- end }} 56 | - name: create_config_only 57 | value: "true" 58 | volumeMounts: 59 | - mountPath: /etc/keepalived.d/src 60 | name: config 61 | readOnly: true 62 | - mountPath: /etc/keepalived.d/dst 63 | name: config-dst 64 | securityContext: 65 | runAsUser: 0 66 | containers: 67 | - name: keepalived 68 | image: {{ .KeepalivedGroup.Spec.Image }} 69 | command: 70 | - /bin/bash 71 | args: 72 | - -c 73 | - > 74 | exec /usr/sbin/keepalived 75 | --log-console 76 | --log-detail 77 | --dont-fork 78 | --config-id=${POD_NAME} 79 | --use-file=/etc/keepalived.d/keepalived.conf 80 | --pid=/etc/keepalived.pid/keepalived.pid 81 | env: 82 | - name: POD_NAME 83 | valueFrom: 84 | fieldRef: 85 | fieldPath: metadata.name 86 | volumeMounts: 87 | - mountPath: /lib/modules 88 | name: lib-modules 89 | readOnly: true 90 | - mountPath: /etc/keepalived.d 91 | name: config-dst 92 | readOnly: true 93 | - mountPath: /etc/keepalived.pid 94 | name: pid 95 | - mountPath: /tmp 96 | name: stats 97 | securityContext: 98 | privileged: true 99 | - name: config-reloader 100 | image: {{ .Misc.image }} 101 | imagePullPolicy: Always 102 | command: 103 | - bash 104 | - -c 105 | - /usr/local/bin/notify.sh 106 | env: 107 | - name: pid 108 | value: /etc/keepalived.pid/keepalived.pid 109 | - name: file 110 | value: /etc/keepalived.d/src/keepalived.conf 111 | - name: dst_file 112 | value: /etc/keepalived.d/dst/keepalived.conf 113 | - name: reachip 114 | {{- if .KeepalivedGroup.Spec.InterfaceFromIP }} 115 | value: {{ .KeepalivedGroup.Spec.InterfaceFromIP }} 116 | {{- else }} 117 | value: "" 118 | {{- end }} 119 | - name: create_config_only 120 | value: "false" 121 | volumeMounts: 122 | - mountPath: /etc/keepalived.d/src 123 | name: config 124 | readOnly: true 125 | - mountPath: /etc/keepalived.d/dst 126 | name: config-dst 127 | - mountPath: /etc/keepalived.pid 128 | name: pid 129 | securityContext: 130 | runAsUser: 0 131 | - name: prometheus-exporter 132 | image: {{ .Misc.image }} 133 | imagePullPolicy: Always 134 | command: 135 | - /usr/local/bin/keepalived_exporter 136 | args: 137 | - '-web.listen-address' 138 | - ':9650' 139 | - '-web.telemetry-path' 140 | - '/metrics' 141 | securityContext: 142 | privileged: true 143 | ports: 144 | - name: metrics 145 | containerPort: 9650 146 | protocol: TCP 147 | volumeMounts: 148 | - mountPath: /lib/modules 149 | name: lib-modules 150 | readOnly: true 151 | - mountPath: /tmp 152 | name: stats 153 | volumes: 154 | - hostPath: 155 | path: /lib/modules 156 | name: lib-modules 157 | - name: config 158 | configMap: 159 | name: {{ .KeepalivedGroup.ObjectMeta.Name }} 160 | - name: config-dst 161 | emptyDir: {} 162 | - name: pid 163 | emptyDir: 164 | medium: Memory 165 | - name: stats 166 | emptyDir: {} 167 | - apiVersion: v1 168 | kind: ConfigMap 169 | metadata: 170 | name: {{ .KeepalivedGroup.ObjectMeta.Name }} 171 | namespace: {{ .KeepalivedGroup.ObjectMeta.Namespace }} 172 | labels: 173 | keepalivedGroup: {{ .KeepalivedGroup.ObjectMeta.Name }} 174 | data: 175 | keepalived.conf: | 176 | global_defs { 177 | router_id {{ .KeepalivedGroup.ObjectMeta.Name }} 178 | {{ range $key,$value := .KeepalivedGroup.Spec.VerbatimConfig }} 179 | {{ $key }} {{ $value }} 180 | {{ end }} 181 | } 182 | 183 | {{- range $service := .Services }} 184 | {{- if eq $service.Spec.ExternalTrafficPolicy "Local" }} 185 | {{- $namespacedName := printf "%s/%s" $service.ObjectMeta.Namespace $service.ObjectMeta.Name }} 186 | vrrp_script {{ $namespacedName }} { 187 | script "/usr/bin/curl --fail --max-time 1 http://127.0.0.1:{{ $service.Spec.HealthCheckNodePort }}/health" 188 | timeout 10 189 | rise 3 190 | fall 3 191 | } 192 | {{- end }} 193 | {{- end }} 194 | 195 | {{ $root:=. }} 196 | {{ $verbatim_key:="keepalived-operator.redhat-cop.io/verbatimconfig"}} 197 | {{ $spread_key:="keepalived-operator.redhat-cop.io/spreadvips" }} 198 | {{ range $service := .Services }} 199 | {{ $namespacedName:=printf "%s/%s" $service.ObjectMeta.Namespace $service.ObjectMeta.Name }} 200 | {{- if and (eq (index $service.GetAnnotations $spread_key) "true") (gt (len $root.KeepalivedPods) 0) }} 201 | {{- range $i, $ip := (mergeStringSlices $service.Status.LoadBalancer.Ingress $service.Spec.ExternalIPs) }} 202 | {{- $namespacedNameForIP := printf "%s/%s" $namespacedName $ip }} 203 | {{- $owner := index $root.KeepalivedPods (modulus $i (len $root.KeepalivedPods)) }} 204 | vrrp_instance {{ $namespacedNameForIP }} { 205 | @{{ $owner.ObjectMeta.Name }} state MASTER 206 | @^{{ $owner.ObjectMeta.Name }} state BACKUP 207 | @{{ $owner.ObjectMeta.Name }} priority 200 208 | @^{{ $owner.ObjectMeta.Name }} priority 100 209 | interface {{ $root.KeepalivedGroup.Spec.Interface }} 210 | 211 | virtual_router_id {{ index $root.KeepalivedGroup.Status.RouterIDs $namespacedNameForIP }} 212 | 213 | virtual_ipaddress { 214 | {{ $ip }} 215 | } 216 | 217 | {{- if eq $root.KeepalivedGroup.Spec.UnicastEnabled true }} 218 | unicast_peer { 219 | {{ range $pod := $root.KeepalivedPods }} 220 | {{- if $pod.Status.HostIP }} 221 | {{ $pod.Status.HostIP }} 222 | {{- end -}} 223 | {{ end }} 224 | } 225 | {{- end -}} 226 | 227 | {{- if ne $root.Misc.authPass "" }} 228 | authentication { 229 | auth_type PASS 230 | auth_pass {{ $root.Misc.authPass }} 231 | } 232 | {{- end }} 233 | 234 | {{- if eq $service.Spec.ExternalTrafficPolicy "Local" }} 235 | track_script { 236 | {{ $namespacedName }} 237 | } 238 | {{- end }} 239 | 240 | {{ range $key , $value := (parseJson (index $service.GetAnnotations $verbatim_key)) }} 241 | {{ $key }} {{ $value }} 242 | {{ end }} 243 | } 244 | {{- end }} 245 | {{- else }} 246 | vrrp_instance {{ $namespacedName }} { 247 | interface {{ $root.KeepalivedGroup.Spec.Interface }} 248 | 249 | virtual_router_id {{ index $root.KeepalivedGroup.Status.RouterIDs $namespacedName }} 250 | 251 | virtual_ipaddress { 252 | {{ range mergeStringSlices $service.Status.LoadBalancer.Ingress $service.Spec.ExternalIPs }} 253 | {{ . }} 254 | {{ end }} 255 | } 256 | 257 | {{- if eq $root.KeepalivedGroup.Spec.UnicastEnabled true }} 258 | unicast_peer { 259 | {{ range $pod := $root.KeepalivedPods }} 260 | {{- if $pod.Status.HostIP }} 261 | {{ $pod.Status.HostIP }} 262 | {{- end -}} 263 | {{ end }} 264 | } 265 | {{- end -}} 266 | 267 | {{- if ne $root.Misc.authPass "" }} 268 | authentication { 269 | auth_type PASS 270 | auth_pass {{ $root.Misc.authPass }} 271 | } 272 | {{- end }} 273 | 274 | {{- if eq $service.Spec.ExternalTrafficPolicy "Local" }} 275 | track_script { 276 | {{ $namespacedName }} 277 | } 278 | {{- end }} 279 | 280 | {{ range $key , $value := (parseJson (index $service.GetAnnotations $verbatim_key)) }} 281 | {{ $key }} {{ $value }} 282 | {{ end }} 283 | } 284 | {{- end }} 285 | {{ end }} 286 | {{ if eq .Misc.supportsPodMonitor "true" }} 287 | - apiVersion: monitoring.coreos.com/v1 288 | kind: PodMonitor 289 | metadata: 290 | name: {{ .KeepalivedGroup.ObjectMeta.Name }} 291 | namespace: {{ .KeepalivedGroup.ObjectMeta.Namespace }} 292 | labels: 293 | keepalivedGroup: {{ .KeepalivedGroup.ObjectMeta.Name }} 294 | metrics: keepalived 295 | spec: 296 | selector: 297 | matchLabels: 298 | keepalivedGroup: {{ .KeepalivedGroup.ObjectMeta.Name }} 299 | podMetricsEndpoints: 300 | - port: metrics 301 | - apiVersion: rbac.authorization.k8s.io/v1 302 | kind: Role 303 | metadata: 304 | name: {{ .KeepalivedGroup.ObjectMeta.Name }}-prometheus-k8s 305 | rules: 306 | - apiGroups: 307 | - "" 308 | resources: 309 | - endpoints 310 | - pods 311 | - services 312 | verbs: 313 | - get 314 | - list 315 | - watch 316 | - apiVersion: rbac.authorization.k8s.io/v1 317 | kind: RoleBinding 318 | metadata: 319 | name: {{ .KeepalivedGroup.ObjectMeta.Name }}-prometheus-k8s 320 | roleRef: 321 | apiGroup: rbac.authorization.k8s.io 322 | kind: Role 323 | name: {{ .KeepalivedGroup.ObjectMeta.Name }}-prometheus-k8s 324 | subjects: 325 | - kind: ServiceAccount 326 | name: prometheus-k8s 327 | namespace: openshift-monitoring 328 | {{ end}} 329 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CHART_REPO_URL ?= http://example.com 2 | HELM_REPO_DEST ?= /tmp/gh-pages 3 | OPERATOR_NAME ?=$(shell basename -z `pwd`) 4 | HELM_VERSION ?= v3.8.0 5 | KIND_VERSION ?= v0.17.0 6 | KUBECTL_VERSION ?= v1.25.3 7 | K8S_MAJOR_VERSION ?= v1.25.3 8 | 9 | # VERSION defines the project version for the bundle. 10 | # Update this value when you upgrade the version of your project. 11 | # To re-generate a bundle for another specific version without changing the standard setup, you can: 12 | # - use the VERSION as arg of the bundle target (e.g make bundle VERSION=0.0.2) 13 | # - use environment variables to overwrite this value (e.g export VERSION=0.0.2) 14 | VERSION ?= 0.0.1 15 | 16 | # CHANNELS define the bundle channels used in the bundle. 17 | # Add a new line here if you would like to change its default config. (E.g CHANNELS = "candidate,fast,stable") 18 | # To re-generate a bundle for other specific channels without changing the standard setup, you can: 19 | # - use the CHANNELS as arg of the bundle target (e.g make bundle CHANNELS=candidate,fast,stable) 20 | # - use environment variables to overwrite this value (e.g export CHANNELS="candidate,fast,stable") 21 | ifneq ($(origin CHANNELS), undefined) 22 | BUNDLE_CHANNELS := --channels=$(CHANNELS) 23 | endif 24 | 25 | # DEFAULT_CHANNEL defines the default channel used in the bundle. 26 | # Add a new line here if you would like to change its default config. (E.g DEFAULT_CHANNEL = "stable") 27 | # To re-generate a bundle for any other default channel without changing the default setup, you can: 28 | # - use the DEFAULT_CHANNEL as arg of the bundle target (e.g make bundle DEFAULT_CHANNEL=stable) 29 | # - use environment variables to overwrite this value (e.g export DEFAULT_CHANNEL="stable") 30 | ifneq ($(origin DEFAULT_CHANNEL), undefined) 31 | BUNDLE_DEFAULT_CHANNEL := --default-channel=$(DEFAULT_CHANNEL) 32 | endif 33 | BUNDLE_METADATA_OPTS ?= $(BUNDLE_CHANNELS) $(BUNDLE_DEFAULT_CHANNEL) 34 | 35 | # IMAGE_TAG_BASE defines the docker.io namespace and part of the image name for remote images. 36 | # This variable is used to construct full image tags for bundle and catalog images. 37 | # 38 | # For example, running 'make bundle-build bundle-push catalog-build catalog-push' will build and push both 39 | # example.com/memcached-operator-bundle:$VERSION and example.com/memcached-operator-catalog:$VERSION. 40 | IMAGE_TAG_BASE ?= quay.io/redhat-cop/$(OPERATOR_NAME) 41 | 42 | # BUNDLE_GEN_FLAGS are the flags passed to the operator-sdk generate bundle command 43 | BUNDLE_GEN_FLAGS ?= -q --overwrite --version $(VERSION) $(BUNDLE_METADATA_OPTS) 44 | 45 | # USE_IMAGE_DIGESTS defines if images are resolved via tags or digests 46 | # You can enable this value if you would like to use SHA Based Digests 47 | # To enable set flag to true 48 | USE_IMAGE_DIGESTS ?= false 49 | ifeq ($(USE_IMAGE_DIGESTS), true) 50 | BUNDLE_GEN_FLAGS += --use-image-digests 51 | endif 52 | 53 | # BUNDLE_IMG defines the image:tag used for the bundle. 54 | # You can use it as an arg. (E.g make bundle-build BUNDLE_IMG=/:) 55 | BUNDLE_IMG ?= $(IMAGE_TAG_BASE)-bundle:v$(VERSION) 56 | 57 | # Image URL to use all building/pushing image targets 58 | IMG ?= controller:latest 59 | # Produce CRDs that work back to Kubernetes 1.11 (no version conversion) 60 | CRD_OPTIONS ?= "crd:trivialVersions=true,preserveUnknownFields=false" 61 | # ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. 62 | ENVTEST_K8S_VERSION = 1.24.1 63 | 64 | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) 65 | ifeq (,$(shell go env GOBIN)) 66 | GOBIN=$(shell go env GOPATH)/bin 67 | else 68 | GOBIN=$(shell go env GOBIN) 69 | endif 70 | 71 | # Setting SHELL to bash allows bash commands to be executed by recipes. 72 | # This is a requirement for 'setup-envtest.sh' in the test target. 73 | # Options are set to exit when a recipe line exits non-zero or a piped command fails. 74 | SHELL = /usr/bin/env bash -o pipefail 75 | .SHELLFLAGS = -ec 76 | 77 | .PHONY: all 78 | all: build 79 | 80 | ##@ General 81 | 82 | # The help target prints out all targets with their descriptions organized 83 | # beneath their categories. The categories are represented by '##@' and the 84 | # target descriptions by '##'. The awk commands is responsible for reading the 85 | # entire set of makefiles included in this invocation, looking for lines of the 86 | # file as xyz: ## something, and then pretty-format the target and help. Then, 87 | # if there's a line with ##@ something, that gets pretty-printed as a category. 88 | # More info on the usage of ANSI control characters for terminal formatting: 89 | # https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters 90 | # More info on the awk command: 91 | # http://linuxcommand.org/lc3_adv_awk.php 92 | 93 | .PHONY: help 94 | help: ## Display this help. 95 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 96 | 97 | ##@ Development 98 | 99 | .PHONY: manifests 100 | manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. 101 | $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases 102 | 103 | .PHONY: generate 104 | generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. 105 | $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." 106 | 107 | .PHONY: fmt 108 | fmt: ## Run go fmt against code. 109 | go fmt ./... 110 | 111 | .PHONY: vet 112 | vet: ## Run go vet against code. 113 | go vet ./... 114 | 115 | .PHONY: test 116 | test: manifests generate fmt vet envtest ## Run tests. 117 | KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test ./... -coverprofile cover.out 118 | 119 | .PHONY: kind-setup 120 | kind-setup: kind kubectl helm 121 | $(KIND) delete cluster 122 | $(KIND) create cluster --image docker.io/kindest/node:$(K8S_MAJOR_VERSION) --config=./integration/cluster-kind.yaml 123 | $(HELM) upgrade ingress-nginx ./integration/helm/ingress-nginx -i --create-namespace -n ingress-nginx --atomic 124 | $(KUBECTL) wait --namespace ingress-nginx --for=condition=ready pod --selector=app.kubernetes.io/component=controller --timeout=90s 125 | 126 | ##@ Build 127 | 128 | .PHONY: build 129 | build: generate fmt vet ## Build manager binary. 130 | go build -o bin/manager main.go 131 | 132 | .PHONY: run 133 | run: manifests generate fmt vet ## Run a controller from your host. 134 | go run ./main.go 135 | 136 | .PHONY: docker-build 137 | docker-build: test ## Build docker image with the manager. 138 | docker build -t ${IMG} . 139 | 140 | .PHONY: docker-push 141 | docker-push: ## Push docker image with the manager. 142 | docker push ${IMG} 143 | 144 | ##@ Deployment 145 | 146 | .PHONY: install 147 | install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. 148 | $(KUSTOMIZE) build config/crd | kubectl apply -f - 149 | 150 | .PHONY: uninstall 151 | uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. 152 | $(KUSTOMIZE) build config/crd | kubectl delete -f - 153 | 154 | .PHONY: deploy 155 | deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. 156 | cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} 157 | $(KUSTOMIZE) build config/default | kubectl apply -f - 158 | 159 | .PHONY: undeploy 160 | undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. 161 | $(KUSTOMIZE) build config/default | kubectl delete -f - 162 | 163 | LOCALBIN ?= $(shell pwd)/bin 164 | $(LOCALBIN): 165 | mkdir -p $(LOCALBIN) 166 | 167 | ## Tool Binaries 168 | KUSTOMIZE ?= $(LOCALBIN)/kustomize 169 | CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen 170 | ENVTEST ?= $(LOCALBIN)/setup-envtest 171 | 172 | KUSTOMIZE_VERSION ?= v3.8.7 173 | CONTROLLER_TOOLS_VERSION ?= v0.9.0 174 | 175 | 176 | controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. 177 | $(CONTROLLER_GEN): $(LOCALBIN) 178 | GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION) 179 | 180 | KUSTOMIZE_INSTALL_SCRIPT ?= "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" 181 | kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. 182 | $(KUSTOMIZE): $(LOCALBIN) 183 | rm $(KUSTOMIZE) || true 184 | curl -s $(KUSTOMIZE_INSTALL_SCRIPT) | bash -s -- $(subst v,,$(KUSTOMIZE_VERSION)) $(LOCALBIN) 185 | 186 | envtest: $(ENVTEST) ## Download envtest-setup locally if necessary. 187 | $(ENVTEST): $(LOCALBIN) 188 | GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest 189 | 190 | .PHONY: bundle 191 | bundle: manifests kustomize ## Generate bundle manifests and metadata, then validate generated files. 192 | operator-sdk generate kustomize manifests -q 193 | cd config/manager && $(KUSTOMIZE) edit set image controller=$(IMG) 194 | $(KUSTOMIZE) build config/manifests | operator-sdk generate bundle -q --overwrite --version $(VERSION) $(BUNDLE_METADATA_OPTS) 195 | operator-sdk bundle validate ./bundle 196 | 197 | .PHONY: bundle-build 198 | bundle-build: ## Build the bundle image. 199 | docker build -f bundle.Dockerfile -t $(BUNDLE_IMG) . 200 | 201 | .PHONY: bundle-push 202 | bundle-push: ## Push the bundle image. 203 | $(MAKE) docker-push IMG=$(BUNDLE_IMG) 204 | 205 | .PHONY: opm 206 | OPM = ./bin/opm 207 | opm: ## Download opm locally if necessary. 208 | ifeq (,$(wildcard $(OPM))) 209 | ifeq (,$(shell which opm 2>/dev/null)) 210 | @{ \ 211 | set -e ;\ 212 | mkdir -p $(dir $(OPM)) ;\ 213 | OS=$(shell go env GOOS) && ARCH=$(shell go env GOARCH) && \ 214 | curl -sSLo $(OPM) https://github.com/operator-framework/operator-registry/releases/download/v1.15.1/$${OS}-$${ARCH}-opm ;\ 215 | chmod +x $(OPM) ;\ 216 | } 217 | else 218 | OPM = $(shell which opm) 219 | endif 220 | endif 221 | 222 | # A comma-separated list of bundle images (e.g. make catalog-build BUNDLE_IMGS=example.com/operator-bundle:v0.1.0,example.com/operator-bundle:v0.2.0). 223 | # These images MUST exist in a registry and be pull-able. 224 | BUNDLE_IMGS ?= $(BUNDLE_IMG) 225 | 226 | # The image tag given to the resulting catalog image (e.g. make catalog-build CATALOG_IMG=example.com/operator-catalog:v0.2.0). 227 | CATALOG_IMG ?= $(IMAGE_TAG_BASE)-catalog:v$(VERSION) 228 | 229 | # Set CATALOG_BASE_IMG to an existing catalog image tag to add $BUNDLE_IMGS to that image. 230 | ifneq ($(origin CATALOG_BASE_IMG), undefined) 231 | FROM_INDEX_OPT := --from-index $(CATALOG_BASE_IMG) 232 | endif 233 | 234 | # Build a catalog image by adding bundle images to an empty catalog using the operator package manager tool, 'opm'. 235 | # This recipe invokes 'opm' in 'semver' bundle add mode. For more information on add modes, see: 236 | # https://github.com/operator-framework/community-operators/blob/7f1438c/docs/packaging-operator.md#updating-your-existing-operator 237 | .PHONY: catalog-build 238 | catalog-build: opm ## Build a catalog image. 239 | $(OPM) index add --container-tool docker --mode semver --tag $(CATALOG_IMG) --bundles $(BUNDLE_IMGS) $(FROM_INDEX_OPT) 240 | 241 | # Push the catalog image. 242 | .PHONY: catalog-push 243 | catalog-push: ## Push a catalog image. 244 | $(MAKE) docker-push IMG=$(CATALOG_IMG) 245 | 246 | # Generate helm chart 247 | .PHONY: helmchart 248 | helmchart: kustomize helm 249 | mkdir -p ./charts/${OPERATOR_NAME}/templates 250 | mkdir -p ./charts/${OPERATOR_NAME}/crds 251 | repo=${OPERATOR_NAME} envsubst < ./config/local-development/tilt/env-replace-image.yaml > ./config/local-development/tilt/replace-image.yaml 252 | $(KUSTOMIZE) build ./config/helmchart -o ./charts/${OPERATOR_NAME}/templates 253 | sed -i 's/release-namespace/{{.Release.Namespace}}/' ./charts/${OPERATOR_NAME}/templates/*.yaml 254 | rm ./charts/${OPERATOR_NAME}/templates/v1_namespace_release-namespace.yaml ./charts/${OPERATOR_NAME}/templates/apps_v1_deployment_${OPERATOR_NAME}-controller-manager.yaml 255 | mv ./charts/${OPERATOR_NAME}/templates/apiextensions.k8s.io_v1_customresourcedefinition* ./charts/${OPERATOR_NAME}/crds 256 | cp ./config/helmchart/templates/* ./charts/${OPERATOR_NAME}/templates 257 | version=${VERSION} envsubst < ./config/helmchart/Chart.yaml.tpl > ./charts/${OPERATOR_NAME}/Chart.yaml 258 | version=${VERSION} image_repo=$${IMG%:*} envsubst < ./config/helmchart/values.yaml.tpl > ./charts/${OPERATOR_NAME}/values.yaml 259 | sed -i '1s/^/{{ if .Values.enableMonitoring }}/' ./charts/${OPERATOR_NAME}/templates/monitoring.coreos.com_v1_servicemonitor_${OPERATOR_NAME}-controller-manager-metrics-monitor.yaml 260 | echo {{ end }} >> ./charts/${OPERATOR_NAME}/templates/monitoring.coreos.com_v1_servicemonitor_${OPERATOR_NAME}-controller-manager-metrics-monitor.yaml 261 | $(HELM) lint ./charts/${OPERATOR_NAME} 262 | 263 | .PHONY: helmchart-repo 264 | helmchart-repo: helmchart 265 | mkdir -p ${HELM_REPO_DEST}/${OPERATOR_NAME} 266 | $(HELM) package -d ${HELM_REPO_DEST}/${OPERATOR_NAME} ./charts/${OPERATOR_NAME} 267 | $(HELM) repo index --url ${CHART_REPO_URL} ${HELM_REPO_DEST} 268 | 269 | .PHONY: helmchart-repo-push 270 | helmchart-repo-push: helmchart-repo 271 | git -C ${HELM_REPO_DEST} add . 272 | git -C ${HELM_REPO_DEST} status 273 | git -C ${HELM_REPO_DEST} commit -m "Release ${VERSION}" 274 | git -C ${HELM_REPO_DEST} push origin "gh-pages" 275 | 276 | HELM_TEST_IMG_NAME ?= ${OPERATOR_NAME} 277 | HELM_TEST_IMG_TAG ?= helmchart-test 278 | 279 | # Deploy the helmchart to a kind cluster to test deployment. 280 | # If the test-metrics sidecar in the prometheus pod is ready, the metrics work and the test is successful. 281 | .PHONY: helmchart-test 282 | helmchart-test: kind-setup helmchart 283 | $(MAKE) IMG=${HELM_TEST_IMG_NAME}:${HELM_TEST_IMG_TAG} docker-build 284 | docker tag ${HELM_TEST_IMG_NAME}:${HELM_TEST_IMG_TAG} docker.io/library/${HELM_TEST_IMG_NAME}:${HELM_TEST_IMG_TAG} 285 | $(KIND) load docker-image ${HELM_TEST_IMG_NAME}:${HELM_TEST_IMG_TAG} docker.io/library/${HELM_TEST_IMG_NAME}:${HELM_TEST_IMG_TAG} 286 | $(HELM) repo add jetstack https://charts.jetstack.io 287 | $(HELM) install cert-manager jetstack/cert-manager --namespace cert-manager --create-namespace --version v1.7.1 --set installCRDs=true 288 | $(HELM) repo add prometheus-community https://prometheus-community.github.io/helm-charts 289 | $(HELM) install kube-prometheus-stack prometheus-community/kube-prometheus-stack -n default -f integration/kube-prometheus-stack-values.yaml 290 | $(HELM) install prometheus-rbac integration/helm/prometheus-rbac -n default 291 | $(HELM) upgrade -i ${OPERATOR_NAME}-local charts/${OPERATOR_NAME} -n ${OPERATOR_NAME}-local --create-namespace \ 292 | --set enableCertManager=true \ 293 | --set image.repository=${HELM_TEST_IMG_NAME} \ 294 | --set image.tag=${HELM_TEST_IMG_TAG} 295 | $(KUBECTL) wait --namespace ${OPERATOR_NAME}-local --for=condition=ready pod --selector=app.kubernetes.io/name=${OPERATOR_NAME} --timeout=90s 296 | $(KUBECTL) wait --namespace default --for=condition=ready pod prometheus-kube-prometheus-stack-prometheus-0 --timeout=180s 297 | $(KUBECTL) exec prometheus-kube-prometheus-stack-prometheus-0 -n default -c test-metrics -- /bin/sh -c "echo 'Example metrics...' && cat /tmp/ready" 298 | 299 | .PHONY: kind 300 | KIND = ./bin/kind 301 | kind: ## Download kind locally if necessary. 302 | ifeq (,$(wildcard $(KIND))) 303 | ifeq (,$(shell which kind 2>/dev/null)) 304 | $(shell go install sigs.k8s.io/kind@${KIND_VERSION}) 305 | else 306 | KIND = $(shell which kind) 307 | endif 308 | endif 309 | 310 | .PHONY: kubectl 311 | KUBECTL = ./bin/kubectl 312 | kubectl: ## Download kubectl locally if necessary. 313 | ifeq (,$(wildcard $(KUBECTL))) 314 | ifeq (,$(shell which kubectl 2>/dev/null)) 315 | echo "Downloading ${KUBECTL} for managing k8s resources." 316 | OS=$(shell go env GOOS) ;\ 317 | ARCH=$(shell go env GOARCH) ;\ 318 | curl --create-dirs -sSLo ${KUBECTL} https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/$${OS}/$${ARCH}/kubectl ;\ 319 | chmod +x ${KUBECTL} 320 | else 321 | KUBECTL = $(shell which kubectl) 322 | endif 323 | endif 324 | 325 | .PHONY: helm 326 | HELM = ./bin/helm 327 | helm: ## Download helm locally if necessary. 328 | ifeq (,$(wildcard $(HELM))) 329 | ifeq (,$(shell which helm 2>/dev/null)) 330 | echo "Downloading ${HELM}." 331 | OS=$(shell go env GOOS) ;\ 332 | ARCH=$(shell go env GOARCH) ;\ 333 | curl --create-dirs -sSLo ${HELM}.tar.gz https://get.helm.sh/helm-${HELM_VERSION}-$${OS}-$${ARCH}.tar.gz ;\ 334 | tar -xf ${HELM}.tar.gz -C ./bin/ ;\ 335 | mv ./bin/$${OS}-$${ARCH}/helm ${HELM} 336 | else 337 | HELM = $(shell which helm) 338 | endif 339 | endif -------------------------------------------------------------------------------- /integration/helm/ingress-nginx/templates/deploy.yaml: -------------------------------------------------------------------------------- 1 | #GENERATED FOR K8S 1.20 2 | --- 3 | apiVersion: v1 4 | automountServiceAccountToken: true 5 | kind: ServiceAccount 6 | metadata: 7 | labels: 8 | app.kubernetes.io/component: controller 9 | app.kubernetes.io/instance: ingress-nginx 10 | app.kubernetes.io/managed-by: Helm 11 | app.kubernetes.io/name: ingress-nginx 12 | app.kubernetes.io/part-of: ingress-nginx 13 | app.kubernetes.io/version: 1.1.1 14 | helm.sh/chart: ingress-nginx-4.0.16 15 | name: ingress-nginx 16 | namespace: ingress-nginx 17 | --- 18 | apiVersion: v1 19 | kind: ServiceAccount 20 | metadata: 21 | annotations: 22 | helm.sh/hook: pre-install,pre-upgrade,post-install,post-upgrade 23 | helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded 24 | labels: 25 | app.kubernetes.io/component: admission-webhook 26 | app.kubernetes.io/instance: ingress-nginx 27 | app.kubernetes.io/managed-by: Helm 28 | app.kubernetes.io/name: ingress-nginx 29 | app.kubernetes.io/part-of: ingress-nginx 30 | app.kubernetes.io/version: 1.1.1 31 | helm.sh/chart: ingress-nginx-4.0.16 32 | name: ingress-nginx-admission 33 | namespace: ingress-nginx 34 | --- 35 | apiVersion: rbac.authorization.k8s.io/v1 36 | kind: Role 37 | metadata: 38 | labels: 39 | app.kubernetes.io/component: controller 40 | app.kubernetes.io/instance: ingress-nginx 41 | app.kubernetes.io/managed-by: Helm 42 | app.kubernetes.io/name: ingress-nginx 43 | app.kubernetes.io/part-of: ingress-nginx 44 | app.kubernetes.io/version: 1.1.1 45 | helm.sh/chart: ingress-nginx-4.0.16 46 | name: ingress-nginx 47 | namespace: ingress-nginx 48 | rules: 49 | - apiGroups: 50 | - "" 51 | resources: 52 | - namespaces 53 | verbs: 54 | - get 55 | - apiGroups: 56 | - "" 57 | resources: 58 | - configmaps 59 | - pods 60 | - secrets 61 | - endpoints 62 | verbs: 63 | - get 64 | - list 65 | - watch 66 | - apiGroups: 67 | - "" 68 | resources: 69 | - services 70 | verbs: 71 | - get 72 | - list 73 | - watch 74 | - apiGroups: 75 | - networking.k8s.io 76 | resources: 77 | - ingresses 78 | verbs: 79 | - get 80 | - list 81 | - watch 82 | - apiGroups: 83 | - networking.k8s.io 84 | resources: 85 | - ingresses/status 86 | verbs: 87 | - update 88 | - apiGroups: 89 | - networking.k8s.io 90 | resources: 91 | - ingressclasses 92 | verbs: 93 | - get 94 | - list 95 | - watch 96 | - apiGroups: 97 | - "" 98 | resourceNames: 99 | - ingress-controller-leader 100 | resources: 101 | - configmaps 102 | verbs: 103 | - get 104 | - update 105 | - apiGroups: 106 | - "" 107 | resources: 108 | - configmaps 109 | verbs: 110 | - create 111 | - apiGroups: 112 | - "" 113 | resources: 114 | - events 115 | verbs: 116 | - create 117 | - patch 118 | --- 119 | apiVersion: rbac.authorization.k8s.io/v1 120 | kind: Role 121 | metadata: 122 | annotations: 123 | helm.sh/hook: pre-install,pre-upgrade,post-install,post-upgrade 124 | helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded 125 | labels: 126 | app.kubernetes.io/component: admission-webhook 127 | app.kubernetes.io/instance: ingress-nginx 128 | app.kubernetes.io/managed-by: Helm 129 | app.kubernetes.io/name: ingress-nginx 130 | app.kubernetes.io/part-of: ingress-nginx 131 | app.kubernetes.io/version: 1.1.1 132 | helm.sh/chart: ingress-nginx-4.0.16 133 | name: ingress-nginx-admission 134 | namespace: ingress-nginx 135 | rules: 136 | - apiGroups: 137 | - "" 138 | resources: 139 | - secrets 140 | verbs: 141 | - get 142 | - create 143 | --- 144 | apiVersion: rbac.authorization.k8s.io/v1 145 | kind: ClusterRole 146 | metadata: 147 | labels: 148 | app.kubernetes.io/instance: ingress-nginx 149 | app.kubernetes.io/managed-by: Helm 150 | app.kubernetes.io/name: ingress-nginx 151 | app.kubernetes.io/part-of: ingress-nginx 152 | app.kubernetes.io/version: 1.1.1 153 | helm.sh/chart: ingress-nginx-4.0.16 154 | name: ingress-nginx 155 | rules: 156 | - apiGroups: 157 | - "" 158 | resources: 159 | - configmaps 160 | - endpoints 161 | - nodes 162 | - pods 163 | - secrets 164 | - namespaces 165 | verbs: 166 | - list 167 | - watch 168 | - apiGroups: 169 | - "" 170 | resources: 171 | - nodes 172 | verbs: 173 | - get 174 | - apiGroups: 175 | - "" 176 | resources: 177 | - services 178 | verbs: 179 | - get 180 | - list 181 | - watch 182 | - apiGroups: 183 | - networking.k8s.io 184 | resources: 185 | - ingresses 186 | verbs: 187 | - get 188 | - list 189 | - watch 190 | - apiGroups: 191 | - "" 192 | resources: 193 | - events 194 | verbs: 195 | - create 196 | - patch 197 | - apiGroups: 198 | - networking.k8s.io 199 | resources: 200 | - ingresses/status 201 | verbs: 202 | - update 203 | - apiGroups: 204 | - networking.k8s.io 205 | resources: 206 | - ingressclasses 207 | verbs: 208 | - get 209 | - list 210 | - watch 211 | --- 212 | apiVersion: rbac.authorization.k8s.io/v1 213 | kind: ClusterRole 214 | metadata: 215 | annotations: 216 | helm.sh/hook: pre-install,pre-upgrade,post-install,post-upgrade 217 | helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded 218 | labels: 219 | app.kubernetes.io/component: admission-webhook 220 | app.kubernetes.io/instance: ingress-nginx 221 | app.kubernetes.io/managed-by: Helm 222 | app.kubernetes.io/name: ingress-nginx 223 | app.kubernetes.io/part-of: ingress-nginx 224 | app.kubernetes.io/version: 1.1.1 225 | helm.sh/chart: ingress-nginx-4.0.16 226 | name: ingress-nginx-admission 227 | rules: 228 | - apiGroups: 229 | - admissionregistration.k8s.io 230 | resources: 231 | - validatingwebhookconfigurations 232 | verbs: 233 | - get 234 | - update 235 | --- 236 | apiVersion: rbac.authorization.k8s.io/v1 237 | kind: RoleBinding 238 | metadata: 239 | labels: 240 | app.kubernetes.io/component: controller 241 | app.kubernetes.io/instance: ingress-nginx 242 | app.kubernetes.io/managed-by: Helm 243 | app.kubernetes.io/name: ingress-nginx 244 | app.kubernetes.io/part-of: ingress-nginx 245 | app.kubernetes.io/version: 1.1.1 246 | helm.sh/chart: ingress-nginx-4.0.16 247 | name: ingress-nginx 248 | namespace: ingress-nginx 249 | roleRef: 250 | apiGroup: rbac.authorization.k8s.io 251 | kind: Role 252 | name: ingress-nginx 253 | subjects: 254 | - kind: ServiceAccount 255 | name: ingress-nginx 256 | namespace: ingress-nginx 257 | --- 258 | apiVersion: rbac.authorization.k8s.io/v1 259 | kind: RoleBinding 260 | metadata: 261 | annotations: 262 | helm.sh/hook: pre-install,pre-upgrade,post-install,post-upgrade 263 | helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded 264 | labels: 265 | app.kubernetes.io/component: admission-webhook 266 | app.kubernetes.io/instance: ingress-nginx 267 | app.kubernetes.io/managed-by: Helm 268 | app.kubernetes.io/name: ingress-nginx 269 | app.kubernetes.io/part-of: ingress-nginx 270 | app.kubernetes.io/version: 1.1.1 271 | helm.sh/chart: ingress-nginx-4.0.16 272 | name: ingress-nginx-admission 273 | namespace: ingress-nginx 274 | roleRef: 275 | apiGroup: rbac.authorization.k8s.io 276 | kind: Role 277 | name: ingress-nginx-admission 278 | subjects: 279 | - kind: ServiceAccount 280 | name: ingress-nginx-admission 281 | namespace: ingress-nginx 282 | --- 283 | apiVersion: rbac.authorization.k8s.io/v1 284 | kind: ClusterRoleBinding 285 | metadata: 286 | labels: 287 | app.kubernetes.io/instance: ingress-nginx 288 | app.kubernetes.io/managed-by: Helm 289 | app.kubernetes.io/name: ingress-nginx 290 | app.kubernetes.io/part-of: ingress-nginx 291 | app.kubernetes.io/version: 1.1.1 292 | helm.sh/chart: ingress-nginx-4.0.16 293 | name: ingress-nginx 294 | roleRef: 295 | apiGroup: rbac.authorization.k8s.io 296 | kind: ClusterRole 297 | name: ingress-nginx 298 | subjects: 299 | - kind: ServiceAccount 300 | name: ingress-nginx 301 | namespace: ingress-nginx 302 | --- 303 | apiVersion: rbac.authorization.k8s.io/v1 304 | kind: ClusterRoleBinding 305 | metadata: 306 | annotations: 307 | helm.sh/hook: pre-install,pre-upgrade,post-install,post-upgrade 308 | helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded 309 | labels: 310 | app.kubernetes.io/component: admission-webhook 311 | app.kubernetes.io/instance: ingress-nginx 312 | app.kubernetes.io/managed-by: Helm 313 | app.kubernetes.io/name: ingress-nginx 314 | app.kubernetes.io/part-of: ingress-nginx 315 | app.kubernetes.io/version: 1.1.1 316 | helm.sh/chart: ingress-nginx-4.0.16 317 | name: ingress-nginx-admission 318 | roleRef: 319 | apiGroup: rbac.authorization.k8s.io 320 | kind: ClusterRole 321 | name: ingress-nginx-admission 322 | subjects: 323 | - kind: ServiceAccount 324 | name: ingress-nginx-admission 325 | namespace: ingress-nginx 326 | --- 327 | apiVersion: v1 328 | data: 329 | allow-snippet-annotations: "true" 330 | kind: ConfigMap 331 | metadata: 332 | labels: 333 | app.kubernetes.io/component: controller 334 | app.kubernetes.io/instance: ingress-nginx 335 | app.kubernetes.io/managed-by: Helm 336 | app.kubernetes.io/name: ingress-nginx 337 | app.kubernetes.io/part-of: ingress-nginx 338 | app.kubernetes.io/version: 1.1.1 339 | helm.sh/chart: ingress-nginx-4.0.16 340 | name: ingress-nginx-controller 341 | namespace: ingress-nginx 342 | --- 343 | apiVersion: v1 344 | kind: Service 345 | metadata: 346 | annotations: null 347 | labels: 348 | app.kubernetes.io/component: controller 349 | app.kubernetes.io/instance: ingress-nginx 350 | app.kubernetes.io/managed-by: Helm 351 | app.kubernetes.io/name: ingress-nginx 352 | app.kubernetes.io/part-of: ingress-nginx 353 | app.kubernetes.io/version: 1.1.1 354 | helm.sh/chart: ingress-nginx-4.0.16 355 | name: ingress-nginx-controller 356 | namespace: ingress-nginx 357 | spec: 358 | ipFamilies: 359 | - IPv4 360 | ipFamilyPolicy: SingleStack 361 | ports: 362 | - appProtocol: http 363 | name: http 364 | port: 8080 365 | protocol: TCP 366 | targetPort: http 367 | - appProtocol: https 368 | name: https 369 | port: 8443 370 | protocol: TCP 371 | targetPort: https 372 | selector: 373 | app.kubernetes.io/component: controller 374 | app.kubernetes.io/instance: ingress-nginx 375 | app.kubernetes.io/name: ingress-nginx 376 | type: NodePort 377 | --- 378 | apiVersion: v1 379 | kind: Service 380 | metadata: 381 | labels: 382 | app.kubernetes.io/component: controller 383 | app.kubernetes.io/instance: ingress-nginx 384 | app.kubernetes.io/managed-by: Helm 385 | app.kubernetes.io/name: ingress-nginx 386 | app.kubernetes.io/part-of: ingress-nginx 387 | app.kubernetes.io/version: 1.1.1 388 | helm.sh/chart: ingress-nginx-4.0.16 389 | name: ingress-nginx-controller-admission 390 | namespace: ingress-nginx 391 | spec: 392 | ports: 393 | - appProtocol: https 394 | name: https-webhook 395 | port: 443 396 | targetPort: webhook 397 | selector: 398 | app.kubernetes.io/component: controller 399 | app.kubernetes.io/instance: ingress-nginx 400 | app.kubernetes.io/name: ingress-nginx 401 | type: ClusterIP 402 | --- 403 | apiVersion: apps/v1 404 | kind: Deployment 405 | metadata: 406 | labels: 407 | app.kubernetes.io/component: controller 408 | app.kubernetes.io/instance: ingress-nginx 409 | app.kubernetes.io/managed-by: Helm 410 | app.kubernetes.io/name: ingress-nginx 411 | app.kubernetes.io/part-of: ingress-nginx 412 | app.kubernetes.io/version: 1.1.1 413 | helm.sh/chart: ingress-nginx-4.0.16 414 | name: ingress-nginx-controller 415 | namespace: ingress-nginx 416 | spec: 417 | minReadySeconds: 0 418 | revisionHistoryLimit: 10 419 | selector: 420 | matchLabels: 421 | app.kubernetes.io/component: controller 422 | app.kubernetes.io/instance: ingress-nginx 423 | app.kubernetes.io/name: ingress-nginx 424 | strategy: 425 | rollingUpdate: 426 | maxUnavailable: 1 427 | type: RollingUpdate 428 | template: 429 | metadata: 430 | labels: 431 | app.kubernetes.io/component: controller 432 | app.kubernetes.io/instance: ingress-nginx 433 | app.kubernetes.io/name: ingress-nginx 434 | spec: 435 | containers: 436 | - args: 437 | - /nginx-ingress-controller 438 | - --election-id=ingress-controller-leader 439 | - --controller-class=k8s.io/ingress-nginx 440 | - --configmap=$(POD_NAMESPACE)/ingress-nginx-controller 441 | - --validating-webhook=:8443 442 | - --validating-webhook-certificate=/usr/local/certificates/cert 443 | - --validating-webhook-key=/usr/local/certificates/key 444 | - --watch-ingress-without-class=true 445 | - --publish-status-address=localhost 446 | env: 447 | - name: POD_NAME 448 | valueFrom: 449 | fieldRef: 450 | fieldPath: metadata.name 451 | - name: POD_NAMESPACE 452 | valueFrom: 453 | fieldRef: 454 | fieldPath: metadata.namespace 455 | - name: LD_PRELOAD 456 | value: /usr/local/lib/libmimalloc.so 457 | image: k8s.gcr.io/ingress-nginx/controller:v1.1.1@sha256:0bc88eb15f9e7f84e8e56c14fa5735aaa488b840983f87bd79b1054190e660de 458 | imagePullPolicy: IfNotPresent 459 | lifecycle: 460 | preStop: 461 | exec: 462 | command: 463 | - /wait-shutdown 464 | livenessProbe: 465 | failureThreshold: 5 466 | httpGet: 467 | path: /healthz 468 | port: 10254 469 | scheme: HTTP 470 | initialDelaySeconds: 10 471 | periodSeconds: 10 472 | successThreshold: 1 473 | timeoutSeconds: 1 474 | name: controller 475 | ports: 476 | - containerPort: 80 477 | hostPort: 80 478 | name: http 479 | protocol: TCP 480 | - containerPort: 443 481 | hostPort: 443 482 | name: https 483 | protocol: TCP 484 | - containerPort: 8443 485 | name: webhook 486 | protocol: TCP 487 | readinessProbe: 488 | failureThreshold: 3 489 | httpGet: 490 | path: /healthz 491 | port: 10254 492 | scheme: HTTP 493 | initialDelaySeconds: 10 494 | periodSeconds: 10 495 | successThreshold: 1 496 | timeoutSeconds: 1 497 | resources: 498 | requests: 499 | cpu: 100m 500 | memory: 90Mi 501 | securityContext: 502 | allowPrivilegeEscalation: true 503 | capabilities: 504 | add: 505 | - NET_BIND_SERVICE 506 | drop: 507 | - ALL 508 | runAsUser: 101 509 | volumeMounts: 510 | - mountPath: /usr/local/certificates/ 511 | name: webhook-cert 512 | readOnly: true 513 | dnsPolicy: ClusterFirst 514 | nodeSelector: 515 | ingress-ready: "true" 516 | kubernetes.io/os: linux 517 | serviceAccountName: ingress-nginx 518 | terminationGracePeriodSeconds: 0 519 | tolerations: 520 | - effect: NoSchedule 521 | key: node-role.kubernetes.io/master 522 | operator: Equal 523 | volumes: 524 | - name: webhook-cert 525 | secret: 526 | secretName: ingress-nginx-admission 527 | --- 528 | apiVersion: batch/v1 529 | kind: Job 530 | metadata: 531 | annotations: 532 | helm.sh/hook: pre-install,pre-upgrade 533 | helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded 534 | labels: 535 | app.kubernetes.io/component: admission-webhook 536 | app.kubernetes.io/instance: ingress-nginx 537 | app.kubernetes.io/managed-by: Helm 538 | app.kubernetes.io/name: ingress-nginx 539 | app.kubernetes.io/part-of: ingress-nginx 540 | app.kubernetes.io/version: 1.1.1 541 | helm.sh/chart: ingress-nginx-4.0.16 542 | name: ingress-nginx-admission-create 543 | namespace: ingress-nginx 544 | spec: 545 | template: 546 | metadata: 547 | labels: 548 | app.kubernetes.io/component: admission-webhook 549 | app.kubernetes.io/instance: ingress-nginx 550 | app.kubernetes.io/managed-by: Helm 551 | app.kubernetes.io/name: ingress-nginx 552 | app.kubernetes.io/part-of: ingress-nginx 553 | app.kubernetes.io/version: 1.1.1 554 | helm.sh/chart: ingress-nginx-4.0.16 555 | name: ingress-nginx-admission-create 556 | spec: 557 | containers: 558 | - args: 559 | - create 560 | - --host=ingress-nginx-controller-admission,ingress-nginx-controller-admission.$(POD_NAMESPACE).svc 561 | - --namespace=$(POD_NAMESPACE) 562 | - --secret-name=ingress-nginx-admission 563 | env: 564 | - name: POD_NAMESPACE 565 | valueFrom: 566 | fieldRef: 567 | fieldPath: metadata.namespace 568 | image: k8s.gcr.io/ingress-nginx/kube-webhook-certgen:v1.1.1@sha256:64d8c73dca984af206adf9d6d7e46aa550362b1d7a01f3a0a91b20cc67868660 569 | imagePullPolicy: IfNotPresent 570 | name: create 571 | securityContext: 572 | allowPrivilegeEscalation: false 573 | nodeSelector: 574 | kubernetes.io/os: linux 575 | restartPolicy: OnFailure 576 | securityContext: 577 | runAsNonRoot: true 578 | runAsUser: 2000 579 | serviceAccountName: ingress-nginx-admission 580 | --- 581 | apiVersion: batch/v1 582 | kind: Job 583 | metadata: 584 | annotations: 585 | helm.sh/hook: post-install,post-upgrade 586 | helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded 587 | labels: 588 | app.kubernetes.io/component: admission-webhook 589 | app.kubernetes.io/instance: ingress-nginx 590 | app.kubernetes.io/managed-by: Helm 591 | app.kubernetes.io/name: ingress-nginx 592 | app.kubernetes.io/part-of: ingress-nginx 593 | app.kubernetes.io/version: 1.1.1 594 | helm.sh/chart: ingress-nginx-4.0.16 595 | name: ingress-nginx-admission-patch 596 | namespace: ingress-nginx 597 | spec: 598 | template: 599 | metadata: 600 | labels: 601 | app.kubernetes.io/component: admission-webhook 602 | app.kubernetes.io/instance: ingress-nginx 603 | app.kubernetes.io/managed-by: Helm 604 | app.kubernetes.io/name: ingress-nginx 605 | app.kubernetes.io/part-of: ingress-nginx 606 | app.kubernetes.io/version: 1.1.1 607 | helm.sh/chart: ingress-nginx-4.0.16 608 | name: ingress-nginx-admission-patch 609 | spec: 610 | containers: 611 | - args: 612 | - patch 613 | - --webhook-name=ingress-nginx-admission 614 | - --namespace=$(POD_NAMESPACE) 615 | - --patch-mutating=false 616 | - --secret-name=ingress-nginx-admission 617 | - --patch-failure-policy=Fail 618 | env: 619 | - name: POD_NAMESPACE 620 | valueFrom: 621 | fieldRef: 622 | fieldPath: metadata.namespace 623 | image: k8s.gcr.io/ingress-nginx/kube-webhook-certgen:v1.1.1@sha256:64d8c73dca984af206adf9d6d7e46aa550362b1d7a01f3a0a91b20cc67868660 624 | imagePullPolicy: IfNotPresent 625 | name: patch 626 | securityContext: 627 | allowPrivilegeEscalation: false 628 | nodeSelector: 629 | kubernetes.io/os: linux 630 | restartPolicy: OnFailure 631 | securityContext: 632 | runAsNonRoot: true 633 | runAsUser: 2000 634 | serviceAccountName: ingress-nginx-admission 635 | --- 636 | apiVersion: networking.k8s.io/v1 637 | kind: IngressClass 638 | metadata: 639 | labels: 640 | app.kubernetes.io/component: controller 641 | app.kubernetes.io/instance: ingress-nginx 642 | app.kubernetes.io/managed-by: Helm 643 | app.kubernetes.io/name: ingress-nginx 644 | app.kubernetes.io/part-of: ingress-nginx 645 | app.kubernetes.io/version: 1.1.1 646 | helm.sh/chart: ingress-nginx-4.0.16 647 | name: nginx 648 | spec: 649 | controller: k8s.io/ingress-nginx 650 | --- 651 | apiVersion: admissionregistration.k8s.io/v1 652 | kind: ValidatingWebhookConfiguration 653 | metadata: 654 | labels: 655 | app.kubernetes.io/component: admission-webhook 656 | app.kubernetes.io/instance: ingress-nginx 657 | app.kubernetes.io/managed-by: Helm 658 | app.kubernetes.io/name: ingress-nginx 659 | app.kubernetes.io/part-of: ingress-nginx 660 | app.kubernetes.io/version: 1.1.1 661 | helm.sh/chart: ingress-nginx-4.0.16 662 | name: ingress-nginx-admission 663 | webhooks: 664 | - admissionReviewVersions: 665 | - v1 666 | clientConfig: 667 | service: 668 | name: ingress-nginx-controller-admission 669 | namespace: ingress-nginx 670 | path: /networking/v1/ingresses 671 | failurePolicy: Fail 672 | matchPolicy: Equivalent 673 | name: validate.nginx.ingress.kubernetes.io 674 | rules: 675 | - apiGroups: 676 | - networking.k8s.io 677 | apiVersions: 678 | - v1 679 | operations: 680 | - CREATE 681 | - UPDATE 682 | resources: 683 | - ingresses 684 | sideEffects: None 685 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Keepalived operator 2 | 3 | ![build status](https://github.com/redhat-cop/keepalived-operator/workflows/push/badge.svg) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/redhat-cop/keepalived-operator)](https://goreportcard.com/report/github.com/redhat-cop/keepalived-operator) 5 | ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/redhat-cop/keepalived-operator) 6 | 7 | The objective of the keepalived operator is to allow for a way to create self-hosted load balancers in an automated way. From a user experience point of view the behavior is the same as of when creating [`LoadBalancer`](https://kubernetes.io/docs/concepts/services-networking/service/#loadbalancer) services with a cloud provider able to manage them. 8 | 9 | The keepalived operator can be used in all environments that allows nodes to advertise additional IPs on their NICs (and at least for now, in networks that allow multicast), however it's mainly aimed at supporting LoadBalancer services and ExternalIPs on bare metal installations (or other installation environments where a cloud provider is not available). 10 | 11 | One possible use of the keepalived operator is also to support [OpenShift Ingresses](https://docs.openshift.com/container-platform/4.5/networking/configuring_ingress_cluster_traffic/overview-traffic.html) in environments where an external load balancer cannot be provisioned. See this [how-to](./Ingress-how-to.md) on how to configure keepalived-operator to support OpenShift ingresses 12 | 13 | ## How it works 14 | 15 | The keepalived operator will create one or more [VIPs](https://en.wikipedia.org/wiki/Virtual_IP_address) (an HA IP that floats between multiple nodes), based on the [`LoadBalancer`](https://kubernetes.io/docs/concepts/services-networking/service/#loadbalancer) services and/or services requesting [`ExternalIPs`](https://kubernetes.io/docs/concepts/services-networking/service/#external-ips). 16 | 17 | For `LoadBalancer` services the IPs found at `.Status.LoadBalancer.Ingress[].IP` will become VIPs. 18 | 19 | For services requesting a `ExternalIPs`, the IPs found at `.Spec.ExternalIPs[]` will become VIPs. 20 | 21 | Note that a service can be of `LoadBalancer` type and also request `ExternalIPs`, it this case both sets of IPs will become VIPs. 22 | 23 | Due to a [keepalived](https://www.keepalived.org/manpage.html) limitation a single keepalived cluster can manage up to 256 VIP configurations. Multiple keepalived clusters can coexists in the same network as long as they use different multicast ports [TODO verify this statement]. 24 | 25 | To address this limitation the `KeepalivedGroup` [CRD](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/) has been introduced. This CRD is supposed to be configured by an administrator and allows you to specify a node selector to pick on which nodes the keepalived pods should be deployed. Here is an example: 26 | 27 | ```yaml 28 | apiVersion: redhatcop.redhat.io/v1alpha1 29 | kind: KeepalivedGroup 30 | metadata: 31 | name: keepalivedgroup-router 32 | spec: 33 | image: registry.redhat.io/openshift4/ose-keepalived-ipfailover 34 | interface: ens3 35 | nodeSelector: 36 | node-role.kubernetes.io/loadbalancer: "" 37 | blacklistRouterIDs: 38 | - 1 39 | - 2 40 | ``` 41 | 42 | This KeepalivedGroup will be deployed on all the nodes with role `loadbalancer`. Keepalived requires knowledge of the network device on which the VIPs will be exposed. If the interface name is the same on all nodes, it can be specified in the `interface` field. Alternatively, the `interfaceFromIP` field can be set to an IPv4 address to enable interface autodiscovery. In this scenario, the `interface` field will be ignored and each node in the KeepalivedGroup will expose the VIPs on the interface that would be used to reach the provided IP. 43 | 44 | Services must be annotated to opt-in to being observed by the keepalived operator and to specify which KeepalivedGroup they refer to. The annotation looks like this: 45 | 46 | `keepalived-operator.redhat-cop.io/keepalivedgroup: /` 47 | 48 | The image used for the keepalived containers can be specified with `.Spec.Image` it will default to `registry.redhat.io/openshift4/ose-keepalived-ipfailover` if undefined. 49 | 50 | ## Requirements 51 | 52 | ### Security Context Constraints 53 | 54 | Each KeepalivedGroup deploys a [daemonset](https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/) that requires the [privileged scc](https://docs.openshift.com/container-platform/4.5/authentication/managing-security-context-constraints.html), this permission must be given to the `default` service account in the namespace where the keepalived group is created by and administrator. 55 | 56 | ```shell 57 | oc adm policy add-scc-to-user privileged -z default-n 58 | ``` 59 | 60 | ### Cluster Network Operator 61 | 62 | In Openshift, use of an external IP address is governed by the following fields in the `Network.config.openshift.io` CR named `cluster` 63 | 64 | * `spec.externalIP.autoAssignCIDRs` defines an IP address block used by the load balancer when choosing an external IP address for the service. OpenShift supports only a single IP address block for automatic assignment. 65 | 66 | * `spec.externalIP.policy` defines the permissible IP address blocks when manually specifying an IP address. OpenShift does not apply policy rules to IP address blocks defined by `spec.externalIP.autoAssignCIDRs` 67 | 68 | The following patch can be used to configure the Cluster Network Operator: 69 | 70 | ```yaml 71 | spec: 72 | externalIP: 73 | policy: 74 | allowedCIDRs: 75 | - ${ALLOWED_CIDR} 76 | autoAssignCIDRs: 77 | - "${AUTOASSIGNED_CIDR}" 78 | ``` 79 | 80 | Here is an example of how to apply the patch: 81 | 82 | ```shell 83 | export ALLOWED_CIDR="192.168.131.128/26" 84 | export AUTOASSIGNED_CIDR="192.168.131.192/26" 85 | oc patch network cluster -p "$(envsubst < ./network-patch.yaml | yq r -j -)" --type=merge 86 | ``` 87 | 88 | Additionally, the fields can be edited manually via `oc edit Network.config.openshift.io cluster` 89 | 90 | ## Blacklisting router IDs 91 | 92 | If the Keepalived pods are deployed on nodes which are in the same network (same broadcast domain to be precise) with other keepalived the process, it's necessary to ensure that there is no collision between the used routers it. 93 | For this purpose it is possible to provide a `blacklistRouterIDs` field with a list of black-listed IDs that will not be used. 94 | 95 | ## Spreading VIPs across nodes to maximize load balancing 96 | 97 | If a service contains multiple externalIPs or LoadBalancer IPs, it is possible to instruct keepalived-operator to maximize the spread of such VIPs across the nodes in the KeepalivedGroup by specifying the `keepalived-operator.redhat-cop.io/spreadvips: "true"` annotation on the service. This option ensures that different VIPs for the same service are always owned by different nodes (or, if the number of nodes in the group is less than the number of VIPs, that the VIPs are assigned maximizing the spread), to avoid creating a traffic bottleneck. However, in order to achieve this, keepalived-operator will create a separate VRRP instance per VIP of that service, which could exhaust the 256 available instances faster. 98 | 99 | ## OpenShift RHV, vSphere, OSP and bare metal IPI instructions 100 | 101 | When IPI is used for RHV, vSphere, OSP or bare metal platforms, three keepalived VIPs are deployed. To make sure that keepalived-operator can work in these environment we need to discover and blacklist the corresponding VRRP router IDs. 102 | 103 | To discover the VRRP router IDs being used, run the following command, you can run this command from you laptop: 104 | 105 | ```shell 106 | podman run quay.io/openshift/origin-baremetal-runtimecfg:4.5 vr-ids 107 | ``` 108 | 109 | If you don't know your cluster name, run this command: 110 | 111 | ```shell 112 | podman run quay.io/openshift/origin-baremetal-runtimecfg:4.5 vr-ids $(oc get cm cluster-config-v1 -n kube-system -o jsonpath='{.data.install-config}'| yq -r .metadata.name) 113 | ``` 114 | 115 | Then use these [instructions](#Blacklisting-router-IDs) to blacklist those VRRP router IDs. 116 | 117 | ## Verbatim Configurations 118 | 119 | Keepalived has dozens of [configurations](https://www.keepalived.org/manpage.html). At the early stage of this project it's difficult to tell which one should be modeled in the API. Yet, users of this project may still need to use them. To account for that there is a way to pass verbatim options both at the keepalived group level (which maps to the keepalived config `global_defs` section) and at the service level (which maps to the keepalived config `vrrp_instance` section). 120 | 121 | KeepalivedGroup-level verbatim configurations can be passed as in the following example: 122 | 123 | ```yaml 124 | apiVersion: redhatcop.redhat.io/v1alpha1 125 | kind: KeepalivedGroup 126 | metadata: 127 | name: keepalivedgroup-router 128 | spec: 129 | interface: ens3 130 | nodeSelector: 131 | node-role.kubernetes.io/loadbalancer: "" 132 | verbatimConfig: 133 | vrrp_iptables: my-keepalived 134 | ``` 135 | 136 | this will map to the following `global_defs`: 137 | 138 | ```text 139 | global_defs { 140 | router_id keepalivedgroup-router 141 | vrrp_iptables my-keepalived 142 | } 143 | ``` 144 | 145 | Service-level verbatim configurations can be passed as in the following example: 146 | 147 | ```yaml 148 | apiVersion: v1 149 | kind: Service 150 | metadata: 151 | annotations: 152 | keepalived-operator.redhat-cop.io/keepalivedgroup: keepalived-operator/keepalivedgroup-router 153 | keepalived-operator.redhat-cop.io/verbatimconfig: '{ "track_src_ip": "" }' 154 | ``` 155 | 156 | this will map to the following `vrrp_instance` section 157 | 158 | ```text 159 | vrrp_instance openshift-ingress/router-default { 160 | interface ens3 161 | virtual_router_id 1 162 | virtual_ipaddress { 163 | 192.168.131.129 164 | } 165 | track_src_ip 166 | } 167 | ``` 168 | 169 | ## Advanced Users Only: Override Keepalived Configuration Template 170 | 171 | **NOTE**: This config customization feature can only be used via Helm. 172 | 173 | Each of the Keepalived daemon pods gets received it's configuration from a ConfigMap that gets generated by the Keepalived Operator from a configuration file template. 174 | If you need to customize the configuration for your Keepalived daemon pods, you'll want to use the following steps. 175 | 176 | Create a ConfigMap with the full contents of this configuration template file: 177 | https://github.com/redhat-cop/keepalived-operator/blob/master/config/templates/keepalived-template.yaml 178 | 179 | ``` 180 | apiVersion: v1 181 | kind: ConfigMap 182 | metadata: 183 | name: keepalived-template 184 | namespace: {{ .KeepalivedGroup.ObjectMeta.Namespace }} 185 | labels: 186 | keepalivedGroup: {{ .KeepalivedGroup.ObjectMeta.Name }} 187 | data: 188 | keepalived.conf: | 189 | ... 190 | # expected merge structure 191 | # .KeepAlivedGroup 192 | # .Services 193 | - apiVersion: apps/v1 194 | kind: DaemonSet 195 | metadata: 196 | name: {{ .KeepalivedGroup.ObjectMeta.Name }} 197 | namespace: {{ .KeepalivedGroup.ObjectMeta.Namespace }} 198 | spec: 199 | ... 200 | ``` 201 | 202 | Then in the Helm Chart set `keepalivedTemplateFromConfigMap: keepalived-template` 203 | 204 | This will override the `/templates/keepalived-template.yaml` config file in the keepalived-operator pod which will allow you to update the configs without having to rebuild/push the operator docker image. 205 | 206 | 207 | ## Metrics collection 208 | 209 | Each keepalived pod exposes a [Prometheus](https://prometheus.io/) metrics port at `9650`. Metrics are collected with [keepalived_exporter](github.com/gen2brain/keepalived_exporter), the available metrics are described in the project documentation. 210 | 211 | When a keepalived group is created a [`PodMonitor`](https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#podmonitor) rule to collect those metrics. All PodMonitor resources created that way have the label: `metrics: keepalived`. It is up to you to make sure your Prometheus instance watches for those `PodMonitor` rules. Here is an example of a fragment of a `Prometheus` CR configured to collect the keepalived pod metrics: 212 | 213 | ```yaml 214 | podMonitorSelector: 215 | matchLabels: 216 | metrics: keepalived 217 | ``` 218 | 219 | In order to enable the collection of these metrics by the platform prometheus you have to appropriately label the namespace in which the `KeepalivedGroup` CR was created: 220 | 221 | ```shell 222 | oc label namespace openshift.io/cluster-monitoring="true" 223 | ``` 224 | 225 | ## Deploying the Operator 226 | 227 | This is a cluster-level operator that you can deploy in any namespace, `keepalived-operator` is recommended. 228 | 229 | It is recommended to deploy this operator via [`OperatorHub`](https://operatorhub.io/), but you can also deploy it using [`Helm`](https://helm.sh/). 230 | 231 | ### Multiarch Support 232 | 233 | | Arch | Support | 234 | |:-:|:-:| 235 | | amd64 | ✅ | 236 | | arm64 | ✅ | 237 | | ppc64le | ✅ | 238 | | s390x | ❌ | 239 | 240 | ### Deploying from OperatorHub 241 | 242 | > **Note**: This operator supports being installed disconnected environments 243 | 244 | If you want to utilize the Operator Lifecycle Manager (OLM) to install this operator, you can do so in two ways: from the UI or the CLI. 245 | 246 | #### Deploying from OperatorHub UI 247 | 248 | * If you would like to launch this operator from the UI, you'll need to navigate to the OperatorHub tab in the console.Before starting, make sure you've created the namespace that you want to install this operator to with the following: 249 | 250 | ```shell 251 | oc new-project keepalived-operator 252 | ``` 253 | 254 | * Once there, you can search for this operator by name: `keepalived`. This will then return an item for our operator and you can select it to get started. Once you've arrived here, you'll be presented with an option to install, which will begin the process. 255 | * After clicking the install button, you can then select the namespace that you would like to install this to as well as the installation strategy you would like to proceed with (`Automatic` or `Manual`). 256 | * Once you've made your selection, you can select `Subscribe` and the installation will begin. After a few moments you can go ahead and check your namespace and you should see the operator running. 257 | 258 | ![Keepalived Operator](./media/keepalived-operator.png) 259 | 260 | #### Deploying from OperatorHub using CLI 261 | 262 | If you'd like to launch this operator from the command line, you can use the manifests contained in this repository by running the following: 263 | 264 | ```shell 265 | oc new-project keepalived-operator 266 | oc apply -f config/operatorhub -n keepalived-operator 267 | ``` 268 | 269 | This will create the appropriate OperatorGroup and Subscription and will trigger OLM to launch the operator in the specified namespace. 270 | 271 | ### Deploying with Helm 272 | 273 | Here are the instructions to install the latest release with Helm. 274 | 275 | ```shell 276 | oc new-project keepalived-operator 277 | helm repo add keepalived-operator https://redhat-cop.github.io/keepalived-operator 278 | helm repo update 279 | helm install keepalived-operator keepalived-operator/keepalived-operator 280 | ``` 281 | 282 | This can later be updated with the following commands: 283 | 284 | ```shell 285 | helm repo update 286 | helm upgrade keepalived-operator keepalived-operator/keepalived-operator 287 | ``` 288 | 289 | ## Metrics 290 | 291 | Prometheus compatible metrics are exposed by the Operator and can be integrated into OpenShift's default cluster monitoring. To enable OpenShift cluster monitoring, label the namespace the operator is deployed in with the label `openshift.io/cluster-monitoring="true"`. 292 | 293 | ```shell 294 | oc label namespace openshift.io/cluster-monitoring="true" 295 | ``` 296 | 297 | ### Testing metrics 298 | 299 | ```sh 300 | export operatorNamespace=keepalived-operator-local # or keepalived-operator 301 | oc label namespace ${operatorNamespace} openshift.io/cluster-monitoring="true" 302 | oc rsh -n openshift-monitoring -c prometheus prometheus-k8s-0 /bin/bash 303 | export operatorNamespace=keepalived-operator-local # or keepalived-operator 304 | curl -v -s -k -H "Authorization: Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" https://keepalived-operator-controller-manager-metrics.${operatorNamespace}.svc.cluster.local:8443/metrics 305 | exit 306 | ``` 307 | 308 | ## Development 309 | 310 | ### Running the operator locally 311 | 312 | > Note: this operator build process is tested with [podman](https://podman.io/), but some of the build files (Makefile specifically) use docker because they are generated automatically by operator-sdk. It is recommended [remap the docker command to the podman command](https://developers.redhat.com/blog/2020/11/19/transitioning-from-docker-to-podman#transition_to_the_podman_cli). 313 | 314 | ```shell 315 | export repo=raffaelespazzoli 316 | docker login quay.io/$repo 317 | oc new-project keepalived-operator 318 | oc project keepalived-operator 319 | tilt up 320 | ``` 321 | 322 | ### Test helm chart locally 323 | 324 | Define an image and tag. For example... 325 | 326 | ```shell 327 | export imageRepository="quay.io/redhat-cop/keepalived-operator" 328 | export imageTag="$(git -c 'versionsort.suffix=-' ls-remote --exit-code --refs --sort='version:refname' --tags https://github.com/redhat-cop/keepalived-operator.git '*.*.*' | tail --lines=1 | cut --delimiter='/' --fields=3)" 329 | ``` 330 | 331 | Deploy chart... 332 | 333 | ```shell 334 | make helmchart IMG=${imageRepository} VERSION=${imageTag} 335 | helm upgrade -i keepalived-operator-local charts/keepalived-operator -n keepalived-operator-local --create-namespace 336 | ``` 337 | 338 | Delete... 339 | 340 | ```shell 341 | helm delete keepalived-operator-local -n keepalived-operator-local 342 | kubectl delete -f charts/keepalived-operator/crds/crds.yaml 343 | ``` 344 | 345 | ### Building/Pushing the operator image 346 | 347 | ```shell 348 | export repo=raffaelespazzoli #replace with yours 349 | docker login quay.io/$repo/keepalived-operator 350 | make docker-build IMG=quay.io/$repo/keepalived-operator:latest 351 | make docker-push IMG=quay.io/$repo/keepalived-operator:latest 352 | ``` 353 | 354 | ### Deploy to OLM via bundle 355 | 356 | ```shell 357 | make manifests 358 | make bundle IMG=quay.io/$repo/keepalived-operator:latest 359 | operator-sdk bundle validate ./bundle --select-optional name=operatorhub 360 | make bundle-build BUNDLE_IMG=quay.io/$repo/keepalived-operator-bundle:latest 361 | docker login quay.io/$repo/keepalived-operator-bundle 362 | docker push quay.io/$repo/keepalived-operator-bundle:latest 363 | operator-sdk bundle validate quay.io/$repo/keepalived-operator-bundle:latest --select-optional name=operatorhub 364 | oc new-project keepalived-operator 365 | oc label namespace keepalived-operator openshift.io/cluster-monitoring="true" --overwrite 366 | operator-sdk cleanup keepalived-operator -n keepalived-operator 367 | operator-sdk run bundle --install-mode AllNamespaces -n keepalived-operator quay.io/$repo/keepalived-operator-bundle:latest 368 | ``` 369 | 370 | ## Integration Test 371 | 372 | ```sh 373 | make helmchart-test 374 | ``` 375 | 376 | ### Testing 377 | 378 | Add an external IP CIDR to your cluster to manage 379 | 380 | ```shell 381 | export CIDR="192.168.130.128/28" 382 | oc patch network cluster -p "$(envsubst < ./test/externalIP-patch.yaml | yq r -j -)" --type=merge 383 | ``` 384 | 385 | create a project that uses a LoadBalancer Service 386 | 387 | ```shell 388 | oc new-project test-keepalived-operator 389 | oc new-app django-psql-example -n test-keepalived-operator 390 | oc delete route django-psql-example -n test-keepalived-operator 391 | oc patch service django-psql-example -n test-keepalived-operator -p '{"spec":{"type":"LoadBalancer"}}' --type=strategic 392 | export SERVICE_IP=$(oc get svc django-psql-example -n test-keepalived-operator -o jsonpath='{.status.loadBalancer.ingress[0].ip}') 393 | ``` 394 | 395 | create a keepalivedgroup 396 | 397 | ```shell 398 | oc adm policy add-scc-to-user privileged -z default -n test-keepalived-operator 399 | oc apply -f ./test/keepalivedgroup.yaml -n test-keepalived-operator 400 | ``` 401 | 402 | annotate the service to be used by keepalived 403 | 404 | ```shell 405 | oc annotate svc django-psql-example -n test-keepalived-operator keepalived-operator.redhat-cop.io/keepalivedgroup=test-keepalived-operator/keepalivedgroup-test 406 | ``` 407 | 408 | curl the app using the service IP 409 | 410 | ```shell 411 | curl http://$SERVICE_IP:8080 412 | ``` 413 | 414 | test with a second keepalived group 415 | 416 | ```shell 417 | oc apply -f ./test/test-servicemultiple.yaml -n test-keepalived-operator 418 | oc apply -f ./test/keepalivedgroup2.yaml -n test-keepalived-operator 419 | oc apply -f ./test/test-service-g2.yaml -n test-keepalived-operator 420 | ``` 421 | 422 | ### Releasing 423 | 424 | ```shell 425 | git tag -a "" -m "" 426 | git push upstream 427 | ``` 428 | 429 | If you need to remove a release: 430 | 431 | ```shell 432 | git tag -d 433 | git push upstream --delete 434 | ``` 435 | 436 | If you need to "move" a release to the current main 437 | 438 | ```shell 439 | git tag -f 440 | git push upstream -f 441 | ``` 442 | 443 | ### Cleaning up 444 | 445 | ```shell 446 | operator-sdk cleanup keepalived-operator -n keepalived-operator 447 | oc delete operatorgroup operator-sdk-og 448 | oc delete catalogsource keepalived-operator-catalog 449 | ``` 450 | -------------------------------------------------------------------------------- /controllers/keepalivedgroup_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers 18 | 19 | import ( 20 | "context" 21 | "encoding/json" 22 | "errors" 23 | "fmt" 24 | "io/ioutil" 25 | "os" 26 | "sort" 27 | "strings" 28 | "text/template" 29 | 30 | "github.com/go-logr/logr" 31 | redhatcopv1alpha1 "github.com/redhat-cop/keepalived-operator/api/v1alpha1" 32 | "github.com/redhat-cop/operator-utils/pkg/util" 33 | "github.com/redhat-cop/operator-utils/pkg/util/apis" 34 | "github.com/redhat-cop/operator-utils/pkg/util/templates" 35 | "github.com/scylladb/go-set/iset" 36 | "github.com/scylladb/go-set/strset" 37 | corev1 "k8s.io/api/core/v1" 38 | apierrors "k8s.io/apimachinery/pkg/api/errors" 39 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 40 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 41 | "k8s.io/apimachinery/pkg/labels" 42 | "k8s.io/apimachinery/pkg/types" 43 | "k8s.io/client-go/util/workqueue" 44 | ctrl "sigs.k8s.io/controller-runtime" 45 | "sigs.k8s.io/controller-runtime/pkg/builder" 46 | "sigs.k8s.io/controller-runtime/pkg/client" 47 | "sigs.k8s.io/controller-runtime/pkg/event" 48 | "sigs.k8s.io/controller-runtime/pkg/handler" 49 | "sigs.k8s.io/controller-runtime/pkg/predicate" 50 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 51 | "sigs.k8s.io/controller-runtime/pkg/source" 52 | ) 53 | 54 | const ( 55 | controllerName = "keepalived-controller" 56 | templateFileNameEnv = "KEEPALIVEDGROUP_TEMPLATE_FILE_NAME" 57 | imageNameEnv = "KEEPALIVED_OPERATOR_IMAGE_NAME" 58 | keepalivedGroupAnnotation = "keepalived-operator.redhat-cop.io/keepalivedgroup" 59 | keepalivedGroupVerbatimConfigAnnotation = "keepalived-operator.redhat-cop.io/verbatimconfig" 60 | keepalivedSpreadVIPsAnnotation = "keepalived-operator.redhat-cop.io/spreadvips" 61 | keepalivedGroupLabel = "keepalivedGroup" 62 | podMonitorAPIVersion = "monitoring.coreos.com/v1" 63 | podMonitorKind = "PodMonitor" 64 | ) 65 | 66 | // KeepalivedGroupReconciler reconciles a KeepalivedGroup object 67 | type KeepalivedGroupReconciler struct { 68 | util.ReconcilerBase 69 | Log logr.Logger 70 | supportsPodMonitors string 71 | keepalivedTemplate *template.Template 72 | } 73 | 74 | func (r *KeepalivedGroupReconciler) setSupportForPodMonitorAvailable() { 75 | r.supportsPodMonitors = "false" 76 | discoveryClient, err := r.GetDiscoveryClient() 77 | 78 | if err != nil { 79 | r.Log.Error(err, "failed to initialize discovery client") 80 | return 81 | } 82 | 83 | resources, resourcesErr := discoveryClient.ServerResourcesForGroupVersion(podMonitorAPIVersion) 84 | 85 | if resourcesErr != nil { 86 | r.Log.Error(err, "failed to discover resources") 87 | return 88 | } 89 | 90 | for _, apiResource := range resources.APIResources { 91 | if apiResource.Kind == podMonitorKind { 92 | r.supportsPodMonitors = "true" 93 | break 94 | } 95 | } 96 | } 97 | 98 | // +kubebuilder:rbac:groups=redhatcop.redhat.io,resources=keepalivedgroups,verbs=get;list;watch;create;update;patch;delete 99 | // +kubebuilder:rbac:groups=redhatcop.redhat.io,resources=keepalivedgroups/status,verbs=get;update;patch 100 | // +kubebuilder:rbac:groups=redhatcop.redhat.io,resources=keepalivedgroups/finalizers,verbs=update 101 | // +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;patch;delete 102 | // +kubebuilder:rbac:groups="",resources=configmaps/finalizers,verbs=update 103 | // +kubebuilder:rbac:groups="",resources=services;endpoints;pods;secrets,verbs=get;list;watch 104 | // +kubebuilder:rbac:groups="apps",resources=daemonsets;daemonsets/finalizers,verbs=get;list;watch;create;update;patch;delete 105 | // +kubebuilder:rbac:groups="monitoring.coreos.com",resources=podmonitors,verbs=get;list;watch;create;update;patch;delete 106 | // +kubebuilder:rbac:groups="monitoring.coreos.com",resources=podmonitors/finalizers,verbs=update 107 | // +kubebuilder:rbac:groups="",resources=events,verbs=get;list;watch;create;patch 108 | // +kubebuilder:rbac:groups="rbac.authorization.k8s.io",resources=roles;rolebindings,verbs=get;list;watch;create;update;patch;delete 109 | 110 | // Reconcile is part of the main kubernetes reconciliation loop which aims to 111 | // move the current state of the cluster closer to the desired state. 112 | // TODO(user): Modify the Reconcile function to compare the state specified by 113 | // the KeepalivedGroup object against the actual cluster state, and then 114 | // perform operations to make the cluster state reflect the state specified by 115 | // the user. 116 | // 117 | // For more details, check Reconcile and its Result here: 118 | // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.0/pkg/reconcile 119 | func (r *KeepalivedGroupReconciler) Reconcile(context context.Context, req ctrl.Request) (ctrl.Result, error) { 120 | log := r.Log.WithValues("keepalivedgroup", req.NamespacedName) 121 | 122 | // Fetch the KeepalivedGroup instance 123 | instance := &redhatcopv1alpha1.KeepalivedGroup{} 124 | err := r.GetClient().Get(context, req.NamespacedName, instance) 125 | if err != nil { 126 | if apierrors.IsNotFound(err) { 127 | // Request object not found, could have been deleted after reconcile request. 128 | // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. 129 | // Return and don't requeue 130 | return reconcile.Result{}, nil 131 | } 132 | // Error reading the object - requeue the request. 133 | return reconcile.Result{}, err 134 | } 135 | 136 | if ok, err := r.IsValid(instance); !ok { 137 | return r.ManageError(context, instance, err) 138 | } 139 | 140 | if ok := r.IsInitialized(instance); !ok { 141 | err := r.GetClient().Update(context, instance) 142 | if err != nil { 143 | log.Error(err, "unable to update instance", "instance", instance) 144 | return r.ManageError(context, instance, err) 145 | } 146 | return reconcile.Result{}, nil 147 | } 148 | 149 | // Check if VRRP authentication is needed and if so extract credentials 150 | authPass := "" 151 | if instance.Spec.PasswordAuth.SecretRef.Name != "" { 152 | secret := &corev1.Secret{} 153 | err := r.GetClient().Get(context, types.NamespacedName{Namespace: instance.GetNamespace(), Name: instance.Spec.PasswordAuth.SecretRef.Name}, secret) 154 | if err != nil { 155 | // Requeue and log error 156 | log.Error(err, "could not find passwordAuth secret", "instance", instance) 157 | return r.ManageError(context, instance, err) 158 | } 159 | pass, ok := secret.Data[instance.Spec.PasswordAuth.SecretKey] 160 | if !ok { 161 | // Requeue and log error 162 | err = fmt.Errorf("could not find key %s in secret %s in namespace %s", instance.Spec.PasswordAuth.SecretKey, instance.Spec.PasswordAuth.SecretRef.Name, instance.GetNamespace()) 163 | log.Error(err, "could not find referenced key in passwordAuth secret", "instance", instance) 164 | return r.ManageError(context, instance, err) 165 | } 166 | authPass = string(pass) 167 | } 168 | 169 | pods, err := r.getKeepalivedPods(instance) 170 | services, err := r.getReferencingServices(instance) 171 | if err != nil { 172 | log.Error(err, "unable to get referencing services from", "instance", instance) 173 | return r.ManageError(context, instance, err) 174 | } 175 | _, err = r.assignRouterIDs(instance, services) 176 | if err != nil { 177 | log.Error(err, "unable assign router ids to", "instance", instance, "from services", services) 178 | return r.ManageError(context, instance, err) 179 | } 180 | objs, err := r.processTemplate(context, instance, services, pods, authPass) 181 | if err != nil { 182 | log.Error(err, "unable process keepalived template from", "instance", instance, "and from services", services) 183 | return r.ManageError(context, instance, err) 184 | } 185 | 186 | // this code needs to be commented until this bug is resolved: https://github.com/kubernetes-sigs/yaml/issues/47 187 | // lockedResources := []lockedresource.LockedResource{} 188 | // for _, obj := range *objs { 189 | // lockedResource := lockedresource.LockedResource{ 190 | // Unstructured: obj, 191 | // } 192 | // lockedResources = append(lockedResources, lockedResource) 193 | // } 194 | 195 | // err = r.UpdateLockedResources(context, instance, lockedResources, []lockedpatch.LockedPatch{}) 196 | // if err != nil { 197 | // log.Error(err, "unable to update locked resources") 198 | // return r.ManageError(context, instance, err) 199 | // } 200 | 201 | // this code needs to stay here until this bug is resolved: https://github.com/kubernetes-sigs/yaml/issues/47 202 | for _, obj := range *objs { 203 | err = r.CreateOrUpdateResource(context, instance, instance.GetNamespace(), &obj) 204 | if err != nil { 205 | log.Error(err, "unable to create or update resource", "resource", obj) 206 | return r.ManageError(context, instance, err) 207 | } 208 | } 209 | return r.ManageSuccess(context, instance) 210 | } 211 | 212 | func (r *KeepalivedGroupReconciler) assignRouterIDs(instance *redhatcopv1alpha1.KeepalivedGroup, services []corev1.Service) (bool, error) { 213 | assignedInstances := []string{} 214 | assignedIDs := []int{} 215 | if len(instance.Spec.BlacklistRouterIDs) > 0 { 216 | assignedIDs = append(assignedIDs, instance.Spec.BlacklistRouterIDs...) 217 | for key, val := range instance.Status.RouterIDs { 218 | for _, id := range instance.Spec.BlacklistRouterIDs { 219 | if val == id { 220 | delete(instance.Status.RouterIDs, key) 221 | break 222 | } 223 | } 224 | } 225 | } 226 | for key := range instance.Status.RouterIDs { 227 | assignedInstances = append(assignedInstances, key) 228 | } 229 | vrrpInstances := servicesToVRRPInstances(services) 230 | 231 | assignedInstancesSet := strset.New(assignedInstances...) 232 | vrrpInstancesSet := strset.New(vrrpInstances...) 233 | toBeRemovedSet := strset.Difference(assignedInstancesSet, vrrpInstancesSet) 234 | toBeAddedSet := strset.Difference(vrrpInstancesSet, assignedInstancesSet) 235 | 236 | for _, value := range toBeRemovedSet.List() { 237 | delete(instance.Status.RouterIDs, value) 238 | } 239 | for _, value := range instance.Status.RouterIDs { 240 | assignedIDs = append(assignedIDs, value) 241 | } 242 | // remove potential duplicates and sort 243 | assignedIDs = iset.New(assignedIDs...).List() 244 | if instance.Status.RouterIDs == nil { 245 | instance.Status.RouterIDs = map[string]int{} 246 | } 247 | for _, value := range toBeAddedSet.List() { 248 | id, err := findNextAvailableID(assignedIDs) 249 | if err != nil { 250 | r.Log.Error(err, "unable assign a router id to", "service", value) 251 | return false, err 252 | } 253 | instance.Status.RouterIDs[value] = id 254 | assignedIDs = append(assignedIDs, instance.Status.RouterIDs[value]) 255 | } 256 | return (toBeAddedSet.Size() > 0 || toBeRemovedSet.Size() > 0), nil 257 | } 258 | 259 | func findNextAvailableID(ids []int) (int, error) { 260 | if len(ids) == 0 { 261 | return 1, nil 262 | } 263 | usedSet := iset.New(ids...) 264 | for i := 1; i <= 255; i++ { 265 | used := false 266 | if usedSet.Has(i) { 267 | used = true 268 | } 269 | if !used { 270 | return i, nil 271 | } 272 | } 273 | return 0, errors.New("cannot allocate more than 255 ids in one keepalived group") 274 | } 275 | 276 | func servicesToVRRPInstances(services []corev1.Service) []string { 277 | vrrpInstances := []string{} 278 | for _, service := range services { 279 | svcName := apis.GetKeyShort(&service) 280 | if ann, ok := service.GetAnnotations()[keepalivedSpreadVIPsAnnotation]; ok && ann == "true" { 281 | for _, ingress := range service.Status.LoadBalancer.Ingress { 282 | vrrpInstances = append(vrrpInstances, svcName+"/"+ingress.IP) 283 | } 284 | for _, ip := range service.Spec.ExternalIPs { 285 | vrrpInstances = append(vrrpInstances, svcName+"/"+ip) 286 | } 287 | } else { 288 | vrrpInstances = append(vrrpInstances, svcName) 289 | } 290 | } 291 | 292 | return vrrpInstances 293 | } 294 | 295 | func (r *KeepalivedGroupReconciler) processTemplate(ctx context.Context, instance *redhatcopv1alpha1.KeepalivedGroup, services []corev1.Service, pods []corev1.Pod, authPass string) (*[]unstructured.Unstructured, error) { 296 | // sort services and pods to ensure deterministic template output 297 | sort.SliceStable(services, func(i, j int) bool { 298 | if services[i].GetNamespace() == services[j].GetNamespace() { 299 | return services[i].GetName() < services[j].GetName() 300 | } 301 | return services[i].GetNamespace() < services[j].GetNamespace() 302 | }) 303 | sort.SliceStable(pods, func(i, j int) bool { 304 | return pods[i].GetName() < pods[j].GetName() 305 | }) 306 | 307 | imagename, ok := os.LookupEnv(imageNameEnv) 308 | if !ok { 309 | imagename = "quay.io/redhat-cop/keepalived-operator:latest" 310 | } 311 | objs, err := templates.ProcessTemplateArray(ctx, struct { 312 | KeepalivedGroup *redhatcopv1alpha1.KeepalivedGroup 313 | Services []corev1.Service 314 | KeepalivedPods []corev1.Pod 315 | Misc map[string]string 316 | }{ 317 | instance, 318 | services, 319 | pods, 320 | map[string]string{ 321 | "image": imagename, 322 | "supportsPodMonitor": r.supportsPodMonitors, 323 | "authPass": authPass, 324 | }, 325 | }, r.keepalivedTemplate) 326 | if err != nil { 327 | r.Log.Error(err, "unable to process template") 328 | return &[]unstructured.Unstructured{}, err 329 | } 330 | return &objs, nil 331 | } 332 | 333 | func (r *KeepalivedGroupReconciler) getKeepalivedPods(instance *redhatcopv1alpha1.KeepalivedGroup) ([]corev1.Pod, error) { 334 | podList := &corev1.PodList{} 335 | err := r.GetClient().List(context.TODO(), podList, &client.ListOptions{Namespace: instance.GetNamespace(), LabelSelector: labels.SelectorFromSet(map[string]string{keepalivedGroupLabel: instance.GetName()})}) 336 | if err != nil { 337 | r.Log.Error(err, "unable to get list of keepalived pods") 338 | return corev1.PodList{}.Items, err 339 | } 340 | return podList.Items, nil 341 | } 342 | 343 | func (r *KeepalivedGroupReconciler) getReferencingServices(instance *redhatcopv1alpha1.KeepalivedGroup) ([]corev1.Service, error) { 344 | serviceList := &corev1.ServiceList{} 345 | err := r.GetClient().List(context.TODO(), serviceList, &client.ListOptions{}) 346 | if err != nil { 347 | r.Log.Error(err, "unable to get list of load balancer services") 348 | return corev1.ServiceList{}.Items, err 349 | } 350 | //filter the returned list 351 | result := []corev1.Service{} 352 | for _, service := range serviceList.Items { 353 | value, ok := service.GetAnnotations()[keepalivedGroupAnnotation] 354 | if ok && (service.Spec.Type == corev1.ServiceTypeLoadBalancer || len(service.Spec.ExternalIPs) > 0) { 355 | namespacedName, err := getNamespacedName(value) 356 | if err != nil { 357 | r.Log.Error(err, "unable to create namespaced name from ", "service", apis.GetKeyShort(&service), "annotation", keepalivedGroupAnnotation, "value", value) 358 | continue 359 | } 360 | if namespacedName.Name == instance.GetName() && namespacedName.Namespace == instance.GetNamespace() { 361 | result = append(result, service) 362 | } 363 | } 364 | } 365 | return result, nil 366 | } 367 | 368 | // func (r *KeepalivedGroupReconciler) IsInitialized(instance *redhatcopv1alpha1.KeepalivedGroup) bool { 369 | // initialized := true 370 | // if instance.Spec.Image == "" { 371 | // instance.Spec.Image = "registry.redhat.io/openshift4/ose-keepalived-ipfailover" 372 | // initialized = false 373 | // } 374 | // return initialized 375 | // } 376 | 377 | func (r *KeepalivedGroupReconciler) initializeTemplate() (*template.Template, error) { 378 | templateFileName, ok := os.LookupEnv(templateFileNameEnv) 379 | if !ok { 380 | templateFileName = "/etc/templates/job.template.yaml" 381 | } 382 | text, err := ioutil.ReadFile(templateFileName) 383 | if err != nil { 384 | r.Log.Error(err, "Error reading job template file", "filename", templateFileName) 385 | return &template.Template{}, err 386 | } 387 | jobTemplate, err := template.New("KeepalivedGroup").Funcs(template.FuncMap{ 388 | "parseJson": func(jsonstr string) map[string]string { 389 | if jsonstr == "" { 390 | return map[string]string{} 391 | } 392 | var m map[string]string 393 | err := json.Unmarshal([]byte(jsonstr), &m) 394 | if err != nil { 395 | r.Log.Error(err, "unable to unmarshal json ", "string", jsonstr) 396 | return map[string]string{} 397 | } 398 | return m 399 | }, 400 | "mergeStringSlices": func(lbis []corev1.LoadBalancerIngress, s2 []string) []string { 401 | var s1 = []string{} 402 | for _, lbi := range lbis { 403 | if lbi.IP != "" { 404 | s1 = append(s1, lbi.IP) 405 | } 406 | } 407 | return strset.Union(strset.New(s1...), strset.New(s2...)).List() 408 | }, 409 | "modulus": func(a, b int) int { return a % b }, 410 | }).Parse(string(text)) 411 | if err != nil { 412 | r.Log.Error(err, "Error parsing template", "template", string(text)) 413 | return &template.Template{}, err 414 | } 415 | return jobTemplate, err 416 | } 417 | 418 | type enqueueRequestForReferredKeepAlivedGroup struct { 419 | client.Client 420 | Log logr.Logger 421 | } 422 | 423 | func (e *enqueueRequestForReferredKeepAlivedGroup) Create(evt event.CreateEvent, q workqueue.RateLimitingInterface) { 424 | keepalivedGroup, ok := evt.Object.GetAnnotations()[keepalivedGroupAnnotation] 425 | if ok { 426 | namespaced, err := getNamespacedName(keepalivedGroup) 427 | if err != nil { 428 | e.Log.Error(err, "unable to create namespaced name from", "annotation", keepalivedGroupAnnotation, "value", keepalivedGroup) 429 | return 430 | } 431 | q.Add(reconcile.Request{NamespacedName: namespaced}) 432 | } 433 | } 434 | 435 | func (e *enqueueRequestForReferredKeepAlivedGroup) Update(evt event.UpdateEvent, q workqueue.RateLimitingInterface) { 436 | keepalivedGroup, ok := evt.ObjectNew.GetAnnotations()[keepalivedGroupAnnotation] 437 | if ok { 438 | namespaced, err := getNamespacedName(keepalivedGroup) 439 | if err != nil { 440 | e.Log.Info(err.Error(), "unable to create namespaced name from MetaNew", "annotation", keepalivedGroupAnnotation, "value", keepalivedGroup) 441 | } else { 442 | q.Add(reconcile.Request{NamespacedName: namespaced}) 443 | } 444 | } 445 | keepalivedGroup, ok = evt.ObjectOld.GetAnnotations()[keepalivedGroupAnnotation] 446 | if ok { 447 | namespaced, err := getNamespacedName(keepalivedGroup) 448 | if err != nil { 449 | e.Log.Info(err.Error(), "unable to create namespaced name from MetaOld", "annotation", keepalivedGroupAnnotation, "value", keepalivedGroup) 450 | } else { 451 | q.Add(reconcile.Request{NamespacedName: namespaced}) 452 | } 453 | } 454 | } 455 | 456 | // Delete implements EventHandler 457 | func (e *enqueueRequestForReferredKeepAlivedGroup) Delete(evt event.DeleteEvent, q workqueue.RateLimitingInterface) { 458 | keepalivedGroup, ok := evt.Object.GetAnnotations()[keepalivedGroupAnnotation] 459 | if ok { 460 | namespaced, err := getNamespacedName(keepalivedGroup) 461 | if err != nil { 462 | e.Log.Error(err, "unable to create namespaced name from", "annotation", keepalivedGroupAnnotation, "value", keepalivedGroup) 463 | return 464 | } 465 | q.Add(reconcile.Request{NamespacedName: namespaced}) 466 | } 467 | } 468 | 469 | // Generic implements EventHandler 470 | func (e *enqueueRequestForReferredKeepAlivedGroup) Generic(evt event.GenericEvent, q workqueue.RateLimitingInterface) { 471 | return 472 | } 473 | 474 | func getNamespacedName(namespaced string) (types.NamespacedName, error) { 475 | elements := strings.Split(namespaced, "/") 476 | if len(elements) != 2 { 477 | return types.NamespacedName{}, errors.New("unable to split string into name and namespace using '/' as separator: " + namespaced) 478 | } 479 | return types.NamespacedName{ 480 | Name: elements[1], 481 | Namespace: elements[0], 482 | }, nil 483 | } 484 | 485 | // Handler to issue reconciles for KeepalivedGroup resources based on changes on keepalived pods 486 | func (r *KeepalivedGroupReconciler) requestsForKeepalivedPodChange(obj client.Object) []reconcile.Request { 487 | pod, ok := obj.(*corev1.Pod) 488 | if !ok { 489 | r.Log.Error(fmt.Errorf("expected a Pod, got %T", pod), "could not process pod change") 490 | return nil 491 | } 492 | 493 | keepalivedGroup, ok := pod.GetLabels()[keepalivedGroupLabel] 494 | if !ok { 495 | r.Log.Error(fmt.Errorf("could not extract keepalivedGroup from keepalived pod %s in namespace %s", pod.GetName(), pod.GetNamespace()), "could not process pod change") 496 | return nil 497 | } 498 | return []reconcile.Request{{NamespacedName: types.NamespacedName{Namespace: pod.GetNamespace(), Name: keepalivedGroup}}} 499 | } 500 | 501 | // PodChange is a predicate that filters Pod changes to issue KeepalivedGroup reconciles for creation and deletion of keepalived pods 502 | type PodChange struct { 503 | predicate.Funcs 504 | } 505 | 506 | // Update filters out pod updates 507 | func (PodChange) Update(e event.UpdateEvent) bool { 508 | return false 509 | } 510 | 511 | // Create filters out pod creations if they are not keepalived pods 512 | func (PodChange) Create(e event.CreateEvent) bool { 513 | pod, ok := e.Object.(*corev1.Pod) 514 | if !ok { 515 | return false 516 | } 517 | if _, ok := pod.GetLabels()[keepalivedGroupLabel]; !ok { 518 | return false 519 | } 520 | return true 521 | } 522 | 523 | // Delete filters out pod deletions if they are not keepalived pods 524 | func (PodChange) Delete(e event.DeleteEvent) bool { 525 | pod, ok := e.Object.(*corev1.Pod) 526 | if !ok { 527 | return false 528 | } 529 | if _, ok := pod.GetLabels()[keepalivedGroupLabel]; !ok { 530 | return false 531 | } 532 | return true 533 | } 534 | 535 | // SetupWithManager sets up the controller with the Manager. 536 | func (r *KeepalivedGroupReconciler) SetupWithManager(mgr ctrl.Manager) error { 537 | r.setSupportForPodMonitorAvailable() 538 | keepalivedTemplate, err := r.initializeTemplate() 539 | if err != nil { 540 | r.Log.Error(err, "unable to initialize job template") 541 | return err 542 | } 543 | r.keepalivedTemplate = keepalivedTemplate 544 | // this will filter new secrets and secrets where the content changed 545 | // secret that are actually referenced by routes will be filtered by the handler 546 | isAnnotatedService := predicate.Funcs{ 547 | UpdateFunc: func(e event.UpdateEvent) bool { 548 | service, ok := e.ObjectNew.DeepCopyObject().(*corev1.Service) 549 | if ok { 550 | if _, ok := service.GetAnnotations()[keepalivedGroupAnnotation]; ok && (service.Spec.Type == corev1.ServiceTypeLoadBalancer || len(service.Spec.ExternalIPs) > 0) { 551 | return true 552 | } 553 | } 554 | service, ok = e.ObjectOld.DeepCopyObject().(*corev1.Service) 555 | if ok { 556 | if _, ok := service.GetAnnotations()[keepalivedGroupAnnotation]; ok && (service.Spec.Type == corev1.ServiceTypeLoadBalancer || len(service.Spec.ExternalIPs) > 0) { 557 | return true 558 | } 559 | } 560 | return false 561 | }, 562 | CreateFunc: func(e event.CreateEvent) bool { 563 | service, ok := e.Object.DeepCopyObject().(*corev1.Service) 564 | if !ok { 565 | return false 566 | } 567 | if _, ok := service.GetAnnotations()[keepalivedGroupAnnotation]; ok && (service.Spec.Type == corev1.ServiceTypeLoadBalancer || len(service.Spec.ExternalIPs) > 0) { 568 | return true 569 | } 570 | return false 571 | }, 572 | DeleteFunc: func(e event.DeleteEvent) bool { 573 | service, ok := e.Object.DeepCopyObject().(*corev1.Service) 574 | if !ok { 575 | return false 576 | } 577 | if _, ok := service.GetAnnotations()[keepalivedGroupAnnotation]; ok && (service.Spec.Type == corev1.ServiceTypeLoadBalancer || len(service.Spec.ExternalIPs) > 0) { 578 | return true 579 | } 580 | return false 581 | }, 582 | } 583 | 584 | return ctrl.NewControllerManagedBy(mgr). 585 | For(&redhatcopv1alpha1.KeepalivedGroup{}, builder.WithPredicates(util.ResourceGenerationOrFinalizerChangedPredicate{})). 586 | Watches(&source.Kind{Type: &corev1.Service{ 587 | TypeMeta: metav1.TypeMeta{ 588 | Kind: "Service", 589 | }, 590 | }}, &enqueueRequestForReferredKeepAlivedGroup{ 591 | Client: mgr.GetClient(), 592 | }, builder.WithPredicates(isAnnotatedService)). 593 | Watches(&source.Kind{Type: &corev1.Pod{}}, 594 | handler.EnqueueRequestsFromMapFunc(r.requestsForKeepalivedPodChange), 595 | builder.WithPredicates(PodChange{}), 596 | ). 597 | Complete(r) 598 | } 599 | --------------------------------------------------------------------------------