├── config ├── prometheus │ ├── kustomization.yaml │ └── monitor.yaml ├── rbac │ ├── service_account.yaml │ ├── metrics_reader_role.yaml │ ├── role_binding.yaml │ ├── leader_election_role_binding.yaml │ ├── metrics_auth_role_binding.yaml │ ├── metrics_auth_role.yaml │ ├── api_viewer_role.yaml │ ├── group_viewer_role.yaml │ ├── apicheck_viewer_role.yaml │ ├── checkly_alertchannel_viewer_role.yaml │ ├── api_editor_role.yaml │ ├── group_editor_role.yaml │ ├── apicheck_editor_role.yaml │ ├── checkly_alertchannel_editor_role.yaml │ ├── leader_election_role.yaml │ ├── kustomization.yaml │ └── role.yaml ├── network-policy │ ├── kustomization.yaml │ ├── allow-metrics-traffic.yaml │ └── allow-webhook-traffic.yaml ├── scorecard │ ├── bases │ │ └── config.yaml │ ├── patches │ │ ├── basic.config.yaml │ │ └── olm.config.yaml │ └── kustomization.yaml ├── default │ ├── manager_metrics_patch.yaml │ ├── metrics_service.yaml │ └── kustomization.yaml ├── samples │ ├── checkly_v1alpha1_group.yaml │ ├── kustomization.yaml │ ├── checkly_v1alpha1_apicheck.yaml │ ├── checkly_v1alpha1_alertchannel.yaml │ └── network_v1_ingress.yaml ├── crd │ ├── patches │ │ ├── cainjection_in_apis.yaml │ │ ├── cainjection_in_groups.yaml │ │ ├── cainjection_in_apichecks.yaml │ │ ├── cainjection_in_checkly_alertchannels.yaml │ │ ├── webhook_in_apis.yaml │ │ ├── webhook_in_groups.yaml │ │ ├── webhook_in_apichecks.yaml │ │ └── webhook_in_checkly_alertchannels.yaml │ ├── kustomizeconfig.yaml │ ├── kustomization.yaml │ └── bases │ │ ├── checkly.imgarena.com_groups.yaml │ │ ├── k8s.checklyhq.com_groups.yaml │ │ ├── k8s.checklyhq.com_apichecks.yaml │ │ ├── checkly.imgarena.com_apichecks.yaml │ │ └── k8s.checklyhq.com_alertchannels.yaml ├── manager │ ├── controller_manager_config.yaml │ ├── kustomization.yaml │ └── manager.yaml └── manifests │ └── kustomization.yaml ├── .dockerignore ├── .gitignore ├── hack ├── boilerplate.go.txt └── debug-checks.go ├── Dockerfile ├── external └── checkly │ ├── group_test.go │ ├── api_common.go │ ├── alertChannel.go │ ├── api_common_test.go │ ├── group.go │ ├── check.go │ ├── alertChannel_test.go │ └── check_test.go ├── PROJECT ├── api └── checkly │ └── v1alpha1 │ ├── groupversion_info.go │ ├── group_types.go │ ├── alertchannel_types.go │ ├── apicheck_types.go │ └── zz_generated.deepcopy.go ├── package.json ├── docs ├── check-group.md ├── alert-channels.md ├── api-checks.md ├── ingress.md └── README.md ├── CONTRIBUTING.md ├── README.md ├── .github └── workflows │ ├── main-merge.yaml │ ├── codeql-analysis.yml │ └── pull-request.yaml ├── internal └── controller │ ├── checkly │ ├── apicheck_controller_test.go │ ├── group_controller_test.go │ ├── alertchannel_controller_test.go │ ├── suite_test.go │ ├── alertchannel_controller.go │ ├── group_controller.go │ └── apicheck_controller.go │ └── networking │ ├── suite_test.go │ └── ingress_controller_test.go ├── go.mod └── cmd └── main.go /config/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - monitor.yaml 3 | apiVersion: kustomize.config.k8s.io/v1beta1 4 | kind: Kustomization 5 | -------------------------------------------------------------------------------- /config/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | -------------------------------------------------------------------------------- /config/network-policy/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - allow-webhook-traffic.yaml 3 | - allow-metrics-traffic.yaml 4 | apiVersion: kustomize.config.k8s.io/v1beta1 5 | kind: Kustomization 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Ignore build and test binaries. 3 | bin/ 4 | testbin/ 5 | hack/ 6 | docs/ 7 | 8 | # direnv 9 | .envrc 10 | -------------------------------------------------------------------------------- /config/default/manager_metrics_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch adds the args to allow exposing the metrics endpoint using HTTPS 2 | - op: add 3 | path: /spec/template/spec/containers/0/args/0 4 | value: --metrics-bind-address=:8080 5 | -------------------------------------------------------------------------------- /config/rbac/metrics_reader_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: metrics-reader 5 | rules: 6 | - nonResourceURLs: 7 | - "/metrics" 8 | verbs: 9 | - get 10 | -------------------------------------------------------------------------------- /config/samples/checkly_v1alpha1_group.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: k8s.checklyhq.com/v1alpha1 2 | kind: Group 3 | metadata: 4 | name: group-sample 5 | labels: 6 | environment: "local" 7 | spec: 8 | locations: 9 | - eu-west-1 10 | - eu-west-2 11 | alertchannel: 12 | - alertchannel-sample 13 | -------------------------------------------------------------------------------- /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.18.1 8 | labels: 9 | suite: basic 10 | test: basic-check-spec-test 11 | -------------------------------------------------------------------------------- /config/samples/kustomization.yaml: -------------------------------------------------------------------------------- 1 | ## Append samples you want in your CSV to this file as resources ## 2 | resources: 3 | - network_v1_ingress.yaml 4 | - checkly_v1alpha1_apicheck.yaml 5 | - checkly_v1alpha1_group.yaml 6 | - checkly_v1alpha1_alertchannel.yaml 7 | apiVersion: kustomize.config.k8s.io/v1beta1 8 | kind: Kustomization 9 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_apis.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: apis.check.checklyhq.com 8 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_groups.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: groups.k8s.checklyhq.com 8 | -------------------------------------------------------------------------------- /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: 4e7eab13.checklyhq.com 12 | -------------------------------------------------------------------------------- /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/crd/patches/cainjection_in_apichecks.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: apichecks.k8s.checklyhq.com 8 | -------------------------------------------------------------------------------- /config/samples/checkly_v1alpha1_apicheck.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: k8s.checklyhq.com/v1alpha1 2 | kind: ApiCheck 3 | metadata: 4 | name: apicheck-sample 5 | labels: 6 | service: "foo" 7 | spec: 8 | endpoint: "https://foo.bar/baz" 9 | success: "200" 10 | frequency: 10 # Default 5 11 | muted: true # Default "false" 12 | group: "group-sample" 13 | -------------------------------------------------------------------------------- /config/manifests/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # These resources constitute the fully configured set of manifests 2 | # used to generate the 'manifests/' directory in a bundle. 3 | resources: 4 | - bases/checkly-operator.clusterserviceversion.yaml 5 | - ../default 6 | - ../samples 7 | - ../scorecard 8 | apiVersion: kustomize.config.k8s.io/v1beta1 9 | kind: Kustomization 10 | -------------------------------------------------------------------------------- /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/crd/patches/cainjection_in_checkly_alertchannels.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: alertchannels.checkly.checklyhq.com 8 | -------------------------------------------------------------------------------- /config/rbac/metrics_auth_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: metrics-auth-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: metrics-auth-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: controller-manager 12 | namespace: system 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: ghcr.io/checkly/checkly-operator 16 | newTag: 0.0.1 17 | -------------------------------------------------------------------------------- /config/rbac/metrics_auth_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: metrics-auth-role 5 | rules: 6 | - apiGroups: 7 | - authentication.k8s.io 8 | resources: 9 | - tokenreviews 10 | verbs: 11 | - create 12 | - apiGroups: 13 | - authorization.k8s.io 14 | resources: 15 | - subjectaccessreviews 16 | verbs: 17 | - create 18 | -------------------------------------------------------------------------------- /config/rbac/api_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view apis. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: api-viewer-role 6 | rules: 7 | - apiGroups: 8 | - k8s.checklyhq.com 9 | resources: 10 | - apis 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - k8s.checklyhq.com 17 | resources: 18 | - apis/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/rbac/group_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view groups. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: group-viewer-role 6 | rules: 7 | - apiGroups: 8 | - k8s.checklyhq.com 9 | resources: 10 | - groups 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - k8s.checklyhq.com 17 | resources: 18 | - groups/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/rbac/apicheck_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view apichecks. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: apicheck-viewer-role 6 | rules: 7 | - apiGroups: 8 | - k8s.checklyhq.com 9 | resources: 10 | - apichecks 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - k8s.checklyhq.com 17 | resources: 18 | - apichecks/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/default/metrics_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: checkly-operator 7 | app.kubernetes.io/managed-by: kustomize 8 | name: controller-manager-metrics-service 9 | namespace: system 10 | spec: 11 | ports: 12 | - name: https 13 | port: 8080 14 | protocol: TCP 15 | targetPort: 8080 16 | selector: 17 | control-plane: controller-manager 18 | -------------------------------------------------------------------------------- /config/rbac/checkly_alertchannel_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view alertchannels. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: alertchannel-viewer-role 6 | rules: 7 | - apiGroups: 8 | - k8s.checklyhq.com 9 | resources: 10 | - alertchannels 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - k8s.checklyhq.com 17 | resources: 18 | - alertchannels/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_apis.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: apis.check.checklyhq.com 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | conversionReviewVersions: 16 | - v1 17 | -------------------------------------------------------------------------------- /config/rbac/api_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit apis. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: api-editor-role 6 | rules: 7 | - apiGroups: 8 | - k8s.checklyhq.com 9 | resources: 10 | - apis 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - k8s.checklyhq.com 21 | resources: 22 | - apis/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_groups.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: groups.k8s.checklyhq.com 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | conversionReviewVersions: 16 | - v1 17 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_apichecks.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: apichecks.k8s.checklyhq.com 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | conversionReviewVersions: 16 | - v1 17 | -------------------------------------------------------------------------------- /config/rbac/group_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit groups. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: group-editor-role 6 | rules: 7 | - apiGroups: 8 | - k8s.checklyhq.com 9 | resources: 10 | - groups 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - k8s.checklyhq.com 21 | resources: 22 | - groups/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /config/scorecard/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - bases/config.yaml 3 | apiVersion: kustomize.config.k8s.io/v1beta1 4 | kind: Kustomization 5 | patches: 6 | - path: patches/basic.config.yaml 7 | target: 8 | group: scorecard.operatorframework.io 9 | kind: Configuration 10 | name: config 11 | version: v1alpha3 12 | - path: patches/olm.config.yaml 13 | target: 14 | group: scorecard.operatorframework.io 15 | kind: Configuration 16 | name: config 17 | version: v1alpha3 18 | -------------------------------------------------------------------------------- /config/rbac/apicheck_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit apichecks. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: apicheck-editor-role 6 | rules: 7 | - apiGroups: 8 | - k8s.checklyhq.com 9 | resources: 10 | - apichecks 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - k8s.checklyhq.com 21 | resources: 22 | - apichecks/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_checkly_alertchannels.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: alertchannels.checkly.checklyhq.com 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | conversionReviewVersions: 16 | - v1 17 | -------------------------------------------------------------------------------- /config/rbac/checkly_alertchannel_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit alertchannels. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: alertchannel-editor-role 6 | rules: 7 | - apiGroups: 8 | - k8s.checklyhq.com 9 | resources: 10 | - alertchannels 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - k8s.checklyhq.com 21 | resources: 22 | - alertchannels/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /config/samples/checkly_v1alpha1_alertchannel.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: k8s.checklyhq.com/v1alpha1 2 | kind: AlertChannel 3 | metadata: 4 | name: alertchannel-sample 5 | spec: 6 | # only one of the below can be specified at once, either email or opsgenie 7 | email: 8 | address: "foo@bar.baz" 9 | # opsgenie: 10 | # apikey: 11 | # name: test-secret # Name of the secret which holds the API key 12 | # namespace: default # Namespace of the secret 13 | # fieldPath: "TEST" # Key inside the secret 14 | # priority: "P3" 15 | # region: "US" 16 | -------------------------------------------------------------------------------- /.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 | # direnv 28 | .envrc 29 | 30 | # Generated manifest files 31 | dry-run/ 32 | -------------------------------------------------------------------------------- /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 2022. 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/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 | -------------------------------------------------------------------------------- /config/network-policy/allow-metrics-traffic.yaml: -------------------------------------------------------------------------------- 1 | # This NetworkPolicy allows ingress traffic 2 | # with Pods running on namespaces labeled with 'metrics: enabled'. Only Pods on those 3 | # namespaces are able to gathering data from the metrics endpoint. 4 | apiVersion: networking.k8s.io/v1 5 | kind: NetworkPolicy 6 | metadata: 7 | labels: 8 | app.kubernetes.io/name: checkly-operator 9 | app.kubernetes.io/managed-by: kustomize 10 | name: allow-metrics-traffic 11 | namespace: system 12 | spec: 13 | podSelector: 14 | matchLabels: 15 | control-plane: controller-manager 16 | policyTypes: 17 | - Ingress 18 | ingress: 19 | # This allows ingress traffic from any namespace with the label metrics: enabled 20 | - from: 21 | - namespaceSelector: 22 | matchLabels: 23 | metrics: enabled # Only from namespaces with this label 24 | ports: 25 | - port: 8443 26 | protocol: TCP 27 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM golang:1.23 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 cmd/main.go cmd/main.go 14 | COPY api/ api/ 15 | COPY internal/controller internal/controller/ 16 | COPY external/ external/ 17 | 18 | # Build 19 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go 20 | 21 | # Use distroless as minimal base image to package the manager binary 22 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 23 | FROM gcr.io/distroless/static:nonroot 24 | WORKDIR / 25 | COPY --from=builder /workspace/manager . 26 | USER 65532:65532 27 | 28 | ENTRYPOINT ["/manager"] 29 | -------------------------------------------------------------------------------- /config/network-policy/allow-webhook-traffic.yaml: -------------------------------------------------------------------------------- 1 | # This NetworkPolicy allows ingress traffic to your webhook server running 2 | # as part of the controller-manager from specific namespaces and pods. CR(s) which uses webhooks 3 | # will only work when applied in namespaces labeled with 'webhook: enabled' 4 | apiVersion: networking.k8s.io/v1 5 | kind: NetworkPolicy 6 | metadata: 7 | labels: 8 | app.kubernetes.io/name: checkly-operator 9 | app.kubernetes.io/managed-by: kustomize 10 | name: allow-webhook-traffic 11 | namespace: system 12 | spec: 13 | podSelector: 14 | matchLabels: 15 | control-plane: controller-manager 16 | policyTypes: 17 | - Ingress 18 | ingress: 19 | # This allows ingress traffic from any namespace with the label webhook: enabled 20 | - from: 21 | - namespaceSelector: 22 | matchLabels: 23 | webhook: enabled # Only from namespaces with this label 24 | ports: 25 | - port: 443 26 | protocol: TCP 27 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # All RBAC will be applied under this service account in 2 | # the deployment namespace. You may comment out this resource 3 | # if your manager will use a service account that exists at 4 | # runtime. Be sure to update RoleBinding and ClusterRoleBinding 5 | # subjects if changing service account names. 6 | # The following RBAC configurations are used to protect 7 | # the metrics endpoint with authn/authz. These configurations 8 | # ensure that only authorized users and service accounts 9 | # can access the metrics endpoint. Comment the following 10 | # permissions if you want to disable this protection. 11 | # More info: https://book.kubebuilder.io/reference/metrics.html 12 | resources: 13 | - service_account.yaml 14 | - role.yaml 15 | - role_binding.yaml 16 | - leader_election_role.yaml 17 | - leader_election_role_binding.yaml 18 | - metrics_auth_role.yaml 19 | - metrics_auth_role_binding.yaml 20 | - metrics_reader_role.yaml 21 | apiVersion: kustomize.config.k8s.io/v1beta1 22 | kind: Kustomization 23 | -------------------------------------------------------------------------------- /external/checkly/group_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 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 external 18 | 19 | import "testing" 20 | 21 | func TestChecklyGroup(t *testing.T) { 22 | data := Group{ 23 | Name: "foo", 24 | Locations: []string{"basement"}, 25 | PrivateLocations: []string{"ground-floor"}, 26 | } 27 | 28 | testData := checklyGroup(data) 29 | 30 | if testData.Name != data.Name { 31 | t.Errorf("Expected %s, got %s", data.Name, testData.Name) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | domain: checklyhq.com 2 | layout: 3 | - go.kubebuilder.io/v4 4 | multigroup: true 5 | plugins: 6 | manifests.sdk.operatorframework.io/v2: {} 7 | scorecard.sdk.operatorframework.io/v2: {} 8 | projectName: checkly-operator 9 | repo: github.com/checkly/checkly-operator 10 | resources: 11 | - controller: true 12 | domain: k8s.io 13 | group: networking 14 | kind: Ingress 15 | path: k8s.io/api/networking/v1 16 | version: v1 17 | - api: 18 | crdVersion: v1 19 | namespaced: true 20 | controller: true 21 | domain: checklyhq.com 22 | group: k8s 23 | kind: ApiCheck 24 | path: github.com/checkly/checkly-operator/api/checkly/v1alpha1 25 | version: v1alpha1 26 | - api: 27 | crdVersion: v1 28 | namespaced: true 29 | controller: true 30 | domain: checklyhq.com 31 | group: k8s 32 | kind: Group 33 | path: github.com/checkly/checkly-operator/api/checkly/v1alpha1 34 | version: v1alpha1 35 | - api: 36 | crdVersion: v1 37 | controller: true 38 | domain: checklyhq.com 39 | group: k8s 40 | kind: AlertChannel 41 | path: github.com/checkly/checkly-operator/api/checkly/v1alpha1 42 | version: v1alpha1 43 | version: "3" 44 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: manager-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - secrets 11 | verbs: 12 | - get 13 | - list 14 | - apiGroups: 15 | - k8s.checklyhq.com 16 | resources: 17 | - alertchannels 18 | - apichecks 19 | - groups 20 | verbs: 21 | - create 22 | - delete 23 | - get 24 | - list 25 | - patch 26 | - update 27 | - watch 28 | - apiGroups: 29 | - k8s.checklyhq.com 30 | resources: 31 | - alertchannels/finalizers 32 | - apichecks/finalizers 33 | - groups/finalizers 34 | verbs: 35 | - update 36 | - apiGroups: 37 | - k8s.checklyhq.com 38 | resources: 39 | - alertchannels/status 40 | - apichecks/status 41 | - groups/status 42 | verbs: 43 | - get 44 | - patch 45 | - update 46 | - apiGroups: 47 | - networking.k8s.io 48 | resources: 49 | - ingresses 50 | verbs: 51 | - get 52 | - list 53 | - patch 54 | - update 55 | - watch 56 | - apiGroups: 57 | - networking.k8s.io 58 | resources: 59 | - ingresses/finalizers 60 | verbs: 61 | - update 62 | - apiGroups: 63 | - networking.k8s.io 64 | resources: 65 | - ingresses/status 66 | verbs: 67 | - get 68 | - patch 69 | - update 70 | -------------------------------------------------------------------------------- /external/checkly/api_common.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 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 external 18 | 19 | import "fmt" 20 | 21 | func checkValueString(x string, y string) (value string) { 22 | if x == "" { 23 | value = y 24 | } else { 25 | value = x 26 | } 27 | return 28 | } 29 | 30 | func checkValueInt(x int, y int) (value int) { 31 | if x == 0 { 32 | value = y 33 | } else { 34 | value = x 35 | } 36 | return 37 | } 38 | 39 | func checkValueArray(x []string, y []string) (value []string) { 40 | if len(x) == 0 { 41 | value = y 42 | } else { 43 | value = x 44 | } 45 | return 46 | } 47 | 48 | func getTags(labels map[string]string) (tags []string) { 49 | 50 | for k, v := range labels { 51 | tags = append(tags, fmt.Sprintf("%s:%s", k, v)) 52 | } 53 | 54 | return 55 | } 56 | -------------------------------------------------------------------------------- /api/checkly/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 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 checkly v1alpha1 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=k8s.checklyhq.com 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: "k8s.checklyhq.com", 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/prometheus/monitor.yaml: -------------------------------------------------------------------------------- 1 | 2 | # Prometheus Monitor Service (Metrics) 3 | apiVersion: monitoring.coreos.com/v1 4 | kind: ServiceMonitor 5 | metadata: 6 | labels: 7 | control-plane: controller-manager 8 | name: controller-manager-metrics-monitor 9 | namespace: system 10 | spec: 11 | endpoints: 12 | - path: /metrics 13 | port: https # Ensure this is the name of the port that exposes HTTPS metrics 14 | scheme: https 15 | bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token 16 | tlsConfig: 17 | # TODO(user): The option insecureSkipVerify: true is not recommended for production since it disables 18 | # certificate verification. This poses a significant security risk by making the system vulnerable to 19 | # man-in-the-middle attacks, where an attacker could intercept and manipulate the communication between 20 | # Prometheus and the monitored services. This could lead to unauthorized access to sensitive metrics data, 21 | # compromising the integrity and confidentiality of the information. 22 | # Please use the following options for secure configurations: 23 | # caFile: /etc/metrics-certs/ca.crt 24 | # certFile: /etc/metrics-certs/tls.crt 25 | # keyFile: /etc/metrics-certs/tls.key 26 | insecureSkipVerify: true 27 | selector: 28 | matchLabels: 29 | control-plane: controller-manager 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "checkly-operator", 3 | "version": "0.0.0-development", 4 | "description": "A kubernetes operator for checklyhq.com", 5 | "main": "main.go", 6 | "scripts": { 7 | "semantic-release": "semantic-release" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/checkly/checkly-operator.git" 12 | }, 13 | "author": "Akos Veres", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/checkly/checkly-operator/issues" 17 | }, 18 | "homepage": "https://github.com/checkly/checkly-operator#readme", 19 | "devDependencies": { 20 | "@semantic-release/exec": "^6.0.3", 21 | "semantic-release": "^19.0.2" 22 | }, 23 | "release": { 24 | "branches": [ 25 | "main" 26 | ], 27 | "plugins": [ 28 | "@semantic-release/commit-analyzer", 29 | "@semantic-release/release-notes-generator", 30 | [ 31 | "@semantic-release/exec", 32 | { 33 | "prepareCmd": "VERSION=${nextRelease.version} make dry-run" 34 | } 35 | ], 36 | [ 37 | "@semantic-release/github", 38 | { 39 | "assets": [ 40 | { 41 | "path": "dry-run/manifests.yaml", 42 | "name": "install-${nextRelease.gitTag}.yaml", 43 | "label": "install-${nextRelease.gitTag}.yaml" 44 | } 45 | ] 46 | } 47 | ] 48 | ] 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /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.18.1 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.18.1 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.18.1 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.18.1 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.18.1 48 | labels: 49 | suite: olm 50 | test: olm-status-descriptors-test 51 | -------------------------------------------------------------------------------- /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 | # - bases/check.checklyhq.com_apis.yaml 5 | resources: 6 | - bases/k8s.checklyhq.com_apichecks.yaml 7 | - bases/k8s.checklyhq.com_groups.yaml 8 | - bases/k8s.checklyhq.com_alertchannels.yaml 9 | #+kubebuilder:scaffold:crdkustomizeresource 10 | 11 | # patchesStrategicMerge: 12 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 13 | # patches here are for enabling the conversion webhook for each CRD 14 | #- patches/webhook_in_apis.yaml 15 | #- patches/webhook_in_apichecks.yaml 16 | #- patches/webhook_in_groups.yaml 17 | #- patches/webhook_in_alertchannels.yaml 18 | #+kubebuilder:scaffold:crdkustomizewebhookpatch 19 | 20 | # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. 21 | # patches here are for enabling the CA injection for each CRD 22 | #- patches/cainjection_in_apis.yaml 23 | #- patches/cainjection_in_apichecks.yaml 24 | #- patches/cainjection_in_groups.yaml 25 | #- patches/cainjection_in_alertchannels.yaml 26 | #+kubebuilder:scaffold:crdkustomizecainjectionpatch 27 | 28 | # the following config is for teaching kustomize how to do kustomization for CRDs. 29 | configurations: 30 | - kustomizeconfig.yaml 31 | apiVersion: kustomize.config.k8s.io/v1beta1 32 | kind: Kustomization 33 | -------------------------------------------------------------------------------- /config/samples/network_v1_ingress.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: networking.k8s.io/v1 3 | kind: Ingress 4 | metadata: 5 | name: ingress-sample 6 | annotations: 7 | k8s.checklyhq.com/enabled: "true" 8 | # k8s.checklyhq.com/endpoint: "foo.baaz" - Default read from spec.rules[*].host 9 | k8s.checklyhq.com/group: "group-sample" 10 | # k8s.checklyhq.com/muted: "false" # If not set, default "true" 11 | # k8s.checklyhq.com/path: "/baz" - Default read from spec.rules[*].http.paths[*].path 12 | # k8s.checklyhq.com/success: "200" - Default "200" 13 | spec: 14 | rules: 15 | - host: "foo.bar" 16 | http: 17 | paths: 18 | - path: /foo 19 | pathType: ImplementationSpecific 20 | backend: 21 | service: 22 | name: test-service 23 | port: 24 | number: 8080 25 | - path: /bar 26 | pathType: ImplementationSpecific 27 | backend: 28 | service: 29 | name: test-service 30 | port: 31 | number: 8080 32 | - host: "example.com" 33 | http: 34 | paths: 35 | - path: /tea 36 | pathType: ImplementationSpecific 37 | backend: 38 | service: 39 | name: test-service 40 | port: 41 | number: 8080 42 | - path: /coffee 43 | pathType: ImplementationSpecific 44 | backend: 45 | service: 46 | name: test-service 47 | port: 48 | number: 8080 49 | -------------------------------------------------------------------------------- /hack/debug-checks.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "os" 9 | "time" 10 | 11 | "github.com/checkly/checkly-go-sdk" 12 | ctrl "sigs.k8s.io/controller-runtime" 13 | ) 14 | 15 | // The script returns the data for a specific checkly check from the checklyhq API, 16 | // it's meant to be used as a debug tool for issues with checks created. 17 | 18 | func main() { 19 | 20 | setupLog := ctrl.Log.WithName("setup") 21 | 22 | var checklyID string 23 | 24 | flag.StringVar(&checklyID, "c", "", "Specify the checkly check ID") 25 | flag.Parse() 26 | 27 | if checklyID == "" { 28 | setupLog.Error(errors.New("ChecklyID is empty"), "exiting due to missing information") 29 | os.Exit(1) 30 | } 31 | 32 | baseUrl := "https://api.checklyhq.com" 33 | apiKey := os.Getenv("CHECKLY_API_KEY") 34 | if apiKey == "" { 35 | setupLog.Error(errors.New("checklyhq.com API key environment variable is undefined"), "checklyhq.com credentials missing") 36 | os.Exit(1) 37 | } 38 | 39 | accountId := os.Getenv("CHECKLY_ACCOUNT_ID") 40 | if accountId == "" { 41 | setupLog.Error(errors.New("checklyhq.com Account ID environment variable is undefined"), "checklyhq.com credentials missing") 42 | os.Exit(1) 43 | } 44 | 45 | client := checkly.NewClient( 46 | baseUrl, 47 | apiKey, 48 | nil, //custom http client, defaults to http.DefaultClient 49 | nil, //io.Writer to output debug messages 50 | ) 51 | 52 | client.SetAccountId(accountId) 53 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 54 | defer cancel() 55 | 56 | returnedCheck, err := client.Get(ctx, checklyID) 57 | if err != nil { 58 | setupLog.Error(err, "failed to get check") 59 | os.Exit(1) 60 | } 61 | 62 | fmt.Printf("%+v", returnedCheck) 63 | 64 | } 65 | -------------------------------------------------------------------------------- /docs/check-group.md: -------------------------------------------------------------------------------- 1 | # check-group 2 | 3 | See the [official Checkly docs](https://www.checklyhq.com/docs/groups/) on what check groups are. 4 | 5 | We're forcing API checks to be part of groups, this is an opinionated decision, if this does not work for your use case, please raise an Issue so we discuss options or alternate implementations. 6 | 7 | ## Configuration options 8 | 9 | The name of the group is derived from the `metadata.name` of the kubernetes resource. `Group` resources are cluster scoped, meaning they need to be unique in a kubernetes cluster and they don't need a namespace definition. 10 | 11 | ### Labels 12 | 13 | Any `metadata.labels` specified will be transformed into tags, for example `environment: dev` label will be transformed to `environment:dev` tag, these tags then propagate to Prometheus metrics (if you're using [the checkly prometheus endpoint](https://www.checklyhq.com/docs/integrations/prometheus/)). 14 | 15 | ### Spec 16 | 17 | The `spec` field accepts the following options: 18 | 19 | | Option | Details | Default | 20 | |--------------|-----------|------------| 21 | | `locations` | Strings; A list of location where the checks should be running, for a list of locations see [doc](https://www.checklyhq.com/docs/monitoring/global-locations/).| `eu-west-1` | 22 | | `alertchannel` | String; A list of alert channels which subscribe to the checks inside the group | none | 23 | 24 | ### Example 25 | 26 | ```yaml 27 | apiVersion: k8s.checklyhq.com/v1alpha1 28 | kind: Group 29 | metadata: 30 | name: checkly-operator-test-group 31 | labels: 32 | environment: "local" 33 | spec: 34 | locations: 35 | - eu-west-1 36 | - eu-west-2 37 | alertchannel: 38 | - checkly-operator-test-email 39 | - checkly-operator-test-opsgenie 40 | 41 | ``` 42 | 43 | ## Referencing 44 | 45 | You'll need to reference the name of the check group in the api check configuration. See [api-checks](api-checks.md) for more details. 46 | -------------------------------------------------------------------------------- /docs/alert-channels.md: -------------------------------------------------------------------------------- 1 | # alert-channels 2 | 3 | See the [official checkly docs](https://www.checklyhq.com/docs/alerting/) on what Alert channels are. 4 | 5 | ## Configuration options 6 | 7 | The name of the Alert channel derives from the `metadata.name` of the created kubernetes resource. 8 | 9 | We're supporting the email and OpsGenie configurations. You can not specify both in a config as each alert channel can only have one channel, if you want to alert to multiple channels, create a resource for each and later reference them in the check group configuration. 10 | 11 | ### Email 12 | 13 | You can send alerts to an email address of your liking, all you need to do is set the `spec.email.address` field. 14 | Example: 15 | ```yaml 16 | apiVersion: k8s.checklyhq.com/v1alpha1 17 | kind: AlertChannel 18 | metadata: 19 | name: checkly-operator-test-email 20 | spec: 21 | email: 22 | address: "foo@bar.baz" 23 | ``` 24 | 25 | ### OpsGenie 26 | 27 | The OpsGenie integration requires an API key to work. See [docs](https://www.checklyhq.com/docs/integrations/opsgenie/) on how to get the OpsGenie API key and determine your region. 28 | 29 | You have the option of saving this information either as a kubernetes secret or configmap resource, we recommend a secret. 30 | 31 | Once the above information is available, here's an example on how to setup the integration via our CRD: 32 | ```yaml 33 | apiVersion: k8s.checklyhq.com/v1alpha1 34 | kind: AlertChannel 35 | metadata: 36 | name: checkly-operator-test-opsgenie 37 | spec: 38 | opsgenie: 39 | apikey: 40 | name: test-secret # Name of the secret or configmap which holds the API key 41 | namespace: default # Namespace of the secret or configmap 42 | fieldPath: "API_KEY" # Key inside the secret or configmap 43 | priority: "P3" # P1, P2, P3, P4, P5 are the options 44 | region: "EU" # Your OpsGenie region 45 | ``` 46 | 47 | ## Referencing 48 | 49 | You'll need to reference the name of the alert channel in the group check configuration. See [check-group](check-group.md) for more details. 50 | -------------------------------------------------------------------------------- /config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | name: checkly-operator-system 7 | name: system 8 | --- 9 | apiVersion: apps/v1 10 | kind: Deployment 11 | metadata: 12 | name: controller-manager 13 | namespace: system 14 | labels: 15 | control-plane: controller-manager 16 | spec: 17 | selector: 18 | matchLabels: 19 | control-plane: controller-manager 20 | replicas: 1 21 | template: 22 | metadata: 23 | annotations: 24 | kubectl.kubernetes.io/default-container: manager 25 | labels: 26 | control-plane: controller-manager 27 | spec: 28 | securityContext: 29 | runAsNonRoot: true 30 | containers: 31 | - command: 32 | - /manager 33 | args: 34 | - --leader-elect 35 | - --health-probe-bind-address=:8081 36 | - --metrics-secure=false 37 | - --controller-domain=k8s.checklyhq.com 38 | - --zap-log-level=info 39 | image: controller:latest 40 | name: manager 41 | env: 42 | - name: CHECKLY_API_KEY 43 | valueFrom: 44 | secretKeyRef: 45 | name: checkly 46 | key: CHECKLY_API_KEY 47 | - name: CHECKLY_ACCOUNT_ID 48 | valueFrom: 49 | secretKeyRef: 50 | name: checkly 51 | key: CHECKLY_ACCOUNT_ID 52 | securityContext: 53 | allowPrivilegeEscalation: false 54 | readOnlyRootFilesystem: true 55 | runAsNonRoot: true 56 | capabilities: 57 | drop: 58 | - ALL 59 | livenessProbe: 60 | httpGet: 61 | path: /healthz 62 | port: 8081 63 | initialDelaySeconds: 15 64 | periodSeconds: 20 65 | readinessProbe: 66 | httpGet: 67 | path: /readyz 68 | port: 8081 69 | initialDelaySeconds: 5 70 | periodSeconds: 10 71 | # TODO(user): Configure the resources accordingly based on the project requirements. 72 | # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ 73 | resources: 74 | limits: 75 | cpu: 500m 76 | memory: 128Mi 77 | requests: 78 | cpu: 10m 79 | memory: 64Mi 80 | serviceAccountName: controller-manager 81 | terminationGracePeriodSeconds: 10 82 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributor guidelines 2 | 3 | We are excited that you're interested in contributing to this project! Your efforts help us improve and grow. 4 | 5 | # Ways to contribute 6 | 7 | There are many ways for you to contribute to the Checkly operator, for example: 8 | 9 | - **User feedback:** Share your feedback and impressions about the operator. Your insights are incredibly helpful for us to further improve and develop the project. 10 | - **Documentation:** Help us identify and fix gaps or issues in our documentation. 11 | - **Bug reports & fixes:** Encountered a bug or unexpected behavior? Please report it to us, and if possible, contribute a fix. 12 | - **Feature enhancements:** Propose or work on new features for the operator. 13 | 14 | # Finding an issue 15 | 16 | You can discover reported bugs, feature ideas, or discussion topics in the [Issues section](https://github.com/checkly/checkly-operator/issues) of this repository. We strive to tag issues with labels such as "good first issue" and "help wanted" to indicate which tasks are up for grabs. 17 | 18 | # How to get in touch 19 | 20 | You can reach us anytime on **#checkly-k8s-operator** channel in the [Checkly community Slack](https://www.checklyhq.com/slack). 21 | 22 | # Commit message guidelines 23 | We follow the **Angular Conventional Commits** format to ensure consistent and meaningful commit messages, which streamline our release process. 24 | 25 | Here’s the basic format: 26 | 27 | - `(): ` 28 | 29 | - **Type**: What kind of change you’re making. Common examples: 30 | - `feat`: Introducing a new feature to the codebase. 31 | - `fix`: Patching a bug in the codebase. 32 | - `docs`: Updating documentation. 33 | - ... 34 | 35 | - **Scope**: Where the change happens (e.g., `deps`, `readme`). 36 | 37 | - **Description**: A short summary of the change. 38 | 39 | Examples: 40 | - `docs(readme): Add commit message guidelines` 41 | - `build(deps): bump golang.org/x/net from 0.13.0 to 0.23.0` 42 | 43 | For more info, check out the [Conventional Commits guide](https://www.conventionalcommits.org/en/v1.0.0/). 44 | 45 | # Contribution workflow 46 | 47 | Our general approach follows the "fork-and-pull" Git workflow: 48 | 49 | 1. Fork the repository on GitHub. 50 | 2. Clone the project to your local machine. 51 | 3. Make your changes and commit them to a new branch. 52 | 4. Push your branch to your forked repository. 53 | 5. Open a Pull Request so we can review and discuss your changes. 54 | 55 | -------------------------------------------------------------------------------- /docs/api-checks.md: -------------------------------------------------------------------------------- 1 | # api-checks 2 | 3 | See the [official checkly docs](https://www.checklyhq.com/docs/api-checks/) on what API checks are. 4 | 5 | > ***Warning*** 6 | > We currently only support GET requests for API Checks. 7 | 8 | API Checks resources are namespace scoped, meaning they need to be unique inside a namespace and you need to add a `metadata.namespace` field to them. 9 | 10 | We can also create API Checks from `ingress` resources, see [ingress](ingress.md) for more details. 11 | 12 | ## Configuration options 13 | 14 | The name of the API check derives from the `metadata.name` of the created kubernetes resource. 15 | 16 | ### Labels 17 | 18 | Any `metadata.labels` specified will be transformed into tags, for example `environment: dev` label will be transformed to `environment:dev` tag, these tags then propagate to Prometheus metrics (if you're using [the checkly prometheus endpoint](https://www.checklyhq.com/docs/integrations/prometheus/)). 19 | 20 | > ***Note*** 21 | > Labels from `Group` resources are automatically propagated to the API checks which are added to the check group, you don't need to duplicate the labels. 22 | 23 | ### Spec 24 | 25 | | Option | Details | Default | 26 | |--------------|-----------|------------| 27 | | `endpoint` | String; Endpoint to run the check against | none (*required) | 28 | | `success` | String; The expected success code | none (*required) | 29 | | `group` | String; Name of the group to which the check belongs; Kubernetes `Group` resource name` | none (*required)| 30 | | `frequency` | Integer; Frequency of minutes between each check, possible values: 1,2,5,10,15,30,60,120,180 | `5`| 31 | | `muted` | Bool; Is the check muted or not | `false` | 32 | | `maxresponsetime` | Integer; Number of milliseconds to wait for a response | `15000` | 33 | 34 | ### Example 35 | 36 | ```yaml 37 | apiVersion: k8s.checklyhq.com/v1alpha1 38 | kind: ApiCheck 39 | metadata: 40 | name: checkly-operator-test-check-1 41 | namespace: default 42 | labels: 43 | service: "foo" 44 | spec: 45 | endpoint: "https://foo.bar/baz" 46 | success: "200" 47 | frequency: 10 # Default 5 48 | muted: true # Default "false" 49 | group: "checkly-operator-test-group" 50 | --- 51 | apiVersion: k8s.checklyhq.com/v1alpha1 52 | kind: ApiCheck 53 | metadata: 54 | name: checkly-operator-test-check-2 55 | namespace: default 56 | labels: 57 | service: "bar" 58 | spec: 59 | endpoint: "https://foo.bar/baaz" 60 | success: "200" 61 | group: "checkly-operator-test-group" 62 | ``` 63 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Adds namespace to all resources. 2 | namespace: checkly-operator-system 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: checkly-operator- 10 | 11 | # Labels to add to all resources and selectors. 12 | #commonLabels: 13 | # someName: someValue 14 | 15 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 16 | # crd/kustomization.yaml 17 | #- ../webhook 18 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. 19 | #- ../certmanager 20 | # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. 21 | #- ../prometheus+ # [METRICS] Expose the controller manager metrics service. 22 | # - metrics_service.yaml 23 | # [NETWORK POLICY] Protect the /metrics endpoint and Webhook Server with NetworkPolicy. 24 | # Only Pod(s) running a namespace labeled with 'metrics: enabled' will be able to gather the metrics. 25 | # Only CR(s) which requires webhooks and are applied on namespaces labeled with 'webhooks: enabled' will 26 | # be able to communicate with the Webhook Server. 27 | #- ../network-policy 28 | 29 | # Uncomment the patches line if you enable Metrics, and/or are using webhooks and cert-manager 30 | # [METRICS] The following patch will enable the metrics endpoint using HTTPS and the port :8443. 31 | # More info: https://book.kubebuilder.io/reference/metrics 32 | patches: 33 | - path: manager_metrics_patch.yaml 34 | target: 35 | kind: Deployment 36 | 37 | # Mount the controller config file for loading manager configurations 38 | # through a ComponentConfig type 39 | #- manager_config_patch.yaml 40 | 41 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 42 | # crd/kustomization.yaml 43 | #- manager_webhook_patch.yaml 44 | 45 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 46 | # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. 47 | # 'CERTMANAGER' needs to be enabled to use ca injection 48 | #- webhookcainjection_patch.yaml 49 | 50 | # the following config is for teaching kustomize how to do var substitution 51 | apiVersion: kustomize.config.k8s.io/v1beta1 52 | kind: Kustomization 53 | resources: 54 | - ../crd 55 | - ../rbac 56 | - ../manager 57 | -------------------------------------------------------------------------------- /external/checkly/alertChannel.go: -------------------------------------------------------------------------------- 1 | package external 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/checkly/checkly-go-sdk" 8 | checklyv1alpha1 "github.com/checkly/checkly-operator/api/checkly/v1alpha1" 9 | ) 10 | 11 | func checklyAlertChannel(alertChannel *checklyv1alpha1.AlertChannel, opsGenieConfig checkly.AlertChannelOpsgenie) (ac checkly.AlertChannel, err error) { 12 | sslExpiry := false 13 | 14 | ac = checkly.AlertChannel{ 15 | SendRecovery: &alertChannel.Spec.SendRecovery, 16 | SendFailure: &alertChannel.Spec.SendFailure, 17 | SSLExpiry: &sslExpiry, 18 | } 19 | 20 | if opsGenieConfig != (checkly.AlertChannelOpsgenie{}) { 21 | ac.Type = "OPSGENIE" // Type has to be all caps, see https://developers.checklyhq.com/reference/postv1alertchannels 22 | ac.Opsgenie = &opsGenieConfig 23 | return 24 | } 25 | 26 | if alertChannel.Spec.Email != (checkly.AlertChannelEmail{}) { 27 | ac.Type = "EMAIL" // Type has to be all caps, see https://developers.checklyhq.com/reference/postv1alertchannels 28 | ac.Email = &checkly.AlertChannelEmail{ 29 | Address: alertChannel.Spec.Email.Address, 30 | } 31 | return 32 | } 33 | return 34 | } 35 | 36 | func CreateAlertChannel(alertChannel *checklyv1alpha1.AlertChannel, opsGenieConfig checkly.AlertChannelOpsgenie, client checkly.Client) (ID int64, err error) { 37 | 38 | ac, err := checklyAlertChannel(alertChannel, opsGenieConfig) 39 | if err != nil { 40 | return 41 | } 42 | 43 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 44 | defer cancel() 45 | 46 | gotAlertChannel, err := client.CreateAlertChannel(ctx, ac) 47 | if err != nil { 48 | return 49 | } 50 | 51 | ID = gotAlertChannel.ID 52 | 53 | return 54 | } 55 | 56 | func UpdateAlertChannel(alertChannel *checklyv1alpha1.AlertChannel, opsGenieConfig checkly.AlertChannelOpsgenie, client checkly.Client) (err error) { 57 | ac, err := checklyAlertChannel(alertChannel, opsGenieConfig) 58 | if err != nil { 59 | return 60 | } 61 | 62 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 63 | defer cancel() 64 | 65 | _, err = client.UpdateAlertChannel(ctx, alertChannel.Status.ID, ac) 66 | if err != nil { 67 | return 68 | } 69 | 70 | return 71 | } 72 | 73 | func DeleteAlertChannel(alertChannel *checklyv1alpha1.AlertChannel, client checkly.Client) (err error) { 74 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 75 | defer cancel() 76 | 77 | err = client.DeleteAlertChannel(ctx, alertChannel.Status.ID) 78 | if err != nil { 79 | return 80 | } 81 | 82 | return 83 | } 84 | -------------------------------------------------------------------------------- /external/checkly/api_common_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 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 external 18 | 19 | import ( 20 | "testing" 21 | ) 22 | 23 | func TestCheckValueString(t *testing.T) { 24 | defaultValue := "foo" 25 | overrideValue := "bar" 26 | 27 | testValue := checkValueString(overrideValue, defaultValue) 28 | 29 | if testValue != overrideValue { 30 | t.Errorf("Expected %s, got %s", overrideValue, testValue) 31 | } 32 | 33 | overrideValue = "" 34 | 35 | testValue = checkValueString(overrideValue, defaultValue) 36 | if testValue != defaultValue { 37 | t.Errorf("Expected %s, got %s", overrideValue, testValue) 38 | } 39 | 40 | } 41 | 42 | func TestCheckValueInt(t *testing.T) { 43 | defaultValue := 1 44 | overrideValue := 2 45 | 46 | testValue := checkValueInt(overrideValue, defaultValue) 47 | 48 | if testValue != overrideValue { 49 | t.Errorf("Expected %d, got %d", overrideValue, testValue) 50 | } 51 | 52 | overrideValue = 0 53 | 54 | testValue = checkValueInt(overrideValue, defaultValue) 55 | if testValue != defaultValue { 56 | t.Errorf("Expected %d, got %d", overrideValue, testValue) 57 | } 58 | 59 | } 60 | 61 | func TestCheckValueArray(t *testing.T) { 62 | defaultValue := []string{"foo"} 63 | overrideValue := []string{"foo", "bar"} 64 | 65 | testValue := checkValueArray(overrideValue, defaultValue) 66 | 67 | if len(testValue) != len(overrideValue) { 68 | t.Errorf("Expected %d, got %d", len(overrideValue), len(testValue)) 69 | } 70 | 71 | overrideValue = []string{} 72 | 73 | testValue = checkValueArray(overrideValue, defaultValue) 74 | if len(testValue) != len(defaultValue) { 75 | t.Errorf("Expected %d, got %d", len(overrideValue), len(testValue)) 76 | } 77 | 78 | } 79 | 80 | func TestGetTags(t *testing.T) { 81 | var data = make(map[string]string) 82 | data["foo"] = "bar" 83 | 84 | response := getTags(data) 85 | if len(response) != 1 { 86 | t.Errorf("Expected 1 item, got %d", len(response)) 87 | } 88 | 89 | for _, v := range response { 90 | if v != "foo:bar" { 91 | t.Errorf("Expected foo:bar, got %s", v) 92 | } 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # checkly-operator 2 | 3 | [![Build and push](https://github.com/checkly/checkly-operator/actions/workflows/main-merge.yaml/badge.svg)](https://github.com/checkly/checkly-operator/actions/workflows/main-merge.yaml) 4 | 5 | A kubernetes operator for [checklyhq.com](https://checklyhq.com). 6 | 7 | The operator can create checklyhq.com checks, groups and alert channels based of kubernetes CRDs and Ingress object annotations. 8 | 9 | ## Documentation 10 | Please see our [docs](docs/README.md) for more details on how to install and use the operator. 11 | 12 | ## Get involved 13 | 14 | Join us on the **#checkly-k8s-operator** channel in the [Checkly community Slack](https://www.checklyhq.com/slack), where we're discussing everything related to the project. If you're interested in contributing, be sure to check out [CONTRIBUTING.md](CONTRIBUTING.md) for more details. 15 | 16 | ## Development 17 | ### direnv 18 | 19 | We're using [direnv](https://direnv.net/) to manage environment variables for this project (or export them manually and you can skip this step). Make sure you generate a checkly API key and you get the account ID as well. 20 | 21 | ``` 22 | touch .envrc 23 | echo "export CHECKLY_API_KEY=foorbarbaz" > .envrc 24 | echo "export CHECKLY_ACCOUNT_ID=randomnumbers" >> .envrc 25 | direnv allow . 26 | ``` 27 | 28 | ### Makefile 29 | 30 | Make sure your current kubectl context is set to the appropriate kubernetes cluster where you want to test the operator, then run 31 | 32 | ```bash 33 | kubectl apply -f config/crd/bases/k8s.checklyhq.com_apichecks.yaml 34 | kubectl apply -f config/crd/bases/k8s.checklyhq.com_groups.yaml 35 | kubectl apply -f config/crd/bases/k8s.checklyhq.com_alertchannels.yaml 36 | make run 37 | ``` 38 | 39 | If you update any of the types for the CRD, run 40 | ```bash 41 | make manifests 42 | ``` 43 | and re-apply the CRD. 44 | 45 | ### Testing the controller 46 | 47 | #### Unit and integration tests 48 | * Make sure your kubectl context is set to your local k8s cluster 49 | * Run `USE_EXISTING_CLUSTER=true make test` 50 | * To see coverage run `go tool cover -html=cover.out` 51 | 52 | #### Running locally 53 | See [docs](docs/README.md) for details. 54 | 55 | ## Source material 56 | 57 | Sources used for kick starting this project: 58 | * https://sdk.operatorframework.io/docs/building-operators/golang/tutorial/ 59 | * https://kubernetes.io/blog/2021/06/21/writing-a-controller-for-pod-labels/ 60 | * https://github.com/checkly/checkly-go-sdk 61 | * https://docs.okd.io/latest/operators/operator_sdk/golang/osdk-golang-tutorial.html 62 | * https://sdk.operatorframework.io/docs/building-operators/golang/advanced-topics/#external-resources 63 | * https://book.kubebuilder.io/cronjob-tutorial/writing-tests.html 64 | 65 | 66 | ### Versions 67 | 68 | We're using the following versions of packages: 69 | * operator-sdk 1.39.2 70 | * golang 1.23 71 | 72 | Tested with K8s `v1.31.2`. 73 | -------------------------------------------------------------------------------- /config/crd/bases/checkly.imgarena.com_groups.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: groups.k8s.checklyhq.com 9 | spec: 10 | group: k8s.checklyhq.com 11 | names: 12 | kind: Group 13 | listKind: GroupList 14 | plural: groups 15 | singular: group 16 | scope: Namespaced 17 | versions: 18 | - name: v1alpha1 19 | schema: 20 | openAPIV3Schema: 21 | description: Group is the Schema for the groups 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: GroupSpec defines the desired state of Group 37 | properties: 38 | alertchannel: 39 | description: AlertChannel determines where to send alerts 40 | items: 41 | type: string 42 | type: array 43 | locations: 44 | description: Locations determines the locations where the checks are 45 | run from, see https://www.checklyhq.com/docs/monitoring/global-locations/ 46 | for a list, use AWS Region codes, ex. eu-west-1 for Ireland 47 | items: 48 | type: string 49 | type: array 50 | muted: 51 | description: Activated determines if the created group is muted or 52 | not, default false 53 | type: boolean 54 | type: object 55 | status: 56 | description: GroupStatus defines the observed state of Group 57 | properties: 58 | ID: 59 | description: ID holds the ID of the created checklyhq.com group 60 | format: int64 61 | type: integer 62 | required: 63 | - ID 64 | type: object 65 | type: object 66 | served: true 67 | storage: true 68 | subresources: 69 | status: {} 70 | -------------------------------------------------------------------------------- /api/checkly/v1alpha1/group_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 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 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | ) 22 | 23 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 24 | // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. 25 | 26 | // GroupSpec defines the desired state of Group 27 | type GroupSpec struct { 28 | // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster 29 | // Important: Run "make" to regenerate code after modifying this file 30 | 31 | // Locations determines the locations where the checks are run from, see https://www.checklyhq.com/docs/monitoring/global-locations/ for a list, use AWS Region codes, ex. eu-west-1 for Ireland 32 | Locations []string `json:"locations,omitempty"` 33 | 34 | // Locations determines the locations where the checks are run from, see https://www.checklyhq.com/docs/monitoring/global-locations/ for a list, use AWS Region codes, ex. eu-west-1 for Ireland 35 | PrivateLocations []string `json:"privateLocations,omitempty"` 36 | 37 | // Activated determines if the created group is muted or not, default false 38 | Activated bool `json:"muted,omitempty"` 39 | 40 | // AlertChannels determines where to send alerts 41 | AlertChannels []string `json:"alertchannel,omitempty"` 42 | } 43 | 44 | // GroupStatus defines the observed state of Group 45 | type GroupStatus struct { 46 | // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster 47 | // Important: Run "make" to regenerate code after modifying this file 48 | 49 | // ID holds the ID of the created checklyhq.com group 50 | ID int64 `json:"ID"` 51 | } 52 | 53 | //+kubebuilder:object:root=true 54 | //+kubebuilder:subresource:status 55 | //+kubebuilder:resource:scope=Cluster 56 | 57 | // Group is the Schema for the groups API 58 | type Group struct { 59 | metav1.TypeMeta `json:",inline"` 60 | metav1.ObjectMeta `json:"metadata,omitempty"` 61 | 62 | Spec GroupSpec `json:"spec,omitempty"` 63 | Status GroupStatus `json:"status,omitempty"` 64 | } 65 | 66 | //+kubebuilder:object:root=true 67 | 68 | // GroupList contains a list of Group 69 | type GroupList struct { 70 | metav1.TypeMeta `json:",inline"` 71 | metav1.ListMeta `json:"metadata,omitempty"` 72 | Items []Group `json:"items"` 73 | } 74 | 75 | func init() { 76 | SchemeBuilder.Register(&Group{}, &GroupList{}) 77 | } 78 | -------------------------------------------------------------------------------- /.github/workflows/main-merge.yaml: -------------------------------------------------------------------------------- 1 | name: Build and push 2 | on: 3 | push: 4 | branches: [main] 5 | 6 | env: 7 | REGISTRY: ghcr.io 8 | IMAGE_NAME: checkly/checkly-operator 9 | USE_EXISTING_CLUSTER: true 10 | GO_MODULES: on 11 | 12 | jobs: 13 | build: 14 | name: Build 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Check out source code 18 | uses: actions/checkout@v4 19 | 20 | - name: Set up Go 21 | uses: actions/setup-go@v5 22 | with: 23 | go-version-file: "go.mod" 24 | 25 | - name: Start kind cluster 26 | uses: helm/kind-action@v1 27 | with: 28 | version: "v0.22.0" # This starts k8s v1.29 29 | 30 | - name: Test code 31 | env: 32 | USE_EXISTING_CLUSTER: true 33 | run: | 34 | make test-ci 35 | 36 | # Sonarcloud 37 | # - name: SonarCloud Scan 38 | # uses: SonarSource/sonarcloud-github-action@master 39 | # env: 40 | # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any 41 | # SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 42 | # GO_MODULES: "on" 43 | 44 | # Docker buildx does not allow for multi architecture builds and loading the docker container locally, 45 | # this way we can not run trivy against the containers, one solution is to run multiple jobs for each arch 46 | 47 | docker: 48 | name: Docker build 49 | runs-on: ubuntu-latest 50 | steps: 51 | - name: Check out source code 52 | uses: actions/checkout@v4 53 | 54 | # For multi-arch docker builds 55 | # https://github.com/docker/setup-qemu-action 56 | - name: Set up QEMU 57 | uses: docker/setup-qemu-action@v3 58 | 59 | # https://github.com/docker/setup-buildx-action 60 | - name: Set up Docker Buildx 61 | id: buildx 62 | uses: docker/setup-buildx-action@v3 63 | 64 | - name: Log in to the Container registry 65 | uses: docker/login-action@v3 66 | with: 67 | registry: ${{ env.REGISTRY }} 68 | username: ${{ github.actor }} 69 | password: ${{ secrets.GITHUB_TOKEN }} 70 | 71 | - name: NPM Install packages for semantic-release 72 | run: | 73 | npm install 74 | 75 | - name: Release 76 | uses: cycjimmy/semantic-release-action@v4 77 | id: semantic # The `id` for output variables 78 | env: 79 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 80 | 81 | - name: Build multi-platform images 82 | uses: docker/build-push-action@v6 83 | if: ${{ steps.semantic.outputs.new_release_version }} 84 | env: 85 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 86 | IMAGE: ${{ env.REGISTRY }}/${{env.IMAGE_NAME}}:${{ steps.semantic.outputs.new_release_version }} 87 | with: 88 | context: . 89 | platforms: linux/amd64, linux/arm, linux/arm64 90 | push: true 91 | tags: ${{ env.IMAGE }} 92 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '24 12 * * 0' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'go' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | -------------------------------------------------------------------------------- /docs/ingress.md: -------------------------------------------------------------------------------- 1 | # ingress 2 | 3 | Support for kubernetes native `ingress` resources. See [official docs](https://kubernetes.io/docs/concepts/services-networking/ingress/) for more details on what they are and what they do. 4 | 5 | We pull out information with the use of `annotations` and use the built in spec. The information from the annotations is used to create `ApiCheck` resources, we make use of [ownerReferences](https://kubernetes.io/docs/concepts/overview/working-with-objects/owners-dependents/) to link ingress resources to ApiCheck resources. 6 | 7 | > ***Warning*** 8 | > We currently only support API checks for ingress resources. 9 | 10 | ## Logic of discovery 11 | 12 | We iterate over the ingress resource's specifications to work out what needs to be created. The operator creates one ApiCheck resource for each `host` + `path`, if in your ingress resource you have 2 hosts with 3 paths each, you'll end up with 6 ApiChecks created. 13 | 14 | Specific annotations are optional, as we can't automatically discover the group you want the Checkly APIChecks to be deployd in. 15 | 16 | ## Configuration options 17 | 18 | The name of the API Check derives from the `metadata.name` of the `ingress` resource and the corresponding API Check is created in the same namespace where the `ingress` object resides. 19 | 20 | | Annotation | Details | Default | 21 | |--------------------|-------------|---------| 22 | | `k8s.checklyhq.com/enabled` | Bool; Should the operator read the annotations or not | `false` (*required) | 23 | | `k8s.checklyhq.com/endpoint` | String; The host of the URL, for example `/` | Value of `spec.rules[0].Host`, defaults to `https://` | 24 | | `k8s.checklyhq.com/group` | String; Name of the group to which the check belongs; Kubernetes `Group` resource name` | none (*required)| 25 | | `k8s.checklyhq.com/muted` | String; Is the check muted or not | `true` | 26 | | `k8s.checklyhq.com/path` | String; The URI to put after the `endpoint`, for example `/path` | ""| 27 | | `k8s.checklyhq.com/success` | String; The expected success code | `200` | 28 | 29 | ### Example 30 | 31 | ```yaml 32 | apiVersion: networking.k8s.io/v1 33 | kind: Ingress 34 | metadata: 35 | name: checkly-operator-ingress 36 | annotations: 37 | k8s.checklyhq.com/enabled: "true" 38 | # k8s.checklyhq.com/path: "/baz" - Default read from spec.rules[0].http.paths[*].path 39 | # k8s.checklyhq.com/endpoint: "foo.baaz" - Default read from spec.rules[0].host 40 | # k8s.checklyhq.com/success: "200" - Default "200" 41 | k8s.checklyhq.com/group: "group-sample" 42 | # k8s.checklyhq.com/muted: "false" # If not set, default "true" 43 | spec: 44 | rules: 45 | - host: "foo.bar" 46 | http: 47 | paths: 48 | - path: /foo 49 | pathType: ImplementationSpecific 50 | backend: 51 | service: 52 | name: test-service 53 | port: 54 | number: 8080 55 | - path: /bar 56 | pathType: ImplementationSpecific 57 | backend: 58 | service: 59 | name: test-service 60 | port: 61 | number: 8080 62 | ``` 63 | -------------------------------------------------------------------------------- /api/checkly/v1alpha1/alertchannel_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 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 | "github.com/checkly/checkly-go-sdk" 21 | corev1 "k8s.io/api/core/v1" 22 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | ) 24 | 25 | // AlertChannelSpec defines the desired state of AlertChannel 26 | type AlertChannelSpec struct { 27 | // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster 28 | // Important: Run "make" to regenerate code after modifying this file 29 | 30 | // SendRecovery determines if the Recovery event should be sent to the alert channel 31 | SendRecovery bool `json:"sendrecovery,omitempty"` 32 | 33 | // SendFailure determines if the Failure event should be sent to the alerting channel 34 | SendFailure bool `json:"sendfailure,omitempty"` 35 | 36 | // OpsGenie holds information about the Opsgenie alert configuration 37 | OpsGenie AlertChannelOpsGenie `json:"opsgenie,omitempty"` 38 | 39 | // Email holds information about the Email alert configuration 40 | Email checkly.AlertChannelEmail `json:"email,omitempty"` 41 | } 42 | 43 | type AlertChannelOpsGenie struct { 44 | // APISecret determines where the secret ref is to pull the OpsGenie API key from 45 | APISecret corev1.ObjectReference `json:"apisecret"` 46 | 47 | // Region holds information about the OpsGenie region (EU or US) 48 | Region string `json:"region,omitempty"` 49 | 50 | // Priority assigned to the alerts sent from checklyhq.com 51 | Priority string `json:"priority,omitempty"` 52 | } 53 | 54 | // AlertChannelStatus defines the observed state of AlertChannel 55 | type AlertChannelStatus struct { 56 | // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster 57 | // Important: Run "make" to regenerate code after modifying this file 58 | ID int64 `json:"id"` 59 | } 60 | 61 | //+kubebuilder:object:root=true 62 | //+kubebuilder:subresource:status 63 | //+kubebuilder:resource:scope=Cluster 64 | 65 | // AlertChannel is the Schema for the alertchannels API 66 | type AlertChannel struct { 67 | metav1.TypeMeta `json:",inline"` 68 | metav1.ObjectMeta `json:"metadata,omitempty"` 69 | 70 | Spec AlertChannelSpec `json:"spec,omitempty"` 71 | Status AlertChannelStatus `json:"status,omitempty"` 72 | } 73 | 74 | //+kubebuilder:object:root=true 75 | 76 | // AlertChannelList contains a list of AlertChannel 77 | type AlertChannelList struct { 78 | metav1.TypeMeta `json:",inline"` 79 | metav1.ListMeta `json:"metadata,omitempty"` 80 | Items []AlertChannel `json:"items"` 81 | } 82 | 83 | func init() { 84 | SchemeBuilder.Register(&AlertChannel{}, &AlertChannelList{}) 85 | } 86 | -------------------------------------------------------------------------------- /config/crd/bases/k8s.checklyhq.com_groups.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.16.1 7 | name: groups.k8s.checklyhq.com 8 | spec: 9 | group: k8s.checklyhq.com 10 | names: 11 | kind: Group 12 | listKind: GroupList 13 | plural: groups 14 | singular: group 15 | scope: Cluster 16 | versions: 17 | - name: v1alpha1 18 | schema: 19 | openAPIV3Schema: 20 | description: Group is the Schema for the groups API 21 | properties: 22 | apiVersion: 23 | description: |- 24 | APIVersion defines the versioned schema of this representation of an object. 25 | Servers should convert recognized schemas to the latest internal value, and 26 | may reject unrecognized values. 27 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 28 | type: string 29 | kind: 30 | description: |- 31 | Kind is a string value representing the REST resource this object represents. 32 | Servers may infer this from the endpoint the client submits requests to. 33 | Cannot be updated. 34 | In CamelCase. 35 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 36 | type: string 37 | metadata: 38 | type: object 39 | spec: 40 | description: GroupSpec defines the desired state of Group 41 | properties: 42 | alertchannel: 43 | description: AlertChannels determines where to send alerts 44 | items: 45 | type: string 46 | type: array 47 | locations: 48 | description: Locations determines the locations where the checks are 49 | run from, see https://www.checklyhq.com/docs/monitoring/global-locations/ 50 | for a list, use AWS Region codes, ex. eu-west-1 for Ireland 51 | items: 52 | type: string 53 | type: array 54 | muted: 55 | description: Activated determines if the created group is muted or 56 | not, default false 57 | type: boolean 58 | privateLocations: 59 | description: Locations determines the locations where the checks are 60 | run from, see https://www.checklyhq.com/docs/monitoring/global-locations/ 61 | for a list, use AWS Region codes, ex. eu-west-1 for Ireland 62 | items: 63 | type: string 64 | type: array 65 | type: object 66 | status: 67 | description: GroupStatus defines the observed state of Group 68 | properties: 69 | ID: 70 | description: ID holds the ID of the created checklyhq.com group 71 | format: int64 72 | type: integer 73 | required: 74 | - ID 75 | type: object 76 | type: object 77 | served: true 78 | storage: true 79 | subresources: 80 | status: {} 81 | -------------------------------------------------------------------------------- /external/checkly/group.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 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 external 18 | 19 | import ( 20 | "context" 21 | "time" 22 | 23 | "github.com/checkly/checkly-go-sdk" 24 | ) 25 | 26 | type Group struct { 27 | Name string 28 | ID int64 29 | Locations []string 30 | PrivateLocations []string 31 | Activated bool 32 | AlertChannels []checkly.AlertChannelSubscription 33 | Labels map[string]string 34 | } 35 | 36 | func checklyGroup(group Group) (check checkly.Group) { 37 | 38 | tags := getTags(group.Labels) 39 | tags = append(tags, "checkly-operator") 40 | 41 | alertSettings := checkly.AlertSettings{ 42 | EscalationType: checkly.RunBased, 43 | RunBasedEscalation: checkly.RunBasedEscalation{ 44 | FailedRunThreshold: 5, 45 | }, 46 | TimeBasedEscalation: checkly.TimeBasedEscalation{ 47 | MinutesFailingThreshold: 5, 48 | }, 49 | Reminders: checkly.Reminders{ 50 | Interval: 5, 51 | }, 52 | SSLCertificates: checkly.SSLCertificates{ 53 | Enabled: false, 54 | AlertThreshold: 3, 55 | }, 56 | } 57 | 58 | locations := []string{} 59 | 60 | if len(group.PrivateLocations) != 0 { 61 | checkValueArray(group.Locations, []string{"eu-west-1"}) 62 | } 63 | 64 | check = checkly.Group{ 65 | Name: group.Name, 66 | Activated: true, 67 | Muted: false, // muted for development 68 | DoubleCheck: false, 69 | LocalSetupScript: "", 70 | LocalTearDownScript: "", 71 | Concurrency: 2, 72 | Locations: locations, 73 | PrivateLocations: &group.PrivateLocations, 74 | Tags: tags, 75 | AlertSettings: alertSettings, 76 | UseGlobalAlertSettings: false, 77 | AlertChannelSubscriptions: group.AlertChannels, 78 | } 79 | 80 | return 81 | } 82 | 83 | func GroupCreate(group Group, client checkly.Client) (ID int64, err error) { 84 | groupSetup := checklyGroup(group) 85 | 86 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 87 | defer cancel() 88 | 89 | gotGroup, err := client.CreateGroup(ctx, groupSetup) 90 | if err != nil { 91 | return 92 | } 93 | 94 | ID = gotGroup.ID 95 | 96 | return 97 | } 98 | 99 | func GroupUpdate(group Group, client checkly.Client) (err error) { 100 | 101 | groupSetup := checklyGroup(group) 102 | 103 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 104 | defer cancel() 105 | 106 | _, err = client.UpdateGroup(ctx, group.ID, groupSetup) 107 | if err != nil { 108 | return 109 | } 110 | 111 | return 112 | } 113 | 114 | func GroupDelete(ID int64, client checkly.Client) (err error) { 115 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 116 | defer cancel() 117 | 118 | err = client.DeleteGroup(ctx, ID) 119 | 120 | return 121 | } 122 | -------------------------------------------------------------------------------- /api/checkly/v1alpha1/apicheck_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 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 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | ) 22 | 23 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 24 | // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. 25 | 26 | // ApiCheckSpec defines the desired state of ApiCheck 27 | type ApiCheckSpec struct { 28 | // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster 29 | // Important: Run "make" to regenerate code after modifying this file 30 | 31 | // Frequency is used to determine the frequency of the checks in minutes, default 5 32 | Frequency int `json:"frequency,omitempty"` 33 | 34 | // Muted determines if the created alert is muted or not, default false 35 | Muted bool `json:"muted,omitempty"` 36 | 37 | // Endpoint determines which URL to monitor, ex. https://foo.bar/baz 38 | Endpoint string `json:"endpoint"` 39 | 40 | // Success determines the returned success code, ex. 200 41 | Success string `json:"success"` 42 | 43 | // MaxResponseTime determines what the maximum number of miliseconds can pass before the check fails, default 15000 44 | MaxResponseTime int `json:"maxresponsetime,omitempty"` 45 | 46 | // Group determines in which group does the check belong to 47 | Group string `json:"group"` 48 | } 49 | 50 | // ApiCheckStatus defines the observed state of ApiCheck 51 | type ApiCheckStatus struct { 52 | // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster 53 | // Important: Run "make" to regenerate code after modifying this file 54 | 55 | // ID holds the checklyhq.com internal ID of the check 56 | ID string `json:"id"` 57 | 58 | // GroupID holds the ID of the group where the check belongs to 59 | GroupID int64 `json:"groupId"` 60 | } 61 | 62 | //+kubebuilder:object:root=true 63 | //+kubebuilder:printcolumn:name="Endpoint",type="string",JSONPath=".spec.endpoint",description="Name of the monitored endpoint" 64 | //+kubebuilder:printcolumn:name="Status code",type="string",JSONPath=".spec.success",description="Expected status code" 65 | //+kubebuilder:printcolumn:name="Muted",type="boolean",JSONPath=".spec.muted" 66 | //+kubebuilder:printcolumn:name="Group",type="string",JSONPath=".spec.group" 67 | //+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" 68 | //+kubebuilder:subresource:status 69 | 70 | // ApiCheck is the Schema for the apichecks API 71 | type ApiCheck struct { 72 | metav1.TypeMeta `json:",inline"` 73 | metav1.ObjectMeta `json:"metadata,omitempty"` 74 | 75 | Spec ApiCheckSpec `json:"spec,omitempty"` 76 | Status ApiCheckStatus `json:"status,omitempty"` 77 | } 78 | 79 | //+kubebuilder:object:root=true 80 | 81 | // ApiCheckList contains a list of ApiCheck 82 | type ApiCheckList struct { 83 | metav1.TypeMeta `json:",inline"` 84 | metav1.ListMeta `json:"metadata,omitempty"` 85 | Items []ApiCheck `json:"items"` 86 | } 87 | 88 | func init() { 89 | SchemeBuilder.Register(&ApiCheck{}, &ApiCheckList{}) 90 | } 91 | -------------------------------------------------------------------------------- /config/crd/bases/k8s.checklyhq.com_apichecks.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.16.1 7 | name: apichecks.k8s.checklyhq.com 8 | spec: 9 | group: k8s.checklyhq.com 10 | names: 11 | kind: ApiCheck 12 | listKind: ApiCheckList 13 | plural: apichecks 14 | singular: apicheck 15 | scope: Namespaced 16 | versions: 17 | - additionalPrinterColumns: 18 | - description: Name of the monitored endpoint 19 | jsonPath: .spec.endpoint 20 | name: Endpoint 21 | type: string 22 | - description: Expected status code 23 | jsonPath: .spec.success 24 | name: Status code 25 | type: string 26 | - jsonPath: .spec.muted 27 | name: Muted 28 | type: boolean 29 | - jsonPath: .spec.group 30 | name: Group 31 | type: string 32 | - jsonPath: .metadata.creationTimestamp 33 | name: Age 34 | type: date 35 | name: v1alpha1 36 | schema: 37 | openAPIV3Schema: 38 | description: ApiCheck is the Schema for the apichecks API 39 | properties: 40 | apiVersion: 41 | description: |- 42 | APIVersion defines the versioned schema of this representation of an object. 43 | Servers should convert recognized schemas to the latest internal value, and 44 | may reject unrecognized values. 45 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 46 | type: string 47 | kind: 48 | description: |- 49 | Kind is a string value representing the REST resource this object represents. 50 | Servers may infer this from the endpoint the client submits requests to. 51 | Cannot be updated. 52 | In CamelCase. 53 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 54 | type: string 55 | metadata: 56 | type: object 57 | spec: 58 | description: ApiCheckSpec defines the desired state of ApiCheck 59 | properties: 60 | endpoint: 61 | description: Endpoint determines which URL to monitor, ex. https://foo.bar/baz 62 | type: string 63 | frequency: 64 | description: Frequency is used to determine the frequency of the checks 65 | in minutes, default 5 66 | type: integer 67 | group: 68 | description: Group determines in which group does the check belong 69 | to 70 | type: string 71 | maxresponsetime: 72 | description: MaxResponseTime determines what the maximum number of 73 | miliseconds can pass before the check fails, default 15000 74 | type: integer 75 | muted: 76 | description: Muted determines if the created alert is muted or not, 77 | default false 78 | type: boolean 79 | success: 80 | description: Success determines the returned success code, ex. 200 81 | type: string 82 | required: 83 | - endpoint 84 | - group 85 | - success 86 | type: object 87 | status: 88 | description: ApiCheckStatus defines the observed state of ApiCheck 89 | properties: 90 | groupId: 91 | description: GroupID holds the ID of the group where the check belongs 92 | to 93 | format: int64 94 | type: integer 95 | id: 96 | description: ID holds the checklyhq.com internal ID of the check 97 | type: string 98 | required: 99 | - groupId 100 | - id 101 | type: object 102 | type: object 103 | served: true 104 | storage: true 105 | subresources: 106 | status: {} 107 | -------------------------------------------------------------------------------- /config/crd/bases/checkly.imgarena.com_apichecks.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: apichecks.k8s.checklyhq.com 9 | spec: 10 | group: k8s.checklyhq.com 11 | names: 12 | kind: ApiCheck 13 | listKind: ApiCheckList 14 | plural: apichecks 15 | singular: apicheck 16 | scope: Namespaced 17 | versions: 18 | - additionalPrinterColumns: 19 | - description: Name of the monitored endpoint 20 | jsonPath: .spec.endpoint 21 | name: Endpoint 22 | type: string 23 | - description: Expected status code 24 | jsonPath: .spec.success 25 | name: Status code 26 | type: string 27 | - jsonPath: .spec.muted 28 | name: Muted 29 | type: boolean 30 | - jsonPath: .spec.group 31 | name: Group 32 | type: string 33 | - jsonPath: .metadata.creationTimestamp 34 | name: Age 35 | type: date 36 | name: v1alpha1 37 | schema: 38 | openAPIV3Schema: 39 | description: ApiCheck is the Schema for the apichecks API 40 | properties: 41 | apiVersion: 42 | description: 'APIVersion defines the versioned schema of this representation 43 | of an object. Servers should convert recognized schemas to the latest 44 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 45 | type: string 46 | kind: 47 | description: 'Kind is a string value representing the REST resource this 48 | object represents. Servers may infer this from the endpoint the client 49 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 50 | type: string 51 | metadata: 52 | type: object 53 | spec: 54 | description: ApiCheckSpec defines the desired state of ApiCheck 55 | properties: 56 | endpoint: 57 | description: Endpoint determines which URL to monitor, ex. https://foo.bar/baz 58 | type: string 59 | frequency: 60 | description: Frequency is used to determine the frequency of the checks 61 | in minutes, default 5 62 | type: integer 63 | group: 64 | description: Group determines in which group does the check belong 65 | to 66 | type: string 67 | locations: 68 | description: Locations determines the locations where the checks are 69 | run from, see https://www.checklyhq.com/docs/monitoring/global-locations/ 70 | for a list, use AWS Region codes, ex. eu-west-1 for Ireland 71 | items: 72 | type: string 73 | type: array 74 | maxresponsetime: 75 | description: MaxResponseTime determines what the maximum number of 76 | miliseconds can pass before the check fails, default 15000 77 | type: integer 78 | muted: 79 | description: Muted determines if the created alert is muted or not, 80 | default false 81 | type: boolean 82 | success: 83 | description: Success determines the returned success code, ex. 200 84 | type: string 85 | required: 86 | - endpoint 87 | - group 88 | - success 89 | type: object 90 | status: 91 | description: ApiCheckStatus defines the observed state of ApiCheck 92 | properties: 93 | groupId: 94 | description: GroupID holds the ID of the group where the check belongs 95 | to 96 | format: int64 97 | type: integer 98 | id: 99 | description: ID holds the checklyhq.com internal ID of the check 100 | type: string 101 | required: 102 | - groupId 103 | - id 104 | type: object 105 | type: object 106 | served: true 107 | storage: true 108 | subresources: 109 | status: {} 110 | -------------------------------------------------------------------------------- /internal/controller/checkly/apicheck_controller_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 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 | // Example code used for influence: https://github.com/Azure/azure-databricks-operator/blob/0f722a710fea06b86ecdccd9455336ca712bf775/controllers/dcluster_controller_test.go 18 | 19 | package checkly 20 | 21 | import ( 22 | "context" 23 | "time" 24 | 25 | . "github.com/onsi/ginkgo" 26 | . "github.com/onsi/gomega" 27 | 28 | checklyv1alpha1 "github.com/checkly/checkly-operator/api/checkly/v1alpha1" 29 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 | "k8s.io/apimachinery/pkg/types" 31 | ) 32 | 33 | var _ = Describe("ApiCheck Controller", func() { 34 | 35 | // Define utility constants for object names and testing timeouts/durations and intervals. 36 | const ( 37 | timeout = time.Second * 10 38 | duration = time.Second * 10 39 | interval = time.Millisecond * 250 40 | ) 41 | 42 | BeforeEach(func() { 43 | // Add any setup steps that needs to be executed before each test 44 | }) 45 | 46 | AfterEach(func() { 47 | // Add any teardown steps that needs to be executed after each test 48 | }) 49 | 50 | // Add Tests for OpenAPI validation (or additonal CRD features) specified in 51 | // your API definition. 52 | // Avoid adding tests for vanilla CRUD operations because they would 53 | // test Kubernetes API server, which isn't the goal here. 54 | Context("ApiCheck", func() { 55 | It("Full reconciliation", func() { 56 | 57 | key := types.NamespacedName{ 58 | Name: "test-apicheck", 59 | Namespace: "default", 60 | } 61 | 62 | groupKey := types.NamespacedName{ 63 | Name: "test-apicheck-group", 64 | } 65 | 66 | group := &checklyv1alpha1.Group{ 67 | ObjectMeta: metav1.ObjectMeta{ 68 | Name: groupKey.Name, 69 | }, 70 | } 71 | 72 | apiCheck := &checklyv1alpha1.ApiCheck{ 73 | ObjectMeta: metav1.ObjectMeta{ 74 | Name: key.Name, 75 | Namespace: key.Namespace, 76 | }, 77 | Spec: checklyv1alpha1.ApiCheckSpec{ 78 | Endpoint: "http://bar.baz/quoz", 79 | Success: "200", 80 | Group: groupKey.Name, 81 | Muted: true, 82 | }, 83 | } 84 | 85 | // Create 86 | Expect(k8sClient.Create(context.Background(), group)).Should(Succeed()) 87 | Expect(k8sClient.Create(context.Background(), apiCheck)).Should(Succeed()) 88 | 89 | By("Expecting submitted") 90 | Eventually(func() bool { 91 | f := &checklyv1alpha1.ApiCheck{} 92 | err := k8sClient.Get(context.Background(), key, f) 93 | if err != nil { 94 | return false 95 | } 96 | return true 97 | }, timeout, interval).Should(BeTrue()) 98 | 99 | // Status.ID should be present 100 | By("Expecting group ID") 101 | Eventually(func() bool { 102 | f := &checklyv1alpha1.ApiCheck{} 103 | err := k8sClient.Get(context.Background(), key, f) 104 | if f.Status.ID != "2" && err != nil { 105 | return false 106 | } 107 | 108 | if f.Spec.Muted != true { 109 | return false 110 | } 111 | 112 | return true 113 | }, timeout, interval).Should(BeTrue()) 114 | 115 | // Finalizer should be present 116 | By("Expecting finalizer") 117 | Eventually(func() bool { 118 | f := &checklyv1alpha1.ApiCheck{} 119 | err := k8sClient.Get(context.Background(), key, f) 120 | if err != nil { 121 | return false 122 | } 123 | 124 | for _, finalizer := range f.Finalizers { 125 | Expect(finalizer).To(Equal("testing.domain.tld/finalizer"), "Finalizer should match") 126 | } 127 | 128 | return true 129 | }, timeout, interval).Should(BeTrue()) 130 | 131 | // Delete 132 | Expect(k8sClient.Delete(context.Background(), group)).Should(Succeed()) 133 | 134 | By("Expecting to delete successfully") 135 | Eventually(func() error { 136 | f := &checklyv1alpha1.ApiCheck{} 137 | k8sClient.Get(context.Background(), key, f) 138 | return k8sClient.Delete(context.Background(), f) 139 | }, timeout, interval).Should(Succeed()) 140 | 141 | By("Expecting delete to finish") 142 | Eventually(func() error { 143 | f := &checklyv1alpha1.ApiCheck{} 144 | return k8sClient.Get(context.Background(), key, f) 145 | }, timeout, interval).ShouldNot(Succeed()) 146 | }) 147 | }) 148 | }) 149 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yaml: -------------------------------------------------------------------------------- 1 | name: Build and test Go 2 | on: 3 | pull_request: 4 | branches: [main] 5 | 6 | env: 7 | REGISTRY: ghcr.io 8 | IMAGE_NAME: checkly/checkly-operator 9 | USE_EXISTING_CLUSTER: true 10 | GO_MODULES: on 11 | 12 | jobs: 13 | golang: 14 | name: Golang build 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Check out source code 18 | uses: actions/checkout@v4 19 | 20 | - name: Set up Go 21 | uses: actions/setup-go@v5 22 | with: 23 | go-version-file: "go.mod" 24 | 25 | # We need kind to test the code 26 | - name: Start kind cluster 27 | uses: helm/kind-action@v1 28 | with: 29 | version: "v0.22.0" # This starts k8s v1.29 30 | 31 | - name: Test code 32 | env: 33 | USE_EXISTING_CLUSTER: true 34 | run: | 35 | make test-ci 36 | 37 | # Sonarcloud 38 | # - name: SonarCloud Scan 39 | # uses: SonarSource/sonarcloud-github-action@master 40 | # env: 41 | # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any 42 | # SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 43 | # GO_MODULES: "on" 44 | 45 | # Docker buildx does not allow for multi architecture builds and loading the docker container locally, 46 | # this way we can not run trivy against the containers, one solution is to run multiple jobs for each arch 47 | # ARM builds are really slow, we're only doing it in the `main-merge` workflow. 48 | docker: 49 | name: Docker build 50 | runs-on: ubuntu-latest 51 | strategy: 52 | matrix: 53 | # arch: ["amd64", "arm64", "arm"] 54 | arch: ["amd64"] 55 | steps: 56 | - name: Check out source code 57 | uses: actions/checkout@v4 58 | 59 | # For multi-arch docker builds 60 | # https://github.com/docker/setup-qemu-action 61 | - name: Set up QEMU 62 | uses: docker/setup-qemu-action@v3 63 | 64 | # https://github.com/docker/setup-buildx-action 65 | - name: Set up Docker Buildx 66 | id: buildx 67 | uses: docker/setup-buildx-action@v3 68 | 69 | # Docker build specifics 70 | - name: Docker meta 71 | id: docker_meta # you'll use this in the next step 72 | uses: docker/metadata-action@v5 73 | with: 74 | # list of Docker images to use as base name for tags 75 | images: | 76 | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 77 | # Docker tags based on the following events/attributes 78 | tags: | 79 | type=sha 80 | 81 | # Check Dockerfile with hadolint 82 | - uses: hadolint/hadolint-action@v3.1.0 83 | with: 84 | dockerfile: Dockerfile 85 | 86 | - name: Build multi-platform images 87 | uses: docker/build-push-action@v6 88 | with: 89 | context: . 90 | platforms: ${{ matrix.arch }} 91 | load: true 92 | tags: ${{ steps.docker_meta.outputs.tags }} 93 | 94 | - name: Trivy vulnerability scanner 95 | uses: aquasecurity/trivy-action@0.28.0 96 | env: 97 | IMAGE: ${{ steps.docker_meta.outputs.tags }} 98 | TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db 99 | with: 100 | image-ref: ${{ env.IMAGE }} 101 | format: "table" 102 | exit-code: "1" 103 | 104 | semantic-validate: 105 | name: Validate PR title 106 | runs-on: ubuntu-latest 107 | steps: 108 | - uses: amannn/action-semantic-pull-request@v5 109 | id: lint_pr_title 110 | env: 111 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 112 | 113 | - uses: marocchino/sticky-pull-request-comment@v2 114 | # When the previous steps fails, the workflow would stop. By adding this 115 | # condition you can continue the execution with the populated error message. 116 | if: always() && (steps.lint_pr_title.outputs.error_message != null) 117 | with: 118 | header: pr-title-lint-error 119 | message: | 120 | Hey there and thank you for opening this pull request! 👋🏼 121 | 122 | We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted. 123 | 124 | Details: 125 | 126 | ``` 127 | ${{ steps.lint_pr_title.outputs.error_message }} 128 | ``` 129 | 130 | # Delete a previous comment when the issue has been resolved 131 | - if: ${{ steps.lint_pr_title.outputs.error_message == null }} 132 | uses: marocchino/sticky-pull-request-comment@v2 133 | with: 134 | header: pr-title-lint-error 135 | delete: true 136 | -------------------------------------------------------------------------------- /external/checkly/check.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 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 external 18 | 19 | import ( 20 | "context" 21 | "net/http" 22 | "strconv" 23 | "time" 24 | 25 | "github.com/checkly/checkly-go-sdk" 26 | ) 27 | 28 | // Check is a struct for the internal packages to help put together the checkly check 29 | type Check struct { 30 | Name string 31 | Namespace string 32 | Frequency int 33 | MaxResponseTime int 34 | Endpoint string 35 | SuccessCode string 36 | GroupID int64 37 | ID string 38 | Muted bool 39 | Labels map[string]string 40 | } 41 | 42 | func checklyCheck(apiCheck Check) (check checkly.Check, err error) { 43 | 44 | shouldFail, err := shouldFail(apiCheck.SuccessCode) 45 | if err != nil { 46 | return 47 | } 48 | 49 | tags := getTags(apiCheck.Labels) 50 | tags = append(tags, "checkly-operator") 51 | tags = append(tags, apiCheck.Namespace) 52 | 53 | alertSettings := checkly.AlertSettings{ 54 | EscalationType: checkly.RunBased, 55 | RunBasedEscalation: checkly.RunBasedEscalation{ 56 | FailedRunThreshold: 5, 57 | }, 58 | TimeBasedEscalation: checkly.TimeBasedEscalation{ 59 | MinutesFailingThreshold: 5, 60 | }, 61 | Reminders: checkly.Reminders{ 62 | Interval: 5, 63 | }, 64 | SSLCertificates: checkly.SSLCertificates{ 65 | Enabled: false, 66 | AlertThreshold: 3, 67 | }, 68 | } 69 | 70 | check = checkly.Check{ 71 | Name: apiCheck.Name, 72 | Type: checkly.TypeAPI, 73 | Frequency: checkValueInt(apiCheck.Frequency, 5), 74 | DegradedResponseTime: 5000, 75 | MaxResponseTime: checkValueInt(apiCheck.MaxResponseTime, 15000), 76 | Activated: true, 77 | Muted: apiCheck.Muted, // muted for development 78 | ShouldFail: shouldFail, 79 | DoubleCheck: false, 80 | SSLCheck: false, 81 | LocalSetupScript: "", 82 | LocalTearDownScript: "", 83 | Locations: []string{}, 84 | Tags: tags, 85 | AlertSettings: alertSettings, 86 | UseGlobalAlertSettings: false, 87 | GroupID: apiCheck.GroupID, 88 | Request: checkly.Request{ 89 | Method: http.MethodGet, 90 | URL: apiCheck.Endpoint, 91 | Headers: []checkly.KeyValue{ 92 | // { 93 | // Key: "X-Test", 94 | // Value: "foo", 95 | // }, 96 | }, 97 | QueryParameters: []checkly.KeyValue{ 98 | // { 99 | // Key: "query", 100 | // Value: "foo", 101 | // }, 102 | }, 103 | Assertions: []checkly.Assertion{ 104 | { 105 | Source: checkly.StatusCode, 106 | Comparison: checkly.Equals, 107 | Target: apiCheck.SuccessCode, 108 | }, 109 | }, 110 | Body: "", 111 | BodyType: "NONE", 112 | }, 113 | } 114 | 115 | return 116 | } 117 | 118 | // Create creates a new checklyhq.com check 119 | func Create(apiCheck Check, client checkly.Client) (ID string, err error) { 120 | 121 | check, err := checklyCheck(apiCheck) 122 | if err != nil { 123 | return 124 | } 125 | 126 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 127 | defer cancel() 128 | 129 | gotCheck, err := client.Create(ctx, check) 130 | if err != nil { 131 | return 132 | } 133 | 134 | ID = gotCheck.ID 135 | 136 | return 137 | } 138 | 139 | // Update updates an existing checklyhq.com check 140 | func Update(apiCheck Check, client checkly.Client) (err error) { 141 | 142 | check, err := checklyCheck(apiCheck) 143 | if err != nil { 144 | return 145 | } 146 | 147 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 148 | defer cancel() 149 | 150 | _, err = client.Update(ctx, apiCheck.ID, check) 151 | 152 | return 153 | } 154 | 155 | // Delete deletes an existing checklyhq.com check 156 | func Delete(ID string, client checkly.Client) (err error) { 157 | 158 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 159 | defer cancel() 160 | 161 | err = client.Delete(ctx, ID) 162 | 163 | return 164 | } 165 | 166 | func shouldFail(successCode string) (bool, error) { 167 | code, err := strconv.Atoi(successCode) 168 | if err != nil { 169 | return false, err 170 | } 171 | if code < 400 { 172 | return false, nil 173 | } else { 174 | return true, nil 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/checkly/checkly-operator 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.3 6 | 7 | require ( 8 | github.com/onsi/ginkgo v1.16.5 9 | github.com/onsi/gomega v1.33.1 10 | k8s.io/api v0.31.0 11 | k8s.io/apimachinery v0.31.0 12 | k8s.io/client-go v0.31.0 13 | sigs.k8s.io/controller-runtime v0.19.0 14 | ) 15 | 16 | require ( 17 | github.com/antlr4-go/antlr/v4 v4.13.0 // indirect 18 | github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect 19 | github.com/blang/semver/v4 v4.0.0 // indirect 20 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 21 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 22 | github.com/evanphx/json-patch/v5 v5.9.0 // indirect 23 | github.com/felixge/httpsnoop v1.0.4 // indirect 24 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 25 | github.com/go-logr/stdr v1.2.2 // indirect 26 | github.com/go-openapi/jsonpointer v0.19.6 // indirect 27 | github.com/go-openapi/jsonreference v0.20.2 // indirect 28 | github.com/go-openapi/swag v0.22.4 // indirect 29 | github.com/google/cel-go v0.20.1 // indirect 30 | github.com/google/gnostic-models v0.6.8 // indirect 31 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect 32 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 33 | github.com/josharian/intern v1.0.0 // indirect 34 | github.com/mailru/easyjson v0.7.7 // indirect 35 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 36 | github.com/spf13/cobra v1.8.1 // indirect 37 | github.com/stoewer/go-strcase v1.2.0 // indirect 38 | github.com/x448/float16 v0.8.4 // indirect 39 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect 40 | go.opentelemetry.io/otel v1.28.0 // indirect 41 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect 42 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect 43 | go.opentelemetry.io/otel/metric v1.28.0 // indirect 44 | go.opentelemetry.io/otel/sdk v1.28.0 // indirect 45 | go.opentelemetry.io/otel/trace v1.28.0 // indirect 46 | go.opentelemetry.io/proto/otlp v1.3.1 // indirect 47 | golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect 48 | golang.org/x/sync v0.14.0 // indirect 49 | google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect 50 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect 51 | google.golang.org/grpc v1.65.0 // indirect 52 | k8s.io/apiserver v0.31.0 // indirect 53 | sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 // indirect 54 | ) 55 | 56 | require ( 57 | github.com/beorn7/perks v1.0.1 // indirect 58 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 59 | github.com/checkly/checkly-go-sdk v1.11.0 60 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 61 | github.com/fsnotify/fsnotify v1.7.0 // indirect 62 | github.com/go-logr/logr v1.4.2 // indirect 63 | github.com/go-logr/zapr v1.3.0 // indirect 64 | github.com/gogo/protobuf v1.3.2 // indirect 65 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 66 | github.com/golang/protobuf v1.5.4 // indirect 67 | github.com/google/go-cmp v0.6.0 // indirect 68 | github.com/google/gofuzz v1.2.0 // indirect 69 | github.com/google/uuid v1.6.0 // indirect 70 | github.com/imdario/mergo v0.3.13 // indirect 71 | github.com/json-iterator/go v1.1.12 // indirect 72 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 73 | github.com/modern-go/reflect2 v1.0.2 // indirect 74 | github.com/nxadm/tail v1.4.8 // indirect 75 | github.com/pkg/errors v0.9.1 // indirect 76 | github.com/prometheus/client_golang v1.19.1 // indirect 77 | github.com/prometheus/client_model v0.6.1 // indirect 78 | github.com/prometheus/common v0.55.0 // indirect 79 | github.com/prometheus/procfs v0.15.1 // indirect 80 | github.com/spf13/pflag v1.0.5 // indirect 81 | go.uber.org/multierr v1.11.0 // indirect 82 | go.uber.org/zap v1.26.0 // indirect 83 | golang.org/x/net v0.40.0 // indirect 84 | golang.org/x/oauth2 v0.27.0 // indirect 85 | golang.org/x/sys v0.33.0 // indirect 86 | golang.org/x/term v0.32.0 // indirect 87 | golang.org/x/text v0.25.0 // indirect 88 | golang.org/x/time v0.3.0 // indirect 89 | gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect 90 | google.golang.org/protobuf v1.34.2 // indirect 91 | gopkg.in/inf.v0 v0.9.1 // indirect 92 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 93 | gopkg.in/yaml.v2 v2.4.0 // indirect 94 | gopkg.in/yaml.v3 v3.0.1 // indirect 95 | k8s.io/apiextensions-apiserver v0.31.0 // indirect 96 | k8s.io/component-base v0.31.0 // indirect 97 | k8s.io/klog/v2 v2.130.1 // indirect 98 | k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect 99 | k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect 100 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 101 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect 102 | sigs.k8s.io/yaml v1.4.0 // indirect 103 | ) 104 | -------------------------------------------------------------------------------- /external/checkly/alertChannel_test.go: -------------------------------------------------------------------------------- 1 | package external 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "math/rand" 7 | "net/http" 8 | "testing" 9 | "time" 10 | 11 | "github.com/checkly/checkly-go-sdk" 12 | checklyv1alpha1 "github.com/checkly/checkly-operator/api/checkly/v1alpha1" 13 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | ) 15 | 16 | func TestChecklyAlertChannel(t *testing.T) { 17 | acName := "foo" 18 | acEmailAddress := "foo@bar.baz" 19 | 20 | dataEmpty := checklyv1alpha1.AlertChannel{ 21 | ObjectMeta: metav1.ObjectMeta{ 22 | Name: acName, 23 | }, 24 | Spec: checklyv1alpha1.AlertChannelSpec{ 25 | SendRecovery: false, 26 | }, 27 | } 28 | 29 | opsGenieConfigEmpty := checkly.AlertChannelOpsgenie{} 30 | 31 | returned, err := checklyAlertChannel(&dataEmpty, opsGenieConfigEmpty) 32 | if err != nil { 33 | t.Errorf("Expected no error, got %e", err) 34 | } 35 | 36 | if returned.Opsgenie != nil { 37 | t.Errorf("Expected empty Opsgenie config, got %s", returned.Opsgenie) 38 | } 39 | 40 | dataEmail := dataEmpty 41 | dataEmail.Spec.Email = checkly.AlertChannelEmail{ 42 | Address: acEmailAddress, 43 | } 44 | 45 | returned, err = checklyAlertChannel(&dataEmail, opsGenieConfigEmpty) 46 | if err != nil { 47 | t.Errorf("Expected no error, got %e", err) 48 | } 49 | 50 | if returned.Email.Address != acEmailAddress { 51 | t.Errorf("Expected %s, got %s", acEmailAddress, returned.Email.Address) 52 | } 53 | 54 | dataOpsGenieFull := checkly.AlertChannelOpsgenie{ 55 | APIKey: "foo-bar", 56 | Region: "US", 57 | Priority: "999", 58 | Name: "baz", 59 | } 60 | 61 | returned, err = checklyAlertChannel(&dataEmpty, dataOpsGenieFull) 62 | if err != nil { 63 | t.Errorf("Expected no error, got %e", err) 64 | } 65 | 66 | if returned.Opsgenie == nil { 67 | t.Error("Expected Opsgenie field to tbe populated, it's empty") 68 | } 69 | 70 | if returned.Opsgenie.Priority != "999" { 71 | t.Errorf("Expected %s, got %s", "999", returned.Opsgenie.Priority) 72 | } 73 | 74 | if returned.Opsgenie.Region != "US" { 75 | t.Errorf("Expected %s, got %s", "US", returned.Opsgenie.Region) 76 | } 77 | 78 | if returned.Email != nil { 79 | t.Errorf("Expected nil, got %s", returned.Email) 80 | } 81 | 82 | } 83 | 84 | func TestAlertChannelActions(t *testing.T) { 85 | // Generate a different number each time 86 | rand.Seed(time.Now().UnixNano()) 87 | expectedAlertChannelID := rand.Intn(100) 88 | 89 | acName := "foo" 90 | 91 | testData := &checklyv1alpha1.AlertChannel{ 92 | ObjectMeta: metav1.ObjectMeta{ 93 | Name: acName, 94 | }, 95 | Spec: checklyv1alpha1.AlertChannelSpec{ 96 | SendRecovery: false, 97 | }, 98 | Status: checklyv1alpha1.AlertChannelStatus{ 99 | ID: int64(expectedAlertChannelID), 100 | }, 101 | } 102 | 103 | opsGenieConfigEmpty := checkly.AlertChannelOpsgenie{} 104 | 105 | // Test errors 106 | testClient := checkly.NewClient( 107 | "http://localhost:5557", 108 | "foobarbaz", 109 | nil, 110 | nil, 111 | ) 112 | testClient.SetAccountId("1234567890") 113 | 114 | // Create fail 115 | _, err := CreateAlertChannel(testData, opsGenieConfigEmpty, testClient) 116 | if err == nil { 117 | t.Error("Expected error, got none") 118 | } 119 | 120 | // Update fail 121 | err = UpdateAlertChannel(testData, opsGenieConfigEmpty, testClient) 122 | if err == nil { 123 | t.Error("Expected error, got none") 124 | } 125 | 126 | // Delete fail 127 | err = DeleteAlertChannel(testData, testClient) 128 | if err == nil { 129 | t.Error("Expected error, got none") 130 | } 131 | 132 | go func() { 133 | http.HandleFunc("/v1/alert-channels", func(w http.ResponseWriter, _ *http.Request) { 134 | w.WriteHeader(http.StatusCreated) 135 | w.Header().Set("Content-Type", "application/json") 136 | resp := make(map[string]interface{}) 137 | resp["id"] = expectedAlertChannelID 138 | jsonResp, _ := json.Marshal(resp) 139 | w.Write(jsonResp) 140 | return 141 | }) 142 | http.HandleFunc(fmt.Sprintf("/v1/alert-channels/%d", expectedAlertChannelID), func(w http.ResponseWriter, r *http.Request) { 143 | r.ParseForm() 144 | method := r.Method 145 | switch method { 146 | case "PUT": 147 | w.WriteHeader(http.StatusOK) 148 | w.Header().Set("Content-Type", "application/json") 149 | resp := make(map[string]interface{}) 150 | resp["id"] = expectedAlertChannelID 151 | jsonResp, _ := json.Marshal(resp) 152 | w.Write(jsonResp) 153 | case "DELETE": 154 | w.WriteHeader(http.StatusNoContent) 155 | } 156 | return 157 | }) 158 | http.ListenAndServe(":5557", nil) 159 | }() 160 | 161 | // Create success 162 | testID, err := CreateAlertChannel(testData, opsGenieConfigEmpty, testClient) 163 | if err != nil { 164 | t.Errorf("Expected no error, got %e", err) 165 | } 166 | if testID != int64(expectedAlertChannelID) { 167 | t.Errorf("Expected %d, got %d", testID, int64(expectedAlertChannelID)) 168 | } 169 | 170 | // Update success 171 | err = UpdateAlertChannel(testData, opsGenieConfigEmpty, testClient) 172 | if err != nil { 173 | t.Errorf("Expected no error, got %e", err) 174 | } 175 | 176 | // Delete success 177 | err = DeleteAlertChannel(testData, testClient) 178 | if err != nil { 179 | t.Errorf("Expecte no error, got %e", err) 180 | } 181 | 182 | } 183 | -------------------------------------------------------------------------------- /internal/controller/checkly/group_controller_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 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 | package checkly 17 | 18 | import ( 19 | "context" 20 | "time" 21 | 22 | "github.com/checkly/checkly-go-sdk" 23 | . "github.com/onsi/ginkgo" 24 | . "github.com/onsi/gomega" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | "k8s.io/apimachinery/pkg/types" 27 | 28 | checklyv1alpha1 "github.com/checkly/checkly-operator/api/checkly/v1alpha1" 29 | ) 30 | 31 | var _ = Describe("ApiCheck Controller", func() { 32 | 33 | // Define utility constants for object names and testing timeouts/durations and intervals. 34 | const ( 35 | timeout = time.Second * 10 36 | duration = time.Second * 10 37 | interval = time.Millisecond * 250 38 | ) 39 | 40 | BeforeEach(func() { 41 | }) 42 | 43 | AfterEach(func() { 44 | // Add any teardown steps that needs to be executed after each test 45 | }) 46 | 47 | // Add Tests for OpenAPI validation (or additonal CRD features) specified in 48 | // your API definition. 49 | // Avoid adding tests for vanilla CRUD operations because they would 50 | // test Kubernetes API server, which isn't the goal here. 51 | Context("Group", func() { 52 | It("Full reconciliation", func() { 53 | 54 | updatedLocations := []string{"eu-west-2", "eu-west-1"} 55 | updatedPrivateLocations := []string{"ground-floor"} 56 | groupKey := types.NamespacedName{ 57 | Name: "test-group", 58 | } 59 | 60 | alertChannelKey := types.NamespacedName{ 61 | Name: "test-alertchannel", 62 | } 63 | 64 | group := &checklyv1alpha1.Group{ 65 | ObjectMeta: metav1.ObjectMeta{ 66 | Name: groupKey.Name, 67 | }, 68 | Spec: checklyv1alpha1.GroupSpec{ 69 | Locations: []string{"eu-west-1"}, 70 | PrivateLocations: []string{}, 71 | AlertChannels: []string{alertChannelKey.Name}, 72 | }, 73 | } 74 | 75 | alertChannel := &checklyv1alpha1.AlertChannel{ 76 | ObjectMeta: metav1.ObjectMeta{ 77 | Name: alertChannelKey.Name, 78 | }, 79 | Spec: checklyv1alpha1.AlertChannelSpec{ 80 | Email: checkly.AlertChannelEmail{ 81 | Address: "foo@bar.baz", 82 | }, 83 | }, 84 | } 85 | 86 | // Create 87 | Expect(k8sClient.Create(context.Background(), alertChannel)).Should(Succeed()) 88 | Expect(k8sClient.Create(context.Background(), group)).Should(Succeed()) 89 | 90 | By("Expecting submitted") 91 | Eventually(func() bool { 92 | f := &checklyv1alpha1.Group{} 93 | err := k8sClient.Get(context.Background(), groupKey, f) 94 | if err != nil { 95 | return false 96 | } 97 | return true 98 | }, timeout, interval).Should(BeTrue()) 99 | 100 | // Status.ID should be present 101 | By("Expecting group ID") 102 | Eventually(func() bool { 103 | f := &checklyv1alpha1.Group{} 104 | err := k8sClient.Get(context.Background(), groupKey, f) 105 | if f.Status.ID == 1 && err == nil { 106 | return true 107 | } else { 108 | return false 109 | } 110 | }, timeout, interval).Should(BeTrue()) 111 | 112 | // Finalizer should be present 113 | By("Expecting finalizer") 114 | Eventually(func() bool { 115 | f := &checklyv1alpha1.Group{} 116 | err := k8sClient.Get(context.Background(), groupKey, f) 117 | if err != nil { 118 | return false 119 | } 120 | 121 | for _, finalizer := range f.Finalizers { 122 | Expect(finalizer).To(Equal("testing.domain.tld/finalizer"), "Finalizer should match") 123 | } 124 | 125 | return true 126 | }, timeout, interval).Should(BeTrue()) 127 | 128 | // Update 129 | updated := &checklyv1alpha1.Group{} 130 | Expect(k8sClient.Get(context.Background(), groupKey, updated)).Should(Succeed()) 131 | 132 | updated.Spec.Locations = updatedLocations 133 | Expect(k8sClient.Update(context.Background(), updated)).Should(Succeed()) 134 | 135 | By("Expecting update") 136 | Eventually(func() bool { 137 | f := &checklyv1alpha1.Group{} 138 | err := k8sClient.Get(context.Background(), groupKey, f) 139 | if len(f.Spec.Locations) == 2 && err == nil { 140 | return true 141 | } else { 142 | return false 143 | } 144 | }, timeout, interval).Should(BeTrue()) 145 | 146 | updated.Spec.PrivateLocations = updatedPrivateLocations 147 | Expect(k8sClient.Update(context.Background(), updated)).Should(Succeed()) 148 | 149 | By("Expecting update") 150 | Eventually(func() bool { 151 | f := &checklyv1alpha1.Group{} 152 | err := k8sClient.Get(context.Background(), groupKey, f) 153 | if len(f.Spec.PrivateLocations) == 1 && err == nil { 154 | return true 155 | } else { 156 | return false 157 | } 158 | }, timeout, interval).Should(BeTrue()) 159 | 160 | // Delete group 161 | By("Expecting to delete successfully") 162 | Eventually(func() error { 163 | f := &checklyv1alpha1.Group{} 164 | k8sClient.Get(context.Background(), groupKey, f) 165 | return k8sClient.Delete(context.Background(), f) 166 | }, timeout, interval).Should(Succeed()) 167 | 168 | // Delete alertchannel 169 | By("Expecting to delete successfully") 170 | Eventually(func() error { 171 | f := &checklyv1alpha1.AlertChannel{} 172 | k8sClient.Get(context.Background(), alertChannelKey, f) 173 | return k8sClient.Delete(context.Background(), f) 174 | }, timeout, interval).Should(Succeed()) 175 | 176 | By("Expecting delete to finish") 177 | Eventually(func() error { 178 | f := &checklyv1alpha1.Group{} 179 | return k8sClient.Get(context.Background(), groupKey, f) 180 | }, timeout, interval).ShouldNot(Succeed()) 181 | }) 182 | }) 183 | }) 184 | -------------------------------------------------------------------------------- /internal/controller/networking/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 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 networking 18 | 19 | import ( 20 | "encoding/json" 21 | "net/http" 22 | "os" 23 | "path/filepath" 24 | "testing" 25 | 26 | . "github.com/onsi/ginkgo" 27 | . "github.com/onsi/gomega" 28 | networkingv1 "k8s.io/api/networking/v1" 29 | "k8s.io/client-go/kubernetes/scheme" 30 | "k8s.io/client-go/rest" 31 | ctrl "sigs.k8s.io/controller-runtime" 32 | "sigs.k8s.io/controller-runtime/pkg/client" 33 | "sigs.k8s.io/controller-runtime/pkg/envtest" 34 | logf "sigs.k8s.io/controller-runtime/pkg/log" 35 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 36 | 37 | "github.com/checkly/checkly-go-sdk" 38 | checklyv1alpha1 "github.com/checkly/checkly-operator/api/checkly/v1alpha1" 39 | 40 | //+kubebuilder:scaffold:imports 41 | internalController "github.com/checkly/checkly-operator/internal/controller/checkly" 42 | ) 43 | 44 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 45 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 46 | 47 | // var cfg *rest.Config 48 | var k8sClient client.Client 49 | var testEnv *envtest.Environment 50 | 51 | func TestAPIs(t *testing.T) { 52 | RegisterFailHandler(Fail) 53 | 54 | RunSpecs(t, "Controller Suite") 55 | } 56 | 57 | var _ = BeforeSuite(func() { 58 | Expect(os.Setenv("USE_EXISTING_CLUSTER", "true")).To(Succeed()) 59 | Expect(os.Setenv("TEST_ASSET_KUBECTL", "../testbin/bin/kubectl")).To(Succeed()) 60 | logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) 61 | 62 | By("bootstrapping test environment") 63 | testEnv = &envtest.Environment{ 64 | CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, 65 | ErrorIfCRDPathMissing: false, 66 | } 67 | 68 | var err error 69 | // cfg is defined in this file globally. 70 | var cfg *rest.Config 71 | cfg, err = testEnv.Start() 72 | Expect(err).NotTo(HaveOccurred()) 73 | Expect(cfg).NotTo(BeNil()) 74 | 75 | err = networkingv1.AddToScheme(scheme.Scheme) 76 | Expect(err).NotTo(HaveOccurred()) 77 | 78 | err = checklyv1alpha1.AddToScheme(scheme.Scheme) 79 | Expect(err).NotTo(HaveOccurred()) 80 | 81 | //+kubebuilder:scaffold:scheme 82 | 83 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) 84 | Expect(err).NotTo(HaveOccurred()) 85 | Expect(k8sClient).NotTo(BeNil()) 86 | 87 | k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ 88 | Scheme: scheme.Scheme, 89 | }) 90 | Expect(err).ToNot(HaveOccurred()) 91 | 92 | testControllerDomain := "testing.domain.tld" 93 | 94 | err = (&IngressReconciler{ 95 | Client: k8sManager.GetClient(), 96 | Scheme: k8sManager.GetScheme(), 97 | ControllerDomain: testControllerDomain, 98 | }).SetupWithManager(k8sManager) 99 | Expect(err).ToNot(HaveOccurred()) 100 | 101 | // Stub checkly client 102 | testClient := checkly.NewClient( 103 | "http://localhost:5557", 104 | "foobarbaz", 105 | nil, 106 | nil, 107 | ) 108 | testClient.SetAccountId("1234567890") 109 | go func() { 110 | http.HandleFunc("/v1/checks", func(w http.ResponseWriter, _ *http.Request) { 111 | w.WriteHeader(http.StatusCreated) 112 | w.Header().Set("Content-Type", "application/json") 113 | resp := make(map[string]string) 114 | resp["id"] = "2" 115 | jsonResp, _ := json.Marshal(resp) 116 | w.Write(jsonResp) 117 | }) 118 | http.HandleFunc("/v1/checks/2", func(w http.ResponseWriter, r *http.Request) { 119 | r.ParseForm() 120 | method := r.Method 121 | switch method { 122 | case "PUT": 123 | w.WriteHeader(http.StatusOK) 124 | w.Header().Set("Content-Type", "application/json") 125 | resp := make(map[string]string) 126 | resp["id"] = "2" 127 | jsonResp, _ := json.Marshal(resp) 128 | w.Write(jsonResp) 129 | case "DELETE": 130 | w.WriteHeader(http.StatusNoContent) 131 | } 132 | }) 133 | http.HandleFunc("/v1/check-groups", func(w http.ResponseWriter, _ *http.Request) { 134 | w.WriteHeader(http.StatusCreated) 135 | w.Header().Set("Content-Type", "application/json") 136 | resp := make(map[string]interface{}) 137 | resp["id"] = 1 138 | jsonResp, _ := json.Marshal(resp) 139 | w.Write(jsonResp) 140 | }) 141 | http.HandleFunc("/v1/check-groups/1", func(w http.ResponseWriter, r *http.Request) { 142 | r.ParseForm() 143 | method := r.Method 144 | switch method { 145 | case "PUT": 146 | w.WriteHeader(http.StatusOK) 147 | w.Header().Set("Content-Type", "application/json") 148 | resp := make(map[string]interface{}) 149 | resp["id"] = 1 150 | jsonResp, _ := json.Marshal(resp) 151 | w.Write(jsonResp) 152 | case "DELETE": 153 | w.WriteHeader(http.StatusNoContent) 154 | } 155 | }) 156 | http.ListenAndServe(":5557", nil) 157 | }() 158 | 159 | err = (&internalController.ApiCheckReconciler{ 160 | Client: k8sManager.GetClient(), 161 | Scheme: k8sManager.GetScheme(), 162 | ApiClient: testClient, 163 | ControllerDomain: testControllerDomain, 164 | }).SetupWithManager(k8sManager) 165 | Expect(err).ToNot(HaveOccurred()) 166 | 167 | err = (&internalController.GroupReconciler{ 168 | Client: k8sManager.GetClient(), 169 | Scheme: k8sManager.GetScheme(), 170 | ApiClient: testClient, 171 | ControllerDomain: testControllerDomain, 172 | }).SetupWithManager(k8sManager) 173 | Expect(err).ToNot(HaveOccurred()) 174 | 175 | go func() { 176 | defer GinkgoRecover() 177 | err = k8sManager.Start(ctrl.SetupSignalHandler()) 178 | Expect(err).ToNot(HaveOccurred(), "failed to run manager") 179 | }() 180 | 181 | }, 60) 182 | 183 | var _ = AfterSuite(func() { 184 | By("tearing down the test environment") 185 | err := testEnv.Stop() 186 | Expect(err).NotTo(HaveOccurred()) 187 | }) 188 | -------------------------------------------------------------------------------- /internal/controller/checkly/alertchannel_controller_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 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 | // Example code used for influence: https://github.com/Azure/azure-databricks-operator/blob/0f722a710fea06b86ecdccd9455336ca712bf775/controllers/dcluster_controller_test.go 18 | 19 | package checkly 20 | 21 | import ( 22 | "context" 23 | "time" 24 | 25 | "github.com/checkly/checkly-go-sdk" 26 | checklyv1alpha1 "github.com/checkly/checkly-operator/api/checkly/v1alpha1" 27 | . "github.com/onsi/ginkgo" 28 | . "github.com/onsi/gomega" 29 | corev1 "k8s.io/api/core/v1" 30 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 31 | "k8s.io/apimachinery/pkg/types" 32 | ) 33 | 34 | var _ = Describe("ApiCheck Controller", func() { 35 | 36 | // Define utility constants for object names and testing timeouts/durations and intervals. 37 | const ( 38 | timeout = time.Second * 10 39 | duration = time.Second * 10 40 | interval = time.Millisecond * 250 41 | ) 42 | 43 | BeforeEach(func() { 44 | // Add any setup steps that needs to be executed before each test 45 | }) 46 | 47 | AfterEach(func() { 48 | // Add any teardown steps that needs to be executed after each test 49 | acKey := types.NamespacedName{ 50 | Name: "test-alert-channel", 51 | } 52 | f := &checklyv1alpha1.AlertChannel{} 53 | k8sClient.Get(context.Background(), acKey, f) 54 | k8sClient.Delete(context.Background(), f) 55 | }) 56 | 57 | // Add Tests for OpenAPI validation (or additonal CRD features) specified in 58 | // your API definition. 59 | // Avoid adding tests for vanilla CRUD operations because they would 60 | // test Kubernetes API server, which isn't the goal here. 61 | Context("AlertChannels", func() { 62 | It("Full reconciliation", func() { 63 | 64 | acKey := types.NamespacedName{ 65 | Name: "test-alert-channel", 66 | } 67 | 68 | secretKey := types.NamespacedName{ 69 | Name: "test-secret", 70 | Namespace: "default", 71 | } 72 | 73 | secretData := map[string][]byte{ 74 | "TEST": []byte("test"), 75 | } 76 | 77 | alertChannel := &checklyv1alpha1.AlertChannel{ 78 | ObjectMeta: metav1.ObjectMeta{ 79 | Name: acKey.Name, 80 | }, 81 | Spec: checklyv1alpha1.AlertChannelSpec{ 82 | SendFailure: false, 83 | Email: checkly.AlertChannelEmail{ 84 | Address: "foo@bar.baz", 85 | }, 86 | }, 87 | } 88 | 89 | secret := &corev1.Secret{ 90 | ObjectMeta: metav1.ObjectMeta{ 91 | Name: secretKey.Name, 92 | Namespace: secretKey.Namespace, 93 | }, 94 | Data: secretData, 95 | } 96 | 97 | // Create 98 | Expect(k8sClient.Create(context.Background(), alertChannel)).Should(Succeed()) 99 | Expect(k8sClient.Create(context.Background(), secret)).Should(Succeed()) 100 | 101 | // Status.ID should be present 102 | By("Expecting AlertChannel ID") 103 | Eventually(func() bool { 104 | f := &checklyv1alpha1.AlertChannel{} 105 | err := k8sClient.Get(context.Background(), acKey, f) 106 | if f.Status.ID == 3 && err == nil { 107 | return true 108 | } else { 109 | return false 110 | } 111 | }, timeout, interval).Should(BeTrue()) 112 | 113 | // Finalizer should be present 114 | By("Expecting finalizer") 115 | Eventually(func() bool { 116 | f := &checklyv1alpha1.AlertChannel{} 117 | err := k8sClient.Get(context.Background(), acKey, f) 118 | if err != nil { 119 | return false 120 | } 121 | 122 | for _, finalizer := range f.Finalizers { 123 | Expect(finalizer).To(Equal("testing.domain.tld/finalizer"), "Finalizer should match") 124 | } 125 | 126 | return true 127 | }, timeout, interval).Should(BeTrue()) 128 | 129 | // Update 130 | By("Expecting field update") 131 | Eventually(func() bool { 132 | f := &checklyv1alpha1.AlertChannel{} 133 | err := k8sClient.Get(context.Background(), acKey, f) 134 | if err != nil { 135 | return false 136 | } 137 | 138 | f.Spec.Email = checkly.AlertChannelEmail{} 139 | f.Spec.SendFailure = true 140 | f.Spec.OpsGenie = checklyv1alpha1.AlertChannelOpsGenie{ 141 | APISecret: corev1.ObjectReference{ 142 | Namespace: secretKey.Namespace, 143 | Name: secretKey.Name, 144 | FieldPath: "TEST", 145 | }, 146 | Priority: "999", 147 | Region: "US", 148 | } 149 | err = k8sClient.Update(context.Background(), f) 150 | if err != nil { 151 | return false 152 | } 153 | 154 | u := &checklyv1alpha1.AlertChannel{} 155 | err = k8sClient.Get(context.Background(), acKey, u) 156 | if err != nil { 157 | return false 158 | } 159 | 160 | if u.Spec.SendFailure != true { 161 | return false 162 | } 163 | 164 | if u.Spec.Email != (checkly.AlertChannelEmail{}) { 165 | return false 166 | } 167 | 168 | if u.Spec.OpsGenie.Priority != "999" { 169 | return false 170 | } 171 | 172 | return true 173 | }, timeout, interval).Should(BeTrue()) 174 | 175 | // Delete AlertChannel 176 | By("Expecting to delete alertchannel successfully") 177 | Eventually(func() error { 178 | f := &checklyv1alpha1.AlertChannel{} 179 | k8sClient.Get(context.Background(), acKey, f) 180 | return k8sClient.Delete(context.Background(), f) 181 | }, timeout, interval).Should(Succeed()) 182 | 183 | By("Expecting delete to finish") 184 | Eventually(func() error { 185 | f := &checklyv1alpha1.AlertChannel{} 186 | return k8sClient.Get(context.Background(), acKey, f) 187 | }, timeout, interval).ShouldNot(Succeed()) 188 | 189 | // Delete secret 190 | By("Expecting to delete secret successfully") 191 | Eventually(func() error { 192 | f := &corev1.Secret{} 193 | k8sClient.Get(context.Background(), secretKey, f) 194 | return k8sClient.Delete(context.Background(), f) 195 | }, timeout, interval).Should(Succeed()) 196 | }) 197 | // return 198 | }) 199 | }) 200 | -------------------------------------------------------------------------------- /config/crd/bases/k8s.checklyhq.com_alertchannels.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.16.1 7 | name: alertchannels.k8s.checklyhq.com 8 | spec: 9 | group: k8s.checklyhq.com 10 | names: 11 | kind: AlertChannel 12 | listKind: AlertChannelList 13 | plural: alertchannels 14 | singular: alertchannel 15 | scope: Cluster 16 | versions: 17 | - name: v1alpha1 18 | schema: 19 | openAPIV3Schema: 20 | description: AlertChannel is the Schema for the alertchannels API 21 | properties: 22 | apiVersion: 23 | description: |- 24 | APIVersion defines the versioned schema of this representation of an object. 25 | Servers should convert recognized schemas to the latest internal value, and 26 | may reject unrecognized values. 27 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 28 | type: string 29 | kind: 30 | description: |- 31 | Kind is a string value representing the REST resource this object represents. 32 | Servers may infer this from the endpoint the client submits requests to. 33 | Cannot be updated. 34 | In CamelCase. 35 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 36 | type: string 37 | metadata: 38 | type: object 39 | spec: 40 | description: AlertChannelSpec defines the desired state of AlertChannel 41 | properties: 42 | email: 43 | description: Email holds information about the Email alert configuration 44 | properties: 45 | address: 46 | type: string 47 | required: 48 | - address 49 | type: object 50 | opsgenie: 51 | description: OpsGenie holds information about the Opsgenie alert configuration 52 | properties: 53 | apisecret: 54 | description: APISecret determines where the secret ref is to pull 55 | the OpsGenie API key from 56 | properties: 57 | apiVersion: 58 | description: API version of the referent. 59 | type: string 60 | fieldPath: 61 | description: |- 62 | If referring to a piece of an object instead of an entire object, this string 63 | should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. 64 | For example, if the object reference is to a container within a pod, this would take on a value like: 65 | "spec.containers{name}" (where "name" refers to the name of the container that triggered 66 | the event) or if no container name is specified "spec.containers[2]" (container with 67 | index 2 in this pod). This syntax is chosen only to have some well-defined way of 68 | referencing a part of an object. 69 | type: string 70 | kind: 71 | description: |- 72 | Kind of the referent. 73 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 74 | type: string 75 | name: 76 | description: |- 77 | Name of the referent. 78 | More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names 79 | type: string 80 | namespace: 81 | description: |- 82 | Namespace of the referent. 83 | More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ 84 | type: string 85 | resourceVersion: 86 | description: |- 87 | Specific resourceVersion to which this reference is made, if any. 88 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency 89 | type: string 90 | uid: 91 | description: |- 92 | UID of the referent. 93 | More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids 94 | type: string 95 | type: object 96 | x-kubernetes-map-type: atomic 97 | priority: 98 | description: Priority assigned to the alerts sent from checklyhq.com 99 | type: string 100 | region: 101 | description: Region holds information about the OpsGenie region 102 | (EU or US) 103 | type: string 104 | required: 105 | - apisecret 106 | type: object 107 | sendfailure: 108 | description: SendFailure determines if the Failure event should be 109 | sent to the alerting channel 110 | type: boolean 111 | sendrecovery: 112 | description: SendRecovery determines if the Recovery event should 113 | be sent to the alert channel 114 | type: boolean 115 | type: object 116 | status: 117 | description: AlertChannelStatus defines the observed state of AlertChannel 118 | properties: 119 | id: 120 | description: |- 121 | INSERT ADDITIONAL STATUS FIELD - define observed state of cluster 122 | Important: Run "make" to regenerate code after modifying this file 123 | format: int64 124 | type: integer 125 | required: 126 | - id 127 | type: object 128 | type: object 129 | served: true 130 | storage: true 131 | subresources: 132 | status: {} 133 | -------------------------------------------------------------------------------- /internal/controller/checkly/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 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 checkly 18 | 19 | import ( 20 | "encoding/json" 21 | "net/http" 22 | "os" 23 | "path/filepath" 24 | "testing" 25 | //+kubebuilder:scaffold:imports 26 | 27 | "github.com/checkly/checkly-go-sdk" 28 | . "github.com/onsi/ginkgo" 29 | . "github.com/onsi/gomega" 30 | "k8s.io/client-go/kubernetes/scheme" 31 | "k8s.io/client-go/rest" 32 | ctrl "sigs.k8s.io/controller-runtime" 33 | "sigs.k8s.io/controller-runtime/pkg/client" 34 | "sigs.k8s.io/controller-runtime/pkg/envtest" 35 | logf "sigs.k8s.io/controller-runtime/pkg/log" 36 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 37 | 38 | checklyv1alpha1 "github.com/checkly/checkly-operator/api/checkly/v1alpha1" 39 | ) 40 | 41 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 42 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 43 | 44 | var cfg *rest.Config 45 | var k8sClient client.Client 46 | var testEnv *envtest.Environment 47 | 48 | func TestAPIs(t *testing.T) { 49 | RegisterFailHandler(Fail) 50 | 51 | RunSpecs(t, "Controller Suite") 52 | } 53 | 54 | var _ = BeforeSuite(func() { 55 | Expect(os.Setenv("USE_EXISTING_CLUSTER", "true")).To(Succeed()) 56 | Expect(os.Setenv("TEST_ASSET_KUBECTL", "../testbin/bin/kubectl")).To(Succeed()) 57 | logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) 58 | 59 | By("bootstrapping test environment") 60 | testEnv = &envtest.Environment{ 61 | CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, 62 | ErrorIfCRDPathMissing: true, 63 | } 64 | 65 | var err error 66 | cfg, err := testEnv.Start() 67 | Expect(err).NotTo(HaveOccurred()) 68 | Expect(cfg).NotTo(BeNil()) 69 | 70 | err = checklyv1alpha1.AddToScheme(scheme.Scheme) 71 | Expect(err).NotTo(HaveOccurred()) 72 | 73 | //+kubebuilder:scaffold:scheme 74 | 75 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) 76 | Expect(err).NotTo(HaveOccurred()) 77 | Expect(k8sClient).NotTo(BeNil()) 78 | 79 | k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ 80 | Scheme: scheme.Scheme, 81 | }) 82 | Expect(err).ToNot(HaveOccurred()) 83 | 84 | // Stub checkly client 85 | testClient := checkly.NewClient( 86 | "http://localhost:5555", 87 | "foobarbaz", 88 | nil, 89 | nil, 90 | ) 91 | testClient.SetAccountId("1234567890") 92 | go func() { 93 | http.HandleFunc("/v1/checks", func(w http.ResponseWriter, _ *http.Request) { 94 | w.WriteHeader(http.StatusCreated) 95 | w.Header().Set("Content-Type", "application/json") 96 | resp := make(map[string]string) 97 | resp["id"] = "2" 98 | jsonResp, _ := json.Marshal(resp) 99 | w.Write(jsonResp) 100 | return 101 | }) 102 | http.HandleFunc("/v1/checks/2", func(w http.ResponseWriter, r *http.Request) { 103 | r.ParseForm() 104 | method := r.Method 105 | switch method { 106 | case "PUT": 107 | w.WriteHeader(http.StatusOK) 108 | w.Header().Set("Content-Type", "application/json") 109 | resp := make(map[string]string) 110 | resp["id"] = "2" 111 | jsonResp, _ := json.Marshal(resp) 112 | w.Write(jsonResp) 113 | case "DELETE": 114 | w.WriteHeader(http.StatusNoContent) 115 | } 116 | return 117 | }) 118 | http.HandleFunc("/v1/check-groups", func(w http.ResponseWriter, _ *http.Request) { 119 | w.WriteHeader(http.StatusCreated) 120 | w.Header().Set("Content-Type", "application/json") 121 | resp := make(map[string]interface{}) 122 | resp["id"] = 1 123 | jsonResp, _ := json.Marshal(resp) 124 | w.Write(jsonResp) 125 | return 126 | }) 127 | http.HandleFunc("/v1/check-groups/1", func(w http.ResponseWriter, r *http.Request) { 128 | r.ParseForm() 129 | method := r.Method 130 | switch method { 131 | case "PUT": 132 | w.WriteHeader(http.StatusOK) 133 | w.Header().Set("Content-Type", "application/json") 134 | resp := make(map[string]interface{}) 135 | resp["id"] = 1 136 | jsonResp, _ := json.Marshal(resp) 137 | w.Write(jsonResp) 138 | case "DELETE": 139 | w.WriteHeader(http.StatusNoContent) 140 | } 141 | return 142 | }) 143 | http.HandleFunc("/v1/alert-channels", func(w http.ResponseWriter, _ *http.Request) { 144 | w.WriteHeader(http.StatusCreated) 145 | w.Header().Set("Content-Type", "application/json") 146 | resp := make(map[string]interface{}) 147 | resp["id"] = 3 148 | jsonResp, _ := json.Marshal(resp) 149 | w.Write(jsonResp) 150 | return 151 | }) 152 | http.HandleFunc("/v1/alert-channels/3", func(w http.ResponseWriter, r *http.Request) { 153 | r.ParseForm() 154 | method := r.Method 155 | switch method { 156 | case "PUT": 157 | w.WriteHeader(http.StatusOK) 158 | w.Header().Set("Content-Type", "application/json") 159 | resp := make(map[string]interface{}) 160 | resp["id"] = 3 161 | jsonResp, _ := json.Marshal(resp) 162 | w.Write(jsonResp) 163 | case "DELETE": 164 | w.WriteHeader(http.StatusNoContent) 165 | } 166 | return 167 | }) 168 | http.ListenAndServe(":5555", nil) 169 | }() 170 | 171 | testControllerDomain := "testing.domain.tld" 172 | 173 | err = (&ApiCheckReconciler{ 174 | Client: k8sManager.GetClient(), 175 | Scheme: k8sManager.GetScheme(), 176 | ApiClient: testClient, 177 | ControllerDomain: testControllerDomain, 178 | }).SetupWithManager(k8sManager) 179 | Expect(err).ToNot(HaveOccurred()) 180 | 181 | err = (&GroupReconciler{ 182 | Client: k8sManager.GetClient(), 183 | Scheme: k8sManager.GetScheme(), 184 | ApiClient: testClient, 185 | ControllerDomain: testControllerDomain, 186 | }).SetupWithManager(k8sManager) 187 | Expect(err).ToNot(HaveOccurred()) 188 | 189 | err = (&AlertChannelReconciler{ 190 | Client: k8sManager.GetClient(), 191 | Scheme: k8sManager.GetScheme(), 192 | ApiClient: testClient, 193 | ControllerDomain: testControllerDomain, 194 | }).SetupWithManager(k8sManager) 195 | Expect(err).ToNot(HaveOccurred()) 196 | 197 | go func() { 198 | defer GinkgoRecover() 199 | err = k8sManager.Start(ctrl.SetupSignalHandler()) 200 | Expect(err).ToNot(HaveOccurred(), "failed to run manager") 201 | }() 202 | 203 | }, 60) 204 | 205 | var _ = AfterSuite(func() { 206 | By("tearing down the test environment") 207 | err := testEnv.Stop() 208 | Expect(err).NotTo(HaveOccurred()) 209 | }) 210 | -------------------------------------------------------------------------------- /external/checkly/check_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 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 external 18 | 19 | import ( 20 | "encoding/json" 21 | "net/http" 22 | "testing" 23 | 24 | "github.com/checkly/checkly-go-sdk" 25 | ) 26 | 27 | func TestChecklyCheck(t *testing.T) { 28 | 29 | data1 := Check{ 30 | Name: "foo", 31 | Namespace: "bar", 32 | Frequency: 15, 33 | MaxResponseTime: 2000, 34 | Endpoint: "https://foo.bar/baz", 35 | SuccessCode: "403", 36 | Muted: true, 37 | } 38 | 39 | testData, _ := checklyCheck(data1) 40 | 41 | if testData.Name != data1.Name { 42 | t.Errorf("Expected %s, got %s", data1.Name, testData.Name) 43 | } 44 | 45 | if testData.Frequency != data1.Frequency { 46 | t.Errorf("Expected %d, got %d", data1.Frequency, testData.Frequency) 47 | } 48 | 49 | if testData.MaxResponseTime != data1.MaxResponseTime { 50 | t.Errorf("Expected %d, got %d", data1.MaxResponseTime, testData.MaxResponseTime) 51 | } 52 | 53 | if testData.Muted != data1.Muted { 54 | t.Errorf("Expected %t, got %t", data1.Muted, testData.Muted) 55 | } 56 | 57 | if testData.ShouldFail != true { 58 | t.Errorf("Expected %t, got %t", true, testData.ShouldFail) 59 | } 60 | 61 | data2 := Check{ 62 | Name: "foo", 63 | Namespace: "bar", 64 | Endpoint: "https://foo.bar/baz", 65 | SuccessCode: "200", 66 | } 67 | 68 | testData, _ = checklyCheck(data2) 69 | 70 | if testData.Frequency != 5 { 71 | t.Errorf("Expected %d, got %d", 5, testData.Frequency) 72 | } 73 | 74 | if testData.MaxResponseTime != 15000 { 75 | t.Errorf("Expected %d, got %d", 15000, testData.MaxResponseTime) 76 | } 77 | 78 | if testData.ShouldFail != false { 79 | t.Errorf("Expected %t, got %t", false, testData.ShouldFail) 80 | } 81 | 82 | failData := Check{ 83 | Name: "fail", 84 | Namespace: "bar", 85 | Endpoint: "https://foo.bar/baz", 86 | SuccessCode: "foo", 87 | } 88 | 89 | _, err := checklyCheck(failData) 90 | if err == nil { 91 | t.Error("Expected error, got nil") 92 | } 93 | 94 | return 95 | } 96 | 97 | func TestChecklyCheckActions(t *testing.T) { 98 | 99 | expectedCheckID := "2" 100 | expectedGroupID := 1 101 | testData := Check{ 102 | Name: "foo", 103 | Namespace: "bar", 104 | Frequency: 15, 105 | MaxResponseTime: 2000, 106 | Endpoint: "https://foo.bar/baz", 107 | SuccessCode: "200", 108 | ID: "", 109 | } 110 | 111 | // Test errors 112 | testClientFail := checkly.NewClient( 113 | "http://localhost:5556", 114 | "foobarbaz", 115 | nil, 116 | nil, 117 | ) 118 | // Create 119 | _, err := Create(testData, testClientFail) 120 | if err == nil { 121 | t.Error("Expected error, got none") 122 | } 123 | 124 | // Update 125 | err = Update(testData, testClientFail) 126 | if err == nil { 127 | t.Error("Expected error, got none") 128 | } 129 | 130 | // Delete 131 | err = Delete(expectedCheckID, testClientFail) 132 | if err == nil { 133 | t.Error("Expected error, got none") 134 | } 135 | 136 | // Test happy path 137 | testClient := checkly.NewClient( 138 | "http://localhost:5555", 139 | "foobarbaz", 140 | nil, 141 | nil, 142 | ) 143 | testClient.SetAccountId("1234567890") 144 | 145 | go func() { 146 | http.HandleFunc("/v1/checks", func(w http.ResponseWriter, _ *http.Request) { 147 | w.WriteHeader(http.StatusCreated) 148 | w.Header().Set("Content-Type", "application/json") 149 | resp := make(map[string]string) 150 | resp["id"] = expectedCheckID 151 | jsonResp, _ := json.Marshal(resp) 152 | w.Write(jsonResp) 153 | return 154 | }) 155 | http.HandleFunc("/v1/checks/2", func(w http.ResponseWriter, r *http.Request) { 156 | r.ParseForm() 157 | method := r.Method 158 | switch method { 159 | case "PUT": 160 | w.WriteHeader(http.StatusOK) 161 | w.Header().Set("Content-Type", "application/json") 162 | resp := make(map[string]string) 163 | resp["id"] = expectedCheckID 164 | jsonResp, _ := json.Marshal(resp) 165 | w.Write(jsonResp) 166 | case "DELETE": 167 | w.WriteHeader(http.StatusNoContent) 168 | } 169 | return 170 | }) 171 | http.HandleFunc("/v1/check-groups", func(w http.ResponseWriter, _ *http.Request) { 172 | w.WriteHeader(http.StatusCreated) 173 | w.Header().Set("Content-Type", "application/json") 174 | resp := make(map[string]interface{}) 175 | resp["id"] = expectedGroupID 176 | jsonResp, _ := json.Marshal(resp) 177 | w.Write(jsonResp) 178 | return 179 | }) 180 | http.HandleFunc("/v1/check-groups/1", func(w http.ResponseWriter, r *http.Request) { 181 | r.ParseForm() 182 | method := r.Method 183 | switch method { 184 | case "PUT": 185 | w.WriteHeader(http.StatusOK) 186 | w.Header().Set("Content-Type", "application/json") 187 | resp := make(map[string]interface{}) 188 | resp["id"] = expectedGroupID 189 | jsonResp, _ := json.Marshal(resp) 190 | w.Write(jsonResp) 191 | case "DELETE": 192 | w.WriteHeader(http.StatusNoContent) 193 | } 194 | return 195 | }) 196 | http.ListenAndServe(":5555", nil) 197 | }() 198 | 199 | testID, err := Create(testData, testClient) 200 | if err != nil { 201 | t.Errorf("Expected no error, got %e", err) 202 | } 203 | 204 | if testID != expectedCheckID { 205 | t.Errorf("Expected %s, got %s", expectedCheckID, testID) 206 | } 207 | 208 | testData.ID = expectedCheckID 209 | 210 | err = Update(testData, testClient) 211 | if err != nil { 212 | t.Errorf("Expected no error, got %e", err) 213 | } 214 | 215 | err = Delete(expectedCheckID, testClient) 216 | if err != nil { 217 | t.Errorf("Expected no error, got %e", err) 218 | } 219 | 220 | return 221 | } 222 | 223 | func TestShouldFail(t *testing.T) { 224 | testTrue := "401" 225 | testFalse := "200" 226 | testErr := "foo" 227 | 228 | testResponse, err := shouldFail(testTrue) 229 | if err != nil { 230 | t.Errorf("Expected no error, got %e", err) 231 | } 232 | if testResponse != true { 233 | t.Errorf("Expected true, got %t", testResponse) 234 | } 235 | 236 | testResponse, err = shouldFail(testFalse) 237 | if err != nil { 238 | t.Errorf("Expected no error, got %e", err) 239 | } 240 | if testResponse != false { 241 | t.Errorf("Expected false, got %t", testResponse) 242 | } 243 | 244 | _, err = shouldFail(testErr) 245 | if err == nil { 246 | t.Errorf("Expected error, got none") 247 | } 248 | 249 | } 250 | -------------------------------------------------------------------------------- /internal/controller/checkly/alertchannel_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 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 checkly 18 | 19 | import ( 20 | "context" 21 | errs "errors" 22 | "fmt" 23 | 24 | corev1 "k8s.io/api/core/v1" 25 | "k8s.io/apimachinery/pkg/api/errors" 26 | "k8s.io/apimachinery/pkg/runtime" 27 | "k8s.io/apimachinery/pkg/types" 28 | ctrl "sigs.k8s.io/controller-runtime" 29 | "sigs.k8s.io/controller-runtime/pkg/client" 30 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 31 | "sigs.k8s.io/controller-runtime/pkg/log" 32 | 33 | "github.com/checkly/checkly-go-sdk" 34 | checklyv1alpha1 "github.com/checkly/checkly-operator/api/checkly/v1alpha1" 35 | external "github.com/checkly/checkly-operator/external/checkly" 36 | ) 37 | 38 | // AlertChannelReconciler reconciles a AlertChannel object 39 | type AlertChannelReconciler struct { 40 | client.Client 41 | Scheme *runtime.Scheme 42 | ApiClient checkly.Client 43 | ControllerDomain string 44 | } 45 | 46 | //+kubebuilder:rbac:groups=k8s.checklyhq.com,resources=alertchannels,verbs=get;list;watch;create;update;patch;delete 47 | //+kubebuilder:rbac:groups=k8s.checklyhq.com,resources=alertchannels/status,verbs=get;update;patch 48 | //+kubebuilder:rbac:groups=k8s.checklyhq.com,resources=alertchannels/finalizers,verbs=update 49 | //+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list 50 | 51 | // Reconcile is part of the main kubernetes reconciliation loop which aims to 52 | // move the current state of the cluster closer to the desired state. 53 | // 54 | // For more details, check Reconcile and its Result here: 55 | // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.12.1/pkg/reconcile 56 | func (r *AlertChannelReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 57 | logger := log.FromContext(ctx) 58 | 59 | logger.V(1).Info("Reconciler started") 60 | 61 | acFinalizer := fmt.Sprintf("%s/finalizer", r.ControllerDomain) 62 | 63 | ac := &checklyv1alpha1.AlertChannel{} 64 | 65 | err := r.Get(ctx, req.NamespacedName, ac) 66 | 67 | // //////////////////////////////// 68 | // Delete Logic 69 | // /////////////////////////////// 70 | if err != nil { 71 | if errors.IsNotFound(err) { 72 | // The resource has been deleted 73 | logger.V(1).Info("Deleted", "checkly AlertChannel ID", ac.Status.ID) 74 | return ctrl.Result{}, nil 75 | } 76 | // Error reading the object 77 | logger.Error(err, "can't read the object") 78 | return ctrl.Result{}, nil 79 | } 80 | 81 | // //////////////////////////////// 82 | // Remove Finalizer Logic 83 | // /////////////////////////////// 84 | 85 | if ac.GetDeletionTimestamp() != nil { 86 | if controllerutil.ContainsFinalizer(ac, acFinalizer) { 87 | logger.V(1).Info("Finalizer is present, trying to delete Checkly AlertChannel", "ID", ac.Status.ID) 88 | err := external.DeleteAlertChannel(ac, r.ApiClient) 89 | if err != nil { 90 | logger.Error(err, "Failed to delete checkly AlertChannel") 91 | return ctrl.Result{}, err 92 | } 93 | 94 | logger.V(1).Info("Successfully deleted checkly AlertChannel", "ID", ac.Status.ID) 95 | 96 | controllerutil.RemoveFinalizer(ac, acFinalizer) 97 | err = r.Update(ctx, ac) 98 | if err != nil { 99 | logger.Error(err, "Failed to delete finalizer.") 100 | return ctrl.Result{}, err 101 | } 102 | logger.V(1).Info("Successfully deleted finalizer from AlertChannel") 103 | } 104 | return ctrl.Result{}, nil 105 | } 106 | 107 | // ///////////////////////////// 108 | // Add Finalizer logic 109 | // //////////////////////////// 110 | if !controllerutil.ContainsFinalizer(ac, acFinalizer) { 111 | controllerutil.AddFinalizer(ac, acFinalizer) 112 | err = r.Update(ctx, ac) 113 | if err != nil { 114 | logger.Error(err, "Failed to update AlertChannel status") 115 | return ctrl.Result{}, err 116 | } 117 | logger.V(1).Info("Added finalizer", "checkly AlertChannel ID", ac.Status.ID) 118 | return ctrl.Result{}, nil 119 | } 120 | 121 | // ///////////////////////////// 122 | // OpsGenie logic + secret retrieval 123 | // //////////////////////////// 124 | opsGenieConfig := checkly.AlertChannelOpsgenie{} 125 | if ac.Spec.OpsGenie.APISecret != (corev1.ObjectReference{}) { 126 | secret := &corev1.Secret{} 127 | err := r.Get(ctx, 128 | types.NamespacedName{ 129 | Name: ac.Spec.OpsGenie.APISecret.Name, 130 | Namespace: ac.Spec.OpsGenie.APISecret.Namespace}, 131 | secret) 132 | if err != nil { 133 | logger.Error(err, "Unable to read secret for API Key") 134 | return ctrl.Result{}, err 135 | } 136 | 137 | secretValue := string(secret.Data[ac.Spec.OpsGenie.APISecret.FieldPath]) 138 | if secretValue == "" { 139 | secretErr := errs.New("secret value is empty") 140 | logger.Error(secretErr, "Please add Opsgenie secret") 141 | return ctrl.Result{}, err 142 | } 143 | 144 | opsGenieConfig = checkly.AlertChannelOpsgenie{ 145 | Name: ac.Name, 146 | APIKey: secretValue, 147 | Region: ac.Spec.OpsGenie.Region, 148 | Priority: ac.Spec.OpsGenie.Priority, 149 | } 150 | 151 | } 152 | 153 | // ///////////////////////////// 154 | // Update logic 155 | // //////////////////////////// 156 | 157 | // Determine if it's a new object or if it's an update to an existing object 158 | if ac.Status.ID != 0 { 159 | // Existing object, we need to update it 160 | logger.V(1).Info("Existing object, with ID", "checkly AlertChannel ID", ac.Status.ID) 161 | err := external.UpdateAlertChannel(ac, opsGenieConfig, r.ApiClient) 162 | if err != nil { 163 | logger.Error(err, "Failed to update checkly AlertChannel") 164 | return ctrl.Result{}, err 165 | } 166 | logger.V(1).Info("Updated checkly AlertChannel", "ID", ac.Status.ID) 167 | return ctrl.Result{}, nil 168 | } 169 | 170 | // ///////////////////////////// 171 | // Create logic 172 | // //////////////////////////// 173 | acID, err := external.CreateAlertChannel(ac, opsGenieConfig, r.ApiClient) 174 | if err != nil { 175 | logger.Error(err, "Failed to create checkly AlertChannel") 176 | return ctrl.Result{}, err 177 | } 178 | 179 | // Update the custom resource Status with the returned ID 180 | ac.Status.ID = acID 181 | err = r.Status().Update(ctx, ac) 182 | if err != nil { 183 | logger.Error(err, "Failed to update AlertChannel status", "ID", ac.Status.ID) 184 | return ctrl.Result{}, err 185 | } 186 | logger.V(1).Info("New checkly AlertChannel created", "ID", ac.Status.ID) 187 | 188 | return ctrl.Result{}, nil 189 | } 190 | 191 | // SetupWithManager sets up the controller with the Manager. 192 | func (r *AlertChannelReconciler) SetupWithManager(mgr ctrl.Manager) error { 193 | return ctrl.NewControllerManagedBy(mgr). 194 | For(&checklyv1alpha1.AlertChannel{}). 195 | Complete(r) 196 | } 197 | -------------------------------------------------------------------------------- /internal/controller/checkly/group_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 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 checkly 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | 23 | "github.com/checkly/checkly-go-sdk" 24 | "k8s.io/apimachinery/pkg/api/errors" 25 | "k8s.io/apimachinery/pkg/runtime" 26 | "k8s.io/apimachinery/pkg/types" 27 | ctrl "sigs.k8s.io/controller-runtime" 28 | "sigs.k8s.io/controller-runtime/pkg/client" 29 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 30 | "sigs.k8s.io/controller-runtime/pkg/log" 31 | 32 | checklyv1alpha1 "github.com/checkly/checkly-operator/api/checkly/v1alpha1" 33 | external "github.com/checkly/checkly-operator/external/checkly" 34 | ) 35 | 36 | // GroupReconciler reconciles a Group object 37 | type GroupReconciler struct { 38 | client.Client 39 | Scheme *runtime.Scheme 40 | ApiClient checkly.Client 41 | ControllerDomain string 42 | } 43 | 44 | //+kubebuilder:rbac:groups=k8s.checklyhq.com,resources=groups,verbs=get;list;watch;create;update;patch;delete 45 | //+kubebuilder:rbac:groups=k8s.checklyhq.com,resources=groups/status,verbs=get;update;patch 46 | //+kubebuilder:rbac:groups=k8s.checklyhq.com,resources=groups/finalizers,verbs=update 47 | 48 | // Reconcile is part of the main kubernetes reconciliation loop which aims to 49 | // move the current state of the cluster closer to the desired state. 50 | // 51 | // For more details, check Reconcile and its Result here: 52 | // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.11.0/pkg/reconcile 53 | func (r *GroupReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 54 | logger := log.FromContext(ctx) 55 | 56 | logger.V(1).Info("Reconciler started") 57 | 58 | groupFinalizer := fmt.Sprintf("%s/finalizer", r.ControllerDomain) 59 | 60 | group := &checklyv1alpha1.Group{} 61 | 62 | // //////////////////////////////// 63 | // Delete Logic 64 | // TODO: Add logic to determine if there are any checks that are part of the group. If yes, throw error and do not delete the group until the checks have been deleted first. 65 | // /////////////////////////////// 66 | err := r.Get(ctx, req.NamespacedName, group) 67 | if err != nil { 68 | if errors.IsNotFound(err) { 69 | // The resource has been deleted 70 | logger.Info("Deleted", "group ID", group.Status.ID, "name", group.Name) 71 | return ctrl.Result{}, nil 72 | } 73 | // Error reading the object 74 | logger.Error(err, "can't read the Group object") 75 | return ctrl.Result{}, nil 76 | } 77 | 78 | // If DeletionTimestamp is present, the object is marked for deletion, we need to remove the finalizer 79 | if group.GetDeletionTimestamp() != nil { 80 | if controllerutil.ContainsFinalizer(group, groupFinalizer) { 81 | logger.V(1).Info("Finalizer is present, trying to delete Checkly group", "checkly group ID", group.Status.ID) 82 | err := external.GroupDelete(group.Status.ID, r.ApiClient) 83 | if err != nil { 84 | logger.Error(err, "Failed to delete checkly group") 85 | return ctrl.Result{}, err 86 | } 87 | 88 | logger.Info("Successfully deleted checkly group", "checkly group ID", group.Status.ID) 89 | 90 | controllerutil.RemoveFinalizer(group, groupFinalizer) 91 | err = r.Update(ctx, group) 92 | if err != nil { 93 | logger.Error(err, "Failed to delete finalizer") 94 | return ctrl.Result{}, err 95 | } 96 | logger.V(1).Info("Successfully deleted finalizer") 97 | } 98 | return ctrl.Result{}, nil 99 | } 100 | 101 | // Object found, let's do something with it. It's either updated, or it's new. 102 | logger.V(1).Info("Checkly group found") 103 | 104 | // ///////////////////////////// 105 | // Finalizer logic 106 | // //////////////////////////// 107 | if !controllerutil.ContainsFinalizer(group, groupFinalizer) { 108 | controllerutil.AddFinalizer(group, groupFinalizer) 109 | err = r.Update(ctx, group) 110 | if err != nil { 111 | logger.Error(err, "Failed to add Group finalizer") 112 | return ctrl.Result{}, err 113 | } 114 | logger.V(1).Info("Added finalizer", "checkly group ID", group.Status.ID) 115 | return ctrl.Result{}, nil 116 | } 117 | 118 | // ///////////////////////////// 119 | // AlertChannelsSubscription logic 120 | // //////////////////////////// 121 | var alertChannels []checkly.AlertChannelSubscription 122 | 123 | if len(group.Spec.AlertChannels) != 0 { 124 | for _, alertChannel := range group.Spec.AlertChannels { 125 | ac := &checklyv1alpha1.AlertChannel{} 126 | err := r.Get(ctx, types.NamespacedName{Name: alertChannel}, ac) 127 | if err != nil { 128 | logger.Error(err, "Could not find alertChannel resource", "name", alertChannel) 129 | return ctrl.Result{}, err 130 | } 131 | if ac.Status.ID == 0 { 132 | logger.Info("AlertChannel ID not yet populated, we'll retry") 133 | return ctrl.Result{Requeue: true}, nil 134 | } 135 | alertChannels = append(alertChannels, checkly.AlertChannelSubscription{ 136 | ChannelID: ac.Status.ID, 137 | Activated: true, 138 | }) 139 | } 140 | } 141 | 142 | // Create internal Check type 143 | internalCheck := external.Group{ 144 | Name: group.Name, 145 | Activated: group.Spec.Activated, 146 | Locations: group.Spec.Locations, 147 | PrivateLocations: group.Spec.PrivateLocations, 148 | AlertChannels: alertChannels, 149 | ID: group.Status.ID, 150 | Labels: group.Labels, 151 | } 152 | 153 | // ///////////////////////////// 154 | // Update logic 155 | // //////////////////////////// 156 | 157 | // Determine if it's a new object or if it's an update to an existing object 158 | if group.Status.ID != 0 { 159 | // Existing object, we need to update it 160 | logger.V(1).Info("Existing object, with ID", "checkly group ID", group.Status.ID) 161 | err := external.GroupUpdate(internalCheck, r.ApiClient) 162 | if err != nil { 163 | logger.Error(err, "Failed to update the checkly group") 164 | return ctrl.Result{}, err 165 | } 166 | logger.V(1).Info("Updated checkly check", "checkly group ID", group.Status.ID) 167 | return ctrl.Result{}, nil 168 | } 169 | 170 | // ///////////////////////////// 171 | // Create logic 172 | // //////////////////////////// 173 | checklyID, err := external.GroupCreate(internalCheck, r.ApiClient) 174 | if err != nil { 175 | logger.Error(err, "Failed to create checkly group") 176 | return ctrl.Result{}, err 177 | } 178 | 179 | // Update the custom resource Status with the returned ID 180 | group.Status.ID = checklyID 181 | err = r.Status().Update(ctx, group) 182 | if err != nil { 183 | logger.Error(err, "Failed to update group status", "ID", group.Status.ID) 184 | return ctrl.Result{}, err 185 | } 186 | logger.Info("New checkly group created", "ID", group.Status.ID) 187 | 188 | return ctrl.Result{}, nil 189 | } 190 | 191 | // SetupWithManager sets up the controller with the Manager. 192 | func (r *GroupReconciler) SetupWithManager(mgr ctrl.Manager) error { 193 | return ctrl.NewControllerManagedBy(mgr). 194 | For(&checklyv1alpha1.Group{}). 195 | Complete(r) 196 | } 197 | -------------------------------------------------------------------------------- /internal/controller/checkly/apicheck_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 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 checkly 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | 23 | "k8s.io/apimachinery/pkg/api/errors" 24 | "k8s.io/apimachinery/pkg/runtime" 25 | "k8s.io/apimachinery/pkg/types" 26 | ctrl "sigs.k8s.io/controller-runtime" 27 | "sigs.k8s.io/controller-runtime/pkg/client" 28 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 29 | "sigs.k8s.io/controller-runtime/pkg/log" 30 | 31 | "github.com/checkly/checkly-go-sdk" 32 | checklyv1alpha1 "github.com/checkly/checkly-operator/api/checkly/v1alpha1" 33 | external "github.com/checkly/checkly-operator/external/checkly" 34 | ) 35 | 36 | // ApiCheckReconciler reconciles a ApiCheck object 37 | type ApiCheckReconciler struct { 38 | client.Client 39 | Scheme *runtime.Scheme 40 | ApiClient checkly.Client 41 | ControllerDomain string 42 | } 43 | 44 | //+kubebuilder:rbac:groups=k8s.checklyhq.com,resources=apichecks,verbs=get;list;watch;create;update;patch;delete 45 | //+kubebuilder:rbac:groups=k8s.checklyhq.com,resources=apichecks/status,verbs=get;update;patch 46 | //+kubebuilder:rbac:groups=k8s.checklyhq.com,resources=apichecks/finalizers,verbs=update 47 | //+kubebuilder:rbac:groups=k8s.checklyhq.com,resources=groups,verbs=get;list 48 | 49 | // Reconcile is part of the main kubernetes reconciliation loop which aims to 50 | // move the current state of the cluster closer to the desired state. 51 | // TODO(user): Modify the Reconcile function to compare the state specified by 52 | // the ApiCheck object against the actual cluster state, and then 53 | // perform operations to make the cluster state reflect the state specified by 54 | // the user. 55 | // 56 | // For more details, check Reconcile and its Result here: 57 | // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.11.0/pkg/reconcile 58 | func (r *ApiCheckReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 59 | logger := log.FromContext(ctx) 60 | 61 | apiCheckFinalizer := fmt.Sprintf("%s/finalizer", r.ControllerDomain) 62 | logger.V(1).Info("Reconciler started") 63 | 64 | apiCheck := &checklyv1alpha1.ApiCheck{} 65 | 66 | // //////////////////////////////// 67 | // Delete Logic 68 | // /////////////////////////////// 69 | err := r.Get(ctx, req.NamespacedName, apiCheck) 70 | if err != nil { 71 | if errors.IsNotFound(err) { 72 | // The resource has been deleted 73 | logger.V(1).Info("Deleted", "checkly ID", apiCheck.Status.ID, "endpoint", apiCheck.Spec.Endpoint, "name", apiCheck.Name) 74 | return ctrl.Result{}, nil 75 | } 76 | // Error reading the object 77 | logger.Error(err, "can't read the object") 78 | return ctrl.Result{}, nil 79 | } 80 | 81 | if apiCheck.GetDeletionTimestamp() != nil { 82 | if controllerutil.ContainsFinalizer(apiCheck, apiCheckFinalizer) { 83 | logger.V(1).Info("Finalizer is present, trying to delete Checkly check", "checkly ID", apiCheck.Status.ID) 84 | err := external.Delete(apiCheck.Status.ID, r.ApiClient) 85 | if err != nil { 86 | logger.Error(err, "Failed to delete checkly API check") 87 | return ctrl.Result{}, err 88 | } 89 | 90 | logger.Info("Successfully deleted checkly API check", "checkly ID", apiCheck.Status.ID) 91 | 92 | controllerutil.RemoveFinalizer(apiCheck, apiCheckFinalizer) 93 | err = r.Update(ctx, apiCheck) 94 | if err != nil { 95 | logger.Error(err, "Failed to delete finalizer") 96 | return ctrl.Result{}, err 97 | } 98 | logger.V(1).Info("Successfully deleted finalizer") 99 | } 100 | return ctrl.Result{}, nil 101 | } 102 | 103 | // Object found, let's do something with it. It's either updated, or it's new. 104 | logger.V(1).Info("Object found", "endpoint", apiCheck.Spec.Endpoint) 105 | 106 | // ///////////////////////////// 107 | // Finalizer logic 108 | // //////////////////////////// 109 | if !controllerutil.ContainsFinalizer(apiCheck, apiCheckFinalizer) { 110 | controllerutil.AddFinalizer(apiCheck, apiCheckFinalizer) 111 | err = r.Update(ctx, apiCheck) 112 | if err != nil { 113 | logger.Error(err, "Failed to update ApiCheck status") 114 | return ctrl.Result{}, err 115 | } 116 | logger.V(1).Info("Added finalizer", "checkly ID", apiCheck.Status.ID, "endpoint", apiCheck.Spec.Endpoint) 117 | return ctrl.Result{}, nil 118 | } 119 | 120 | // ///////////////////////////// 121 | // Lookup group ID 122 | // //////////////////////////// 123 | group := &checklyv1alpha1.Group{} 124 | err = r.Get(ctx, types.NamespacedName{Name: apiCheck.Spec.Group}, group) 125 | if err != nil { 126 | if errors.IsNotFound(err) { 127 | // The resource has been deleted 128 | logger.Error(err, "Group not found, probably deleted or does not exist", "name", apiCheck.Spec.Group) 129 | return ctrl.Result{}, err 130 | } 131 | // Error reading the object 132 | logger.Error(err, "can't read the group object") 133 | return ctrl.Result{}, err 134 | } 135 | 136 | if group.Status.ID == 0 { 137 | logger.V(1).Info("Group ID has not been populated, we're too quick, requeining for retry", "group name", apiCheck.Spec.Group) 138 | return ctrl.Result{Requeue: true}, nil 139 | } 140 | 141 | // Create internal Check type 142 | internalCheck := external.Check{ 143 | Name: apiCheck.Name, 144 | Namespace: apiCheck.Namespace, 145 | Frequency: apiCheck.Spec.Frequency, 146 | MaxResponseTime: apiCheck.Spec.MaxResponseTime, 147 | Endpoint: apiCheck.Spec.Endpoint, 148 | SuccessCode: apiCheck.Spec.Success, 149 | ID: apiCheck.Status.ID, 150 | GroupID: group.Status.ID, 151 | Muted: apiCheck.Spec.Muted, 152 | Labels: apiCheck.Labels, 153 | } 154 | 155 | // ///////////////////////////// 156 | // Update logic 157 | // //////////////////////////// 158 | 159 | // Determine if it's a new object or if it's an update to an existing object 160 | if apiCheck.Status.ID != "" { 161 | // Existing object, we need to update it 162 | logger.V(1).Info("Existing object, with ID", "checkly ID", apiCheck.Status.ID, "endpoint", apiCheck.Spec.Endpoint) 163 | err := external.Update(internalCheck, r.ApiClient) 164 | // err := 165 | if err != nil { 166 | logger.Error(err, "Failed to update the checkly check") 167 | return ctrl.Result{}, err 168 | } 169 | logger.Info("Updated checkly check", "checkly ID", apiCheck.Status.ID) 170 | return ctrl.Result{}, nil 171 | } 172 | 173 | // ///////////////////////////// 174 | // Create logic 175 | // //////////////////////////// 176 | 177 | checklyID, err := external.Create(internalCheck, r.ApiClient) 178 | if err != nil { 179 | logger.Error(err, "Failed to create checkly alert") 180 | return ctrl.Result{}, err 181 | } 182 | 183 | // Update the custom resource Status with the returned ID 184 | 185 | apiCheck.Status.ID = checklyID 186 | apiCheck.Status.GroupID = group.Status.ID 187 | err = r.Status().Update(ctx, apiCheck) 188 | if err != nil { 189 | logger.Error(err, "Failed to update ApiCheck status") 190 | return ctrl.Result{}, err 191 | } 192 | logger.V(1).Info("New checkly check created with", "checkly ID", apiCheck.Status.ID, "spec", apiCheck.Spec) 193 | 194 | return ctrl.Result{}, nil 195 | } 196 | 197 | // SetupWithManager sets up the controller with the Manager. 198 | func (r *ApiCheckReconciler) SetupWithManager(mgr ctrl.Manager) error { 199 | return ctrl.NewControllerManagedBy(mgr). 200 | For(&checklyv1alpha1.ApiCheck{}). 201 | Complete(r) 202 | } 203 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 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 | "crypto/tls" 21 | "errors" 22 | "flag" 23 | "os" 24 | 25 | // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) 26 | // to ensure that exec-entrypoint and run can make use of them. 27 | 28 | _ "k8s.io/client-go/plugin/pkg/client/auth" 29 | 30 | "k8s.io/apimachinery/pkg/runtime" 31 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 32 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 33 | ctrl "sigs.k8s.io/controller-runtime" 34 | "sigs.k8s.io/controller-runtime/pkg/healthz" 35 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 36 | "sigs.k8s.io/controller-runtime/pkg/metrics/filters" 37 | metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" 38 | 39 | "github.com/checkly/checkly-go-sdk" 40 | 41 | checklyv1alpha1 "github.com/checkly/checkly-operator/api/checkly/v1alpha1" 42 | checklycontrollers "github.com/checkly/checkly-operator/internal/controller/checkly" 43 | networkingcontrollers "github.com/checkly/checkly-operator/internal/controller/networking" 44 | //kubebuilder:scaffold:imports 45 | ) 46 | 47 | var ( 48 | scheme = runtime.NewScheme() 49 | setupLog = ctrl.Log.WithName("setup") 50 | ) 51 | 52 | func init() { 53 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 54 | 55 | utilruntime.Must(checklyv1alpha1.AddToScheme(scheme)) 56 | //kubebuilder:scaffold:scheme 57 | } 58 | 59 | func main() { 60 | var metricsAddr string 61 | var enableLeaderElection bool 62 | var probeAddr string 63 | var secureMetrics bool 64 | var controllerDomain string 65 | var tlsOpts []func(*tls.Config) 66 | flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+ 67 | "Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.") 68 | flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") 69 | flag.BoolVar(&enableLeaderElection, "leader-elect", false, 70 | "Enable leader election for controller manager. "+ 71 | "Enabling this will ensure there is only one active controller manager.") 72 | flag.BoolVar(&secureMetrics, "metrics-secure", true, 73 | "If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.") 74 | flag.StringVar(&controllerDomain, "controller-domain", "k8s.checklyhq.com", "Domain to use for annotations and finalizers.") 75 | opts := zap.Options{ 76 | // Development: true, 77 | } 78 | 79 | opts.BindFlags(flag.CommandLine) 80 | flag.Parse() 81 | 82 | // Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server. 83 | // More info: 84 | // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.0/pkg/metrics/server 85 | // - https://book.kubebuilder.io/reference/metrics.html 86 | metricsServerOptions := metricsserver.Options{ 87 | BindAddress: metricsAddr, 88 | SecureServing: secureMetrics, 89 | // TODO(user): TLSOpts is used to allow configuring the TLS config used for the server. If certificates are 90 | // not provided, self-signed certificates will be generated by default. This option is not recommended for 91 | // production environments as self-signed certificates do not offer the same level of trust and security 92 | // as certificates issued by a trusted Certificate Authority (CA). The primary risk is potentially allowing 93 | // unauthorized access to sensitive metrics data. Consider replacing with CertDir, CertName, and KeyName 94 | // to provide certificates, ensuring the server communicates using trusted and secure certificates. 95 | TLSOpts: tlsOpts, 96 | } 97 | 98 | if secureMetrics { 99 | // FilterProvider is used to protect the metrics endpoint with authn/authz. 100 | // These configurations ensure that only authorized users and service accounts 101 | // can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info: 102 | // https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.0/pkg/metrics/filters#WithAuthenticationAndAuthorization 103 | metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization 104 | } 105 | 106 | ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) 107 | 108 | setupLog.Info("Controller domain setup", "value", controllerDomain) 109 | 110 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 111 | Scheme: scheme, 112 | Metrics: metricsServerOptions, 113 | HealthProbeBindAddress: probeAddr, 114 | LeaderElection: enableLeaderElection, 115 | LeaderElectionID: "4e7eab13.checklyhq.com", 116 | }) 117 | if err != nil { 118 | setupLog.Error(err, "unable to start manager") 119 | os.Exit(1) 120 | } 121 | 122 | baseUrl := "https://api.checklyhq.com" 123 | apiKey := os.Getenv("CHECKLY_API_KEY") 124 | if apiKey == "" { 125 | setupLog.Error(errors.New("checklyhq.com API key environment variable is undefined"), "checklyhq.com credentials missing") 126 | os.Exit(1) 127 | } 128 | 129 | accountId := os.Getenv("CHECKLY_ACCOUNT_ID") 130 | if accountId == "" { 131 | setupLog.Error(errors.New("checklyhq.com Account ID environment variable is undefined"), "checklyhq.com credentials missing") 132 | os.Exit(1) 133 | } 134 | 135 | client := checkly.NewClient( 136 | baseUrl, 137 | apiKey, 138 | nil, //custom http client, defaults to http.DefaultClient 139 | nil, //io.Writer to output debug messages 140 | ) 141 | 142 | client.SetAccountId(accountId) 143 | 144 | if err = (&networkingcontrollers.IngressReconciler{ 145 | Client: mgr.GetClient(), 146 | Scheme: mgr.GetScheme(), 147 | ControllerDomain: controllerDomain, 148 | }).SetupWithManager(mgr); err != nil { 149 | setupLog.Error(err, "unable to create controller", "controller", "Ingress") 150 | os.Exit(1) 151 | } 152 | if err = (&checklycontrollers.ApiCheckReconciler{ 153 | Client: mgr.GetClient(), 154 | Scheme: mgr.GetScheme(), 155 | ApiClient: client, 156 | ControllerDomain: controllerDomain, 157 | }).SetupWithManager(mgr); err != nil { 158 | setupLog.Error(err, "unable to create controller", "controller", "ApiCheck") 159 | os.Exit(1) 160 | } 161 | if err = (&checklycontrollers.GroupReconciler{ 162 | Client: mgr.GetClient(), 163 | Scheme: mgr.GetScheme(), 164 | ApiClient: client, 165 | ControllerDomain: controllerDomain, 166 | }).SetupWithManager(mgr); err != nil { 167 | setupLog.Error(err, "unable to create controller", "controller", "Group") 168 | os.Exit(1) 169 | } 170 | if err = (&checklycontrollers.AlertChannelReconciler{ 171 | Client: mgr.GetClient(), 172 | Scheme: mgr.GetScheme(), 173 | ApiClient: client, 174 | ControllerDomain: controllerDomain, 175 | }).SetupWithManager(mgr); err != nil { 176 | setupLog.Error(err, "unable to create controller", "controller", "AlertChannel") 177 | os.Exit(1) 178 | } 179 | //kubebuilder:scaffold:builder 180 | 181 | setupLog.V(1).Info("starting health endpoint") 182 | if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { 183 | setupLog.Error(err, "unable to set up health check") 184 | os.Exit(1) 185 | } 186 | 187 | setupLog.V(1).Info("starting ready endpoint") 188 | if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { 189 | setupLog.Error(err, "unable to set up ready check") 190 | os.Exit(1) 191 | } 192 | 193 | setupLog.V(1).Info("starting manager") 194 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 195 | setupLog.Error(err, "problem running manager") 196 | os.Exit(1) 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /internal/controller/networking/ingress_controller_test.go: -------------------------------------------------------------------------------- 1 | package networking 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | checklyv1alpha1 "github.com/checkly/checkly-operator/api/checkly/v1alpha1" 9 | . "github.com/onsi/ginkgo" 10 | . "github.com/onsi/gomega" 11 | networkingv1 "k8s.io/api/networking/v1" 12 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | "k8s.io/apimachinery/pkg/types" 14 | ) 15 | 16 | var _ = Describe("Ingress Controller", func() { 17 | 18 | // Define utility constants for object names and testing timeouts/durations and intervals. 19 | const ( 20 | timeout = time.Second * 10 21 | duration = time.Second * 10 22 | interval = time.Millisecond * 250 23 | ) 24 | 25 | BeforeEach(func() { 26 | }) 27 | 28 | AfterEach(func() { 29 | // Add any teardown steps that needs to be executed after each test 30 | }) 31 | 32 | Context("Ingress", func() { 33 | 34 | // Test happy path 35 | It("full reconciliation", func() { 36 | 37 | testHost := "foo.bar" 38 | testPath := "baz" 39 | testGroup := "ingress-group" 40 | testSuccessCode := "200" 41 | 42 | apiCheckName := fmt.Sprintf("%s-%s-%s", "test-ingress", "foobar", testPath) 43 | 44 | group := &checklyv1alpha1.Group{ 45 | ObjectMeta: metav1.ObjectMeta{ 46 | Name: testGroup, 47 | }, 48 | Spec: checklyv1alpha1.GroupSpec{ 49 | Locations: []string{"eu-west-1"}, 50 | }, 51 | } 52 | 53 | ingressKey := types.NamespacedName{ 54 | Name: "test-ingress", 55 | Namespace: "default", 56 | } 57 | 58 | apiCheckKey := types.NamespacedName{ 59 | Name: apiCheckName, 60 | Namespace: "default", 61 | } 62 | 63 | annotation := make(map[string]string) 64 | annotation["testing.domain.tld/enabled"] = "true" 65 | annotation["testing.domain.tld/success"] = testSuccessCode 66 | annotation["testing.domain.tld/group"] = testGroup 67 | 68 | pathTypeImplementationSpecific := networkingv1.PathTypeImplementationSpecific 69 | 70 | var rules []networkingv1.IngressRule 71 | rules = append(rules, networkingv1.IngressRule{ 72 | Host: testHost, 73 | IngressRuleValue: networkingv1.IngressRuleValue{ 74 | HTTP: &networkingv1.HTTPIngressRuleValue{ 75 | Paths: []networkingv1.HTTPIngressPath{ 76 | { 77 | Path: fmt.Sprintf("/%s", testPath), 78 | PathType: &pathTypeImplementationSpecific, 79 | Backend: networkingv1.IngressBackend{ 80 | Service: &networkingv1.IngressServiceBackend{ 81 | Name: "test-service", 82 | Port: networkingv1.ServiceBackendPort{ 83 | Number: 7777, 84 | }, 85 | }, 86 | }, 87 | }, 88 | }, 89 | }, 90 | }, 91 | }) 92 | 93 | ingress := &networkingv1.Ingress{ 94 | ObjectMeta: metav1.ObjectMeta{ 95 | Name: ingressKey.Name, 96 | Namespace: ingressKey.Namespace, 97 | Annotations: annotation, 98 | }, 99 | Spec: networkingv1.IngressSpec{ 100 | Rules: rules, 101 | }, 102 | } 103 | 104 | // Create group 105 | Expect(k8sClient.Create(context.Background(), group)).Should(Succeed()) 106 | 107 | // Create 108 | Expect(k8sClient.Create(context.Background(), ingress)).Should(Succeed()) 109 | 110 | By("Expecting submitted") 111 | Eventually(func() bool { 112 | f := &networkingv1.Ingress{} 113 | err := k8sClient.Get(context.Background(), ingressKey, f) 114 | return err == nil 115 | }, timeout, interval).Should(BeTrue()) 116 | 117 | By("Expecting ApiCheck and OwnerReference to exist") 118 | Eventually(func() bool { 119 | f := &checklyv1alpha1.ApiCheck{} 120 | err := k8sClient.Get(context.Background(), apiCheckKey, f) 121 | if err != nil { 122 | return false 123 | } 124 | 125 | if len(f.OwnerReferences) != 1 { 126 | return false 127 | } 128 | 129 | Expect(f.Spec.Endpoint == fmt.Sprintf("https://%s/%s", testHost, testPath)).To(BeTrue(), "Hosts should match.") 130 | Expect(f.Spec.Group).To(Equal(testGroup), "Group should match") 131 | Expect(f.Spec.Success).To(Equal(testSuccessCode), "Success code should match") 132 | Expect(f.Spec.Muted).To(Equal(true), "Mute should match") 133 | 134 | for _, o := range f.OwnerReferences { 135 | Expect(o.Name).To(Equal(ingressKey.Name), "OwnerReference should be equal") 136 | } 137 | 138 | return true 139 | }, timeout, interval).Should(BeTrue(), "Timed out waiting for success") 140 | 141 | // Update path and use annotation 142 | By("Expecting path to update successfully") 143 | Eventually(func() error { 144 | // Get existing ingress object 145 | f := &networkingv1.Ingress{} 146 | err := k8sClient.Get(context.Background(), ingressKey, f) 147 | if err != nil { 148 | return err 149 | } 150 | 151 | newPath := "new-path" 152 | // Update annotations with new path 153 | f.Annotations["testing.domain.tld/path"] = newPath 154 | err = k8sClient.Update(context.Background(), f) 155 | if err != nil { 156 | return err 157 | } 158 | u := &networkingv1.Ingress{} 159 | err = k8sClient.Get(context.Background(), ingressKey, u) 160 | if err != nil { 161 | return err 162 | } 163 | Expect(u.Annotations["testing.domain.tld/path"]).To(Equal("new-path"), "Path annotation should be updated") 164 | 165 | apiCheckKeyNewPath := types.NamespacedName{ 166 | Name: fmt.Sprintf("%s-%s-%s", "test-ingress", "foobar", newPath), 167 | Namespace: "default", 168 | } 169 | // Expect API Check to be updated 170 | f2 := &checklyv1alpha1.ApiCheck{} 171 | err = k8sClient.Get(context.Background(), apiCheckKeyNewPath, f2) 172 | if err != nil { 173 | return err 174 | } 175 | Expect(f2.Spec.Endpoint).To(Equal(fmt.Sprintf("https://%s/%s", testHost, newPath)), "Endpoint should be updated") 176 | 177 | return nil 178 | }, timeout, interval).Should(Succeed(), "Timeout waiting for update") 179 | 180 | // Set enabled false 181 | By("Expecting enabled false to remove ApiCheck") 182 | Eventually(func() error { 183 | // Get existing ingress object 184 | f := &networkingv1.Ingress{} 185 | err := k8sClient.Get(context.Background(), ingressKey, f) 186 | if err != nil { 187 | return err 188 | } 189 | 190 | // Update annotations with `enabled` set to false 191 | f.Annotations["testing.domain.tld/enabled"] = "false" 192 | err = k8sClient.Update(context.Background(), f) 193 | if err != nil { 194 | return err 195 | } 196 | 197 | u := &networkingv1.Ingress{} 198 | err = k8sClient.Get(context.Background(), ingressKey, u) 199 | if err != nil { 200 | return err 201 | } 202 | 203 | Expect(u.Annotations["testing.domain.tld/enabled"]).To(Equal("false"), "Enabled annotation should be false") 204 | 205 | // Expect API Check to not exist anymore 206 | Expect(k8sClient.Get(context.Background(), apiCheckKey, f)).ShouldNot(Succeed()) 207 | 208 | return nil 209 | }, timeout, interval).Should(Succeed(), "Timeout waiting for update") 210 | 211 | // Delete 212 | By("Expecting to delete successfully") 213 | Eventually(func() error { 214 | f := &networkingv1.Ingress{} 215 | k8sClient.Get(context.Background(), ingressKey, f) 216 | return k8sClient.Delete(context.Background(), f) 217 | }, timeout, interval).Should(Succeed()) 218 | 219 | By("Expecting delete to finish") 220 | Eventually(func() error { 221 | f := &networkingv1.Ingress{} 222 | return k8sClient.Get(context.Background(), ingressKey, f) 223 | }, timeout, interval).ShouldNot(Succeed()) 224 | 225 | // Delete group 226 | Expect(k8sClient.Delete(context.Background(), group)).Should(Succeed(), "Group deletion should succeed") 227 | 228 | }) 229 | 230 | // Testing failures 231 | It("Some failures", func() { 232 | testHost := "foo.bar" 233 | testPath := "baz" 234 | testSuccessCode := "200" 235 | 236 | key := types.NamespacedName{ 237 | Name: "test-fail-ingress", 238 | Namespace: "default", 239 | } 240 | 241 | annotation := make(map[string]string) 242 | annotation["testing.domain.tld/enabled"] = "false" 243 | annotation["testing.domain.tld/path"] = testPath 244 | annotation["testing.domain.tld/success"] = testSuccessCode 245 | annotation["testing.domain.tld/muted"] = "false" 246 | 247 | rules := make([]networkingv1.IngressRule, 0) 248 | rules = append(rules, networkingv1.IngressRule{ 249 | Host: testHost, 250 | }) 251 | 252 | ingress := &networkingv1.Ingress{ 253 | ObjectMeta: metav1.ObjectMeta{ 254 | Name: key.Name, 255 | Namespace: key.Namespace, 256 | Annotations: annotation, 257 | }, 258 | Spec: networkingv1.IngressSpec{ 259 | Rules: rules, 260 | DefaultBackend: &networkingv1.IngressBackend{ 261 | Service: &networkingv1.IngressServiceBackend{ 262 | Name: "test-service", 263 | Port: networkingv1.ServiceBackendPort{ 264 | Number: 7777, 265 | }, 266 | }, 267 | }, 268 | }, 269 | } 270 | Expect(k8sClient.Create(context.Background(), ingress)).Should(Succeed()) 271 | 272 | // Delete 273 | By("Expecting to delete successfully") 274 | Eventually(func() error { 275 | f := &networkingv1.Ingress{} 276 | k8sClient.Get(context.Background(), key, f) 277 | return k8sClient.Delete(context.Background(), f) 278 | }, timeout, interval).Should(Succeed()) 279 | 280 | By("Expecting delete to finish") 281 | Eventually(func() error { 282 | f := &networkingv1.Ingress{} 283 | return k8sClient.Get(context.Background(), key, f) 284 | }, timeout, interval).ShouldNot(Succeed()) 285 | 286 | }) 287 | }) 288 | 289 | }) 290 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # docs 2 | 3 | The checkly-operator was designed to run inside a kubernetes cluster and listen for events on specific CRDs and ingress resources. With the help of it you can set up: 4 | * [Alert channels](alert-channels.md) 5 | * [Check groups](check-group.md) 6 | * [API Checks](api-checks.md) 7 | 8 | ## Installation 9 | 10 | We currently supply an installation yaml file, this is present in the [releases](https://github.com/checkly/checkly-operator/releases). 11 | 12 | The file holds the following resources: 13 | * namespace 14 | * CRDs 15 | * RBAC 16 | * deployment 17 | 18 | In order for the operator to work, you need to supply secrets which hold your [checklyhq.com](checklyhq.com) API Key and Account ID. See [docs on how to create an API key and get account ID](https://www.checklyhq.com/docs/integrations/pulumi/#define-your-checkly-account-id-and-api-key). The operator expects the following environment variables: 19 | ``` 20 | env: 21 | - name: CHECKLY_API_KEY 22 | valueFrom: 23 | secretKeyRef: 24 | key: CHECKLY_API_KEY 25 | name: checkly 26 | - name: CHECKLY_ACCOUNT_ID 27 | valueFrom: 28 | secretKeyRef: 29 | key: CHECKLY_ACCOUNT_ID 30 | name: checkly 31 | ``` 32 | 33 | The following steps are an easy example on how to get started with the operator, it is not a production ready method, for example we're not using any secrets managers, you should not create secrets and commit them to git like in the below example, we're only deploying one replica, while the operator does support HA deployments. 34 | 35 | If you just want to try out the checkly-operator, you need a local kubernetes installation, the easiest might be [Rancher Desktop](https://rancherdesktop.io/), see [the docs](https://docs.rancherdesktop.io/getting-started/installation/) for the installation, once done, come back to this doc. 36 | 37 | ### Download install.yaml 38 | 39 | First we'll download the provided `install.yaml` files, please change the version number accordingly, we might have newer [releases](https://github.com/checkly/checkly-operator/releases) since we've written these docs. 40 | 41 | ```bash 42 | export CHECKLY_OPERATOR_RELEASE=v1.7.0 43 | wget "https://github.com/checkly/checkly-operator/releases/download/$CHECKLY_OPERATOR_RELEASE/install-$CHECKLY_OPERATOR_RELEASE.yaml" -O install.yaml 44 | unset CHECKLY_OPERATOR_RELEASE 45 | ``` 46 | 47 | Feel free to edit the `install.yaml` file to your liking, usually you'd want to change: 48 | * checkly-operator deployment replica count 49 | * checkly-operator deployment CPU and Memory resources 50 | * log levels via the `--zap-log-level` CLI options, valid options are `debug`, `info`, `error` 51 | 52 | You can apply the `install.yaml`, this will create the namespace, we need this to create the secrets in the next step: 53 | ```bash 54 | kubectl apply -f install.yaml 55 | ``` 56 | 57 | #### Controller Domain 58 | 59 | We're using a domain name for finalizers and annotations, the default value is `k8s.checklyhq.com`, but it can be changed by supplying the `--controller-domain=other.domain.tld` runtime option. 60 | 61 | This option allows you to run multiple independent deployments of the operator and each would handle different resources based on the controller domain configuration. 62 | 63 | ### Create secret 64 | 65 | Grab your [checklyhq.com](checklyhq.com) API key and Account ID, [the official docs](https://www.checklyhq.com/docs/integrations/pulumi/#define-your-checkly-account-id-and-api-key) can help you get this information. Substitute the values into the below command: 66 | 67 | ```bash 68 | export CHECKLY_API_KEY= 69 | export CHECKLY_ACCOUNT_ID= 70 | kubectl create secret generic -n checkly-operator-system checkly \ 71 | --from-literal=CHECKLY_API_KEY=$CHECKLY_API_KEY \ 72 | --from-literal=CHECKLY_ACCOUNT_ID=$CHECKLY_ACCOUNT_ID 73 | unset CHECKLY_API_KEY 74 | unset CHECKLY_ACCOUNT_ID 75 | ``` 76 | 77 | If you check your pod, you should be able to see the pods starting: 78 | ```bash 79 | kubectl get pods -n checkly-operator-system 80 | ``` 81 | 82 | ## Configuration 83 | 84 | We will next create a check group, alert channels and api checks through the custom CRDs. The operator was written with a specific opinion: 85 | * all checks should belong to a specific group 86 | * group configuration should be inherited by each check which is part of it 87 | * alert channels are added to groups, not to individual checks 88 | 89 | Based on the above, the order of creation should be: 90 | 1. alert channel 91 | 2. check group 92 | 3. api checks 93 | 94 | Reference to resources are done based on the kubernetes internal naming, as in the `metadata.name` field. 95 | 96 | Please look at the below examples and change the supplied data so it fits your needs the best. Save the example into individual files and apply them when ready: 97 | ```bash 98 | kubectl apply -f .yaml 99 | ``` 100 | 101 | ### Alert channel 102 | 103 | See the [docs](https://www.checklyhq.com/docs/alerting/) on what alert channels are and [alert-channels](alert-channels.md) for the options we support. 104 | 105 | The following configuration will send all alerts to an email address: 106 | ```yaml 107 | apiVersion: k8s.checklyhq.com/v1alpha1 108 | kind: AlertChannel 109 | metadata: 110 | name: checkly-operator-test-alertchannel 111 | spec: 112 | email: 113 | address: "foo@bar.baz" 114 | ``` 115 | 116 | The `AlertChannel` resource is cluster scoped, it can be used in any check group but it also means that the name of the resource has to be unique in the kubernetes cluster. 117 | 118 | Once applied you can check if it worked: 119 | ```bash 120 | kubectl get alertchannel checkly-operator-test-alertchannel 121 | ``` 122 | 123 | You can also view the alert channel on the [checklyhq.com dashboard](https://app.checklyhq.com/alert-settings). 124 | 125 | ### Check group 126 | 127 | See the [docs](https://www.checklyhq.com/docs/groups/) on what check groups are and [check-group](check-group.md) for the options we support. 128 | 129 | The following configuration adds the above alert channel to all the checks that will be part of the group check: 130 | ```yaml 131 | apiVersion: k8s.checklyhq.com/v1alpha1 132 | kind: Group 133 | metadata: 134 | name: checkly-operator-test-group 135 | labels: 136 | environment: "local" 137 | spec: 138 | locations: 139 | - eu-west-1 140 | - eu-west-2 141 | alertchannel: 142 | - checkly-operator-test-alertchannel 143 | ``` 144 | 145 | The `Group` resource is cluster scoped, it can be used in any api check but it also means that the name of the resource has to be unique in the kubernetes cluster. 146 | 147 | Once applied you can check if it worked: 148 | ```bash 149 | kubectl get group checkly-operator-test-group 150 | ``` 151 | 152 | You can also view the check group on the [checklyhq.com dashboard](https://app.checklyhq.com/). 153 | 154 | #### Tags 155 | 156 | Any labels added to the `Group` resource will be added as tags to the group, these groups are inherited by the checks. 157 | 158 | #### Locations 159 | 160 | To see a full list of locations supported by [checklyhq.com](checklyhq.com), see [the docs](https://www.checklyhq.com/docs/monitoring/global-locations/). We're using the location codes, private locations should be technically supported, we just haven't tested them. 161 | 162 | ### API Checks 163 | 164 | See the [docs](https://www.checklyhq.com/docs/api-checks/) on what API checks are and [api-checks](api-checks.md) for the options we support. 165 | 166 | We're currently only supporting API checks which perform a GET request. 167 | 168 | The following configuration monitors the `https://foo.bar/baz` endpoint, expects a return code of 200, it's added to the above created group and are muted so they do not send an alert: 169 | ```yaml 170 | apiVersion: k8s.checklyhq.com/v1alpha1 171 | kind: ApiCheck 172 | metadata: 173 | name: checkly-operator-test-1 174 | namespace: default 175 | labels: 176 | service: "foo" 177 | spec: 178 | endpoint: "http://foo.bar/baz" 179 | success: "200" 180 | frequency: 10 # Default 5 181 | muted: true # Default "false" 182 | group: "checkly-operator-test-group" 183 | 184 | ``` 185 | 186 | You can create multiple `ApiCheck` resources which point to the same group: 187 | ```yaml 188 | apiVersion: k8s.checklyhq.com/v1alpha1 189 | kind: ApiCheck 190 | metadata: 191 | name: checkly-operator-test-2 192 | namespace: default 193 | labels: 194 | service: "bar" 195 | spec: 196 | endpoint: "https://checklyhq.com" 197 | success: "200" 198 | frequency: 10 # Default 5 199 | muted: true # Default "false" 200 | group: "checkly-operator-test-group" 201 | ``` 202 | 203 | `ApiCheck` resources are namespace scoped, they have to have a unique name in each namespace. 204 | 205 | Once applied you can check if it worked: 206 | ```bash 207 | kubectl get apichecks -n default 208 | ``` 209 | 210 | You can also view the checks on the [checklyhq.com dashboard](https://app.checklyhq.com/). 211 | 212 | ### Ingresses 213 | 214 | See [ingress](ingress.md) for more details on how we utilize ingress resources. 215 | 216 | We can create an ingress object and add annotations to it: 217 | ```yaml 218 | --- 219 | apiVersion: networking.k8s.io/v1 220 | kind: Ingress 221 | metadata: 222 | name: ingress-sample 223 | namespace: default 224 | annotations: 225 | k8s.checklyhq.com/enabled: "true" 226 | k8s.checklyhq.com/path: "/baz" 227 | # k8s.checklyhq.com/endpoint: "foo.baaz" - Default read from spec.rules[0].host 228 | # k8s.checklyhq.com/success: "200" - Default "200" 229 | k8s.checklyhq.com/group: "checkly-operator-test-group" 230 | # k8s.checklyhq.com/muted: "false" # If not set, default "true" 231 | spec: 232 | rules: 233 | - host: "foo.bar" 234 | http: 235 | paths: 236 | - path: / 237 | pathType: ImplementationSpecific 238 | backend: 239 | service: 240 | name: test-service 241 | port: 242 | number: 8080 243 | 244 | ``` 245 | 246 | Check if it worked: 247 | ```bash 248 | kubectl get ingress -n default 249 | kubectl get apicheck -n default 250 | ``` 251 | 252 | You can also view the checks on the [checklyhq.com dashboard](https://app.checklyhq.com/). 253 | -------------------------------------------------------------------------------- /api/checkly/v1alpha1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | 3 | /* 4 | Copyright 2022. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | // Code generated by controller-gen. DO NOT EDIT. 20 | 21 | package v1alpha1 22 | 23 | import ( 24 | runtime "k8s.io/apimachinery/pkg/runtime" 25 | ) 26 | 27 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 28 | func (in *AlertChannel) DeepCopyInto(out *AlertChannel) { 29 | *out = *in 30 | out.TypeMeta = in.TypeMeta 31 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 32 | out.Spec = in.Spec 33 | out.Status = in.Status 34 | } 35 | 36 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AlertChannel. 37 | func (in *AlertChannel) DeepCopy() *AlertChannel { 38 | if in == nil { 39 | return nil 40 | } 41 | out := new(AlertChannel) 42 | in.DeepCopyInto(out) 43 | return out 44 | } 45 | 46 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 47 | func (in *AlertChannel) DeepCopyObject() runtime.Object { 48 | if c := in.DeepCopy(); c != nil { 49 | return c 50 | } 51 | return nil 52 | } 53 | 54 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 55 | func (in *AlertChannelList) DeepCopyInto(out *AlertChannelList) { 56 | *out = *in 57 | out.TypeMeta = in.TypeMeta 58 | in.ListMeta.DeepCopyInto(&out.ListMeta) 59 | if in.Items != nil { 60 | in, out := &in.Items, &out.Items 61 | *out = make([]AlertChannel, len(*in)) 62 | for i := range *in { 63 | (*in)[i].DeepCopyInto(&(*out)[i]) 64 | } 65 | } 66 | } 67 | 68 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AlertChannelList. 69 | func (in *AlertChannelList) DeepCopy() *AlertChannelList { 70 | if in == nil { 71 | return nil 72 | } 73 | out := new(AlertChannelList) 74 | in.DeepCopyInto(out) 75 | return out 76 | } 77 | 78 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 79 | func (in *AlertChannelList) DeepCopyObject() runtime.Object { 80 | if c := in.DeepCopy(); c != nil { 81 | return c 82 | } 83 | return nil 84 | } 85 | 86 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 87 | func (in *AlertChannelOpsGenie) DeepCopyInto(out *AlertChannelOpsGenie) { 88 | *out = *in 89 | out.APISecret = in.APISecret 90 | } 91 | 92 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AlertChannelOpsGenie. 93 | func (in *AlertChannelOpsGenie) DeepCopy() *AlertChannelOpsGenie { 94 | if in == nil { 95 | return nil 96 | } 97 | out := new(AlertChannelOpsGenie) 98 | in.DeepCopyInto(out) 99 | return out 100 | } 101 | 102 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 103 | func (in *AlertChannelSpec) DeepCopyInto(out *AlertChannelSpec) { 104 | *out = *in 105 | out.OpsGenie = in.OpsGenie 106 | out.Email = in.Email 107 | } 108 | 109 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AlertChannelSpec. 110 | func (in *AlertChannelSpec) DeepCopy() *AlertChannelSpec { 111 | if in == nil { 112 | return nil 113 | } 114 | out := new(AlertChannelSpec) 115 | in.DeepCopyInto(out) 116 | return out 117 | } 118 | 119 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 120 | func (in *AlertChannelStatus) DeepCopyInto(out *AlertChannelStatus) { 121 | *out = *in 122 | } 123 | 124 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AlertChannelStatus. 125 | func (in *AlertChannelStatus) DeepCopy() *AlertChannelStatus { 126 | if in == nil { 127 | return nil 128 | } 129 | out := new(AlertChannelStatus) 130 | in.DeepCopyInto(out) 131 | return out 132 | } 133 | 134 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 135 | func (in *ApiCheck) DeepCopyInto(out *ApiCheck) { 136 | *out = *in 137 | out.TypeMeta = in.TypeMeta 138 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 139 | out.Spec = in.Spec 140 | out.Status = in.Status 141 | } 142 | 143 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApiCheck. 144 | func (in *ApiCheck) DeepCopy() *ApiCheck { 145 | if in == nil { 146 | return nil 147 | } 148 | out := new(ApiCheck) 149 | in.DeepCopyInto(out) 150 | return out 151 | } 152 | 153 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 154 | func (in *ApiCheck) DeepCopyObject() runtime.Object { 155 | if c := in.DeepCopy(); c != nil { 156 | return c 157 | } 158 | return nil 159 | } 160 | 161 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 162 | func (in *ApiCheckList) DeepCopyInto(out *ApiCheckList) { 163 | *out = *in 164 | out.TypeMeta = in.TypeMeta 165 | in.ListMeta.DeepCopyInto(&out.ListMeta) 166 | if in.Items != nil { 167 | in, out := &in.Items, &out.Items 168 | *out = make([]ApiCheck, len(*in)) 169 | for i := range *in { 170 | (*in)[i].DeepCopyInto(&(*out)[i]) 171 | } 172 | } 173 | } 174 | 175 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApiCheckList. 176 | func (in *ApiCheckList) DeepCopy() *ApiCheckList { 177 | if in == nil { 178 | return nil 179 | } 180 | out := new(ApiCheckList) 181 | in.DeepCopyInto(out) 182 | return out 183 | } 184 | 185 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 186 | func (in *ApiCheckList) DeepCopyObject() runtime.Object { 187 | if c := in.DeepCopy(); c != nil { 188 | return c 189 | } 190 | return nil 191 | } 192 | 193 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 194 | func (in *ApiCheckSpec) DeepCopyInto(out *ApiCheckSpec) { 195 | *out = *in 196 | } 197 | 198 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApiCheckSpec. 199 | func (in *ApiCheckSpec) DeepCopy() *ApiCheckSpec { 200 | if in == nil { 201 | return nil 202 | } 203 | out := new(ApiCheckSpec) 204 | in.DeepCopyInto(out) 205 | return out 206 | } 207 | 208 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 209 | func (in *ApiCheckStatus) DeepCopyInto(out *ApiCheckStatus) { 210 | *out = *in 211 | } 212 | 213 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApiCheckStatus. 214 | func (in *ApiCheckStatus) DeepCopy() *ApiCheckStatus { 215 | if in == nil { 216 | return nil 217 | } 218 | out := new(ApiCheckStatus) 219 | in.DeepCopyInto(out) 220 | return out 221 | } 222 | 223 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 224 | func (in *Group) DeepCopyInto(out *Group) { 225 | *out = *in 226 | out.TypeMeta = in.TypeMeta 227 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 228 | in.Spec.DeepCopyInto(&out.Spec) 229 | out.Status = in.Status 230 | } 231 | 232 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Group. 233 | func (in *Group) DeepCopy() *Group { 234 | if in == nil { 235 | return nil 236 | } 237 | out := new(Group) 238 | in.DeepCopyInto(out) 239 | return out 240 | } 241 | 242 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 243 | func (in *Group) DeepCopyObject() runtime.Object { 244 | if c := in.DeepCopy(); c != nil { 245 | return c 246 | } 247 | return nil 248 | } 249 | 250 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 251 | func (in *GroupList) DeepCopyInto(out *GroupList) { 252 | *out = *in 253 | out.TypeMeta = in.TypeMeta 254 | in.ListMeta.DeepCopyInto(&out.ListMeta) 255 | if in.Items != nil { 256 | in, out := &in.Items, &out.Items 257 | *out = make([]Group, len(*in)) 258 | for i := range *in { 259 | (*in)[i].DeepCopyInto(&(*out)[i]) 260 | } 261 | } 262 | } 263 | 264 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GroupList. 265 | func (in *GroupList) DeepCopy() *GroupList { 266 | if in == nil { 267 | return nil 268 | } 269 | out := new(GroupList) 270 | in.DeepCopyInto(out) 271 | return out 272 | } 273 | 274 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 275 | func (in *GroupList) DeepCopyObject() runtime.Object { 276 | if c := in.DeepCopy(); c != nil { 277 | return c 278 | } 279 | return nil 280 | } 281 | 282 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 283 | func (in *GroupSpec) DeepCopyInto(out *GroupSpec) { 284 | *out = *in 285 | if in.Locations != nil { 286 | in, out := &in.Locations, &out.Locations 287 | *out = make([]string, len(*in)) 288 | copy(*out, *in) 289 | } 290 | if in.AlertChannels != nil { 291 | in, out := &in.AlertChannels, &out.AlertChannels 292 | *out = make([]string, len(*in)) 293 | copy(*out, *in) 294 | } 295 | } 296 | 297 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GroupSpec. 298 | func (in *GroupSpec) DeepCopy() *GroupSpec { 299 | if in == nil { 300 | return nil 301 | } 302 | out := new(GroupSpec) 303 | in.DeepCopyInto(out) 304 | return out 305 | } 306 | 307 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 308 | func (in *GroupStatus) DeepCopyInto(out *GroupStatus) { 309 | *out = *in 310 | } 311 | 312 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GroupStatus. 313 | func (in *GroupStatus) DeepCopy() *GroupStatus { 314 | if in == nil { 315 | return nil 316 | } 317 | out := new(GroupStatus) 318 | in.DeepCopyInto(out) 319 | return out 320 | } 321 | --------------------------------------------------------------------------------