├── config ├── manager │ ├── kustomization.yaml │ └── manager.yaml ├── prometheus │ ├── kustomization.yaml │ └── monitor.yaml ├── release │ └── kustomization.yaml ├── samples │ ├── kustomization.yaml │ └── _v1alpha1_operatormanager.yaml ├── scorecard │ ├── bases │ │ └── config.yaml │ ├── patches │ │ ├── basic.config.yaml │ │ └── olm.config.yaml │ └── kustomization.yaml ├── default │ ├── manager_config_patch.yaml │ ├── manager_auth_proxy_patch.yaml │ └── kustomization.yaml ├── crd │ ├── patches │ │ ├── cainjection_in_operatormanagers.yaml │ │ └── webhook_in_operatormanagers.yaml │ ├── kustomizeconfig.yaml │ ├── kustomization.yaml │ └── bases │ │ └── kom.kkb0318.github.io_operatormanagers.yaml ├── rbac │ ├── service_account.yaml │ ├── auth_proxy_client_clusterrole.yaml │ ├── role_binding.yaml │ ├── auth_proxy_role_binding.yaml │ ├── leader_election_role_binding.yaml │ ├── auth_proxy_role.yaml │ ├── auth_proxy_service.yaml │ ├── kustomization.yaml │ ├── operatormanager_viewer_role.yaml │ ├── operatormanager_editor_role.yaml │ ├── leader_election_role.yaml │ └── role.yaml └── manifests │ └── kustomization.yaml ├── cr.yaml ├── .dockerignore ├── docs ├── config.yaml └── api.md ├── internal ├── kubernetes │ ├── owner.go │ ├── delete.go │ └── apply.go ├── tool │ ├── flux │ │ ├── testdata │ │ │ ├── helm_repository.yaml │ │ │ ├── git_repository.yaml │ │ │ ├── kustomization.yaml │ │ │ ├── helm_release.yaml │ │ │ └── testdata.go │ │ ├── flux.go │ │ ├── manifests │ │ │ ├── helm_repository.go │ │ │ ├── kustomization.go │ │ │ ├── git_repository.go │ │ │ └── helm_release.go │ │ ├── git.go │ │ ├── helm_test.go │ │ ├── helm.go │ │ └── git_test.go │ ├── argo │ │ ├── testdata │ │ │ ├── git_secret.yaml │ │ │ ├── helm_secret.yaml │ │ │ ├── git_application.yaml │ │ │ ├── helm_application.yaml │ │ │ └── testdata.go │ │ ├── argo.go │ │ ├── git_test.go │ │ ├── manifests │ │ │ ├── secret.go │ │ │ └── application.go │ │ ├── git.go │ │ ├── helm.go │ │ └── helm_test.go │ ├── interface.go │ └── factory │ │ └── factory.go ├── utils │ └── files.go ├── status │ ├── resources.go │ └── resources_test.go └── controller │ ├── operatormanager_controller_flux_test.go │ ├── operatormanager_controller_no_cleanup_test.go │ ├── operatormanager_controller_argo_test.go │ ├── operatormanager_controller.go │ └── suite_test.go ├── .github └── workflows │ ├── ci.yaml │ └── release.yaml ├── charts └── kom │ ├── templates │ ├── serviceaccount.yaml │ ├── metrics-reader-rbac.yaml │ ├── metrics-service.yaml │ ├── proxy-rbac.yaml │ ├── leader-election-rbac.yaml │ ├── _helpers.tpl │ ├── deployment.yaml │ └── manager-rbac.yaml │ ├── .helmignore │ ├── README.md.gotmpl │ ├── values.yaml │ ├── Chart.yaml │ ├── README.md │ └── crds │ └── operatormanager-crd.yaml ├── .gitignore ├── hack └── boilerplate.go.txt ├── PROJECT ├── examples ├── basic_argo.yaml └── basic_flux.yaml ├── LICENSE ├── Dockerfile ├── api └── v1alpha1 │ ├── groupversion_info.go │ ├── zz_generated.deepcopy.go │ └── operatormanager_types.go ├── README.md ├── go.mod ├── cmd └── main.go └── Makefile /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manager.yaml 3 | -------------------------------------------------------------------------------- /config/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - monitor.yaml 3 | -------------------------------------------------------------------------------- /cr.yaml: -------------------------------------------------------------------------------- 1 | owner: kkb0318 2 | git-repo: kom 3 | release-name-template: "helm-{{ .Name }}-{{ .Version }}" 4 | make-release-latest: false 5 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Ignore build and test binaries. 3 | bin/ 4 | -------------------------------------------------------------------------------- /config/release/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | namespace: kom-system 4 | resources: 5 | - ../default 6 | -------------------------------------------------------------------------------- /config/samples/kustomization.yaml: -------------------------------------------------------------------------------- 1 | ## Append samples of your project ## 2 | resources: 3 | - _v1alpha1_operatormanager.yaml 4 | #+kubebuilder:scaffold:manifestskustomizesamples 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/config.yaml: -------------------------------------------------------------------------------- 1 | processor: 2 | ignoreTypes: 3 | - "List$" 4 | - "AppliedResource$" 5 | ignoreFields: 6 | - "status$" 7 | - "TypeMeta$" 8 | 9 | render: 10 | kubernetesVersion: 1.28 11 | -------------------------------------------------------------------------------- /internal/kubernetes/owner.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | // Owner contains options for setting the field manager. 4 | type Owner struct { 5 | // Field sets the field manager name for the given server-side apply patch. 6 | Field string 7 | } 8 | -------------------------------------------------------------------------------- /config/default/manager_config_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: manager 11 | -------------------------------------------------------------------------------- /internal/tool/flux/testdata/helm_repository.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: source.toolkit.fluxcd.io/v1 2 | kind: HelmRepository 3 | metadata: 4 | name: repo1 5 | namespace: repo-ns1 6 | spec: 7 | interval: 1m 8 | url: https://example.com 9 | type: default 10 | -------------------------------------------------------------------------------- /internal/tool/flux/testdata/git_repository.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: source.toolkit.fluxcd.io/v1 2 | kind: GitRepository 3 | metadata: 4 | name: repo1 5 | namespace: repo-ns1 6 | spec: 7 | interval: 1m 8 | url: https://example1.com 9 | ref: 10 | tag: "1.0.0" 11 | -------------------------------------------------------------------------------- /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.33.0 8 | labels: 9 | suite: basic 10 | test: basic-check-spec-test 11 | -------------------------------------------------------------------------------- /internal/tool/argo/testdata/git_secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: repo1 5 | namespace: repo-ns1 6 | labels: 7 | argocd.argoproj.io/secret-type: "repository" 8 | type: Opaque 9 | stringData: 10 | type: "git" 11 | url: "https://example.com" 12 | project: "default" 13 | -------------------------------------------------------------------------------- /internal/tool/flux/testdata/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.toolkit.fluxcd.io/v1 2 | kind: Kustomization 3 | metadata: 4 | name: repo1 5 | namespace: repo-ns1 6 | spec: 7 | prune: true 8 | path: "./path1" 9 | sourceRef: 10 | kind: GitRepository 11 | name: repo1 12 | namespace: repo-ns1 13 | -------------------------------------------------------------------------------- /internal/tool/argo/testdata/helm_secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: repo1 5 | namespace: repo-ns1 6 | labels: 7 | argocd.argoproj.io/secret-type: "repository" 8 | type: Opaque 9 | stringData: 10 | name: "chart1" 11 | type: "helm" 12 | url: "https://example.com" 13 | project: "default" 14 | -------------------------------------------------------------------------------- /internal/tool/interface.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "sigs.k8s.io/controller-runtime/pkg/client" 5 | ) 6 | 7 | type ResourceManager interface { 8 | Helm() ([]Resource, error) 9 | Git() ([]Resource, error) 10 | } 11 | 12 | type Resource interface { 13 | Repositories() []client.Object 14 | Charts() []client.Object 15 | } 16 | -------------------------------------------------------------------------------- /internal/utils/files.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | 6 | yaml "github.com/goccy/go-yaml" 7 | ) 8 | 9 | func LoadYaml(structData any, fileName string) error { 10 | bytes, err := os.ReadFile(fileName) 11 | if err != nil { 12 | panic(err) 13 | } 14 | return yaml.UnmarshalWithOptions(bytes, structData, yaml.UseJSONUnmarshaler()) 15 | } 16 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_operatormanagers.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: operatormanagers.kom.kkb0318.github.io 8 | -------------------------------------------------------------------------------- /internal/tool/flux/testdata/helm_release.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: helm.toolkit.fluxcd.io/v2 2 | kind: HelmRelease 3 | metadata: 4 | name: chart1 5 | namespace: repo-ns1 6 | spec: 7 | chart: 8 | spec: 9 | chart: chart1 10 | version: "x.x.x" 11 | sourceRef: 12 | kind: HelmRepository 13 | name: repo1 14 | namespace: repo-ns1 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - "main" 7 | jobs: 8 | test: 9 | name: Unit, Integration Test 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-go@v5 14 | with: 15 | go-version-file: go.mod 16 | - run: make test 17 | -------------------------------------------------------------------------------- /config/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: serviceaccount 6 | app.kubernetes.io/instance: controller-manager-sa 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: kom 9 | app.kubernetes.io/part-of: kom 10 | app.kubernetes.io/managed-by: kustomize 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /charts/kom/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: {{ include "kom.fullname" . }}-controller-manager 5 | labels: 6 | app.kubernetes.io/component: rbac 7 | app.kubernetes.io/created-by: kom 8 | app.kubernetes.io/part-of: kom 9 | {{- include "kom.labels" . | nindent 4 }} 10 | annotations: 11 | {{- toYaml .Values.controllerManager.serviceAccount.annotations | nindent 4 }} -------------------------------------------------------------------------------- /charts/kom/templates/metrics-reader-rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: {{ include "kom.fullname" . }}-metrics-reader 5 | labels: 6 | app.kubernetes.io/component: kube-rbac-proxy 7 | app.kubernetes.io/created-by: kom 8 | app.kubernetes.io/part-of: kom 9 | {{- include "kom.labels" . | nindent 4 }} 10 | rules: 11 | - nonResourceURLs: 12 | - /metrics 13 | verbs: 14 | - get -------------------------------------------------------------------------------- /config/samples/_v1alpha1_operatormanager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kom.kkb0318.github.io/v1alpha1 2 | kind: OperatorManager 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: operatormanager 6 | app.kubernetes.io/instance: operatormanager-sample 7 | app.kubernetes.io/part-of: kom 8 | app.kubernetes.io/managed-by: kustomize 9 | app.kubernetes.io/created-by: kom 10 | name: operatormanager-sample 11 | spec: 12 | # TODO(user): Add fields here 13 | -------------------------------------------------------------------------------- /charts/kom/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /config/scorecard/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - bases/config.yaml 3 | patchesJson6902: 4 | - path: patches/basic.config.yaml 5 | target: 6 | group: scorecard.operatorframework.io 7 | version: v1alpha3 8 | kind: Configuration 9 | name: config 10 | - path: patches/olm.config.yaml 11 | target: 12 | group: scorecard.operatorframework.io 13 | version: v1alpha3 14 | kind: Configuration 15 | name: config 16 | #+kubebuilder:scaffold:patchesJson6902 17 | -------------------------------------------------------------------------------- /internal/tool/argo/testdata/git_application.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: Application 3 | metadata: 4 | name: repo1 5 | namespace: repo-ns1 6 | spec: 7 | destination: 8 | namespace: repo-ns1 9 | server: https://kubernetes.default.svc 10 | project: default 11 | source: 12 | path: ./path1 13 | repoURL: https://example.com 14 | targetRevision: "1.0.0" 15 | syncPolicy: 16 | automated: 17 | prune: true 18 | selfHeal: true 19 | -------------------------------------------------------------------------------- /internal/tool/argo/testdata/helm_application.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: Application 3 | metadata: 4 | name: chart1 5 | namespace: repo-ns1 6 | spec: 7 | destination: 8 | namespace: repo-ns1 9 | server: https://kubernetes.default.svc 10 | project: default 11 | source: 12 | chart: chart1 13 | repoURL: https://example.com 14 | targetRevision: "1.0.0" 15 | syncPolicy: 16 | automated: 17 | prune: true 18 | selfHeal: true 19 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_operatormanagers.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: operatormanagers.kom.kkb0318.github.io 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | conversionReviewVersions: 16 | - v1 17 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_client_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrole 6 | app.kubernetes.io/instance: metrics-reader 7 | app.kubernetes.io/component: kube-rbac-proxy 8 | app.kubernetes.io/created-by: kom 9 | app.kubernetes.io/part-of: kom 10 | app.kubernetes.io/managed-by: kustomize 11 | name: metrics-reader 12 | rules: 13 | - nonResourceURLs: 14 | - "/metrics" 15 | verbs: 16 | - get 17 | -------------------------------------------------------------------------------- /charts/kom/README.md.gotmpl: -------------------------------------------------------------------------------- 1 | {{ template "chart.header" . }} 2 | 3 | [kom](https://github.com/kkb0318/kom) Kubernetes Operator Manager is an open-source software tool designed to streamline the management of Kubernetes operators. 4 | 5 | 6 | ## Get Repo Info 7 | 8 | ```console 9 | helm repo add kkb0318 https://kkb0318.github.io/kom 10 | helm repo update 11 | ``` 12 | 13 | ## Install Chart 14 | 15 | ```console 16 | helm install kom kkb0318/kom -n kom-system --create-namespace 17 | ``` 18 | 19 | {{ template "chart.valuesSection" . }} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | 23 | bin 24 | -------------------------------------------------------------------------------- /internal/tool/factory/factory.go: -------------------------------------------------------------------------------- 1 | package factory 2 | 3 | import ( 4 | komv1alpha1 "github.com/kkb0318/kom/api/v1alpha1" 5 | komtool "github.com/kkb0318/kom/internal/tool" 6 | "github.com/kkb0318/kom/internal/tool/argo" 7 | "github.com/kkb0318/kom/internal/tool/flux" 8 | ) 9 | 10 | func NewResourceManager(obj komv1alpha1.OperatorManager) komtool.ResourceManager { 11 | switch obj.Spec.Tool { 12 | case komv1alpha1.FluxCDTool: 13 | return flux.NewFlux(obj) 14 | case komv1alpha1.ArgoCDTool: 15 | return argo.NewArgo(obj) 16 | default: 17 | return flux.NewFlux(obj) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /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 2023. 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/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrolebinding 6 | app.kubernetes.io/instance: manager-rolebinding 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: kom 9 | app.kubernetes.io/part-of: kom 10 | app.kubernetes.io/managed-by: kustomize 11 | name: manager-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: ClusterRole 15 | name: manager-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: controller-manager 19 | namespace: system 20 | -------------------------------------------------------------------------------- /charts/kom/templates/metrics-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "kom.fullname" . }}-controller-manager-metrics-service 5 | labels: 6 | app.kubernetes.io/component: kube-rbac-proxy 7 | app.kubernetes.io/created-by: kom 8 | app.kubernetes.io/part-of: kom 9 | control-plane: controller-manager 10 | {{- include "kom.labels" . | nindent 4 }} 11 | spec: 12 | type: {{ .Values.metricsService.type }} 13 | selector: 14 | control-plane: controller-manager 15 | {{- include "kom.selectorLabels" . | nindent 4 }} 16 | ports: 17 | {{- .Values.metricsService.ports | toYaml | nindent 2 }} -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrolebinding 6 | app.kubernetes.io/instance: proxy-rolebinding 7 | app.kubernetes.io/component: kube-rbac-proxy 8 | app.kubernetes.io/created-by: kom 9 | app.kubernetes.io/part-of: kom 10 | app.kubernetes.io/managed-by: kustomize 11 | name: proxy-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: ClusterRole 15 | name: proxy-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: controller-manager 19 | namespace: system 20 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: rolebinding 6 | app.kubernetes.io/instance: leader-election-rolebinding 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: kom 9 | app.kubernetes.io/part-of: kom 10 | app.kubernetes.io/managed-by: kustomize 11 | name: leader-election-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: Role 15 | name: leader-election-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: controller-manager 19 | namespace: system 20 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrole 6 | app.kubernetes.io/instance: proxy-role 7 | app.kubernetes.io/component: kube-rbac-proxy 8 | app.kubernetes.io/created-by: kom 9 | app.kubernetes.io/part-of: kom 10 | app.kubernetes.io/managed-by: kustomize 11 | name: proxy-role 12 | rules: 13 | - apiGroups: 14 | - authentication.k8s.io 15 | resources: 16 | - tokenreviews 17 | verbs: 18 | - create 19 | - apiGroups: 20 | - authorization.k8s.io 21 | resources: 22 | - subjectaccessreviews 23 | verbs: 24 | - create 25 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: service 7 | app.kubernetes.io/instance: controller-manager-metrics-service 8 | app.kubernetes.io/component: kube-rbac-proxy 9 | app.kubernetes.io/created-by: kom 10 | app.kubernetes.io/part-of: kom 11 | app.kubernetes.io/managed-by: kustomize 12 | name: controller-manager-metrics-service 13 | namespace: system 14 | spec: 15 | ports: 16 | - name: https 17 | port: 8443 18 | protocol: TCP 19 | targetPort: https 20 | selector: 21 | control-plane: controller-manager 22 | -------------------------------------------------------------------------------- /charts/kom/values.yaml: -------------------------------------------------------------------------------- 1 | controllerManager: 2 | manager: 3 | args: 4 | - --leader-elect 5 | containerSecurityContext: 6 | allowPrivilegeEscalation: false 7 | capabilities: 8 | drop: 9 | - ALL 10 | image: 11 | repository: ghcr.io/kkb0318/kom 12 | tag: APP_VERSION 13 | resources: 14 | limits: 15 | cpu: 500m 16 | memory: 128Mi 17 | requests: 18 | cpu: 10m 19 | memory: 64Mi 20 | replicas: 1 21 | serviceAccount: 22 | annotations: {} 23 | kubernetesClusterDomain: cluster.local 24 | metricsService: 25 | ports: 26 | - name: https 27 | port: 8443 28 | protocol: TCP 29 | targetPort: https 30 | type: ClusterIP 31 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | # Code generated by tool. DO NOT EDIT. 2 | # This file is used to track the info used to scaffold your project 3 | # and allow the plugins properly work. 4 | # More info: https://book.kubebuilder.io/reference/project-config.html 5 | domain: kkb0318.github.io 6 | layout: 7 | - go.kubebuilder.io/v4 8 | plugins: 9 | manifests.sdk.operatorframework.io/v2: {} 10 | scorecard.sdk.operatorframework.io/v2: {} 11 | projectName: kom 12 | repo: github.com/kkb0318/kom 13 | resources: 14 | - api: 15 | crdVersion: v1 16 | namespaced: true 17 | controller: true 18 | domain: kkb0318.github.io 19 | group: kom 20 | kind: OperatorManager 21 | path: github.com/kkb0318/kom/api/v1alpha1 22 | version: v1alpha1 23 | version: "3" 24 | -------------------------------------------------------------------------------- /internal/tool/argo/argo.go: -------------------------------------------------------------------------------- 1 | package argo 2 | 3 | import ( 4 | komv1alpha1 "github.com/kkb0318/kom/api/v1alpha1" 5 | komtool "github.com/kkb0318/kom/internal/tool" 6 | ) 7 | 8 | type Argo struct { 9 | resource komv1alpha1.Resource 10 | } 11 | 12 | func NewArgo(obj komv1alpha1.OperatorManager) *Argo { 13 | return &Argo{obj.Spec.Resource} 14 | } 15 | 16 | func (f *Argo) Helm() ([]komtool.Resource, error) { 17 | helmResources, err := NewArgoHelmList(f.resource.Helm) 18 | if err != nil { 19 | return nil, err 20 | } 21 | return helmResources, nil 22 | } 23 | 24 | func (f *Argo) Git() ([]komtool.Resource, error) { 25 | gitResources, err := NewArgoGitList(f.resource.Git) 26 | if err != nil { 27 | return nil, err 28 | } 29 | return gitResources, nil 30 | } 31 | -------------------------------------------------------------------------------- /internal/tool/flux/flux.go: -------------------------------------------------------------------------------- 1 | package flux 2 | 3 | import ( 4 | komv1alpha1 "github.com/kkb0318/kom/api/v1alpha1" 5 | komtool "github.com/kkb0318/kom/internal/tool" 6 | ) 7 | 8 | type Flux struct { 9 | resource komv1alpha1.Resource 10 | } 11 | 12 | func NewFlux(obj komv1alpha1.OperatorManager) *Flux { 13 | return &Flux{obj.Spec.Resource} 14 | } 15 | 16 | func (f *Flux) Helm() ([]komtool.Resource, error) { 17 | helmResources, err := NewFluxHelmList(f.resource.Helm) 18 | if err != nil { 19 | return nil, err 20 | } 21 | return helmResources, nil 22 | } 23 | 24 | func (f *Flux) Git() ([]komtool.Resource, error) { 25 | gitResources, err := NewFluxGitList(f.resource.Git) 26 | if err != nil { 27 | return nil, err 28 | } 29 | return gitResources, nil 30 | } 31 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | # All RBAC will be applied under this service account in 3 | # the deployment namespace. You may comment out this resource 4 | # if your manager will use a service account that exists at 5 | # runtime. Be sure to update RoleBinding and ClusterRoleBinding 6 | # subjects if changing service account names. 7 | - service_account.yaml 8 | - role.yaml 9 | - role_binding.yaml 10 | - leader_election_role.yaml 11 | - leader_election_role_binding.yaml 12 | # Comment the following 4 lines if you want to disable 13 | # the auth proxy (https://github.com/brancz/kube-rbac-proxy) 14 | # which protects your /metrics endpoint. 15 | - auth_proxy_service.yaml 16 | - auth_proxy_role.yaml 17 | - auth_proxy_role_binding.yaml 18 | - auth_proxy_client_clusterrole.yaml 19 | -------------------------------------------------------------------------------- /config/rbac/operatormanager_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view operatormanagers. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: operatormanager-viewer-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: kom 10 | app.kubernetes.io/part-of: kom 11 | app.kubernetes.io/managed-by: kustomize 12 | name: operatormanager-viewer-role 13 | rules: 14 | - apiGroups: 15 | - kom.kkb0318.github.io 16 | resources: 17 | - operatormanagers 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - apiGroups: 23 | - kom.kkb0318.github.io 24 | resources: 25 | - operatormanagers/status 26 | verbs: 27 | - get 28 | -------------------------------------------------------------------------------- /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 | app.kubernetes.io/name: servicemonitor 9 | app.kubernetes.io/instance: controller-manager-metrics-monitor 10 | app.kubernetes.io/component: metrics 11 | app.kubernetes.io/created-by: kom 12 | app.kubernetes.io/part-of: kom 13 | app.kubernetes.io/managed-by: kustomize 14 | name: controller-manager-metrics-monitor 15 | namespace: system 16 | spec: 17 | endpoints: 18 | - path: /metrics 19 | port: https 20 | scheme: https 21 | bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token 22 | tlsConfig: 23 | insecureSkipVerify: true 24 | selector: 25 | matchLabels: 26 | control-plane: controller-manager 27 | -------------------------------------------------------------------------------- /config/rbac/operatormanager_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit operatormanagers. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: operatormanager-editor-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: kom 10 | app.kubernetes.io/part-of: kom 11 | app.kubernetes.io/managed-by: kustomize 12 | name: operatormanager-editor-role 13 | rules: 14 | - apiGroups: 15 | - kom.kkb0318.github.io 16 | resources: 17 | - operatormanagers 18 | verbs: 19 | - create 20 | - delete 21 | - get 22 | - list 23 | - patch 24 | - update 25 | - watch 26 | - apiGroups: 27 | - kom.kkb0318.github.io 28 | resources: 29 | - operatormanagers/status 30 | verbs: 31 | - get 32 | -------------------------------------------------------------------------------- /examples/basic_argo.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kom.kkb0318.github.io/v1alpha1 2 | kind: OperatorManager 3 | metadata: 4 | name: kom 5 | namespace: kom-system 6 | spec: 7 | tool: argo 8 | cleanup: true 9 | resource: 10 | helm: 11 | - name: jetstack 12 | url: https://charts.jetstack.io 13 | charts: 14 | - name: cert-manager 15 | version: v1.14.4 16 | values: 17 | installCRDs: true 18 | prometheus: 19 | enabled: false 20 | - name: repo1 21 | url: https://helm.github.io/examples 22 | charts: 23 | - name: hello-world 24 | version: x.x.x 25 | git: 26 | - name: "gitrepo1" 27 | url: "https://github.com/operator-framework/operator-sdk" 28 | path: "testdata/helm/memcached-operator/config/default" 29 | reference: 30 | value: "v1.33.0" 31 | -------------------------------------------------------------------------------- /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 | labels: 6 | app.kubernetes.io/name: role 7 | app.kubernetes.io/instance: leader-election-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: kom 10 | app.kubernetes.io/part-of: kom 11 | app.kubernetes.io/managed-by: kustomize 12 | name: leader-election-role 13 | rules: 14 | - apiGroups: 15 | - "" 16 | resources: 17 | - configmaps 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - create 23 | - update 24 | - patch 25 | - delete 26 | - apiGroups: 27 | - coordination.k8s.io 28 | resources: 29 | - leases 30 | verbs: 31 | - get 32 | - list 33 | - watch 34 | - create 35 | - update 36 | - patch 37 | - delete 38 | - apiGroups: 39 | - "" 40 | resources: 41 | - events 42 | verbs: 43 | - create 44 | - patch 45 | -------------------------------------------------------------------------------- /examples/basic_flux.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kom.kkb0318.github.io/v1alpha1 2 | kind: OperatorManager 3 | metadata: 4 | name: kom 5 | namespace: kom-system 6 | spec: 7 | tool: flux 8 | cleanup: true 9 | resource: 10 | helm: 11 | - name: jetstack 12 | url: https://charts.jetstack.io 13 | charts: 14 | - name: cert-manager 15 | version: v1.14.4 16 | values: 17 | installCRDs: true 18 | prometheus: 19 | enabled: false 20 | - name: repo1 21 | url: https://helm.github.io/examples 22 | charts: 23 | - name: hello-world 24 | version: x.x.x 25 | git: 26 | - name: "gitrepo1" 27 | url: "https://github.com/operator-framework/operator-sdk" 28 | path: "testdata/helm/memcached-operator/config/default" 29 | reference: 30 | type: tag # tag or branch or semver 31 | value: "v1.33.0" 32 | -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # This kustomization.yaml is not intended to be run by itself, 2 | # since it depends on service name and namespace that are out of this kustomize package. 3 | # It should be run by config/default 4 | resources: 5 | - bases/kom.kkb0318.github.io_operatormanagers.yaml 6 | #+kubebuilder:scaffold:crdkustomizeresource 7 | 8 | patches: 9 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 10 | # patches here are for enabling the conversion webhook for each CRD 11 | #- path: patches/webhook_in_operatormanagers.yaml 12 | #+kubebuilder:scaffold:crdkustomizewebhookpatch 13 | 14 | # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. 15 | # patches here are for enabling the CA injection for each CRD 16 | #- path: patches/cainjection_in_operatormanagers.yaml 17 | #+kubebuilder:scaffold:crdkustomizecainjectionpatch 18 | 19 | # the following config is for teaching kustomize how to do kustomization for CRDs. 20 | configurations: 21 | - kustomizeconfig.yaml 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 kkb 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /charts/kom/templates/proxy-rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: {{ include "kom.fullname" . }}-proxy-role 5 | labels: 6 | app.kubernetes.io/component: kube-rbac-proxy 7 | app.kubernetes.io/created-by: kom 8 | app.kubernetes.io/part-of: kom 9 | {{- include "kom.labels" . | nindent 4 }} 10 | rules: 11 | - apiGroups: 12 | - authentication.k8s.io 13 | resources: 14 | - tokenreviews 15 | verbs: 16 | - create 17 | - apiGroups: 18 | - authorization.k8s.io 19 | resources: 20 | - subjectaccessreviews 21 | verbs: 22 | - create 23 | --- 24 | apiVersion: rbac.authorization.k8s.io/v1 25 | kind: ClusterRoleBinding 26 | metadata: 27 | name: {{ include "kom.fullname" . }}-proxy-rolebinding 28 | labels: 29 | app.kubernetes.io/component: kube-rbac-proxy 30 | app.kubernetes.io/created-by: kom 31 | app.kubernetes.io/part-of: kom 32 | {{- include "kom.labels" . | nindent 4 }} 33 | roleRef: 34 | apiGroup: rbac.authorization.k8s.io 35 | kind: ClusterRole 36 | name: '{{ include "kom.fullname" . }}-proxy-role' 37 | subjects: 38 | - kind: ServiceAccount 39 | name: '{{ include "kom.fullname" . }}-controller-manager' 40 | namespace: '{{ .Release.Namespace }}' -------------------------------------------------------------------------------- /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/kom.clusterserviceversion.yaml 5 | - ../default 6 | - ../samples 7 | - ../scorecard 8 | 9 | # [WEBHOOK] To enable webhooks, uncomment all the sections with [WEBHOOK] prefix. 10 | # Do NOT uncomment sections with prefix [CERTMANAGER], as OLM does not support cert-manager. 11 | # These patches remove the unnecessary "cert" volume and its manager container volumeMount. 12 | #patchesJson6902: 13 | #- target: 14 | # group: apps 15 | # version: v1 16 | # kind: Deployment 17 | # name: controller-manager 18 | # namespace: system 19 | # patch: |- 20 | # # Remove the manager container's "cert" volumeMount, since OLM will create and mount a set of certs. 21 | # # Update the indices in this path if adding or removing containers/volumeMounts in the manager's Deployment. 22 | # - op: remove 23 | 24 | # path: /spec/template/spec/containers/0/volumeMounts/0 25 | # # Remove the "cert" volume, since OLM will create and mount a set of certs. 26 | # # Update the indices in this path if adding or removing volumes in the manager's Deployment. 27 | # - op: remove 28 | # path: /spec/template/spec/volumes/0 29 | -------------------------------------------------------------------------------- /config/default/manager_auth_proxy_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch inject a sidecar container which is a HTTP proxy for the 2 | # controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: controller-manager 7 | namespace: system 8 | spec: 9 | template: 10 | spec: 11 | containers: 12 | - name: kube-rbac-proxy 13 | securityContext: 14 | allowPrivilegeEscalation: false 15 | capabilities: 16 | drop: 17 | - "ALL" 18 | image: gcr.io/kubebuilder/kube-rbac-proxy:v0.14.4 19 | args: 20 | - "--secure-listen-address=0.0.0.0:8443" 21 | - "--upstream=http://127.0.0.1:8080/" 22 | - "--logtostderr=true" 23 | - "--v=0" 24 | ports: 25 | - containerPort: 8443 26 | protocol: TCP 27 | name: https 28 | resources: 29 | limits: 30 | cpu: 500m 31 | memory: 128Mi 32 | requests: 33 | cpu: 5m 34 | memory: 64Mi 35 | - name: manager 36 | args: 37 | - "--health-probe-bind-address=:8081" 38 | - "--metrics-bind-address=127.0.0.1:8080" 39 | - "--leader-elect" 40 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM golang:1.24 as builder 3 | ARG TARGETOS 4 | ARG TARGETARCH 5 | 6 | WORKDIR /workspace 7 | # Copy the Go Modules manifests 8 | COPY go.mod go.mod 9 | COPY go.sum go.sum 10 | # cache deps before building and copying source so that we don't need to re-download as much 11 | # and so that source changes don't invalidate our downloaded layer 12 | RUN go mod download 13 | 14 | # Copy the go source 15 | COPY cmd/main.go cmd/main.go 16 | COPY api/ api/ 17 | COPY internal/ internal/ 18 | 19 | # Build 20 | # the GOARCH has not a default value to allow the binary be built according to the host where the command 21 | # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO 22 | # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, 23 | # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. 24 | RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go 25 | 26 | # Use distroless as minimal base image to package the manager binary 27 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 28 | FROM gcr.io/distroless/static:nonroot 29 | WORKDIR / 30 | COPY --from=builder /workspace/manager . 31 | USER 65532:65532 32 | 33 | ENTRYPOINT ["/manager"] 34 | -------------------------------------------------------------------------------- /api/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023. 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 v1alpha1 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=kom.kkb0318.github.io 20 | package v1alpha1 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // GroupVersion is group version used to register these objects 29 | GroupVersion = schema.GroupVersion{Group: "kom.kkb0318.github.io", Version: "v1alpha1"} 30 | 31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 33 | 34 | // AddToScheme adds the types in this group-version to the given scheme. 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | ) 37 | -------------------------------------------------------------------------------- /internal/tool/argo/git_test.go: -------------------------------------------------------------------------------- 1 | package argo 2 | 3 | import ( 4 | "testing" 5 | 6 | komv1alpha1 "github.com/kkb0318/kom/api/v1alpha1" 7 | komtool "github.com/kkb0318/kom/internal/tool" 8 | "github.com/kkb0318/kom/internal/tool/argo/testdata" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestArgoGit_New(t *testing.T) { 13 | tests := []struct { 14 | name string 15 | inputs []komv1alpha1.Git 16 | expected []komtool.Resource 17 | expectedErr bool 18 | }{ 19 | { 20 | name: "create manifest for installing helm with argo", 21 | inputs: []komv1alpha1.Git{ 22 | { 23 | Name: "repo1", 24 | Namespace: "repo-ns1", 25 | Url: "https://example.com", 26 | Path: "./path1", 27 | Reference: komv1alpha1.GitReference{ 28 | Value: "1.0.0", 29 | }, 30 | }, 31 | }, 32 | expected: []komtool.Resource{ 33 | &ArgoGit{ 34 | source: testdata.NewMockSecretBuilder().Build(t, "git_secret.yaml"), 35 | app: testdata.NewMockApplicationBuilder().Build(t, "git_application.yaml"), 36 | }, 37 | }, 38 | }, 39 | } 40 | for _, tt := range tests { 41 | t.Run(tt.name, func(t *testing.T) { 42 | actual, err := NewArgoGitList(tt.inputs) 43 | if tt.expectedErr { 44 | assert.Error(t, err, "") 45 | } else { 46 | assert.NoError(t, err) 47 | assert.Equal(t, tt.expected, actual) 48 | } 49 | }) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /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.33.0 8 | labels: 9 | suite: olm 10 | test: olm-bundle-validation-test 11 | - op: add 12 | path: /stages/0/tests/- 13 | value: 14 | entrypoint: 15 | - scorecard-test 16 | - olm-crds-have-validation 17 | image: quay.io/operator-framework/scorecard-test:v1.33.0 18 | labels: 19 | suite: olm 20 | test: olm-crds-have-validation-test 21 | - op: add 22 | path: /stages/0/tests/- 23 | value: 24 | entrypoint: 25 | - scorecard-test 26 | - olm-crds-have-resources 27 | image: quay.io/operator-framework/scorecard-test:v1.33.0 28 | labels: 29 | suite: olm 30 | test: olm-crds-have-resources-test 31 | - op: add 32 | path: /stages/0/tests/- 33 | value: 34 | entrypoint: 35 | - scorecard-test 36 | - olm-spec-descriptors 37 | image: quay.io/operator-framework/scorecard-test:v1.33.0 38 | labels: 39 | suite: olm 40 | test: olm-spec-descriptors-test 41 | - op: add 42 | path: /stages/0/tests/- 43 | value: 44 | entrypoint: 45 | - scorecard-test 46 | - olm-status-descriptors 47 | image: quay.io/operator-framework/scorecard-test:v1.33.0 48 | labels: 49 | suite: olm 50 | test: olm-status-descriptors-test 51 | -------------------------------------------------------------------------------- /internal/tool/argo/manifests/secret.go: -------------------------------------------------------------------------------- 1 | package manifests 2 | 3 | import ( 4 | "fmt" 5 | 6 | corev1 "k8s.io/api/core/v1" 7 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | ) 9 | 10 | type SecretBuilder struct { 11 | stringData map[string]string 12 | } 13 | 14 | func NewSecretBuilder() *SecretBuilder { 15 | return &SecretBuilder{} 16 | } 17 | 18 | func (b *SecretBuilder) WithGit(url string) *SecretBuilder { 19 | b.stringData = map[string]string{ 20 | "type": "git", 21 | "url": url, 22 | "project": "default", 23 | } 24 | return b 25 | } 26 | 27 | func (b *SecretBuilder) WithHelm(chartName, url string) *SecretBuilder { 28 | b.stringData = map[string]string{ 29 | "name": chartName, 30 | "type": "helm", 31 | "url": url, 32 | "project": "default", 33 | } 34 | return b 35 | } 36 | 37 | func (b *SecretBuilder) Build(name, ns string) (*corev1.Secret, error) { 38 | if b.stringData == nil { 39 | return nil, fmt.Errorf("argocd Secret.StringData is empty") 40 | } 41 | secret := &corev1.Secret{ 42 | ObjectMeta: v1.ObjectMeta{ 43 | Name: name, 44 | Namespace: ns, 45 | Labels: map[string]string{ 46 | "argocd.argoproj.io/secret-type": "repository", 47 | }, 48 | }, 49 | TypeMeta: v1.TypeMeta{ 50 | APIVersion: corev1.SchemeGroupVersion.String(), 51 | Kind: "Secret", 52 | }, 53 | Type: corev1.SecretTypeOpaque, 54 | StringData: b.stringData, 55 | } 56 | return secret, nil 57 | } 58 | -------------------------------------------------------------------------------- /internal/tool/flux/manifests/helm_repository.go: -------------------------------------------------------------------------------- 1 | package manifests 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | "time" 7 | 8 | sourcev1 "github.com/fluxcd/source-controller/api/v1" 9 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | ) 11 | 12 | type HelmRepositoryBuilder struct { 13 | url string 14 | interval v1.Duration 15 | } 16 | 17 | func NewHelmRepositoryBuilder() *HelmRepositoryBuilder { 18 | return &HelmRepositoryBuilder{ 19 | interval: v1.Duration{Duration: time.Minute}, 20 | } 21 | } 22 | 23 | func (b *HelmRepositoryBuilder) WithUrl(value string) *HelmRepositoryBuilder { 24 | b.url = value 25 | return b 26 | } 27 | 28 | func (b *HelmRepositoryBuilder) Build(name, ns string) (*sourcev1.HelmRepository, error) { 29 | if b.url == "" { 30 | return nil, errors.New("the 'url' field is empty. Please specify a valid URL") 31 | } 32 | helmrepo := &sourcev1.HelmRepository{ 33 | ObjectMeta: v1.ObjectMeta{ 34 | Name: name, 35 | Namespace: ns, 36 | }, 37 | TypeMeta: v1.TypeMeta{ 38 | APIVersion: sourcev1.GroupVersion.String(), 39 | Kind: sourcev1.HelmRepositoryKind, 40 | }, 41 | Spec: sourcev1.HelmRepositorySpec{ 42 | Type: repositoryType(b.url), 43 | Interval: b.interval, 44 | URL: b.url, 45 | }, 46 | } 47 | return helmrepo, nil 48 | } 49 | 50 | func repositoryType(url string) string { 51 | if strings.HasPrefix(url, "oci:") { 52 | return "oci" 53 | } 54 | return "default" 55 | } 56 | -------------------------------------------------------------------------------- /charts/kom/templates/leader-election-rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: Role 3 | metadata: 4 | name: {{ include "kom.fullname" . }}-leader-election-role 5 | labels: 6 | app.kubernetes.io/component: rbac 7 | app.kubernetes.io/created-by: kom 8 | app.kubernetes.io/part-of: kom 9 | {{- include "kom.labels" . | nindent 4 }} 10 | rules: 11 | - apiGroups: 12 | - "" 13 | resources: 14 | - configmaps 15 | verbs: 16 | - get 17 | - list 18 | - watch 19 | - create 20 | - update 21 | - patch 22 | - delete 23 | - apiGroups: 24 | - coordination.k8s.io 25 | resources: 26 | - leases 27 | verbs: 28 | - get 29 | - list 30 | - watch 31 | - create 32 | - update 33 | - patch 34 | - delete 35 | - apiGroups: 36 | - "" 37 | resources: 38 | - events 39 | verbs: 40 | - create 41 | - patch 42 | --- 43 | apiVersion: rbac.authorization.k8s.io/v1 44 | kind: RoleBinding 45 | metadata: 46 | name: {{ include "kom.fullname" . }}-leader-election-rolebinding 47 | labels: 48 | app.kubernetes.io/component: rbac 49 | app.kubernetes.io/created-by: kom 50 | app.kubernetes.io/part-of: kom 51 | {{- include "kom.labels" . | nindent 4 }} 52 | roleRef: 53 | apiGroup: rbac.authorization.k8s.io 54 | kind: Role 55 | name: '{{ include "kom.fullname" . }}-leader-election-role' 56 | subjects: 57 | - kind: ServiceAccount 58 | name: '{{ include "kom.fullname" . }}-controller-manager' 59 | namespace: '{{ .Release.Namespace }}' -------------------------------------------------------------------------------- /charts/kom/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: kom 3 | description: Kubernetes Operator Manager manages Operator easily 4 | # A chart can be either an 'application' or a 'library' chart. 5 | # 6 | # Application charts are a collection of templates that can be packaged into versioned archives 7 | # to be deployed. 8 | # 9 | # Library charts provide useful utilities or functions for the chart developer. They're included as 10 | # a dependency of application charts to inject those utilities and functions into the rendering 11 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 12 | type: application 13 | # This is the chart version. This version number should be incremented each time you make changes 14 | # to the chart and its templates, including the app version. 15 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 16 | version: CHART_VERSION 17 | # This is the version number of the application being deployed. This version number should be 18 | # incremented each time you make changes to the application. Versions are not expected to 19 | # follow Semantic Versioning. They should reflect the version the application is using. 20 | # It is recommended to use it with quotes. 21 | appVersion: APP_VERSION 22 | 23 | keywords: 24 | - kubernetes 25 | - kom 26 | - operator-manager 27 | - operator 28 | - controller 29 | home: https://github.com/kkb0318/kom 30 | sources: 31 | - https://github.com/kkb0318/kom 32 | maintainers: 33 | - name: kkb 34 | email: nkkb0318@gmail.com 35 | annotations: 36 | artifacthub.io/prerelease: "false" 37 | -------------------------------------------------------------------------------- /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 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - argoproj.io 21 | resources: 22 | - applications 23 | - appprojects 24 | verbs: 25 | - '*' 26 | - apiGroups: 27 | - helm.toolkit.fluxcd.io 28 | resources: 29 | - helmreleases 30 | verbs: 31 | - create 32 | - delete 33 | - get 34 | - list 35 | - patch 36 | - update 37 | - watch 38 | - apiGroups: 39 | - kom.kkb0318.github.io 40 | resources: 41 | - operatormanagers 42 | verbs: 43 | - create 44 | - delete 45 | - get 46 | - list 47 | - patch 48 | - update 49 | - watch 50 | - apiGroups: 51 | - kom.kkb0318.github.io 52 | resources: 53 | - operatormanagers/finalizers 54 | verbs: 55 | - create 56 | - delete 57 | - get 58 | - patch 59 | - update 60 | - apiGroups: 61 | - kom.kkb0318.github.io 62 | resources: 63 | - operatormanagers/status 64 | verbs: 65 | - get 66 | - patch 67 | - update 68 | - apiGroups: 69 | - kustomize.toolkit.fluxcd.io 70 | resources: 71 | - kustomizations 72 | verbs: 73 | - create 74 | - delete 75 | - get 76 | - list 77 | - patch 78 | - update 79 | - watch 80 | - apiGroups: 81 | - source.toolkit.fluxcd.io 82 | resources: 83 | - gitrepositories 84 | - helmrepositories 85 | verbs: 86 | - create 87 | - delete 88 | - get 89 | - list 90 | - patch 91 | - update 92 | - watch 93 | -------------------------------------------------------------------------------- /internal/tool/argo/git.go: -------------------------------------------------------------------------------- 1 | package argo 2 | 3 | import ( 4 | argov1alpha1 "github.com/kkb0318/argo-cd-api/api/v1alpha1" 5 | komv1alpha1 "github.com/kkb0318/kom/api/v1alpha1" 6 | komtool "github.com/kkb0318/kom/internal/tool" 7 | "github.com/kkb0318/kom/internal/tool/argo/manifests" 8 | corev1 "k8s.io/api/core/v1" 9 | "sigs.k8s.io/controller-runtime/pkg/client" 10 | ) 11 | 12 | type ArgoGit struct { 13 | source *corev1.Secret 14 | app *argov1alpha1.Application 15 | } 16 | 17 | func (f *ArgoGit) Repositories() []client.Object { 18 | return []client.Object{f.source} 19 | } 20 | 21 | func (f *ArgoGit) Charts() []client.Object { 22 | return []client.Object{f.app} 23 | } 24 | 25 | func NewArgoGitList(objs []komv1alpha1.Git) ([]komtool.Resource, error) { 26 | gitList := make([]komtool.Resource, len(objs)) 27 | for i, obj := range objs { 28 | git, err := NewArgoGit(obj) 29 | if err != nil { 30 | return nil, err 31 | } 32 | gitList[i] = git 33 | } 34 | return gitList, nil 35 | } 36 | 37 | func NewArgoGit(obj komv1alpha1.Git) (*ArgoGit, error) { 38 | var namespace string 39 | if obj.Namespace == "" { 40 | namespace = komv1alpha1.ArgoCDDefaultNamespace 41 | } else { 42 | namespace = obj.Namespace 43 | } 44 | secret, err := manifests.NewSecretBuilder(). 45 | WithGit(obj.Url). 46 | Build(obj.Name, namespace) 47 | if err != nil { 48 | return nil, err 49 | } 50 | git, err := manifests.NewApplicationBuilder(). 51 | WithGit(obj.Path, obj.Reference.Value, obj.Url). 52 | Build(obj.Name, namespace) 53 | if err != nil { 54 | return nil, err 55 | } 56 | f := &ArgoGit{ 57 | source: secret, 58 | app: git, 59 | } 60 | return f, nil 61 | } 62 | -------------------------------------------------------------------------------- /internal/tool/flux/manifests/kustomization.go: -------------------------------------------------------------------------------- 1 | package manifests 2 | 3 | import ( 4 | "errors" 5 | 6 | kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" 7 | sourcev1 "github.com/fluxcd/source-controller/api/v1" 8 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | type KustomizationBuilder struct { 12 | ref *kustomizev1.CrossNamespaceSourceReference 13 | path string 14 | } 15 | 16 | func NewKustomizationBuilder() *KustomizationBuilder { 17 | return &KustomizationBuilder{ 18 | path: "./", 19 | } 20 | } 21 | 22 | // WithReference set KustomizationRef 23 | func (b *KustomizationBuilder) WithReference(name, namespace string) *KustomizationBuilder { 24 | ref := &kustomizev1.CrossNamespaceSourceReference{ 25 | Kind: sourcev1.GitRepositoryKind, 26 | Name: name, 27 | Namespace: namespace, 28 | } 29 | b.ref = ref 30 | return b 31 | } 32 | 33 | func (b *KustomizationBuilder) WithPath(value string) *KustomizationBuilder { 34 | b.path = value 35 | return b 36 | } 37 | 38 | func (b *KustomizationBuilder) Build(name, ns string) (*kustomizev1.Kustomization, error) { 39 | if b.ref == nil { 40 | return nil, errors.New("the 'ref' field is nil and must be provided with a valid reference") 41 | } 42 | gitrepo := &kustomizev1.Kustomization{ 43 | ObjectMeta: v1.ObjectMeta{ 44 | Name: name, 45 | Namespace: ns, 46 | }, 47 | TypeMeta: v1.TypeMeta{ 48 | APIVersion: kustomizev1.GroupVersion.String(), 49 | Kind: kustomizev1.KustomizationKind, 50 | }, 51 | Spec: kustomizev1.KustomizationSpec{ 52 | Prune: true, 53 | Path: b.path, 54 | SourceRef: *b.ref, 55 | }, 56 | } 57 | return gitrepo, nil 58 | } 59 | -------------------------------------------------------------------------------- /charts/kom/README.md: -------------------------------------------------------------------------------- 1 | # kom 2 | 3 | [kom](https://github.com/kkb0318/kom) Kubernetes Operator Manager is an open-source software tool designed to streamline the management of Kubernetes operators. 4 | 5 | ## Get Repo Info 6 | 7 | ```console 8 | helm repo add kkb0318 https://kkb0318.github.io/kom 9 | helm repo update 10 | ``` 11 | 12 | ## Install Chart 13 | 14 | ```console 15 | helm install kom kkb0318/kom -n kom-system --create-namespace 16 | ``` 17 | 18 | ## Values 19 | 20 | | Key | Type | Default | Description | 21 | |-----|------|---------|-------------| 22 | | controllerManager.manager.args[0] | string | `"--leader-elect"` | | 23 | | controllerManager.manager.containerSecurityContext.allowPrivilegeEscalation | bool | `false` | | 24 | | controllerManager.manager.containerSecurityContext.capabilities.drop[0] | string | `"ALL"` | | 25 | | controllerManager.manager.image.repository | string | `"ghcr.io/kkb0318/kom"` | | 26 | | controllerManager.manager.image.tag | string | `"APP_VERSION"` | | 27 | | controllerManager.manager.resources.limits.cpu | string | `"500m"` | | 28 | | controllerManager.manager.resources.limits.memory | string | `"128Mi"` | | 29 | | controllerManager.manager.resources.requests.cpu | string | `"10m"` | | 30 | | controllerManager.manager.resources.requests.memory | string | `"64Mi"` | | 31 | | controllerManager.replicas | int | `1` | | 32 | | controllerManager.serviceAccount.annotations | object | `{}` | | 33 | | kubernetesClusterDomain | string | `"cluster.local"` | | 34 | | metricsService.ports[0].name | string | `"https"` | | 35 | | metricsService.ports[0].port | int | `8443` | | 36 | | metricsService.ports[0].protocol | string | `"TCP"` | | 37 | | metricsService.ports[0].targetPort | string | `"https"` | | 38 | | metricsService.type | string | `"ClusterIP"` | | 39 | -------------------------------------------------------------------------------- /internal/tool/flux/manifests/git_repository.go: -------------------------------------------------------------------------------- 1 | package manifests 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | sourcev1 "github.com/fluxcd/source-controller/api/v1" 8 | komv1alpha1 "github.com/kkb0318/kom/api/v1alpha1" 9 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | ) 11 | 12 | type GitRepositoryBuilder struct { 13 | ref *sourcev1.GitRepositoryRef 14 | url string 15 | } 16 | 17 | func NewGitRepositoryBuilder() *GitRepositoryBuilder { 18 | return &GitRepositoryBuilder{} 19 | } 20 | 21 | // WithReference set GitRepositoryRef 22 | func (b *GitRepositoryBuilder) WithReference(refType komv1alpha1.GitReferenceType, value string) *GitRepositoryBuilder { 23 | ref := &sourcev1.GitRepositoryRef{} 24 | switch refType { 25 | case komv1alpha1.GitBranch: 26 | ref.Branch = value 27 | case komv1alpha1.GitTag: 28 | ref.Tag = value 29 | case komv1alpha1.GitSemver: 30 | ref.SemVer = value 31 | default: 32 | return b 33 | } 34 | b.ref = ref 35 | return b 36 | } 37 | 38 | func (b *GitRepositoryBuilder) WithUrl(value string) *GitRepositoryBuilder { 39 | b.url = value 40 | return b 41 | } 42 | 43 | func (b *GitRepositoryBuilder) Build(name, ns string) (*sourcev1.GitRepository, error) { 44 | if b.ref == nil { 45 | return nil, errors.New("the 'ref' field is nil and must be provided with a valid reference") 46 | } 47 | if b.url == "" { 48 | return nil, errors.New("the 'url' field is empty. Please specify a valid URL") 49 | } 50 | gitrepo := &sourcev1.GitRepository{ 51 | ObjectMeta: v1.ObjectMeta{ 52 | Name: name, 53 | Namespace: ns, 54 | }, 55 | TypeMeta: v1.TypeMeta{ 56 | APIVersion: sourcev1.GroupVersion.String(), 57 | Kind: sourcev1.GitRepositoryKind, 58 | }, 59 | Spec: sourcev1.GitRepositorySpec{ 60 | Interval: v1.Duration{Duration: time.Minute}, 61 | URL: b.url, 62 | Reference: b.ref, 63 | }, 64 | } 65 | return gitrepo, nil 66 | } 67 | -------------------------------------------------------------------------------- /internal/tool/flux/git.go: -------------------------------------------------------------------------------- 1 | package flux 2 | 3 | import ( 4 | kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" 5 | sourcev1 "github.com/fluxcd/source-controller/api/v1" 6 | komv1alpha1 "github.com/kkb0318/kom/api/v1alpha1" 7 | komtool "github.com/kkb0318/kom/internal/tool" 8 | "github.com/kkb0318/kom/internal/tool/flux/manifests" 9 | "sigs.k8s.io/controller-runtime/pkg/client" 10 | ) 11 | 12 | type FluxGit struct { 13 | source *sourcev1.GitRepository 14 | ks *kustomizev1.Kustomization 15 | } 16 | 17 | type GitValues struct { 18 | FullnameOverride string 19 | } 20 | 21 | func (f *FluxGit) Repositories() []client.Object { 22 | return []client.Object{f.source} 23 | } 24 | 25 | func (f *FluxGit) Charts() []client.Object { 26 | return []client.Object{f.ks} 27 | } 28 | 29 | func NewFluxGitList(objs []komv1alpha1.Git) ([]komtool.Resource, error) { 30 | gitList := make([]komtool.Resource, len(objs)) 31 | for i, obj := range objs { 32 | git, err := NewFluxGit(obj) 33 | if err != nil { 34 | return nil, err 35 | } 36 | gitList[i] = git 37 | } 38 | return gitList, nil 39 | } 40 | 41 | func NewFluxGit(obj komv1alpha1.Git) (*FluxGit, error) { 42 | repoName := obj.Name 43 | var namespace string 44 | if obj.Namespace == "" { 45 | namespace = komv1alpha1.DefaultNamespace 46 | } else { 47 | namespace = obj.Namespace 48 | } 49 | gitrepo, err := manifests.NewGitRepositoryBuilder(). 50 | WithReference(obj.Reference.Type, obj.Reference.Value). 51 | WithUrl(obj.Url). 52 | Build(repoName, namespace) 53 | if err != nil { 54 | return nil, err 55 | } 56 | ks, err := manifests.NewKustomizationBuilder(). 57 | WithReference(gitrepo.GetName(), gitrepo.GetNamespace()). 58 | WithPath(obj.Path). 59 | Build(repoName, namespace) 60 | if err != nil { 61 | return nil, err 62 | } 63 | f := &FluxGit{ 64 | source: gitrepo, 65 | ks: ks, 66 | } 67 | return f, nil 68 | } 69 | -------------------------------------------------------------------------------- /charts/kom/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "kom.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "kom.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "kom.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "kom.labels" -}} 37 | helm.sh/chart: {{ include "kom.chart" . }} 38 | {{ include "kom.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "kom.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "kom.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "kom.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "kom.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /internal/tool/flux/helm_test.go: -------------------------------------------------------------------------------- 1 | package flux 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | helmv2 "github.com/fluxcd/helm-controller/api/v2" 8 | komv1alpha1 "github.com/kkb0318/kom/api/v1alpha1" 9 | komtool "github.com/kkb0318/kom/internal/tool" 10 | "github.com/kkb0318/kom/internal/tool/flux/testdata" 11 | "github.com/stretchr/testify/assert" 12 | apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 13 | "sigs.k8s.io/yaml" 14 | ) 15 | 16 | func values(hrValues string) *apiextensionsv1.JSON { 17 | v, err := yaml.YAMLToJSON([]byte(hrValues)) 18 | if err != nil { 19 | fmt.Println(err) 20 | } 21 | return &apiextensionsv1.JSON{Raw: v} 22 | } 23 | 24 | func TestFluxHelm_New(t *testing.T) { 25 | tests := []struct { 26 | name string 27 | inputs []komv1alpha1.Helm 28 | expected []komtool.Resource 29 | expectedErr bool 30 | }{ 31 | { 32 | name: "continue if not in previous", 33 | inputs: []komv1alpha1.Helm{ 34 | { 35 | Name: "repo1", 36 | Namespace: "repo-ns1", 37 | Url: "https://example.com", 38 | Charts: []komv1alpha1.Chart{ 39 | { 40 | Name: "chart1", 41 | Version: "x.x.x", 42 | Values: values(` 43 | key1: val1 44 | `), 45 | }, 46 | }, 47 | }, 48 | }, 49 | expected: []komtool.Resource{ 50 | &FluxHelm{ 51 | source: testdata.NewMockHelmRepositoryBuilder(). 52 | Build(t, "helm_repository.yaml"), 53 | helm: []*helmv2.HelmRelease{ 54 | testdata.NewMockHelmReleaseBuilder(). 55 | WithValues(values(` 56 | key1: val1 57 | `)). 58 | Build(t, "helm_release.yaml"), 59 | }, 60 | }, 61 | }, 62 | }, 63 | } 64 | for _, tt := range tests { 65 | t.Run(tt.name, func(t *testing.T) { 66 | actual, err := NewFluxHelmList(tt.inputs) 67 | if tt.expectedErr { 68 | assert.Error(t, err, "") 69 | } else { 70 | assert.NoError(t, err) 71 | assert.Equal(t, tt.expected, actual) 72 | } 73 | }) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /charts/kom/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "kom.fullname" . }}-controller-manager 5 | labels: 6 | app.kubernetes.io/component: manager 7 | app.kubernetes.io/created-by: kom 8 | app.kubernetes.io/part-of: kom 9 | control-plane: controller-manager 10 | {{- include "kom.labels" . | nindent 4 }} 11 | spec: 12 | replicas: {{ .Values.controllerManager.replicas }} 13 | selector: 14 | matchLabels: 15 | control-plane: controller-manager 16 | {{- include "kom.selectorLabels" . | nindent 6 }} 17 | template: 18 | metadata: 19 | labels: 20 | control-plane: controller-manager 21 | {{- include "kom.selectorLabels" . | nindent 8 }} 22 | annotations: 23 | kubectl.kubernetes.io/default-container: manager 24 | spec: 25 | containers: 26 | - args: {{- toYaml .Values.controllerManager.manager.args | nindent 8 }} 27 | command: 28 | - /manager 29 | env: 30 | - name: KUBERNETES_CLUSTER_DOMAIN 31 | value: {{ quote .Values.kubernetesClusterDomain }} 32 | image: {{ .Values.controllerManager.manager.image.repository }}:{{ .Values.controllerManager.manager.image.tag 33 | | default .Chart.AppVersion }} 34 | livenessProbe: 35 | httpGet: 36 | path: /healthz 37 | port: 8081 38 | initialDelaySeconds: 15 39 | periodSeconds: 20 40 | name: manager 41 | readinessProbe: 42 | httpGet: 43 | path: /readyz 44 | port: 8081 45 | initialDelaySeconds: 5 46 | periodSeconds: 10 47 | resources: {{- toYaml .Values.controllerManager.manager.resources | nindent 10 48 | }} 49 | securityContext: {{- toYaml .Values.controllerManager.manager.containerSecurityContext 50 | | nindent 10 }} 51 | securityContext: 52 | runAsNonRoot: true 53 | serviceAccountName: {{ include "kom.fullname" . }}-controller-manager 54 | terminationGracePeriodSeconds: 10 -------------------------------------------------------------------------------- /internal/tool/argo/helm.go: -------------------------------------------------------------------------------- 1 | package argo 2 | 3 | import ( 4 | argov1alpha1 "github.com/kkb0318/argo-cd-api/api/v1alpha1" 5 | komv1alpha1 "github.com/kkb0318/kom/api/v1alpha1" 6 | komtool "github.com/kkb0318/kom/internal/tool" 7 | "github.com/kkb0318/kom/internal/tool/argo/manifests" 8 | corev1 "k8s.io/api/core/v1" 9 | "sigs.k8s.io/controller-runtime/pkg/client" 10 | ) 11 | 12 | type ArgoHelm struct { 13 | source []*corev1.Secret 14 | helm []*argov1alpha1.Application 15 | } 16 | 17 | func (f *ArgoHelm) Repositories() []client.Object { 18 | objs := make([]client.Object, len(f.source)) 19 | for i, source := range f.source { 20 | objs[i] = source 21 | } 22 | return objs 23 | } 24 | 25 | func (f *ArgoHelm) Charts() []client.Object { 26 | objs := make([]client.Object, len(f.helm)) 27 | for i, helm := range f.helm { 28 | objs[i] = helm 29 | } 30 | return objs 31 | } 32 | 33 | func NewArgoHelmList(objs []komv1alpha1.Helm) ([]komtool.Resource, error) { 34 | helmList := make([]komtool.Resource, len(objs)) 35 | for i, obj := range objs { 36 | helm, err := NewArgoHelm(obj) 37 | if err != nil { 38 | return nil, err 39 | } 40 | helmList[i] = helm 41 | } 42 | return helmList, nil 43 | } 44 | 45 | func NewArgoHelm(obj komv1alpha1.Helm) (*ArgoHelm, error) { 46 | var namespace string 47 | if obj.Namespace == "" { 48 | namespace = komv1alpha1.ArgoCDDefaultNamespace 49 | } else { 50 | namespace = obj.Namespace 51 | } 52 | charts := obj.Charts 53 | secrets := make([]*corev1.Secret, len(charts)) 54 | apps := make([]*argov1alpha1.Application, len(charts)) 55 | for i, chart := range charts { 56 | secret, err := manifests.NewSecretBuilder(). 57 | WithHelm(chart.Name, obj.Url). 58 | Build(obj.Name, namespace) 59 | if err != nil { 60 | return nil, err 61 | } 62 | app, err := manifests.NewApplicationBuilder(). 63 | WithHelm(chart.Name, chart.Version, obj.Url). 64 | WithHelmValues(chart.Values). 65 | Build(chart.Name, namespace) 66 | if err != nil { 67 | return nil, err 68 | } 69 | apps[i] = app 70 | secrets[i] = secret 71 | } 72 | f := &ArgoHelm{ 73 | source: secrets, 74 | helm: apps, 75 | } 76 | return f, nil 77 | } 78 | -------------------------------------------------------------------------------- /internal/kubernetes/delete.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | 9 | "k8s.io/apimachinery/pkg/api/errors" 10 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 11 | "k8s.io/apimachinery/pkg/labels" 12 | 13 | "sigs.k8s.io/controller-runtime/pkg/client" 14 | ) 15 | 16 | // DeleteOptions contains options for delete requests. 17 | type DeleteOptions struct { 18 | // DeletionPropagation decides how the garbage collector will handle the propagation. 19 | DeletionPropagation metav1.DeletionPropagation 20 | 21 | // Inclusions determines which in-cluster objects are subject to deletion 22 | // based on the labels. 23 | // A nil Inclusions map means all objects are subject to deletion 24 | Inclusions map[string]string 25 | } 26 | 27 | func (h *Handler) DeleteAll(ctx context.Context, resources []*unstructured.Unstructured, opts DeleteOptions) error { 28 | if !h.cleanup { 29 | return nil 30 | } 31 | for _, r := range resources { 32 | err := h.Delete(ctx, r, opts) 33 | if err != nil { 34 | return err 35 | } 36 | } 37 | return nil 38 | } 39 | 40 | // Delete deletes the given object (not found errors are ignored). 41 | func (h *Handler) Delete(ctx context.Context, object *unstructured.Unstructured, opts DeleteOptions) error { 42 | if !h.cleanup { 43 | return nil 44 | } 45 | existingObject := &unstructured.Unstructured{} 46 | existingObject.SetGroupVersionKind(object.GroupVersionKind()) 47 | err := h.client.Get(ctx, client.ObjectKeyFromObject(object), existingObject) 48 | if err != nil { 49 | if !errors.IsNotFound(err) { 50 | return fmt.Errorf("failed to delete: %w", err) 51 | } 52 | return nil // already deleted 53 | } 54 | 55 | sel, err := metav1.LabelSelectorAsSelector(&metav1.LabelSelector{MatchLabels: opts.Inclusions}) 56 | if err != nil { 57 | return fmt.Errorf("label selector failed: %w", err) 58 | } 59 | 60 | if !sel.Matches(labels.Set(existingObject.GetLabels())) { 61 | return nil 62 | } 63 | 64 | if err := h.client.Delete(ctx, existingObject, client.PropagationPolicy(opts.DeletionPropagation)); err != nil { 65 | return fmt.Errorf("delete failed: %w", err) 66 | } 67 | 68 | return nil 69 | } 70 | -------------------------------------------------------------------------------- /internal/tool/flux/manifests/helm_release.go: -------------------------------------------------------------------------------- 1 | package manifests 2 | 3 | import ( 4 | "errors" 5 | 6 | helmv2 "github.com/fluxcd/helm-controller/api/v2" 7 | sourcev1 "github.com/fluxcd/source-controller/api/v1" 8 | apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 9 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | ) 11 | 12 | type HelmReleaseBuilder struct { 13 | ref *helmv2.CrossNamespaceObjectReference 14 | chart string 15 | version string 16 | values *apiextensionsv1.JSON 17 | } 18 | 19 | func NewHelmReleaseBuilder() *HelmReleaseBuilder { 20 | return &HelmReleaseBuilder{ 21 | version: "x", 22 | } 23 | } 24 | 25 | func (b *HelmReleaseBuilder) WithChart(value string) *HelmReleaseBuilder { 26 | b.chart = value 27 | return b 28 | } 29 | 30 | func (b *HelmReleaseBuilder) WithVersion(value string) *HelmReleaseBuilder { 31 | b.version = value 32 | return b 33 | } 34 | 35 | func (b *HelmReleaseBuilder) WithReference(name, namespace string) *HelmReleaseBuilder { 36 | ref := &helmv2.CrossNamespaceObjectReference{ 37 | Kind: sourcev1.HelmRepositoryKind, 38 | Name: name, 39 | Namespace: namespace, 40 | } 41 | b.ref = ref 42 | return b 43 | } 44 | 45 | func (b *HelmReleaseBuilder) WithValues(values *apiextensionsv1.JSON) *HelmReleaseBuilder { 46 | b.values = values 47 | return b 48 | } 49 | 50 | func (b *HelmReleaseBuilder) Build(name, ns string) (*helmv2.HelmRelease, error) { 51 | if b.ref == nil { 52 | return nil, errors.New("the 'ref' field is nil and must be provided with a valid reference") 53 | } 54 | if b.chart == "" { 55 | return nil, errors.New("the 'chart' field is empty. Please specify a valid URL") 56 | } 57 | helmrelease := &helmv2.HelmRelease{ 58 | ObjectMeta: v1.ObjectMeta{ 59 | Name: name, 60 | Namespace: ns, 61 | }, 62 | TypeMeta: v1.TypeMeta{ 63 | APIVersion: helmv2.GroupVersion.String(), 64 | Kind: helmv2.HelmReleaseKind, 65 | }, 66 | Spec: helmv2.HelmReleaseSpec{ 67 | Values: b.values, 68 | Chart: &helmv2.HelmChartTemplate{ 69 | Spec: helmv2.HelmChartTemplateSpec{ 70 | Chart: b.chart, 71 | Version: b.version, 72 | SourceRef: *b.ref, 73 | }, 74 | }, 75 | }, 76 | } 77 | return helmrelease, nil 78 | } 79 | -------------------------------------------------------------------------------- /internal/tool/flux/helm.go: -------------------------------------------------------------------------------- 1 | package flux 2 | 3 | import ( 4 | "strings" 5 | 6 | helmv2 "github.com/fluxcd/helm-controller/api/v2" 7 | sourcev1 "github.com/fluxcd/source-controller/api/v1" 8 | komv1alpha1 "github.com/kkb0318/kom/api/v1alpha1" 9 | komtool "github.com/kkb0318/kom/internal/tool" 10 | "github.com/kkb0318/kom/internal/tool/flux/manifests" 11 | "sigs.k8s.io/controller-runtime/pkg/client" 12 | ) 13 | 14 | type FluxHelm struct { 15 | source *sourcev1.HelmRepository 16 | helm []*helmv2.HelmRelease 17 | } 18 | 19 | type HelmValues struct { 20 | FullnameOverride string 21 | } 22 | 23 | func (f *FluxHelm) Repositories() []client.Object { 24 | return []client.Object{f.source} 25 | } 26 | 27 | func (f *FluxHelm) Charts() []client.Object { 28 | objs := make([]client.Object, len(f.helm)) 29 | for i, helm := range f.helm { 30 | objs[i] = helm 31 | } 32 | return objs 33 | } 34 | 35 | func NewFluxHelmList(objs []komv1alpha1.Helm) ([]komtool.Resource, error) { 36 | helmList := make([]komtool.Resource, len(objs)) 37 | for i, obj := range objs { 38 | helm, err := NewFluxHelm(obj) 39 | if err != nil { 40 | return nil, err 41 | } 42 | helmList[i] = helm 43 | } 44 | return helmList, nil 45 | } 46 | 47 | func RepositoryType(url string) string { 48 | if strings.HasPrefix(url, "oci:") { 49 | return "oci" 50 | } 51 | return "default" 52 | } 53 | 54 | func NewFluxHelm(obj komv1alpha1.Helm) (*FluxHelm, error) { 55 | repoName := obj.Name 56 | var namespace string 57 | if obj.Namespace == "" { 58 | namespace = komv1alpha1.DefaultNamespace 59 | } else { 60 | namespace = obj.Namespace 61 | } 62 | repoUrl := obj.Url 63 | charts := obj.Charts 64 | helmrepo, err := manifests.NewHelmRepositoryBuilder(). 65 | WithUrl(repoUrl). 66 | Build(repoName, namespace) 67 | if err != nil { 68 | return nil, err 69 | } 70 | hrs := make([]*helmv2.HelmRelease, len(charts)) 71 | for i, chart := range charts { 72 | hr, err := manifests.NewHelmReleaseBuilder(). 73 | WithReference(repoName, namespace). 74 | WithChart(chart.Name). 75 | WithVersion(chart.Version). 76 | WithValues(chart.Values). 77 | Build(chart.Name, namespace) 78 | if err != nil { 79 | return nil, err 80 | } 81 | hrs[i] = hr 82 | } 83 | f := &FluxHelm{ 84 | source: helmrepo, 85 | helm: hrs, 86 | } 87 | return f, nil 88 | } 89 | -------------------------------------------------------------------------------- /internal/tool/argo/manifests/application.go: -------------------------------------------------------------------------------- 1 | package manifests 2 | 3 | import ( 4 | "fmt" 5 | 6 | argoapi "github.com/kkb0318/argo-cd-api/api" 7 | argov1alpha1 "github.com/kkb0318/argo-cd-api/api/v1alpha1" 8 | apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 9 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "k8s.io/apimachinery/pkg/runtime" 11 | ) 12 | 13 | type ApplicationBuilder struct { 14 | source *argov1alpha1.ApplicationSource 15 | } 16 | 17 | func NewApplicationBuilder() *ApplicationBuilder { 18 | return &ApplicationBuilder{} 19 | } 20 | 21 | func (b *ApplicationBuilder) WithHelm(name, version, url string) *ApplicationBuilder { 22 | source := &argov1alpha1.ApplicationSource{ 23 | Chart: name, 24 | TargetRevision: version, 25 | RepoURL: url, 26 | } 27 | b.source = source 28 | return b 29 | } 30 | 31 | func (b *ApplicationBuilder) WithHelmValues(values *apiextensionsv1.JSON) *ApplicationBuilder { 32 | if values == nil { 33 | return b 34 | } 35 | if b.source.Helm == nil { 36 | b.source.Helm = &argov1alpha1.ApplicationSourceHelm{} 37 | } 38 | b.source.Helm.ValuesObject = &runtime.RawExtension{ 39 | Raw: values.Raw, 40 | } 41 | return b 42 | } 43 | 44 | func (b *ApplicationBuilder) WithGit(path, version, url string) *ApplicationBuilder { 45 | source := &argov1alpha1.ApplicationSource{ 46 | Path: path, 47 | TargetRevision: version, 48 | RepoURL: url, 49 | } 50 | b.source = source 51 | return b 52 | } 53 | 54 | func (b *ApplicationBuilder) Build(name, ns string) (*argov1alpha1.Application, error) { 55 | if b.source == nil { 56 | return nil, fmt.Errorf("argocd ApplicationSource is empty") 57 | } 58 | app := &argov1alpha1.Application{ 59 | ObjectMeta: v1.ObjectMeta{ 60 | Name: name, 61 | Namespace: ns, 62 | }, 63 | TypeMeta: v1.TypeMeta{ 64 | APIVersion: argov1alpha1.SchemeGroupVersion.String(), 65 | Kind: argoapi.ApplicationKind, 66 | }, 67 | Spec: argov1alpha1.ApplicationSpec{ 68 | Source: b.source, 69 | Destination: argov1alpha1.ApplicationDestination{ 70 | Namespace: ns, 71 | Server: "https://kubernetes.default.svc", 72 | }, 73 | Project: "default", 74 | SyncPolicy: &argov1alpha1.SyncPolicy{ 75 | Automated: &argov1alpha1.SyncPolicyAutomated{ 76 | Prune: true, 77 | SelfHeal: true, 78 | }, 79 | }, 80 | }, 81 | } 82 | return app, nil 83 | } 84 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: build-docker-image 2 | 3 | on: 4 | push: 5 | tags: 6 | - "[0-2].[0-9]+.[0-9]+*" 7 | workflow_dispatch: 8 | 9 | env: 10 | REGISTRY: ghcr.io 11 | IMAGE_NAME: ${{ github.repository }} 12 | 13 | jobs: 14 | build-and-push-image: 15 | runs-on: ubuntu-latest 16 | permissions: 17 | contents: read 18 | packages: write 19 | 20 | steps: 21 | - name: Checkout repository 22 | uses: actions/checkout@v4 23 | 24 | # For multi platform build. See https://docs.docker.com/build/ci/github-actions/multi-platform/ 25 | - name: Set up QEMU 26 | uses: docker/setup-qemu-action@v3 27 | - name: Set up Docker Buildx 28 | uses: docker/setup-buildx-action@v3 29 | 30 | - name: Extract metadata (tags, labels) for Docker 31 | id: meta 32 | uses: docker/metadata-action@v5 33 | with: 34 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 35 | tags: | 36 | type=semver,pattern={{version}} 37 | - name: Login to Container Registry 38 | uses: docker/login-action@v3 39 | with: 40 | registry: ${{ env.REGISTRY }} 41 | username: ${{ github.actor }} 42 | password: ${{ secrets.PAT }} 43 | 44 | - name: Build and push Docker image 45 | uses: docker/build-push-action@v5 46 | with: 47 | context: . 48 | push: true 49 | tags: ${{ steps.meta.outputs.tags }} 50 | labels: ${{ steps.meta.outputs.labels }} 51 | platforms: linux/amd64,linux/arm64 52 | chart-release: 53 | runs-on: ubuntu-latest 54 | needs: build-and-push-image 55 | steps: 56 | - name: Checkout 57 | uses: actions/checkout@v4 58 | with: 59 | fetch-depth: 0 60 | - name: Configure Git 61 | run: | 62 | git config user.name "$GITHUB_ACTOR" 63 | git config user.email "$GITHUB_ACTOR@users.noreply.github.com" 64 | - name: Install Helm 65 | uses: azure/setup-helm@v3 66 | - name: sed 67 | run: | 68 | sed -i "s/CHART_VERSION/${{ github.ref_name }}/" charts/kom/Chart.yaml 69 | sed -i "s/APP_VERSION/${{ github.ref_name }}/" charts/kom/Chart.yaml 70 | sed -i "s/APP_VERSION/${{ github.ref_name }}/" charts/kom/values.yaml 71 | - name: Run chart-releaser 72 | uses: helm/chart-releaser-action@v1.6.0 73 | with: 74 | config: cr.yaml 75 | env: 76 | CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 77 | -------------------------------------------------------------------------------- /charts/kom/templates/manager-rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: {{ include "kom.fullname" . }}-manager-role 5 | labels: 6 | {{- include "kom.labels" . | nindent 4 }} 7 | rules: 8 | - apiGroups: 9 | - "" 10 | resources: 11 | - secrets 12 | verbs: 13 | - create 14 | - delete 15 | - get 16 | - list 17 | - patch 18 | - update 19 | - watch 20 | - apiGroups: 21 | - argoproj.io 22 | resources: 23 | - applications 24 | - appprojects 25 | verbs: 26 | - '*' 27 | - apiGroups: 28 | - helm.toolkit.fluxcd.io 29 | resources: 30 | - helmreleases 31 | verbs: 32 | - create 33 | - delete 34 | - get 35 | - list 36 | - patch 37 | - update 38 | - watch 39 | - apiGroups: 40 | - kom.kkb0318.github.io 41 | resources: 42 | - operatormanagers 43 | verbs: 44 | - create 45 | - delete 46 | - get 47 | - list 48 | - patch 49 | - update 50 | - watch 51 | - apiGroups: 52 | - kom.kkb0318.github.io 53 | resources: 54 | - operatormanagers/finalizers 55 | verbs: 56 | - create 57 | - delete 58 | - get 59 | - patch 60 | - update 61 | - apiGroups: 62 | - kom.kkb0318.github.io 63 | resources: 64 | - operatormanagers/status 65 | verbs: 66 | - get 67 | - patch 68 | - update 69 | - apiGroups: 70 | - kustomize.toolkit.fluxcd.io 71 | resources: 72 | - kustomizations 73 | verbs: 74 | - create 75 | - delete 76 | - get 77 | - list 78 | - patch 79 | - update 80 | - watch 81 | - apiGroups: 82 | - source.toolkit.fluxcd.io 83 | resources: 84 | - gitrepositories 85 | verbs: 86 | - create 87 | - delete 88 | - get 89 | - list 90 | - patch 91 | - update 92 | - watch 93 | - apiGroups: 94 | - source.toolkit.fluxcd.io 95 | resources: 96 | - helmrepositories 97 | verbs: 98 | - create 99 | - delete 100 | - get 101 | - list 102 | - patch 103 | - update 104 | - watch 105 | --- 106 | apiVersion: rbac.authorization.k8s.io/v1 107 | kind: ClusterRoleBinding 108 | metadata: 109 | name: {{ include "kom.fullname" . }}-manager-rolebinding 110 | labels: 111 | app.kubernetes.io/component: rbac 112 | app.kubernetes.io/created-by: kom 113 | app.kubernetes.io/part-of: kom 114 | {{- include "kom.labels" . | nindent 4 }} 115 | roleRef: 116 | apiGroup: rbac.authorization.k8s.io 117 | kind: ClusterRole 118 | name: '{{ include "kom.fullname" . }}-manager-role' 119 | subjects: 120 | - kind: ServiceAccount 121 | name: '{{ include "kom.fullname" . }}-controller-manager' 122 | namespace: '{{ .Release.Namespace }}' -------------------------------------------------------------------------------- /internal/status/resources.go: -------------------------------------------------------------------------------- 1 | package status 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | komv1alpha1 "github.com/kkb0318/kom/api/v1alpha1" 8 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 9 | "k8s.io/apimachinery/pkg/runtime/schema" 10 | ) 11 | 12 | // Diff returns the unstructed objects that do not exist in the b resources (a-b) 13 | func Diff(a, b komv1alpha1.AppliedResourceList) ([]*unstructured.Unstructured, error) { 14 | return ToListUnstructured(a.Diff(b)) 15 | } 16 | 17 | func ToListUnstructured(resources komv1alpha1.AppliedResourceList) ([]*unstructured.Unstructured, error) { 18 | objects := make([]*unstructured.Unstructured, 0) 19 | for _, r := range resources { 20 | u, err := ToUnstructured(r) 21 | if err != nil { 22 | return nil, err 23 | } 24 | objects = append(objects, u) 25 | 26 | } 27 | return objects, nil 28 | } 29 | 30 | // ToUnstructured converts an AppliedResource into an Unstructured object. 31 | // It returns an error if the conversion fails or if the Unstructured object cannot be created. 32 | func ToUnstructured(a komv1alpha1.AppliedResource) (*unstructured.Unstructured, error) { 33 | gvk := schema.FromAPIVersionAndKind(a.APIVersion, a.Kind) 34 | // Verify if the GroupVersionKind (GVK) is properly parsed 35 | if gvk.Group == "" && gvk.Version == "" { 36 | return nil, fmt.Errorf("failed to parse GroupVersionKind from APIVersion and Kind: %v", gvk) 37 | } 38 | if gvk.Kind == "" { 39 | return nil, fmt.Errorf("failed to parse GroupVersionKind from APIVersion and Kind: %v", gvk) 40 | } 41 | // Ensure the resource name is not empty 42 | if a.Name == "" { 43 | return nil, fmt.Errorf("resource name is required but was not provided") 44 | } 45 | // Ensure the namespace is provided for namespaced resources 46 | if a.Namespace == "" { 47 | return nil, fmt.Errorf("namespace is required for namespaced resources but was not provided") 48 | } 49 | // Create and populate the Unstructured object 50 | u := &unstructured.Unstructured{} 51 | u.SetGroupVersionKind(schema.GroupVersionKind{ 52 | Group: gvk.Group, 53 | Kind: gvk.Kind, 54 | Version: gvk.Version, 55 | }) 56 | u.SetName(a.Name) 57 | u.SetNamespace(a.Namespace) 58 | return u, nil 59 | } 60 | 61 | func ToAppliedResource(u unstructured.Unstructured) (*komv1alpha1.AppliedResource, error) { 62 | name := u.GetName() 63 | namespace := u.GetNamespace() 64 | kind := u.GetObjectKind().GroupVersionKind().Kind 65 | apiVersion := u.GetAPIVersion() 66 | 67 | if name == "" { 68 | return nil, errors.New("missing required field: name") 69 | } 70 | if namespace == "" { 71 | return nil, errors.New("missing required field: namespace") 72 | } 73 | if kind == "" { 74 | return nil, errors.New("missing required field: kind") 75 | } 76 | if apiVersion == "" { 77 | return nil, errors.New("missing required field: apiVersion") 78 | } 79 | 80 | a := &komv1alpha1.AppliedResource{ 81 | Name: name, 82 | Namespace: namespace, 83 | Kind: kind, 84 | APIVersion: apiVersion, 85 | } 86 | return a, nil 87 | } 88 | -------------------------------------------------------------------------------- /internal/tool/argo/helm_test.go: -------------------------------------------------------------------------------- 1 | package argo 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | argov1alpha1 "github.com/kkb0318/argo-cd-api/api/v1alpha1" 8 | komv1alpha1 "github.com/kkb0318/kom/api/v1alpha1" 9 | komtool "github.com/kkb0318/kom/internal/tool" 10 | "github.com/kkb0318/kom/internal/tool/argo/testdata" 11 | "github.com/stretchr/testify/assert" 12 | corev1 "k8s.io/api/core/v1" 13 | apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 14 | "sigs.k8s.io/yaml" 15 | ) 16 | 17 | func TestArgoHelm_New(t *testing.T) { 18 | tests := []struct { 19 | name string 20 | inputs []komv1alpha1.Helm 21 | expected []komtool.Resource 22 | expectedErr bool 23 | }{ 24 | { 25 | name: "create manifest for installing helm with argo", 26 | inputs: []komv1alpha1.Helm{ 27 | { 28 | Name: "repo1", 29 | Namespace: "repo-ns1", 30 | Url: "https://example.com", 31 | Charts: []komv1alpha1.Chart{ 32 | { 33 | Name: "chart1", 34 | Version: "1.0.0", 35 | Values: values(` 36 | key1: val1 37 | `), 38 | }, 39 | }, 40 | }, 41 | { 42 | Name: "repo2", 43 | Namespace: "repo-ns2", 44 | Url: "https://example.com", 45 | Charts: []komv1alpha1.Chart{ 46 | { 47 | Name: "chart2", 48 | Version: "x.x.x", 49 | }, 50 | }, 51 | }, 52 | }, 53 | expected: []komtool.Resource{ 54 | &ArgoHelm{ 55 | source: []*corev1.Secret{ 56 | testdata.NewMockSecretBuilder().Build(t, "helm_secret.yaml"), 57 | }, 58 | helm: []*argov1alpha1.Application{ 59 | testdata.NewMockApplicationBuilder(). 60 | WithValues(values(` 61 | key1: val1 62 | `), 63 | ). 64 | Build(t, "helm_application.yaml"), 65 | }, 66 | }, 67 | &ArgoHelm{ 68 | source: []*corev1.Secret{ 69 | testdata.NewMockSecretBuilder(). 70 | WithName("repo2"). 71 | WithNamespace("repo-ns2"). 72 | WithChartName("chart2"). 73 | Build(t, "helm_secret.yaml"), 74 | }, 75 | helm: []*argov1alpha1.Application{ 76 | testdata.NewMockApplicationBuilder(). 77 | WithName("chart2"). 78 | WithNamespace("repo-ns2"). 79 | WithDestNamespace("repo-ns2"). 80 | WithChartName("chart2"). 81 | WithVersion("x.x.x"). 82 | Build(t, "helm_application.yaml"), 83 | }, 84 | }, 85 | }, 86 | }, 87 | } 88 | for _, tt := range tests { 89 | t.Run(tt.name, func(t *testing.T) { 90 | actual, err := NewArgoHelmList(tt.inputs) 91 | if tt.expectedErr { 92 | assert.Error(t, err, "") 93 | } else { 94 | assert.NoError(t, err) 95 | assert.Equal(t, tt.expected, actual) 96 | } 97 | }) 98 | } 99 | } 100 | 101 | func values(hrValues string) *apiextensionsv1.JSON { 102 | v, err := yaml.YAMLToJSON([]byte(hrValues)) 103 | if err != nil { 104 | fmt.Println(err) 105 | } 106 | return &apiextensionsv1.JSON{Raw: v} 107 | } 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KOM (Kubernetes Operator Manager) 2 | 3 | [![GitHub release](https://img.shields.io/github/release/kkb0318/kom.svg?maxAge=60)](https://github.com/kkb0318/kom/releases) 4 | [![CI](https://github.com/kkb0318/kom/actions/workflows/ci.yaml/badge.svg)](https://github.com/kkb0318/kom/actions/workflows/ci.yaml) 5 | [![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/kom)](https://artifacthub.io/packages/search?repo=kom) 6 | 7 | ## Overview 8 | 9 | KOM, which stands for Kubernetes Operator Manager, is an open-source software tool designed to streamline the management of Kubernetes operators. It acts as an operator itself, facilitating the deployment, management, and removal of Kubernetes operators with minimal hassle. 10 | 11 | ## Features 12 | 13 | - **Git Repository Support**: Enables integration with Git repositories to manage operator configurations and their versions effectively. 14 | - **Chart Release Mechanism**: Incorporates a system for deploying Helm charts, allowing for easy installation and management of Kubernetes applications. 15 | 16 | ## Getting Started 17 | 18 | ### Prerequisites 19 | 20 | - Kubernetes cluster with Flux 2.x or Argo CD installed: KOM requires a Kubernetes cluster that is already equipped with either Flux version 2.x or Argo CD for GitOps-based management. 21 | 22 | ### Installation 23 | 24 | To install KOM on your Kubernetes cluster, follow these steps: 25 | 26 | 1. Add the KOM Helm repository: 27 | 28 | ```sh 29 | helm repo add kkb0318 https://kkb0318.github.io/kom 30 | helm repo update 31 | ``` 32 | 33 | 2. Install KOM using Helm: 34 | 35 | ```sh 36 | helm install kom kkb0318/kom -n kom-system --create-namespace 37 | ``` 38 | 39 | This command deploys KOM on the Kubernetes cluster in the default configuration. For more advanced configurations, refer to the Configuration section. 40 | 41 | ## Deploying with KOM 42 | 43 | After installing KOM, you can deploy the operator using `OperatorManager` manifest. 44 | 45 | ```yaml 46 | apiVersion: kom.kkb0318.github.io/v1alpha1 47 | kind: OperatorManager 48 | metadata: 49 | name: kom 50 | namespace: kom-system 51 | spec: 52 | tool: flux 53 | cleanup: true 54 | resource: 55 | helm: 56 | - name: jetstack 57 | url: https://charts.jetstack.io 58 | charts: 59 | - name: cert-manager 60 | version: v1.14.4 61 | values: 62 | installCRDs: true 63 | prometheus: 64 | enabled: false 65 | - name: repo1 66 | url: https://helm.github.io/examples 67 | charts: 68 | - name: hello-world 69 | version: x.x.x 70 | ``` 71 | 72 | You can find more details about the example manifests in the `examples/` directory. 73 | 74 | ## API Reference 75 | 76 | You can find the reference in the [Reference](./docs/api.md) file. 77 | 78 | ## Future Plans 79 | 80 | - **Access to Private Repositories**: We are planning to enhance KOM's capabilities by enabling it to access and manage operators from private Git repositories. 81 | 82 | ## License 83 | 84 | This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details. 85 | -------------------------------------------------------------------------------- /internal/kubernetes/apply.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import ( 4 | "context" 5 | 6 | komv1alpha1 "github.com/kkb0318/kom/api/v1alpha1" 7 | komstatus "github.com/kkb0318/kom/internal/status" 8 | komtool "github.com/kkb0318/kom/internal/tool" 9 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 10 | "k8s.io/apimachinery/pkg/runtime" 11 | 12 | "sigs.k8s.io/controller-runtime/pkg/client" 13 | "sigs.k8s.io/controller-runtime/pkg/client/apiutil" 14 | ) 15 | 16 | type Handler struct { 17 | cleanup bool 18 | client client.Client 19 | owner Owner 20 | } 21 | 22 | // NewHelper returns an initialized Helper. 23 | func NewHandler(c client.Client, owner Owner, cleanup bool) (*Handler, error) { 24 | return &Handler{ 25 | cleanup: cleanup, 26 | client: c, 27 | owner: owner, 28 | }, nil 29 | } 30 | 31 | func (h Handler) ApplyAll(ctx context.Context, r komtool.ResourceManager) ([]komv1alpha1.AppliedResource, error) { 32 | var appliedResources []komv1alpha1.AppliedResource 33 | resources := make([]komtool.Resource, 0) 34 | helm, err := r.Helm() 35 | if err != nil { 36 | return nil, err 37 | } 38 | git, err := r.Git() 39 | if err != nil { 40 | return nil, err 41 | } 42 | resources = append(resources, helm...) 43 | resources = append(resources, git...) 44 | for _, resource := range resources { 45 | repos := resource.Repositories() 46 | for _, repo := range repos { 47 | applied, err := h.Apply(ctx, repo) 48 | if err != nil { 49 | return nil, err 50 | } 51 | appliedResources = append(appliedResources, *applied) 52 | } 53 | charts := resource.Charts() 54 | for _, chart := range charts { 55 | applied, err := h.Apply(ctx, chart) 56 | if err != nil { 57 | return nil, err 58 | } 59 | appliedResources = append(appliedResources, *applied) 60 | } 61 | } 62 | return appliedResources, nil 63 | } 64 | 65 | func (h Handler) Apply(ctx context.Context, obj client.Object) (*komv1alpha1.AppliedResource, error) { 66 | opts := []client.PatchOption{ 67 | client.ForceOwnership, 68 | client.FieldOwner(h.owner.Field), 69 | } 70 | gvk, err := apiutil.GVKForObject(obj, h.client.Scheme()) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | u := &unstructured.Unstructured{} 76 | unstructured, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) 77 | if err != nil { 78 | return nil, err 79 | } 80 | u.Object = unstructured 81 | u.SetGroupVersionKind(gvk) 82 | u.SetManagedFields(nil) 83 | err = h.client.Patch(ctx, u, client.Apply, opts...) 84 | if err != nil { 85 | return nil, err 86 | } 87 | return komstatus.ToAppliedResource(*u) 88 | } 89 | 90 | func (h Handler) PatchStatus(ctx context.Context, obj client.Object) error { 91 | opts := &client.SubResourcePatchOptions{ 92 | PatchOptions: client.PatchOptions{ 93 | FieldManager: h.owner.Field, 94 | }, 95 | } 96 | gvk, err := apiutil.GVKForObject(obj, h.client.Scheme()) 97 | if err != nil { 98 | return err 99 | } 100 | 101 | u := &unstructured.Unstructured{} 102 | unstructured, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) 103 | if err != nil { 104 | return err 105 | } 106 | u.Object = unstructured 107 | u.SetGroupVersionKind(gvk) 108 | u.SetManagedFields(nil) 109 | return h.client.Status().Patch(ctx, u, client.Apply, opts) 110 | } 111 | -------------------------------------------------------------------------------- /internal/tool/flux/git_test.go: -------------------------------------------------------------------------------- 1 | package flux 2 | 3 | import ( 4 | "testing" 5 | 6 | kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" 7 | sourcev1 "github.com/fluxcd/source-controller/api/v1" 8 | komv1alpha1 "github.com/kkb0318/kom/api/v1alpha1" 9 | komtool "github.com/kkb0318/kom/internal/tool" 10 | "github.com/kkb0318/kom/internal/tool/flux/testdata" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestFluxGit_New(t *testing.T) { 15 | tests := []struct { 16 | name string 17 | inputs []komv1alpha1.Git 18 | expected []komtool.Resource 19 | expectedErr bool 20 | }{ 21 | { 22 | name: "continue if not in previous", 23 | inputs: []komv1alpha1.Git{ 24 | { 25 | Name: "repo1", 26 | Namespace: "repo-ns1", 27 | Url: "https://example1.com", 28 | Path: "./path1", 29 | Reference: komv1alpha1.GitReference{ 30 | Type: komv1alpha1.GitTag, 31 | Value: "1.0.0", 32 | }, 33 | }, 34 | { 35 | Name: "repo2", 36 | Namespace: "repo-ns2", 37 | Url: "https://example2.com", 38 | Path: "./path2", 39 | Reference: komv1alpha1.GitReference{ 40 | Type: komv1alpha1.GitBranch, 41 | Value: "main", 42 | }, 43 | }, 44 | { 45 | Name: "repo3", 46 | Namespace: "repo-ns3", 47 | Url: "https://example3.com", 48 | Path: "./path3", 49 | Reference: komv1alpha1.GitReference{ 50 | Type: komv1alpha1.GitSemver, 51 | Value: "x.x.x", 52 | }, 53 | }, 54 | }, 55 | expected: []komtool.Resource{ 56 | &FluxGit{ 57 | source: testdata.NewMockGitRepositoryBuilder().Build(t, "git_repository.yaml"), 58 | ks: testdata.NewMockKustomizationBuilder().Build(t, "kustomization.yaml"), 59 | }, 60 | &FluxGit{ 61 | source: testdata.NewMockGitRepositoryBuilder(). 62 | WithName("repo2"). 63 | WithNamespace("repo-ns2"). 64 | WithUrl("https://example2.com"). 65 | WithRef(&sourcev1.GitRepositoryRef{Branch: "main"}). 66 | Build(t, "git_repository.yaml"), 67 | ks: testdata.NewMockKustomizationBuilder(). 68 | WithName("repo2"). 69 | WithNamespace("repo-ns2"). 70 | WithRef(&kustomizev1.CrossNamespaceSourceReference{Kind: "GitRepository", Name: "repo2", Namespace: "repo-ns2"}). 71 | WithPath("./path2"). 72 | Build(t, "kustomization.yaml"), 73 | }, 74 | &FluxGit{ 75 | source: testdata.NewMockGitRepositoryBuilder(). 76 | WithName("repo3"). 77 | WithNamespace("repo-ns3"). 78 | WithUrl("https://example3.com"). 79 | WithRef(&sourcev1.GitRepositoryRef{SemVer: "x.x.x"}). 80 | Build(t, "git_repository.yaml"), 81 | ks: testdata.NewMockKustomizationBuilder(). 82 | WithName("repo3"). 83 | WithNamespace("repo-ns3"). 84 | WithRef(&kustomizev1.CrossNamespaceSourceReference{Kind: "GitRepository", Name: "repo3", Namespace: "repo-ns3"}). 85 | WithPath("./path3"). 86 | Build(t, "kustomization.yaml"), 87 | }, 88 | }, 89 | }, 90 | } 91 | for _, tt := range tests { 92 | t.Run(tt.name, func(t *testing.T) { 93 | actual, err := NewFluxGitList(tt.inputs) 94 | if tt.expectedErr { 95 | assert.Error(t, err, "") 96 | } else { 97 | assert.NoError(t, err) 98 | assert.Equal(t, tt.expected, actual) 99 | } 100 | }) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kkb0318/kom 2 | 3 | go 1.24.0 4 | 5 | toolchain go1.24.4 6 | 7 | require ( 8 | github.com/fluxcd/helm-controller/api v1.3.0 9 | github.com/fluxcd/kustomize-controller/api v1.6.0 10 | github.com/fluxcd/source-controller/api v1.6.1 11 | github.com/goccy/go-yaml v1.18.0 12 | github.com/kkb0318/argo-cd-api v0.0.0-20240228234702-edc335e3db22 13 | github.com/onsi/ginkgo/v2 v2.23.4 14 | github.com/onsi/gomega v1.37.0 15 | github.com/stretchr/testify v1.10.0 16 | k8s.io/api v0.33.2 17 | k8s.io/apiextensions-apiserver v0.33.2 18 | k8s.io/apimachinery v0.33.2 19 | k8s.io/client-go v0.33.2 20 | sigs.k8s.io/controller-runtime v0.21.0 21 | sigs.k8s.io/yaml v1.4.0 22 | ) 23 | 24 | require ( 25 | github.com/beorn7/perks v1.0.1 // indirect 26 | github.com/blang/semver/v4 v4.0.0 // indirect 27 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 28 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 29 | github.com/emicklei/go-restful/v3 v3.12.2 // indirect 30 | github.com/evanphx/json-patch/v5 v5.9.11 // indirect 31 | github.com/fluxcd/pkg/apis/acl v0.7.0 // indirect 32 | github.com/fluxcd/pkg/apis/kustomize v1.10.0 // indirect 33 | github.com/fluxcd/pkg/apis/meta v1.13.0 // indirect 34 | github.com/fsnotify/fsnotify v1.9.0 // indirect 35 | github.com/fxamacker/cbor/v2 v2.8.0 // indirect 36 | github.com/go-logr/logr v1.4.3 // indirect 37 | github.com/go-logr/zapr v1.3.0 // indirect 38 | github.com/go-openapi/jsonpointer v0.21.1 // indirect 39 | github.com/go-openapi/jsonreference v0.21.0 // indirect 40 | github.com/go-openapi/swag v0.23.1 // indirect 41 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 42 | github.com/gogo/protobuf v1.3.2 // indirect 43 | github.com/google/btree v1.1.3 // indirect 44 | github.com/google/gnostic-models v0.6.9 // indirect 45 | github.com/google/go-cmp v0.7.0 // indirect 46 | github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a // indirect 47 | github.com/google/uuid v1.6.0 // indirect 48 | github.com/josharian/intern v1.0.0 // indirect 49 | github.com/json-iterator/go v1.1.12 // indirect 50 | github.com/mailru/easyjson v0.9.0 // indirect 51 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 52 | github.com/modern-go/reflect2 v1.0.2 // indirect 53 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 54 | github.com/pkg/errors v0.9.1 // indirect 55 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 56 | github.com/prometheus/client_golang v1.22.0 // indirect 57 | github.com/prometheus/client_model v0.6.2 // indirect 58 | github.com/prometheus/common v0.65.0 // indirect 59 | github.com/prometheus/procfs v0.16.1 // indirect 60 | github.com/spf13/pflag v1.0.6 // indirect 61 | github.com/x448/float16 v0.8.4 // indirect 62 | go.uber.org/automaxprocs v1.6.0 // indirect 63 | go.uber.org/multierr v1.11.0 // indirect 64 | go.uber.org/zap v1.27.0 // indirect 65 | golang.org/x/net v0.41.0 // indirect 66 | golang.org/x/oauth2 v0.30.0 // indirect 67 | golang.org/x/sync v0.15.0 // indirect 68 | golang.org/x/sys v0.33.0 // indirect 69 | golang.org/x/term v0.32.0 // indirect 70 | golang.org/x/text v0.26.0 // indirect 71 | golang.org/x/time v0.12.0 // indirect 72 | golang.org/x/tools v0.34.0 // indirect 73 | gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect 74 | google.golang.org/protobuf v1.36.6 // indirect 75 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 76 | gopkg.in/inf.v0 v0.9.1 // indirect 77 | gopkg.in/yaml.v3 v3.0.1 // indirect 78 | k8s.io/klog/v2 v2.130.1 // indirect 79 | k8s.io/kube-openapi v0.0.0-20250610211856-8b98d1ed966a // indirect 80 | k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect 81 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect 82 | sigs.k8s.io/randfill v1.0.0 // indirect 83 | sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect 84 | ) 85 | -------------------------------------------------------------------------------- /config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: namespace 7 | app.kubernetes.io/instance: system 8 | app.kubernetes.io/component: manager 9 | app.kubernetes.io/created-by: kom 10 | app.kubernetes.io/part-of: kom 11 | app.kubernetes.io/managed-by: kustomize 12 | name: system 13 | --- 14 | apiVersion: apps/v1 15 | kind: Deployment 16 | metadata: 17 | name: controller-manager 18 | namespace: system 19 | labels: 20 | control-plane: controller-manager 21 | app.kubernetes.io/name: deployment 22 | app.kubernetes.io/instance: controller-manager 23 | app.kubernetes.io/component: manager 24 | app.kubernetes.io/created-by: kom 25 | app.kubernetes.io/part-of: kom 26 | app.kubernetes.io/managed-by: kustomize 27 | spec: 28 | selector: 29 | matchLabels: 30 | control-plane: controller-manager 31 | replicas: 1 32 | template: 33 | metadata: 34 | annotations: 35 | kubectl.kubernetes.io/default-container: manager 36 | labels: 37 | control-plane: controller-manager 38 | spec: 39 | # TODO(user): Uncomment the following code to configure the nodeAffinity expression 40 | # according to the platforms which are supported by your solution. 41 | # It is considered best practice to support multiple architectures. You can 42 | # build your manager image using the makefile target docker-buildx. 43 | # affinity: 44 | # nodeAffinity: 45 | # requiredDuringSchedulingIgnoredDuringExecution: 46 | # nodeSelectorTerms: 47 | # - matchExpressions: 48 | # - key: kubernetes.io/arch 49 | # operator: In 50 | # values: 51 | # - amd64 52 | # - arm64 53 | # - ppc64le 54 | # - s390x 55 | # - key: kubernetes.io/os 56 | # operator: In 57 | # values: 58 | # - linux 59 | securityContext: 60 | runAsNonRoot: true 61 | # TODO(user): For common cases that do not require escalating privileges 62 | # it is recommended to ensure that all your Pods/Containers are restrictive. 63 | # More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted 64 | # Please uncomment the following code if your project does NOT have to work on old Kubernetes 65 | # versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ). 66 | # seccompProfile: 67 | # type: RuntimeDefault 68 | containers: 69 | - command: 70 | - /manager 71 | args: 72 | - --leader-elect 73 | image: ghcr.io/kkb0318/kom:APP_VERSION 74 | name: manager 75 | securityContext: 76 | allowPrivilegeEscalation: false 77 | capabilities: 78 | drop: 79 | - "ALL" 80 | livenessProbe: 81 | httpGet: 82 | path: /healthz 83 | port: 8081 84 | initialDelaySeconds: 15 85 | periodSeconds: 20 86 | readinessProbe: 87 | httpGet: 88 | path: /readyz 89 | port: 8081 90 | initialDelaySeconds: 5 91 | periodSeconds: 10 92 | # TODO(user): Configure the resources accordingly based on the project requirements. 93 | # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ 94 | resources: 95 | limits: 96 | cpu: 500m 97 | memory: 128Mi 98 | requests: 99 | cpu: 10m 100 | memory: 64Mi 101 | serviceAccountName: controller-manager 102 | terminationGracePeriodSeconds: 10 103 | -------------------------------------------------------------------------------- /internal/tool/argo/testdata/testdata.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | import ( 4 | "path/filepath" 5 | "runtime" 6 | "testing" 7 | 8 | argov1alpha1 "github.com/kkb0318/argo-cd-api/api/v1alpha1" 9 | apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 10 | "github.com/kkb0318/kom/internal/utils" 11 | corev1 "k8s.io/api/core/v1" 12 | k8sruntime "k8s.io/apimachinery/pkg/runtime" 13 | ) 14 | 15 | type mockSecretBuilder struct { 16 | name string 17 | namespace string 18 | stringData map[string]string 19 | } 20 | 21 | func NewMockSecretBuilder() *mockSecretBuilder { 22 | return &mockSecretBuilder{ 23 | stringData: map[string]string{}, 24 | } 25 | } 26 | 27 | func (f *mockSecretBuilder) WithName(val string) *mockSecretBuilder { 28 | f.name = val 29 | return f 30 | } 31 | 32 | func (f *mockSecretBuilder) WithNamespace(val string) *mockSecretBuilder { 33 | f.namespace = val 34 | return f 35 | } 36 | 37 | func (f *mockSecretBuilder) WithChartName(val string) *mockSecretBuilder { 38 | f.stringData["name"] = val 39 | return f 40 | } 41 | func (f *mockSecretBuilder) WithUrl(val string) *mockSecretBuilder { 42 | f.stringData["url"] = val 43 | return f 44 | } 45 | 46 | func (f *mockSecretBuilder) Build(t *testing.T, testdataFileName string) *corev1.Secret { 47 | baseFilePath := filepath.Join(currentDir(t), testdataFileName) 48 | Secret := &corev1.Secret{} 49 | utils.LoadYaml(Secret, baseFilePath) 50 | if f.name != "" { 51 | Secret.ObjectMeta.SetName(f.name) 52 | } 53 | if f.namespace != "" { 54 | Secret.ObjectMeta.SetNamespace(f.namespace) 55 | } 56 | for k, v := range f.stringData { 57 | Secret.StringData[k] = v 58 | } 59 | return Secret 60 | } 61 | 62 | type mockApplicationBuilder struct{ 63 | name string 64 | namespace string 65 | destNamespace string 66 | chartName string 67 | version string 68 | values *apiextensionsv1.JSON 69 | } 70 | 71 | func NewMockApplicationBuilder() *mockApplicationBuilder { 72 | return &mockApplicationBuilder{} 73 | } 74 | 75 | func (f *mockApplicationBuilder) WithName(val string) *mockApplicationBuilder { 76 | f.name = val 77 | return f 78 | } 79 | 80 | func (f *mockApplicationBuilder) WithNamespace(val string) *mockApplicationBuilder { 81 | f.namespace = val 82 | return f 83 | } 84 | 85 | func (f *mockApplicationBuilder) WithDestNamespace(val string) *mockApplicationBuilder { 86 | f.destNamespace = val 87 | return f 88 | } 89 | 90 | func (f *mockApplicationBuilder) WithChartName(val string) *mockApplicationBuilder { 91 | f.chartName = val 92 | return f 93 | } 94 | 95 | func (f *mockApplicationBuilder) WithVersion(val string) *mockApplicationBuilder { 96 | f.version = val 97 | return f 98 | } 99 | 100 | func (f *mockApplicationBuilder) WithValues(values *apiextensionsv1.JSON) *mockApplicationBuilder { 101 | f.values = values 102 | return f 103 | } 104 | 105 | func (f *mockApplicationBuilder) Build(t *testing.T, testdataFileName string) *argov1alpha1.Application { 106 | baseFilePath := filepath.Join(currentDir(t), testdataFileName) 107 | application := &argov1alpha1.Application{} 108 | utils.LoadYaml(application, baseFilePath) 109 | if f.name != "" { 110 | application.ObjectMeta.SetName(f.name) 111 | } 112 | if f.namespace != "" { 113 | application.ObjectMeta.SetNamespace(f.namespace) 114 | } 115 | if f.destNamespace != "" { 116 | application.Spec.Destination.Namespace = f.destNamespace 117 | } 118 | if f.chartName != "" { 119 | application.Spec.Source.Chart = f.chartName 120 | } 121 | if f.version != "" { 122 | application.Spec.Source.TargetRevision = f.version 123 | } 124 | if f.values != nil { 125 | if application.Spec.Source.Helm == nil { 126 | application.Spec.Source.Helm = &argov1alpha1.ApplicationSourceHelm{} 127 | } 128 | application.Spec.Source.Helm.ValuesObject = &k8sruntime.RawExtension{ 129 | Raw: f.values.Raw, 130 | } 131 | } 132 | return application 133 | } 134 | 135 | func currentDir(t *testing.T) string { 136 | t.Helper() 137 | _, currentFile, _, ok := runtime.Caller(0) 138 | if !ok { 139 | t.Fatal("runtime.Caller() failed to get current file path") 140 | } 141 | return filepath.Dir(currentFile) 142 | } 143 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "flag" 21 | "os" 22 | 23 | // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) 24 | // to ensure that exec-entrypoint and run can make use of them. 25 | _ "k8s.io/client-go/plugin/pkg/client/auth" 26 | 27 | "k8s.io/apimachinery/pkg/runtime" 28 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 29 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 30 | ctrl "sigs.k8s.io/controller-runtime" 31 | "sigs.k8s.io/controller-runtime/pkg/healthz" 32 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 33 | metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" 34 | "sigs.k8s.io/controller-runtime/pkg/webhook" 35 | 36 | helmv1 "github.com/fluxcd/helm-controller/api/v2beta2" 37 | kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" 38 | sourcev1 "github.com/fluxcd/source-controller/api/v1" 39 | sourcev1beta2 "github.com/fluxcd/source-controller/api/v1beta2" 40 | argov1alpha1 "github.com/kkb0318/argo-cd-api/api/v1alpha1" 41 | komv1alpha1 "github.com/kkb0318/kom/api/v1alpha1" 42 | "github.com/kkb0318/kom/internal/controller" 43 | //+kubebuilder:scaffold:imports 44 | ) 45 | 46 | var ( 47 | scheme = runtime.NewScheme() 48 | setupLog = ctrl.Log.WithName("setup") 49 | ) 50 | 51 | func init() { 52 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 53 | 54 | utilruntime.Must(komv1alpha1.AddToScheme(scheme)) 55 | utilruntime.Must(argov1alpha1.AddToScheme(scheme)) 56 | utilruntime.Must(sourcev1beta2.AddToScheme(scheme)) 57 | utilruntime.Must(helmv1.AddToScheme(scheme)) 58 | utilruntime.Must(sourcev1.AddToScheme(scheme)) 59 | utilruntime.Must(kustomizev1.AddToScheme(scheme)) 60 | //+kubebuilder:scaffold:scheme 61 | } 62 | 63 | func main() { 64 | var metricsAddr string 65 | var enableLeaderElection bool 66 | var probeAddr string 67 | flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") 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 | opts := zap.Options{ 73 | Development: true, 74 | } 75 | opts.BindFlags(flag.CommandLine) 76 | flag.Parse() 77 | 78 | ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) 79 | 80 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 81 | Scheme: scheme, 82 | WebhookServer: &webhook.DefaultServer{ 83 | Options: webhook.Options{ 84 | Port: 9443, 85 | }, 86 | }, 87 | Metrics: metricsserver.Options{ 88 | BindAddress: metricsAddr, 89 | }, 90 | HealthProbeBindAddress: probeAddr, 91 | LeaderElection: enableLeaderElection, 92 | LeaderElectionID: "4f44b7cc.kkb0318.github.io", 93 | // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily 94 | // when the Manager ends. This requires the binary to immediately end when the 95 | // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly 96 | // speeds up voluntary leader transitions as the new leader don't have to wait 97 | // LeaseDuration time first. 98 | // 99 | // In the default scaffold provided, the program ends immediately after 100 | // the manager stops, so would be fine to enable this option. However, 101 | // if you are doing or is intended to do any operation such as perform cleanups 102 | // after the manager stops then its usage might be unsafe. 103 | // LeaderElectionReleaseOnCancel: true, 104 | }) 105 | if err != nil { 106 | setupLog.Error(err, "unable to start manager") 107 | os.Exit(1) 108 | } 109 | 110 | if err = (&controller.OperatorManagerReconciler{ 111 | Client: mgr.GetClient(), 112 | Scheme: mgr.GetScheme(), 113 | }).SetupWithManager(mgr); err != nil { 114 | setupLog.Error(err, "unable to create controller", "controller", "OperatorManager") 115 | os.Exit(1) 116 | } 117 | //+kubebuilder:scaffold:builder 118 | 119 | if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { 120 | setupLog.Error(err, "unable to set up health check") 121 | os.Exit(1) 122 | } 123 | if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { 124 | setupLog.Error(err, "unable to set up ready check") 125 | os.Exit(1) 126 | } 127 | 128 | setupLog.Info("starting manager") 129 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 130 | setupLog.Error(err, "problem running manager") 131 | os.Exit(1) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Adds namespace to all resources. 2 | namespace: kom-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: kom- 10 | 11 | # Labels to add to all resources and selectors. 12 | #labels: 13 | #- includeSelectors: true 14 | # pairs: 15 | # someName: someValue 16 | 17 | resources: 18 | - ../crd 19 | - ../rbac 20 | - ../manager 21 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 22 | # crd/kustomization.yaml 23 | #- ../webhook 24 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. 25 | #- ../certmanager 26 | # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. 27 | #- ../prometheus 28 | 29 | patches: 30 | # Protect the /metrics endpoint by putting it behind auth. 31 | # If you want your controller-manager to expose the /metrics 32 | # endpoint w/o any authn/z, please comment the following line. 33 | # - path: manager_auth_proxy_patch.yaml 34 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 35 | # crd/kustomization.yaml 36 | #- manager_webhook_patch.yaml 37 | 38 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 39 | # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. 40 | # 'CERTMANAGER' needs to be enabled to use ca injection 41 | #- webhookcainjection_patch.yaml 42 | 43 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. 44 | # Uncomment the following replacements to add the cert-manager CA injection annotations 45 | #replacements: 46 | # - source: # Add cert-manager annotation to ValidatingWebhookConfiguration, MutatingWebhookConfiguration and CRDs 47 | # kind: Certificate 48 | # group: cert-manager.io 49 | # version: v1 50 | # name: serving-cert # this name should match the one in certificate.yaml 51 | # fieldPath: .metadata.namespace # namespace of the certificate CR 52 | # targets: 53 | # - select: 54 | # kind: ValidatingWebhookConfiguration 55 | # fieldPaths: 56 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 57 | # options: 58 | # delimiter: '/' 59 | # index: 0 60 | # create: true 61 | # - select: 62 | # kind: MutatingWebhookConfiguration 63 | # fieldPaths: 64 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 65 | # options: 66 | # delimiter: '/' 67 | # index: 0 68 | # create: true 69 | # - select: 70 | # kind: CustomResourceDefinition 71 | # fieldPaths: 72 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 73 | # options: 74 | # delimiter: '/' 75 | # index: 0 76 | # create: true 77 | # - source: 78 | # kind: Certificate 79 | # group: cert-manager.io 80 | # version: v1 81 | # name: serving-cert # this name should match the one in certificate.yaml 82 | # fieldPath: .metadata.name 83 | # targets: 84 | # - select: 85 | # kind: ValidatingWebhookConfiguration 86 | # fieldPaths: 87 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 88 | # options: 89 | # delimiter: '/' 90 | # index: 1 91 | # create: true 92 | # - select: 93 | # kind: MutatingWebhookConfiguration 94 | # fieldPaths: 95 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 96 | # options: 97 | # delimiter: '/' 98 | # index: 1 99 | # create: true 100 | # - select: 101 | # kind: CustomResourceDefinition 102 | # fieldPaths: 103 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 104 | # options: 105 | # delimiter: '/' 106 | # index: 1 107 | # create: true 108 | # - source: # Add cert-manager annotation to the webhook Service 109 | # kind: Service 110 | # version: v1 111 | # name: webhook-service 112 | # fieldPath: .metadata.name # namespace of the service 113 | # targets: 114 | # - select: 115 | # kind: Certificate 116 | # group: cert-manager.io 117 | # version: v1 118 | # fieldPaths: 119 | # - .spec.dnsNames.0 120 | # - .spec.dnsNames.1 121 | # options: 122 | # delimiter: '.' 123 | # index: 0 124 | # create: true 125 | # - source: 126 | # kind: Service 127 | # version: v1 128 | # name: webhook-service 129 | # fieldPath: .metadata.namespace # namespace of the service 130 | # targets: 131 | # - select: 132 | # kind: Certificate 133 | # group: cert-manager.io 134 | # version: v1 135 | # fieldPaths: 136 | # - .spec.dnsNames.0 137 | # - .spec.dnsNames.1 138 | # options: 139 | # delimiter: '.' 140 | # index: 1 141 | # create: true 142 | -------------------------------------------------------------------------------- /internal/tool/flux/testdata/testdata.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | import ( 4 | "path/filepath" 5 | "runtime" 6 | "testing" 7 | 8 | helmv2 "github.com/fluxcd/helm-controller/api/v2" 9 | kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" 10 | sourcev1 "github.com/fluxcd/source-controller/api/v1" 11 | "github.com/kkb0318/kom/internal/utils" 12 | apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 13 | ) 14 | 15 | type mockGitRepositoryBuilder struct { 16 | name string 17 | namespace string 18 | url string 19 | ref *sourcev1.GitRepositoryRef 20 | } 21 | 22 | func NewMockGitRepositoryBuilder() *mockGitRepositoryBuilder { 23 | return &mockGitRepositoryBuilder{} 24 | } 25 | 26 | func (f *mockGitRepositoryBuilder) WithName(val string) *mockGitRepositoryBuilder { 27 | f.name = val 28 | return f 29 | } 30 | 31 | func (f *mockGitRepositoryBuilder) WithNamespace(val string) *mockGitRepositoryBuilder { 32 | f.namespace = val 33 | return f 34 | } 35 | 36 | func (f *mockGitRepositoryBuilder) WithUrl(val string) *mockGitRepositoryBuilder { 37 | f.url = val 38 | return f 39 | } 40 | 41 | func (f *mockGitRepositoryBuilder) WithRef(val *sourcev1.GitRepositoryRef) *mockGitRepositoryBuilder { 42 | f.ref = val 43 | return f 44 | } 45 | 46 | func (f *mockGitRepositoryBuilder) Build(t *testing.T, testdataFileName string) *sourcev1.GitRepository { 47 | baseFilePath := filepath.Join(currentDir(t), testdataFileName) 48 | gitrepo := &sourcev1.GitRepository{} 49 | utils.LoadYaml(gitrepo, baseFilePath) 50 | if f.name != "" { 51 | gitrepo.SetName(f.name) 52 | } 53 | if f.namespace != "" { 54 | gitrepo.SetNamespace(f.namespace) 55 | } 56 | if f.ref != nil { 57 | gitrepo.Spec.Reference = f.ref 58 | } 59 | if f.url != "" { 60 | gitrepo.Spec.URL = f.url 61 | } 62 | return gitrepo 63 | } 64 | 65 | type mockKustomizationBuilder struct { 66 | name string 67 | namespace string 68 | path string 69 | url string 70 | ref *kustomizev1.CrossNamespaceSourceReference 71 | } 72 | 73 | func NewMockKustomizationBuilder() *mockKustomizationBuilder { 74 | return &mockKustomizationBuilder{} 75 | } 76 | 77 | func (f *mockKustomizationBuilder) WithName(val string) *mockKustomizationBuilder { 78 | f.name = val 79 | return f 80 | } 81 | 82 | func (f *mockKustomizationBuilder) WithNamespace(val string) *mockKustomizationBuilder { 83 | f.namespace = val 84 | return f 85 | } 86 | 87 | func (f *mockKustomizationBuilder) WithPath(val string) *mockKustomizationBuilder { 88 | f.path = val 89 | return f 90 | } 91 | 92 | func (f *mockKustomizationBuilder) WithRef(val *kustomizev1.CrossNamespaceSourceReference) *mockKustomizationBuilder { 93 | f.ref = val 94 | return f 95 | } 96 | 97 | func (f *mockKustomizationBuilder) Build(t *testing.T, testdataFileName string) *kustomizev1.Kustomization { 98 | baseFilePath := filepath.Join(currentDir(t), testdataFileName) 99 | ks := &kustomizev1.Kustomization{} 100 | utils.LoadYaml(ks, baseFilePath) 101 | if f.name != "" { 102 | ks.SetName(f.name) 103 | } 104 | if f.namespace != "" { 105 | ks.SetNamespace(f.namespace) 106 | } 107 | if f.path != "" { 108 | ks.Spec.Path = f.path 109 | } 110 | if f.ref != nil { 111 | ks.Spec.SourceRef = *f.ref 112 | } 113 | return ks 114 | } 115 | 116 | type mockHelmRepositoryBuilder struct { 117 | name string 118 | namespace string 119 | url string 120 | } 121 | 122 | func NewMockHelmRepositoryBuilder() *mockHelmRepositoryBuilder { 123 | return &mockHelmRepositoryBuilder{} 124 | } 125 | 126 | func (f *mockHelmRepositoryBuilder) WithName(val string) *mockHelmRepositoryBuilder { 127 | f.name = val 128 | return f 129 | } 130 | 131 | func (f *mockHelmRepositoryBuilder) WithNamespace(val string) *mockHelmRepositoryBuilder { 132 | f.namespace = val 133 | return f 134 | } 135 | 136 | func (f *mockHelmRepositoryBuilder) WithUrl(val string) *mockHelmRepositoryBuilder { 137 | f.url = val 138 | return f 139 | } 140 | 141 | func (f *mockHelmRepositoryBuilder) Build(t *testing.T, testdataFileName string) *sourcev1.HelmRepository { 142 | baseFilePath := filepath.Join(currentDir(t), testdataFileName) 143 | helmrepo := &sourcev1.HelmRepository{} 144 | utils.LoadYaml(helmrepo, baseFilePath) 145 | if f.name != "" { 146 | helmrepo.SetName(f.name) 147 | } 148 | if f.namespace != "" { 149 | helmrepo.SetNamespace(f.namespace) 150 | } 151 | if f.url != "" { 152 | helmrepo.Spec.URL = f.url 153 | } 154 | return helmrepo 155 | } 156 | 157 | type mockHelmReleaseBuilder struct { 158 | name string 159 | namespace string 160 | values *apiextensionsv1.JSON 161 | } 162 | 163 | func NewMockHelmReleaseBuilder() *mockHelmReleaseBuilder { 164 | return &mockHelmReleaseBuilder{} 165 | } 166 | 167 | func (f *mockHelmReleaseBuilder) WithName(val string) *mockHelmReleaseBuilder { 168 | f.name = val 169 | return f 170 | } 171 | 172 | func (f *mockHelmReleaseBuilder) WithNamespace(val string) *mockHelmReleaseBuilder { 173 | f.namespace = val 174 | return f 175 | } 176 | 177 | func (f *mockHelmReleaseBuilder) WithValues(values *apiextensionsv1.JSON) *mockHelmReleaseBuilder { 178 | f.values = values 179 | return f 180 | } 181 | 182 | func (f *mockHelmReleaseBuilder) Build(t *testing.T, testdataFileName string) *helmv2.HelmRelease { 183 | baseFilePath := filepath.Join(currentDir(t), testdataFileName) 184 | hr := &helmv2.HelmRelease{} 185 | utils.LoadYaml(hr, baseFilePath) 186 | if f.name != "" { 187 | hr.SetName(f.name) 188 | } 189 | if f.namespace != "" { 190 | hr.SetNamespace(f.namespace) 191 | } 192 | if f.values != nil { 193 | hr.Spec.Values = f.values 194 | } 195 | 196 | return hr 197 | } 198 | 199 | func currentDir(t *testing.T) string { 200 | t.Helper() 201 | _, currentFile, _, ok := runtime.Caller(0) 202 | if !ok { 203 | t.Fatal("runtime.Caller() failed to get current file path") 204 | } 205 | return filepath.Dir(currentFile) 206 | } 207 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | # API Reference 2 | 3 | ## Packages 4 | - [kom.kkb0318.github.io/v1alpha1](#komkkb0318githubiov1alpha1) 5 | 6 | 7 | ## kom.kkb0318.github.io/v1alpha1 8 | 9 | Package v1alpha1 contains API Schema definitions for the v1alpha1 API group 10 | 11 | ### Resource Types 12 | - [OperatorManager](#operatormanager) 13 | 14 | 15 | 16 | #### Chart 17 | 18 | 19 | 20 | Chart defines the details of a Helm chart to be managed. Depending on the GitOps tool (Flux or ArgoCD), it corresponds to a HelmRelease CR or an Application CR, respectively. 21 | 22 | _Appears in:_ 23 | - [Helm](#helm) 24 | 25 | | Field | Description | 26 | | --- | --- | 27 | | `name` _string_ | Name is the name of the Helm chart. | 28 | | `version` _string_ | Version specifies the version of the Helm chart to be deployed. | 29 | | `values` _[JSON](#json)_ | Values specifies Helm values to be passed to helm template, defined as a map. | 30 | 31 | 32 | #### Git 33 | 34 | 35 | 36 | Git defines the configuration for accessing a Git repository. 37 | 38 | _Appears in:_ 39 | - [Resource](#resource) 40 | 41 | | Field | Description | 42 | | --- | --- | 43 | | `name` _string_ | Name is a user-defined identifier for the resource. | 44 | | `namespace` _string_ | Namespace is the Kubernetes namespace where the Helm repository resource is located. The default value depends on the GitOps tool used: "kom-system" for Flux and "argocd" for ArgoCD. | 45 | | `url` _string_ | Url is the URL of the Git repository. | 46 | | `path` _string_ | Path specifies the directory path within the Git repository that contains the desired resources. This allows for selective management of resources located in specific parts of the repository. | 47 | | `reference` _[GitReference](#gitreference)_ | Reference contains the reference information (such as branch, tag, or semver) for the Git repository. This allows for targeting specific versions or configurations of the resources within the repository. | 48 | 49 | 50 | #### GitReference 51 | 52 | 53 | 54 | GitReference specifies the versioning information for tracking changes in the Git repository. 55 | 56 | _Appears in:_ 57 | - [Git](#git) 58 | 59 | | Field | Description | 60 | | --- | --- | 61 | | `type` _[GitReferenceType](#gitreferencetype)_ | Type indicates the method of versioning used in the repository, applicable only for Flux. Valid options are "branch", "semver", or "tag", allowing for different strategies of version management. | 62 | | `value` _string_ | Value specifies the exact reference to track, such as the name of a branch, a semantic versioning pattern, or a tag. This allows for precise control over which version of the resources is deployed. | 63 | 64 | 65 | #### GitReferenceType 66 | 67 | _Underlying type:_ _string_ 68 | 69 | GitReferenceType is applicable only for Flux. Valid options are "branch", "semver", or "tag" 70 | 71 | _Appears in:_ 72 | - [GitReference](#gitreference) 73 | 74 | 75 | 76 | #### Helm 77 | 78 | 79 | 80 | Helm defines the configuration for accessing a Helm repository. Depending on the GitOps tool in use (Flux or ArgoCD), it corresponds to a HelmRepository CR or a Secret, respectively. 81 | 82 | _Appears in:_ 83 | - [Resource](#resource) 84 | 85 | | Field | Description | 86 | | --- | --- | 87 | | `name` _string_ | Name is a user-defined identifier for the resource. | 88 | | `namespace` _string_ | Namespace is the Kubernetes namespace where the Helm repository resource is located. The default value depends on the GitOps tool used: "kom-system" for Flux and "argocd" for ArgoCD. | 89 | | `url` _string_ | Url is the URL of the Helm repository. | 90 | | `charts` _[Chart](#chart) array_ | Charts specifies the Helm charts within the repository to be managed. | 91 | 92 | 93 | #### OperatorManager 94 | 95 | 96 | 97 | OperatorManager is the Schema for the operatormanagers API 98 | 99 | 100 | 101 | | Field | Description | 102 | | --- | --- | 103 | | `apiVersion` _string_ | `kom.kkb0318.github.io/v1alpha1` 104 | | `kind` _string_ | `OperatorManager` 105 | | `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | 106 | | `spec` _[OperatorManagerSpec](#operatormanagerspec)_ | | 107 | 108 | 109 | #### OperatorManagerSpec 110 | 111 | 112 | 113 | OperatorManagerSpec defines the desired state and configuration of an OperatorManager. 114 | 115 | _Appears in:_ 116 | - [OperatorManager](#operatormanager) 117 | 118 | | Field | Description | 119 | | --- | --- | 120 | | `cleanup` _boolean_ | Cleanup, when enabled, allows the OperatorManager to perform garbage collection of resources that are no longer needed or managed. | 121 | | `tool` _[ToolType](#tooltype)_ | Tool specifies the GitOps tool to be used. Users must set this field to either "flux" or "argo". This field is required and determines various default behaviors and configurations. | 122 | | `resource` _[Resource](#resource)_ | Resource specifies the source repository (Helm or Git) for the operators to be managed. | 123 | 124 | 125 | 126 | 127 | #### Resource 128 | 129 | 130 | 131 | Resource represents the source repositories for operators, supporting both Helm and Git repositories. 132 | 133 | _Appears in:_ 134 | - [OperatorManagerSpec](#operatormanagerspec) 135 | 136 | | Field | Description | 137 | | --- | --- | 138 | | `helm` _[Helm](#helm) array_ | Helm specifies one or more Helm repositories containing the operators. This field is optional and only needed if operators are to be sourced from Helm repositories. | 139 | | `git` _[Git](#git) array_ | Git specifies one or more Git repositories containing the operators. This field is optional and only needed if operators are to be sourced from Git repositories. | 140 | 141 | 142 | #### ToolType 143 | 144 | _Underlying type:_ _string_ 145 | 146 | ToolType defines the GitOps tool used for managing resources Valid options are "flux", or "argo". 147 | 148 | _Appears in:_ 149 | - [OperatorManagerSpec](#operatormanagerspec) 150 | 151 | 152 | 153 | -------------------------------------------------------------------------------- /internal/controller/operatormanager_controller_flux_test.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | komv1alpha1 "github.com/kkb0318/kom/api/v1alpha1" 5 | . "github.com/onsi/ginkgo/v2" 6 | . "github.com/onsi/gomega" 7 | "k8s.io/apimachinery/pkg/types" 8 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 9 | ) 10 | 11 | type expected struct { 12 | source types.NamespacedName 13 | charts []types.NamespacedName 14 | } 15 | 16 | var _ = Describe("OperatorManager controller", func() { 17 | Context("OperatorManager controller test", func() { 18 | It("should successfully reconcile a custom resource for kom", func() { 19 | komName := "test-kom-argo" 20 | kom := createKom(komName) 21 | kom.Spec = komv1alpha1.OperatorManagerSpec{ 22 | Cleanup: true, 23 | Tool: komv1alpha1.FluxCDTool, 24 | Resource: komv1alpha1.Resource{ 25 | Helm: []komv1alpha1.Helm{ 26 | { 27 | Name: "helmrepo1", 28 | Url: "https://helm.github.io/examples", 29 | Charts: []komv1alpha1.Chart{ 30 | { 31 | Name: "hello-world", 32 | Version: "x.x.x", 33 | }, 34 | }, 35 | }, 36 | { 37 | Name: "helmrepo2", 38 | Url: "https://stefanprodan.github.io/podinfo", 39 | Charts: []komv1alpha1.Chart{ 40 | { 41 | Name: "podinfo", 42 | Version: "x.x.x", 43 | }, 44 | }, 45 | }, 46 | }, 47 | Git: []komv1alpha1.Git{ 48 | { 49 | Name: "gitrepo1", 50 | Url: "https://github.com/operator-framework/operator-sdk", 51 | Path: "testdata/helm/memcached-operator/config/default", 52 | Reference: komv1alpha1.GitReference{ 53 | Type: komv1alpha1.GitTag, 54 | Value: "v1.33.0", 55 | }, 56 | }, 57 | }, 58 | }, 59 | } 60 | typeNamespaceName := types.NamespacedName{Name: komName, Namespace: testNamespace} 61 | 62 | expectedHelmResources := []expected{ 63 | { 64 | source: types.NamespacedName{ 65 | Name: "helmrepo1", 66 | Namespace: "kom-system", 67 | }, 68 | charts: []types.NamespacedName{ 69 | { 70 | Name: "hello-world", 71 | Namespace: "kom-system", 72 | }, 73 | }, 74 | }, 75 | { 76 | source: types.NamespacedName{ 77 | Name: "helmrepo2", 78 | Namespace: "kom-system", 79 | }, 80 | charts: []types.NamespacedName{ 81 | { 82 | Name: "podinfo", 83 | Namespace: "kom-system", 84 | }, 85 | }, 86 | }, 87 | } 88 | 89 | expectedGitResources := []expected{ 90 | { 91 | source: types.NamespacedName{ 92 | Name: "gitrepo1", 93 | Namespace: "kom-system", 94 | }, 95 | charts: []types.NamespacedName{ 96 | { 97 | Name: "gitrepo1", 98 | Namespace: "kom-system", 99 | }, 100 | }, 101 | }, 102 | } 103 | 104 | err := k8sClient.Create(ctx, kom) 105 | Expect(err).To(Not(HaveOccurred())) 106 | 107 | By("Checking if the custom resource was successfully created") 108 | Eventually(func() error { 109 | found := &komv1alpha1.OperatorManager{} 110 | return k8sClient.Get(ctx, typeNamespaceName, found) 111 | }, timeout).Should(Succeed()) 112 | 113 | By("Reconciling the custom resource created") 114 | komReconciler := &OperatorManagerReconciler{ 115 | Client: k8sClient, 116 | Scheme: k8sClient.Scheme(), 117 | } 118 | // add finalizer 119 | _, err = komReconciler.Reconcile(ctx, reconcile.Request{ 120 | NamespacedName: typeNamespaceName, 121 | }) 122 | Expect(err).To(Not(HaveOccurred())) 123 | _, err = komReconciler.Reconcile(ctx, reconcile.Request{ 124 | NamespacedName: typeNamespaceName, 125 | }) 126 | Expect(err).To(Not(HaveOccurred())) 127 | 128 | By("Checking if Resources were successfully created in the reconciliation") 129 | for _, expected := range expectedHelmResources { 130 | checkExist(expected.source, helmRepo) 131 | for _, fetcher := range expected.charts { 132 | checkExist(fetcher, helmRelease) 133 | } 134 | } 135 | for _, expected := range expectedGitResources { 136 | checkExist(expected.source, gitRepo) 137 | for _, fetcher := range expected.charts { 138 | checkExist(fetcher, kustomization) 139 | } 140 | } 141 | 142 | By("Checking garbage collect of partially deletion") 143 | k8sClient.Get(ctx, typeNamespaceName, kom) 144 | Eventually(func() error { 145 | // delete resource[1] 146 | kom.Spec.Resource.Helm = []komv1alpha1.Helm{kom.Spec.Resource.Helm[0]} 147 | return k8sClient.Update(ctx, kom) 148 | }, timeout).Should(Succeed()) 149 | _, err = komReconciler.Reconcile(ctx, reconcile.Request{ 150 | NamespacedName: typeNamespaceName, 151 | }) 152 | Expect(err).To(Not(HaveOccurred())) 153 | // resource[0] exists 154 | checkExist(expectedHelmResources[0].source, helmRepo) 155 | for _, fetcher := range expectedHelmResources[0].charts { 156 | checkExist(fetcher, helmRelease) 157 | } 158 | // resource[1] does not exist 159 | checkNoExist(expectedHelmResources[1].source, helmRepo) 160 | for _, fetcher := range expectedHelmResources[1].charts { 161 | checkNoExist(fetcher, helmRelease) 162 | } 163 | 164 | By("removing the custom resource for the Kind") 165 | Eventually(func() error { 166 | return k8sClient.Delete(ctx, kom) 167 | }, timeout).Should(Succeed()) 168 | _, err = komReconciler.Reconcile(ctx, reconcile.Request{ 169 | NamespacedName: typeNamespaceName, 170 | }) 171 | Expect(err).To(Not(HaveOccurred())) 172 | checkNoExist(typeNamespaceName, operatorManager) 173 | 174 | By("Checking if Resources were successfully deleted in the reconciliation") 175 | for _, expected := range expectedHelmResources { 176 | checkNoExist(expected.source, helmRepo) 177 | for _, fetcher := range expected.charts { 178 | checkNoExist(fetcher, helmRelease) 179 | } 180 | } 181 | for _, expected := range expectedGitResources { 182 | checkNoExist(expected.source, gitRepo) 183 | for _, fetcher := range expected.charts { 184 | checkNoExist(fetcher, kustomization) 185 | } 186 | } 187 | }) 188 | }) 189 | }) 190 | -------------------------------------------------------------------------------- /internal/controller/operatormanager_controller_no_cleanup_test.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | 6 | komv1alpha1 "github.com/kkb0318/kom/api/v1alpha1" 7 | . "github.com/onsi/ginkgo/v2" 8 | . "github.com/onsi/gomega" 9 | "k8s.io/apimachinery/pkg/types" 10 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 11 | ) 12 | 13 | var _ = Describe("OperatorManager controller", func() { 14 | Context("OperatorManager controller test", func() { 15 | It("no garbage collect test. cleanup: false", func() { 16 | nameSuffix := "-no-cleanup" 17 | komName := fmt.Sprintf("test-kom%s", nameSuffix) 18 | kom := createKom(komName) 19 | kom.Spec = komv1alpha1.OperatorManagerSpec{ 20 | Cleanup: false, 21 | Tool: komv1alpha1.FluxCDTool, 22 | Resource: komv1alpha1.Resource{ 23 | Helm: []komv1alpha1.Helm{ 24 | { 25 | Name: fmt.Sprintf("helmrepo1%s", nameSuffix), 26 | Url: "https://helm.github.io/examples", 27 | Charts: []komv1alpha1.Chart{ 28 | { 29 | Name: "hello-world", 30 | Version: "x.x.x", 31 | }, 32 | }, 33 | }, 34 | { 35 | Name: fmt.Sprintf("helmrepo2%s", nameSuffix), 36 | Url: "https://stefanprodan.github.io/podinfo", 37 | Charts: []komv1alpha1.Chart{ 38 | { 39 | Name: "podinfo", 40 | Version: "x.x.x", 41 | }, 42 | }, 43 | }, 44 | }, 45 | Git: []komv1alpha1.Git{ 46 | { 47 | Name: fmt.Sprintf("gitrepo1%s", nameSuffix), 48 | Url: "https://github.com/operator-framework/operator-sdk", 49 | Path: "testdata/helm/memcached-operator/config/default", 50 | Reference: komv1alpha1.GitReference{ 51 | Type: komv1alpha1.GitTag, 52 | Value: "v1.33.0", 53 | }, 54 | }, 55 | }, 56 | }, 57 | } 58 | typeNamespaceName := types.NamespacedName{Name: komName, Namespace: testNamespace} 59 | 60 | expectedHelmResources := []expected{ 61 | { 62 | source: types.NamespacedName{ 63 | Name: fmt.Sprintf("helmrepo1%s", nameSuffix), 64 | Namespace: "kom-system", 65 | }, 66 | charts: []types.NamespacedName{ 67 | { 68 | Name: "hello-world", 69 | Namespace: "kom-system", 70 | }, 71 | }, 72 | }, 73 | { 74 | source: types.NamespacedName{ 75 | Name: fmt.Sprintf("helmrepo2%s", nameSuffix), 76 | Namespace: "kom-system", 77 | }, 78 | charts: []types.NamespacedName{ 79 | { 80 | Name: "podinfo", 81 | Namespace: "kom-system", 82 | }, 83 | }, 84 | }, 85 | } 86 | 87 | expectedGitResources := []expected{ 88 | { 89 | source: types.NamespacedName{ 90 | Name: fmt.Sprintf("gitrepo1%s", nameSuffix), 91 | Namespace: "kom-system", 92 | }, 93 | charts: []types.NamespacedName{ 94 | { 95 | Name: fmt.Sprintf("gitrepo1%s", nameSuffix), 96 | Namespace: "kom-system", 97 | }, 98 | }, 99 | }, 100 | } 101 | 102 | err := k8sClient.Create(ctx, kom) 103 | Expect(err).To(Not(HaveOccurred())) 104 | 105 | By("Checking if the custom resource was successfully created") 106 | Eventually(func() error { 107 | found := &komv1alpha1.OperatorManager{} 108 | return k8sClient.Get(ctx, typeNamespaceName, found) 109 | }, timeout).Should(Succeed()) 110 | 111 | By("Reconciling the custom resource created") 112 | komReconciler := &OperatorManagerReconciler{ 113 | Client: k8sClient, 114 | Scheme: k8sClient.Scheme(), 115 | } 116 | // add finalizer 117 | _, err = komReconciler.Reconcile(ctx, reconcile.Request{ 118 | NamespacedName: typeNamespaceName, 119 | }) 120 | Expect(err).To(Not(HaveOccurred())) 121 | _, err = komReconciler.Reconcile(ctx, reconcile.Request{ 122 | NamespacedName: typeNamespaceName, 123 | }) 124 | Expect(err).To(Not(HaveOccurred())) 125 | 126 | By("Checking if Resources were successfully created in the reconciliation") 127 | for _, expected := range expectedHelmResources { 128 | checkExist(expected.source, helmRepo) 129 | for _, fetcher := range expected.charts { 130 | checkExist(fetcher, helmRelease) 131 | } 132 | } 133 | for _, expected := range expectedGitResources { 134 | checkExist(expected.source, gitRepo) 135 | for _, fetcher := range expected.charts { 136 | checkExist(fetcher, kustomization) 137 | } 138 | } 139 | 140 | By("Checking no garbage collect") 141 | k8sClient.Get(ctx, typeNamespaceName, kom) 142 | Eventually(func() error { 143 | // delete resource[1] 144 | kom.Spec.Resource.Helm = []komv1alpha1.Helm{kom.Spec.Resource.Helm[0]} 145 | return k8sClient.Update(ctx, kom) 146 | }, timeout).Should(Succeed()) 147 | _, err = komReconciler.Reconcile(ctx, reconcile.Request{ 148 | NamespacedName: typeNamespaceName, 149 | }) 150 | Expect(err).To(Not(HaveOccurred())) 151 | for _, expected := range expectedHelmResources { 152 | checkExist(expected.source, helmRepo) 153 | for _, fetcher := range expected.charts { 154 | checkExist(fetcher, helmRelease) 155 | } 156 | } 157 | for _, expected := range expectedGitResources { 158 | checkExist(expected.source, gitRepo) 159 | for _, fetcher := range expected.charts { 160 | checkExist(fetcher, kustomization) 161 | } 162 | } 163 | 164 | By("removing the custom resource for the Kind") 165 | Eventually(func() error { 166 | return k8sClient.Delete(ctx, kom) 167 | }, timeout).Should(Succeed()) 168 | _, err = komReconciler.Reconcile(ctx, reconcile.Request{ 169 | NamespacedName: typeNamespaceName, 170 | }) 171 | Expect(err).To(Not(HaveOccurred())) 172 | checkNoExist(typeNamespaceName, operatorManager) 173 | 174 | By("Checking no garbage collect") 175 | for _, expected := range expectedHelmResources { 176 | checkExist(expected.source, helmRepo) 177 | for _, fetcher := range expected.charts { 178 | checkExist(fetcher, helmRelease) 179 | } 180 | } 181 | for _, expected := range expectedGitResources { 182 | checkExist(expected.source, gitRepo) 183 | for _, fetcher := range expected.charts { 184 | checkExist(fetcher, kustomization) 185 | } 186 | } 187 | }) 188 | }) 189 | }) 190 | -------------------------------------------------------------------------------- /internal/controller/operatormanager_controller_argo_test.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | komv1alpha1 "github.com/kkb0318/kom/api/v1alpha1" 5 | . "github.com/onsi/ginkgo/v2" 6 | . "github.com/onsi/gomega" 7 | "k8s.io/apimachinery/pkg/types" 8 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 9 | ) 10 | 11 | type expectedArgo struct { 12 | sources []types.NamespacedName 13 | charts []types.NamespacedName 14 | } 15 | 16 | var _ = Describe("OperatorManager controller argocd", func() { 17 | Context("OperatorManager controller argocd test", func() { 18 | It("should successfully reconcile a custom resource for kom", func() { 19 | komName := "test-kom" 20 | kom := createKom(komName) 21 | kom.Spec = komv1alpha1.OperatorManagerSpec{ 22 | Cleanup: true, 23 | Tool: komv1alpha1.ArgoCDTool, 24 | Resource: komv1alpha1.Resource{ 25 | Helm: []komv1alpha1.Helm{ 26 | { 27 | Name: "helmrepo1", 28 | Url: "https://helm.github.io/examples", 29 | Charts: []komv1alpha1.Chart{ 30 | { 31 | Name: "hello-world", 32 | Version: "x.x.x", 33 | }, 34 | }, 35 | }, 36 | { 37 | Name: "helmrepo2", 38 | Url: "https://stefanprodan.github.io/podinfo", 39 | Charts: []komv1alpha1.Chart{ 40 | { 41 | Name: "podinfo", 42 | Version: "x.x.x", 43 | }, 44 | }, 45 | }, 46 | }, 47 | Git: []komv1alpha1.Git{ 48 | { 49 | Name: "gitrepo1", 50 | Url: "https://github.com/operator-framework/operator-sdk", 51 | Path: "testdata/helm/memcached-operator/config/default", 52 | Reference: komv1alpha1.GitReference{ 53 | Type: komv1alpha1.GitTag, 54 | Value: "v1.33.0", 55 | }, 56 | }, 57 | }, 58 | }, 59 | } 60 | typeNamespaceName := types.NamespacedName{Name: komName, Namespace: testNamespace} 61 | 62 | expectedHelmResources := []expectedArgo{ 63 | { 64 | sources: []types.NamespacedName{ 65 | { 66 | Name: "helmrepo1", 67 | Namespace: "argocd", 68 | }, 69 | }, 70 | charts: []types.NamespacedName{ 71 | { 72 | Name: "hello-world", 73 | Namespace: "argocd", 74 | }, 75 | }, 76 | }, 77 | { 78 | sources: []types.NamespacedName{ 79 | { 80 | Name: "helmrepo2", 81 | Namespace: "argocd", 82 | }, 83 | }, 84 | charts: []types.NamespacedName{ 85 | { 86 | Name: "podinfo", 87 | Namespace: "argocd", 88 | }, 89 | }, 90 | }, 91 | } 92 | 93 | expectedGitResources := []expectedArgo{ 94 | { 95 | sources: []types.NamespacedName{ 96 | { 97 | Name: "gitrepo1", 98 | Namespace: "argocd", 99 | }, 100 | }, 101 | charts: []types.NamespacedName{ 102 | { 103 | Name: "gitrepo1", 104 | Namespace: "argocd", 105 | }, 106 | }, 107 | }, 108 | } 109 | 110 | err := k8sClient.Create(ctx, kom) 111 | Expect(err).To(Not(HaveOccurred())) 112 | 113 | By("Checking if the custom resource was successfully created") 114 | Eventually(func() error { 115 | found := &komv1alpha1.OperatorManager{} 116 | return k8sClient.Get(ctx, typeNamespaceName, found) 117 | }, timeout).Should(Succeed()) 118 | 119 | By("Reconciling the custom resource created") 120 | komReconciler := &OperatorManagerReconciler{ 121 | Client: k8sClient, 122 | Scheme: k8sClient.Scheme(), 123 | } 124 | // add finalizer 125 | _, err = komReconciler.Reconcile(ctx, reconcile.Request{ 126 | NamespacedName: typeNamespaceName, 127 | }) 128 | Expect(err).To(Not(HaveOccurred())) 129 | _, err = komReconciler.Reconcile(ctx, reconcile.Request{ 130 | NamespacedName: typeNamespaceName, 131 | }) 132 | Expect(err).To(Not(HaveOccurred())) 133 | 134 | By("Checking if Resources were successfully created in the reconciliation") 135 | for _, expected := range expectedHelmResources { 136 | for _, source := range expected.sources { 137 | checkExist(source, secret) 138 | } 139 | for _, fetcher := range expected.charts { 140 | checkExist(fetcher, application) 141 | } 142 | } 143 | for _, expected := range expectedGitResources { 144 | for _, source := range expected.sources { 145 | checkExist(source, secret) 146 | } 147 | for _, fetcher := range expected.charts { 148 | checkExist(fetcher, application) 149 | } 150 | } 151 | 152 | By("Checking garbage collect of partially deletion") 153 | k8sClient.Get(ctx, typeNamespaceName, kom) 154 | Eventually(func() error { 155 | // delete resource[1] 156 | kom.Spec.Resource.Helm = []komv1alpha1.Helm{kom.Spec.Resource.Helm[0]} 157 | return k8sClient.Update(ctx, kom) 158 | }, timeout).Should(Succeed()) 159 | _, err = komReconciler.Reconcile(ctx, reconcile.Request{ 160 | NamespacedName: typeNamespaceName, 161 | }) 162 | Expect(err).To(Not(HaveOccurred())) 163 | // resource[0] exists 164 | for _, source := range expectedHelmResources[0].sources { 165 | checkExist(source, secret) 166 | } 167 | for _, fetcher := range expectedHelmResources[0].charts { 168 | checkExist(fetcher, application) 169 | } 170 | // resource[1] does not exist 171 | for _, source := range expectedHelmResources[1].sources { 172 | checkNoExist(source, secret) 173 | } 174 | for _, fetcher := range expectedHelmResources[1].charts { 175 | checkNoExist(fetcher, application) 176 | } 177 | 178 | By("removing the custom resource for the Kind") 179 | Eventually(func() error { 180 | return k8sClient.Delete(ctx, kom) 181 | }, timeout).Should(Succeed()) 182 | _, err = komReconciler.Reconcile(ctx, reconcile.Request{ 183 | NamespacedName: typeNamespaceName, 184 | }) 185 | Expect(err).To(Not(HaveOccurred())) 186 | checkNoExist(typeNamespaceName, operatorManager) 187 | 188 | By("Checking if Resources were successfully deleted in the reconciliation") 189 | for _, expected := range expectedHelmResources { 190 | for _, source := range expected.sources { 191 | checkNoExist(source, secret) 192 | } 193 | for _, fetcher := range expected.charts { 194 | checkNoExist(fetcher, application) 195 | } 196 | } 197 | for _, expected := range expectedGitResources { 198 | for _, source := range expected.sources { 199 | checkNoExist(source, secret) 200 | } 201 | for _, fetcher := range expected.charts { 202 | checkNoExist(fetcher, application) 203 | } 204 | } 205 | }) 206 | }) 207 | }) 208 | -------------------------------------------------------------------------------- /internal/controller/operatormanager_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023. 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 controller 18 | 19 | import ( 20 | "context" 21 | 22 | komv1alpha1 "github.com/kkb0318/kom/api/v1alpha1" 23 | komk8s "github.com/kkb0318/kom/internal/kubernetes" 24 | komstatus "github.com/kkb0318/kom/internal/status" 25 | "github.com/kkb0318/kom/internal/tool/factory" 26 | apierrors "k8s.io/apimachinery/pkg/api/errors" 27 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 | "k8s.io/apimachinery/pkg/runtime" 29 | "k8s.io/apimachinery/pkg/util/errors" 30 | ctrl "sigs.k8s.io/controller-runtime" 31 | "sigs.k8s.io/controller-runtime/pkg/client" 32 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 33 | ctrllog "sigs.k8s.io/controller-runtime/pkg/log" 34 | ) 35 | 36 | const komFinalizer = "kom.kkb0318.github.io/finalizers" 37 | 38 | // OperatorManagerReconciler reconciles a OperatorManager object 39 | type OperatorManagerReconciler struct { 40 | client.Client 41 | Scheme *runtime.Scheme 42 | } 43 | 44 | // +kubebuilder:rbac:groups=kom.kkb0318.github.io,resources=operatormanagers,verbs=get;list;watch;create;update;patch;delete 45 | // +kubebuilder:rbac:groups=kom.kkb0318.github.io,resources=operatormanagers/status,verbs=get;update;patch 46 | // +kubebuilder:rbac:groups=kom.kkb0318.github.io,resources=operatormanagers/finalizers,verbs=get;create;update;patch;delete 47 | // +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=gitrepositories,verbs=get;list;watch;create;update;patch;delete 48 | // +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=helmrepositories,verbs=get;list;watch;create;update;patch;delete 49 | // +kubebuilder:rbac:groups=helm.toolkit.fluxcd.io,resources=helmreleases,verbs=get;list;watch;create;update;patch;delete 50 | // +kubebuilder:rbac:groups=kustomize.toolkit.fluxcd.io,resources=kustomizations,verbs=get;list;watch;create;update;patch;delete 51 | // +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete 52 | // +kubebuilder:rbac:groups=argoproj.io,resources=applications;appprojects,verbs=* 53 | 54 | // SetupWithManager sets up the controller with the Manager. 55 | func (r *OperatorManagerReconciler) SetupWithManager(mgr ctrl.Manager) error { 56 | return ctrl.NewControllerManagedBy(mgr). 57 | For(&komv1alpha1.OperatorManager{}). 58 | Complete(r) 59 | } 60 | 61 | // Reconcile is part of the main kubernetes reconciliation loop which aims to 62 | // move the current state of the cluster closer to the desired state. 63 | // TODO(user): Modify the Reconcile function to compare the state specified by 64 | // the OperatorManager object against the actual cluster state, and then 65 | // perform operations to make the cluster state reflect the state specified by 66 | // the user. 67 | // 68 | // For more details, check Reconcile and its Result here: 69 | // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.15.0/pkg/reconcile 70 | func (r *OperatorManagerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, retErr error) { 71 | log := ctrllog.FromContext(ctx) 72 | obj := &komv1alpha1.OperatorManager{} 73 | if err := r.Get(ctx, req.NamespacedName, obj); err != nil { 74 | return ctrl.Result{}, client.IgnoreNotFound(err) 75 | } 76 | handler, err := komk8s.NewHandler(r.Client, komk8s.Owner{Field: "kom"}, obj.Spec.Cleanup) 77 | if err != nil { 78 | return ctrl.Result{}, err 79 | } 80 | defer func() { 81 | if err := handler.PatchStatus(ctx, obj); err != nil { 82 | if !obj.GetDeletionTimestamp().IsZero() { 83 | retErr = errors.FilterOut(err, func(e error) bool { return apierrors.IsNotFound(e) }) 84 | } else { 85 | retErr = errors.NewAggregate([]error{retErr, err}) 86 | } 87 | } 88 | }() 89 | 90 | if !controllerutil.ContainsFinalizer(obj, komFinalizer) { 91 | controllerutil.AddFinalizer(obj, komFinalizer) 92 | if err := r.Update(ctx, obj); err != nil { 93 | log.Error(err, "Failed to update custom resource to add finalizer") 94 | return ctrl.Result{}, err 95 | } 96 | return ctrl.Result{Requeue: true}, nil 97 | } 98 | // Examine if the object is under deletion. 99 | if !obj.DeletionTimestamp.IsZero() { 100 | retErr = r.reconcileDelete(ctx, obj, *handler) 101 | return 102 | } 103 | if err := r.reconcile(ctx, obj, *handler); err != nil { 104 | return ctrl.Result{}, err 105 | } 106 | 107 | log.Info("successfully reconciled") 108 | return ctrl.Result{}, nil 109 | } 110 | 111 | func (r *OperatorManagerReconciler) reconcile(ctx context.Context, obj *komv1alpha1.OperatorManager, handler komk8s.Handler) error { 112 | log := ctrllog.FromContext(ctx) 113 | rm := factory.NewResourceManager(*obj) 114 | beforeResources := obj.Status.AppliedResources.DeepCopy() 115 | 116 | appliedResources, err := handler.ApplyAll(ctx, rm) 117 | if err != nil { 118 | log.Error(err, "server-side apply failed") 119 | return err 120 | } 121 | log.Info("server-side apply completed") 122 | diff, err := komstatus.Diff(beforeResources, appliedResources) 123 | if err != nil { 124 | return err 125 | } 126 | if len(diff) != 0 { 127 | log.Info("garbage collect") 128 | opts := komk8s.DeleteOptions{ 129 | DeletionPropagation: metav1.DeletePropagationBackground, 130 | } 131 | err = handler.DeleteAll(ctx, diff, opts) 132 | if err != nil { 133 | return err 134 | } 135 | } 136 | obj.Status.AppliedResources = appliedResources 137 | return nil 138 | } 139 | 140 | func (r *OperatorManagerReconciler) reconcileDelete(ctx context.Context, obj *komv1alpha1.OperatorManager, handler komk8s.Handler) error { 141 | // Remove our finalizer from the list 142 | log := ctrllog.FromContext(ctx) 143 | resources, err := komstatus.ToListUnstructured(obj.Status.AppliedResources) 144 | if err != nil { 145 | return err 146 | } 147 | opts := komk8s.DeleteOptions{ 148 | DeletionPropagation: metav1.DeletePropagationBackground, 149 | } 150 | err = handler.DeleteAll(ctx, resources, opts) 151 | if err != nil { 152 | log.Error(err, "deletion failed") 153 | return err 154 | } 155 | controllerutil.RemoveFinalizer(obj, komFinalizer) 156 | log.Info("All resources were successfully deleted") 157 | return r.Update(ctx, obj) 158 | } 159 | -------------------------------------------------------------------------------- /internal/controller/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023. 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 controller 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "path/filepath" 23 | "strings" 24 | "testing" 25 | "time" 26 | 27 | . "github.com/onsi/ginkgo/v2" 28 | . "github.com/onsi/gomega" 29 | 30 | helmv2 "github.com/fluxcd/helm-controller/api/v2" 31 | kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" 32 | sourcev1 "github.com/fluxcd/source-controller/api/v1" 33 | argov1alpha1 "github.com/kkb0318/argo-cd-api/api/v1alpha1" 34 | komv1alpha1 "github.com/kkb0318/kom/api/v1alpha1" 35 | corev1 "k8s.io/api/core/v1" 36 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 37 | "k8s.io/apimachinery/pkg/types" 38 | "k8s.io/client-go/kubernetes/scheme" 39 | "k8s.io/client-go/rest" 40 | ctrl "sigs.k8s.io/controller-runtime" 41 | "sigs.k8s.io/controller-runtime/pkg/client" 42 | "sigs.k8s.io/controller-runtime/pkg/envtest" 43 | logf "sigs.k8s.io/controller-runtime/pkg/log" 44 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 45 | //+kubebuilder:scaffold:imports 46 | ) 47 | 48 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 49 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 50 | 51 | var ( 52 | timeout = time.Second * 10 53 | cfg *rest.Config 54 | k8sClient client.Client 55 | testEnv *envtest.Environment 56 | testNamespace = "kom-system" 57 | ctx = ctrl.SetupSignalHandler() 58 | ) 59 | 60 | func TestControllers(t *testing.T) { 61 | RegisterFailHandler(Fail) 62 | 63 | RunSpecs(t, "Controller Suite") 64 | } 65 | 66 | var _ = BeforeSuite(func() { 67 | logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) 68 | 69 | By("bootstrapping test environment") 70 | testEnv = &envtest.Environment{ 71 | CRDDirectoryPaths: []string{ 72 | filepath.Join("..", "..", "config", "crd", "bases"), 73 | filepath.Join(".", "testdata", "crds"), 74 | }, 75 | ErrorIfCRDPathMissing: true, 76 | } 77 | 78 | var err error 79 | // cfg is defined in this file globally. 80 | cfg, err = testEnv.Start() 81 | Expect(err).NotTo(HaveOccurred()) 82 | Expect(cfg).NotTo(BeNil()) 83 | 84 | err = komv1alpha1.AddToScheme(scheme.Scheme) 85 | Expect(err).NotTo(HaveOccurred()) 86 | // for flux 87 | err = sourcev1.SchemeBuilder.AddToScheme(scheme.Scheme) 88 | Expect(err).NotTo(HaveOccurred()) 89 | err = helmv2.SchemeBuilder.AddToScheme(scheme.Scheme) 90 | Expect(err).NotTo(HaveOccurred()) 91 | err = kustomizev1.SchemeBuilder.AddToScheme(scheme.Scheme) 92 | Expect(err).NotTo(HaveOccurred()) 93 | err = argov1alpha1.SchemeBuilder.AddToScheme(scheme.Scheme) 94 | Expect(err).NotTo(HaveOccurred()) 95 | // for argocd 96 | 97 | //+kubebuilder:scaffold:scheme 98 | 99 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) 100 | Expect(err).NotTo(HaveOccurred()) 101 | Expect(k8sClient).NotTo(BeNil()) 102 | k8sClient.Create(ctx, createNamespace(testNamespace)) 103 | k8sClient.Create(ctx, createNamespace("argocd")) 104 | }) 105 | 106 | var _ = AfterSuite(func() { 107 | By("tearing down the test environment") 108 | authUser, _ := testEnv.AddUser(envtest.User{Name: "test", Groups: []string{"system:masters"}}, &rest.Config{}) 109 | kubectl, _ := authUser.Kubectl() 110 | if os.Getenv("DEBUG") == "true" { 111 | fmt.Printf(` 112 | You can use the following command to investigate the failure: 113 | kubectl %s 114 | 115 | When you have finished investigation, clean up with the following commands: 116 | pkill kube-apiserver 117 | pkill etcd 118 | rm -rf %s 119 | `, strings.Join(kubectl.Opts, " "), testEnv.ControlPlane.APIServer.CertDir) 120 | return 121 | } 122 | err := testEnv.Stop() 123 | Expect(err).NotTo(HaveOccurred()) 124 | }) 125 | 126 | func createKom(name string) *komv1alpha1.OperatorManager { 127 | return &komv1alpha1.OperatorManager{ 128 | TypeMeta: metav1.TypeMeta{ 129 | Kind: komv1alpha1.OperatorManagerKind, 130 | APIVersion: komv1alpha1.GroupVersion.String(), 131 | }, 132 | ObjectMeta: metav1.ObjectMeta{ 133 | Name: name, 134 | Namespace: testNamespace, 135 | }, 136 | } 137 | } 138 | 139 | func createNamespace(ns string) *corev1.Namespace { 140 | return &corev1.Namespace{ 141 | ObjectMeta: metav1.ObjectMeta{Name: ns}, 142 | } 143 | } 144 | 145 | func createHelmRepository(name string) *sourcev1.HelmRepository { 146 | return &sourcev1.HelmRepository{ 147 | TypeMeta: metav1.TypeMeta{ 148 | Kind: sourcev1.HelmRepositoryKind, 149 | APIVersion: sourcev1.GroupVersion.String(), 150 | }, 151 | ObjectMeta: metav1.ObjectMeta{ 152 | Name: name, 153 | Namespace: testNamespace, 154 | }, 155 | } 156 | } 157 | 158 | func createHelmRelease(name string) *helmv2.HelmRelease { 159 | return &helmv2.HelmRelease{ 160 | TypeMeta: metav1.TypeMeta{ 161 | Kind: helmv2.HelmReleaseKind, 162 | APIVersion: helmv2.GroupVersion.String(), 163 | }, 164 | ObjectMeta: metav1.ObjectMeta{ 165 | Name: name, 166 | Namespace: testNamespace, 167 | }, 168 | } 169 | } 170 | 171 | func checkExist(expected types.NamespacedName, newFunc func() client.Object) { 172 | Eventually(func() error { 173 | found := newFunc() 174 | return k8sClient.Get(ctx, expected, found) 175 | }, timeout).Should(Succeed()) 176 | } 177 | 178 | func checkNoExist(expected types.NamespacedName, newFunc func() client.Object) { 179 | Eventually(func() error { 180 | found := newFunc() 181 | return k8sClient.Get(ctx, expected, found) 182 | }, timeout).Should(Not(Succeed())) 183 | } 184 | 185 | // -----used for assertion----- 186 | // -----Flux----- 187 | func helmRepo() client.Object { 188 | return &sourcev1.HelmRepository{} 189 | } 190 | 191 | func helmRelease() client.Object { 192 | return &helmv2.HelmRelease{} 193 | } 194 | 195 | func gitRepo() client.Object { 196 | return &sourcev1.GitRepository{} 197 | } 198 | 199 | func kustomization() client.Object { 200 | return &kustomizev1.Kustomization{} 201 | } 202 | 203 | // -----Argo----- 204 | 205 | func application() client.Object { 206 | return &argov1alpha1.Application{} 207 | } 208 | func secret() client.Object { 209 | return &corev1.Secret{} 210 | } 211 | 212 | func operatorManager() client.Object { 213 | return &komv1alpha1.OperatorManager{} 214 | } 215 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # VERSION defines the project version for the bundle. 2 | # Update this value when you upgrade the version of your project. 3 | # To re-generate a bundle for another specific version without changing the standard setup, you can: 4 | # - use the VERSION as arg of the bundle target (e.g make bundle VERSION=0.0.2) 5 | # - use environment variables to overwrite this value (e.g export VERSION=0.0.2) 6 | VERSION ?= 0.0.1 7 | 8 | 9 | # IMAGE_TAG_BASE defines the docker.io namespace and part of the image name for remote images. 10 | # This variable is used to construct full image tags for bundle and catalog images. 11 | # 12 | # For example, running 'make bundle-build bundle-push catalog-build catalog-push' will build and push both 13 | # kom.kkb0318.github.io/kom-bundle:$VERSION and kom.kkb0318.github.io/kom-catalog:$VERSION. 14 | IMAGE_TAG_BASE ?= kom.kkb0318.github.io/kom 15 | 16 | # Image URL to use all building/pushing image targets 17 | IMG ?= controller:latest 18 | 19 | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) 20 | ifeq (,$(shell go env GOBIN)) 21 | GOBIN=$(shell go env GOPATH)/bin 22 | else 23 | GOBIN=$(shell go env GOBIN) 24 | endif 25 | 26 | ## Location to install dependencies to 27 | LOCALBIN ?= $(shell pwd)/bin 28 | $(LOCALBIN): 29 | mkdir -p $(LOCALBIN) 30 | 31 | ## Tool Binaries 32 | KUBECTL ?= kubectl 33 | KUSTOMIZE ?= $(LOCALBIN)/kustomize 34 | CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen 35 | HELM_DOCS ?= $(LOCALBIN)/helm-docs 36 | ENVTEST ?= $(LOCALBIN)/setup-envtest 37 | HELMIFY ?= $(LOCALBIN)/helmify 38 | CRD_REF_DOCS ?= $(LOCALBIN)/crd-ref-docs 39 | 40 | ## Tool Versions 41 | KUSTOMIZE_VERSION ?= v5.6.0 42 | CONTROLLER_TOOLS_VERSION ?= v0.16.0 43 | # Set the Operator SDK version to use. By default, what is installed on the system is used. 44 | # This is useful for CI or a project to utilize a specific version of the operator-sdk toolkit. 45 | OPERATOR_SDK_VERSION ?= v1.34.1 46 | # ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. 47 | ENVTEST_K8S_VERSION = 1.32.0 48 | # CRD_REF_DOCS_VERSION 49 | CRD_REF_DOCS_VERSION = v0.1.0 50 | 51 | # Setting SHELL to bash allows bash commands to be executed by recipes. 52 | # Options are set to exit when a recipe line exits non-zero or a piped command fails. 53 | SHELL = /usr/bin/env bash -o pipefail 54 | .SHELLFLAGS = -ec 55 | 56 | .PHONY: all 57 | all: build helm generate-docs 58 | 59 | ##@ Development 60 | 61 | .PHONY: manifests 62 | manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. 63 | $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases 64 | 65 | helm: manifests kustomize helmify 66 | $(KUSTOMIZE) build config/release | $(HELMIFY) -crd-dir charts/kom 67 | 68 | 69 | .PHONY: generate 70 | generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. 71 | $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." 72 | 73 | .PHONY: fmt 74 | fmt: ## Run go fmt against code. 75 | go fmt ./... 76 | 77 | .PHONY: vet 78 | vet: ## Run go vet against code. 79 | go vet ./... 80 | 81 | .PHONY: test 82 | test: manifests generate fmt vet envtest ## Run tests. 83 | KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test ./... -coverprofile cover.out 84 | 85 | .PHONY: test-debug 86 | test-debug: envtest 87 | KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" DEBUG=true go test ./... 88 | 89 | ##@ Gen Docs 90 | 91 | .PHONY: generate-docs 92 | generate-docs: gen-helm-docs gen-crd-docs 93 | 94 | .PHONY: gen-helm-docs 95 | gen-helm-docs: $(HELM_DOCS) ## create helm docs. 96 | $(HELM_DOCS) --chart-search-root=./charts/kom 97 | 98 | .PHONY: gen-crd-docs 99 | gen-crd-docs: crd-ref-docs 100 | # $(CRD_REF_DOCS) --config=doc/config.yaml --source-path=api/ --renderer=markdown --templates-dir=doc/templates --output-path=doc/api.md 101 | $(CRD_REF_DOCS) --source-path=api/ --renderer=markdown --config=docs/config.yaml --output-path=docs/api.md 102 | 103 | ##@ Build 104 | 105 | .PHONY: build 106 | build: manifests generate fmt vet ## Build manager binary. 107 | go build -o bin/manager cmd/main.go 108 | 109 | .PHONY: run 110 | run: manifests generate fmt vet ## Run a controller from your host. 111 | go run ./cmd/main.go 112 | 113 | ##@ Deployment 114 | 115 | ifndef ignore-not-found 116 | ignore-not-found = false 117 | endif 118 | 119 | .PHONY: install 120 | install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. 121 | $(KUSTOMIZE) build config/crd | $(KUBECTL) apply -f - 122 | 123 | .PHONY: uninstall 124 | uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. 125 | $(KUSTOMIZE) build config/crd | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - 126 | 127 | .PHONY: deploy 128 | deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. 129 | cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} 130 | $(KUSTOMIZE) build config/default | $(KUBECTL) apply -f - 131 | 132 | .PHONY: undeploy 133 | undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. 134 | $(KUSTOMIZE) build config/default | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - 135 | 136 | ##@ Build Dependencies 137 | 138 | 139 | .PHONY: kustomize 140 | kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. If wrong version is installed, it will be removed before downloading. 141 | $(KUSTOMIZE): $(LOCALBIN) 142 | @if test -x $(LOCALBIN)/kustomize && ! $(LOCALBIN)/kustomize version | grep -q $(KUSTOMIZE_VERSION); then \ 143 | echo "$(LOCALBIN)/kustomize version is not expected $(KUSTOMIZE_VERSION). Removing it before installing."; \ 144 | rm -rf $(LOCALBIN)/kustomize; \ 145 | fi 146 | test -s $(LOCALBIN)/kustomize || GOBIN=$(LOCALBIN) GO111MODULE=on go install sigs.k8s.io/kustomize/kustomize/v5@$(KUSTOMIZE_VERSION) 147 | 148 | .PHONY: controller-gen 149 | controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. If wrong version is installed, it will be overwritten. 150 | $(CONTROLLER_GEN): $(LOCALBIN) 151 | test -s $(LOCALBIN)/controller-gen && $(LOCALBIN)/controller-gen --version | grep -q $(CONTROLLER_TOOLS_VERSION) || \ 152 | GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION) 153 | 154 | .PHONY: envtest 155 | envtest: $(ENVTEST) ## Download envtest-setup locally if necessary. 156 | $(ENVTEST): $(LOCALBIN) 157 | test -s $(LOCALBIN)/setup-envtest || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest 158 | 159 | .PHONY: helmify 160 | helmify: $(HELMIFY) ## Download helmify locally if necessary. 161 | $(HELMIFY): $(LOCALBIN) 162 | test -s $(LOCALBIN)/helmify || GOBIN=$(LOCALBIN) go install github.com/arttor/helmify/cmd/helmify@latest 163 | 164 | .PHONY: helm-docs 165 | helm-docs: $(HELM_DOCS) ## Download helm-docs locally if necessary. 166 | $(HELM_DOCS): $(LOCALBIN) 167 | test -s $(LOCALBIN)/helm-docs || GOBIN=$(LOCALBIN) go install github.com/norwoodj/helm-docs/cmd/helm-docs@latest 168 | 169 | .PHONY: crd-ref-docs 170 | crd-ref-docs: $(CRD_REF_DOCS) ## Download crd-ref-docs locally if necessary. 171 | $(CRD_REF_DOCS): $(LOCALBIN) 172 | test -s $(LOCALBIN)/crd-ref-docs || GOBIN=$(LOCALBIN) go install github.com/elastic/crd-ref-docs@$(CRD_REF_DOCS_VERSION) 173 | 174 | .PHONY: operator-sdk 175 | OPERATOR_SDK ?= $(LOCALBIN)/operator-sdk 176 | operator-sdk: ## Download operator-sdk locally if necessary. 177 | ifeq (,$(wildcard $(OPERATOR_SDK))) 178 | ifeq (, $(shell which operator-sdk 2>/dev/null)) 179 | @{ \ 180 | set -e ;\ 181 | mkdir -p $(dir $(OPERATOR_SDK)) ;\ 182 | OS=$(shell go env GOOS) && ARCH=$(shell go env GOARCH) && \ 183 | curl -sSLo $(OPERATOR_SDK) https://github.com/operator-framework/operator-sdk/releases/download/$(OPERATOR_SDK_VERSION)/operator-sdk_$${OS}_$${ARCH} ;\ 184 | chmod +x $(OPERATOR_SDK) ;\ 185 | } 186 | else 187 | OPERATOR_SDK = $(shell which operator-sdk) 188 | endif 189 | endif 190 | -------------------------------------------------------------------------------- /api/v1alpha1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | 3 | /* 4 | Copyright 2023. 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 | "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 25 | runtime "k8s.io/apimachinery/pkg/runtime" 26 | ) 27 | 28 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 29 | func (in *AppliedResource) DeepCopyInto(out *AppliedResource) { 30 | *out = *in 31 | } 32 | 33 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppliedResource. 34 | func (in *AppliedResource) DeepCopy() *AppliedResource { 35 | if in == nil { 36 | return nil 37 | } 38 | out := new(AppliedResource) 39 | in.DeepCopyInto(out) 40 | return out 41 | } 42 | 43 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 44 | func (in AppliedResourceList) DeepCopyInto(out *AppliedResourceList) { 45 | { 46 | in := &in 47 | *out = make(AppliedResourceList, len(*in)) 48 | copy(*out, *in) 49 | } 50 | } 51 | 52 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppliedResourceList. 53 | func (in AppliedResourceList) DeepCopy() AppliedResourceList { 54 | if in == nil { 55 | return nil 56 | } 57 | out := new(AppliedResourceList) 58 | in.DeepCopyInto(out) 59 | return *out 60 | } 61 | 62 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 63 | func (in *Chart) DeepCopyInto(out *Chart) { 64 | *out = *in 65 | if in.Values != nil { 66 | in, out := &in.Values, &out.Values 67 | *out = new(v1.JSON) 68 | (*in).DeepCopyInto(*out) 69 | } 70 | } 71 | 72 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Chart. 73 | func (in *Chart) DeepCopy() *Chart { 74 | if in == nil { 75 | return nil 76 | } 77 | out := new(Chart) 78 | in.DeepCopyInto(out) 79 | return out 80 | } 81 | 82 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 83 | func (in *Git) DeepCopyInto(out *Git) { 84 | *out = *in 85 | out.Reference = in.Reference 86 | } 87 | 88 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Git. 89 | func (in *Git) DeepCopy() *Git { 90 | if in == nil { 91 | return nil 92 | } 93 | out := new(Git) 94 | in.DeepCopyInto(out) 95 | return out 96 | } 97 | 98 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 99 | func (in *GitReference) DeepCopyInto(out *GitReference) { 100 | *out = *in 101 | } 102 | 103 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitReference. 104 | func (in *GitReference) DeepCopy() *GitReference { 105 | if in == nil { 106 | return nil 107 | } 108 | out := new(GitReference) 109 | in.DeepCopyInto(out) 110 | return out 111 | } 112 | 113 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 114 | func (in *Helm) DeepCopyInto(out *Helm) { 115 | *out = *in 116 | if in.Charts != nil { 117 | in, out := &in.Charts, &out.Charts 118 | *out = make([]Chart, len(*in)) 119 | for i := range *in { 120 | (*in)[i].DeepCopyInto(&(*out)[i]) 121 | } 122 | } 123 | } 124 | 125 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Helm. 126 | func (in *Helm) DeepCopy() *Helm { 127 | if in == nil { 128 | return nil 129 | } 130 | out := new(Helm) 131 | in.DeepCopyInto(out) 132 | return out 133 | } 134 | 135 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 136 | func (in *OperatorManager) DeepCopyInto(out *OperatorManager) { 137 | *out = *in 138 | out.TypeMeta = in.TypeMeta 139 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 140 | in.Spec.DeepCopyInto(&out.Spec) 141 | in.Status.DeepCopyInto(&out.Status) 142 | } 143 | 144 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorManager. 145 | func (in *OperatorManager) DeepCopy() *OperatorManager { 146 | if in == nil { 147 | return nil 148 | } 149 | out := new(OperatorManager) 150 | in.DeepCopyInto(out) 151 | return out 152 | } 153 | 154 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 155 | func (in *OperatorManager) DeepCopyObject() runtime.Object { 156 | if c := in.DeepCopy(); c != nil { 157 | return c 158 | } 159 | return nil 160 | } 161 | 162 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 163 | func (in *OperatorManagerList) DeepCopyInto(out *OperatorManagerList) { 164 | *out = *in 165 | out.TypeMeta = in.TypeMeta 166 | in.ListMeta.DeepCopyInto(&out.ListMeta) 167 | if in.Items != nil { 168 | in, out := &in.Items, &out.Items 169 | *out = make([]OperatorManager, len(*in)) 170 | for i := range *in { 171 | (*in)[i].DeepCopyInto(&(*out)[i]) 172 | } 173 | } 174 | } 175 | 176 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorManagerList. 177 | func (in *OperatorManagerList) DeepCopy() *OperatorManagerList { 178 | if in == nil { 179 | return nil 180 | } 181 | out := new(OperatorManagerList) 182 | in.DeepCopyInto(out) 183 | return out 184 | } 185 | 186 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 187 | func (in *OperatorManagerList) DeepCopyObject() runtime.Object { 188 | if c := in.DeepCopy(); c != nil { 189 | return c 190 | } 191 | return nil 192 | } 193 | 194 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 195 | func (in *OperatorManagerSpec) DeepCopyInto(out *OperatorManagerSpec) { 196 | *out = *in 197 | in.Resource.DeepCopyInto(&out.Resource) 198 | } 199 | 200 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorManagerSpec. 201 | func (in *OperatorManagerSpec) DeepCopy() *OperatorManagerSpec { 202 | if in == nil { 203 | return nil 204 | } 205 | out := new(OperatorManagerSpec) 206 | in.DeepCopyInto(out) 207 | return out 208 | } 209 | 210 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 211 | func (in *OperatorManagerStatus) DeepCopyInto(out *OperatorManagerStatus) { 212 | *out = *in 213 | if in.AppliedResources != nil { 214 | in, out := &in.AppliedResources, &out.AppliedResources 215 | *out = make(AppliedResourceList, len(*in)) 216 | copy(*out, *in) 217 | } 218 | } 219 | 220 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorManagerStatus. 221 | func (in *OperatorManagerStatus) DeepCopy() *OperatorManagerStatus { 222 | if in == nil { 223 | return nil 224 | } 225 | out := new(OperatorManagerStatus) 226 | in.DeepCopyInto(out) 227 | return out 228 | } 229 | 230 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 231 | func (in *Resource) DeepCopyInto(out *Resource) { 232 | *out = *in 233 | if in.Helm != nil { 234 | in, out := &in.Helm, &out.Helm 235 | *out = make([]Helm, len(*in)) 236 | for i := range *in { 237 | (*in)[i].DeepCopyInto(&(*out)[i]) 238 | } 239 | } 240 | if in.Git != nil { 241 | in, out := &in.Git, &out.Git 242 | *out = make([]Git, len(*in)) 243 | copy(*out, *in) 244 | } 245 | } 246 | 247 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Resource. 248 | func (in *Resource) DeepCopy() *Resource { 249 | if in == nil { 250 | return nil 251 | } 252 | out := new(Resource) 253 | in.DeepCopyInto(out) 254 | return out 255 | } 256 | -------------------------------------------------------------------------------- /api/v1alpha1/operatormanager_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023. 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 | "slices" 21 | 22 | apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 23 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | ) 25 | 26 | // Constants for OperatorManager configurations. 27 | const ( 28 | // OperatorManagerKind represents the kind attribute of an OperatorManager resource. 29 | OperatorManagerKind = "OperatorManager" 30 | // DefaultNamespace is the default namespace where the OperatorManager operates. 31 | DefaultNamespace = "kom-system" 32 | // ArgoCDDefaultNamespace is the default namespace for ArgoCD resources. 33 | ArgoCDDefaultNamespace = "argocd" 34 | ) 35 | 36 | // OperatorManagerSpec defines the desired state and configuration of an OperatorManager. 37 | type OperatorManagerSpec struct { 38 | // Cleanup, when enabled, allows the OperatorManager to perform garbage collection 39 | // of resources that are no longer needed or managed. 40 | // +required 41 | Cleanup bool `json:"cleanup"` 42 | 43 | // Tool specifies the GitOps tool to be used. Users must set this field to either "flux" or "argo". 44 | // This field is required and determines various default behaviors and configurations. 45 | // +required 46 | Tool ToolType `json:"tool"` 47 | 48 | // Resource specifies the source repository (Helm or Git) for the operators to be managed. 49 | // +required 50 | Resource Resource `json:"resource"` 51 | } 52 | 53 | // Resource represents the source repositories for operators, supporting both Helm and Git repositories. 54 | type Resource struct { 55 | // Helm specifies one or more Helm repositories containing the operators. 56 | // This field is optional and only needed if operators are to be sourced from Helm repositories. 57 | Helm []Helm `json:"helm,omitempty"` 58 | 59 | // Git specifies one or more Git repositories containing the operators. 60 | // This field is optional and only needed if operators are to be sourced from Git repositories. 61 | Git []Git `json:"git,omitempty"` 62 | } 63 | 64 | // Helm defines the configuration for accessing a Helm repository. 65 | // Depending on the GitOps tool in use (Flux or ArgoCD), it corresponds to a HelmRepository CR or a Secret, respectively. 66 | type Helm struct { 67 | // Name is a user-defined identifier for the resource. 68 | Name string `json:"name,omitempty"` 69 | 70 | // Namespace is the Kubernetes namespace where the Helm repository resource is located. 71 | // The default value depends on the GitOps tool used: "kom-system" for Flux and "argocd" for ArgoCD. 72 | Namespace string `json:"namespace,omitempty"` 73 | 74 | // Url is the URL of the Helm repository. 75 | Url string `json:"url,omitempty"` 76 | 77 | // Charts specifies the Helm charts within the repository to be managed. 78 | Charts []Chart `json:"charts,omitempty"` 79 | } 80 | 81 | // Chart defines the details of a Helm chart to be managed. 82 | // Depending on the GitOps tool (Flux or ArgoCD), it corresponds to a HelmRelease CR or an Application CR, respectively. 83 | type Chart struct { 84 | // Name is the name of the Helm chart. 85 | Name string `json:"name,omitempty"` 86 | 87 | // Version specifies the version of the Helm chart to be deployed. 88 | Version string `json:"version,omitempty"` 89 | 90 | // Values specifies Helm values to be passed to helm template, defined as a map. 91 | // +optional 92 | Values *apiextensionsv1.JSON `json:"values,omitempty"` 93 | } 94 | 95 | // Git defines the configuration for accessing a Git repository. 96 | type Git struct { 97 | // Name is a user-defined identifier for the resource. 98 | Name string `json:"name,omitempty"` 99 | 100 | // Namespace is the Kubernetes namespace where the Helm repository resource is located. 101 | // The default value depends on the GitOps tool used: "kom-system" for Flux and "argocd" for ArgoCD. 102 | Namespace string `json:"namespace,omitempty"` 103 | 104 | // Url is the URL of the Git repository. 105 | Url string `json:"url,omitempty"` 106 | 107 | // Path specifies the directory path within the Git repository that contains the desired resources. 108 | // This allows for selective management of resources located in specific parts of the repository. 109 | Path string `json:"path,omitempty"` 110 | 111 | // Reference contains the reference information (such as branch, tag, or semver) for the Git repository. 112 | // This allows for targeting specific versions or configurations of the resources within the repository. 113 | Reference GitReference `json:"reference,omitempty"` 114 | } 115 | 116 | // GitReference specifies the versioning information for tracking changes in the Git repository. 117 | type GitReference struct { 118 | // Type indicates the method of versioning used in the repository, applicable only for Flux. 119 | // Valid options are "branch", "semver", or "tag", allowing for different strategies of version management. 120 | Type GitReferenceType `json:"type,omitempty"` 121 | 122 | // Value specifies the exact reference to track, such as the name of a branch, a semantic versioning pattern, or a tag. 123 | // This allows for precise control over which version of the resources is deployed. 124 | Value string `json:"value,omitempty"` 125 | } 126 | 127 | // GitReferenceType is applicable only for Flux. Valid options are "branch", "semver", or "tag" 128 | type GitReferenceType string 129 | 130 | const ( 131 | GitBranch GitReferenceType = "branch" 132 | GitSemver GitReferenceType = "semver" 133 | GitTag GitReferenceType = "tag" 134 | ) 135 | 136 | // ToolType defines the GitOps tool used for managing resources Valid options are "flux", or "argo". 137 | type ToolType string 138 | 139 | const ( 140 | FluxCDTool ToolType = "flux" 141 | ArgoCDTool ToolType = "argo" 142 | ) 143 | 144 | // OperatorManagerStatus defines the observed state of OperatorManager 145 | type OperatorManagerStatus struct { 146 | // Inventory of applied resources 147 | AppliedResources AppliedResourceList `json:"appliedResources,omitempty"` 148 | } 149 | type AppliedResourceList []AppliedResource 150 | 151 | // Unique identifier for the resource, "namespace-name-kind-group-apiversion" 152 | type AppliedResource struct { 153 | // Kind of the Kubernetes resource, e.g., Deployment, Service, etc. 154 | Kind string `json:"kind"` 155 | // APIVersion of the resource, e.g., "apps/v1" 156 | APIVersion string `json:"apiVersion"` 157 | // Name of the resource 158 | Name string `json:"name"` 159 | // Namespace of the resource, if applicable 160 | Namespace string `json:"namespace,omitempty"` 161 | } 162 | 163 | func (a AppliedResource) Equal(b AppliedResource) bool { 164 | return a.Name == b.Name && 165 | a.Namespace == b.Namespace && 166 | a.Kind == b.Kind && 167 | a.APIVersion == b.APIVersion 168 | } 169 | 170 | // Diff returns the resourceList that exist in listA, but not in listB (A - B). 171 | func (listA AppliedResourceList) Diff(listB AppliedResourceList) AppliedResourceList { 172 | diff := append(AppliedResourceList{}, listA...) 173 | diff = slices.DeleteFunc(diff, func(a AppliedResource) bool { 174 | return slices.ContainsFunc(listB, func(b AppliedResource) bool { 175 | return b.Equal(a) 176 | }) 177 | }) 178 | return diff 179 | } 180 | 181 | //+kubebuilder:object:root=true 182 | //+kubebuilder:subresource:status 183 | 184 | // OperatorManager is the Schema for the operatormanagers API 185 | type OperatorManager struct { 186 | metav1.TypeMeta `json:",inline"` 187 | metav1.ObjectMeta `json:"metadata,omitempty"` 188 | 189 | Spec OperatorManagerSpec `json:"spec,omitempty"` 190 | Status OperatorManagerStatus `json:"status,omitempty"` 191 | } 192 | 193 | //+kubebuilder:object:root=true 194 | 195 | // OperatorManagerList contains a list of OperatorManager 196 | type OperatorManagerList struct { 197 | metav1.TypeMeta `json:",inline"` 198 | metav1.ListMeta `json:"metadata,omitempty"` 199 | Items []OperatorManager `json:"items"` 200 | } 201 | 202 | func init() { 203 | SchemeBuilder.Register(&OperatorManager{}, &OperatorManagerList{}) 204 | } 205 | -------------------------------------------------------------------------------- /internal/status/resources_test.go: -------------------------------------------------------------------------------- 1 | package status 2 | 3 | import ( 4 | "testing" 5 | 6 | komv1alpha1 "github.com/kkb0318/kom/api/v1alpha1" 7 | "github.com/stretchr/testify/assert" 8 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 9 | ) 10 | 11 | func TestStatus_Diff(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | oldList komv1alpha1.AppliedResourceList 15 | newList komv1alpha1.AppliedResourceList 16 | expectedDiff []*unstructured.Unstructured 17 | expectErr bool 18 | }{ 19 | { 20 | name: "Diff with one removed resource", 21 | oldList: komv1alpha1.AppliedResourceList{ 22 | {Name: "name1", Namespace: "ns1", Kind: "Kind1", APIVersion: "v1"}, 23 | {Name: "name2", Namespace: "ns2", Kind: "Kind2", APIVersion: "v2"}, 24 | }, 25 | newList: komv1alpha1.AppliedResourceList{ 26 | {Name: "name1", Namespace: "ns1", Kind: "Kind1", APIVersion: "v1"}, 27 | }, 28 | expectedDiff: []*unstructured.Unstructured{ 29 | { 30 | Object: map[string]interface{}{ 31 | "metadata": map[string]interface{}{"name": "name2", "namespace": "ns2"}, 32 | "apiVersion": "v2", "kind": "Kind2", 33 | }, 34 | }, 35 | }, 36 | }, 37 | { 38 | name: "Diff with multiple changes", 39 | oldList: komv1alpha1.AppliedResourceList{ 40 | {Name: "name1", Namespace: "ns1", Kind: "Kind1", APIVersion: "v1"}, 41 | {Name: "name2", Namespace: "ns2", Kind: "Kind2", APIVersion: "v2"}, 42 | {Name: "name3", Namespace: "ns3", Kind: "Kind3", APIVersion: "v3"}, 43 | }, 44 | newList: komv1alpha1.AppliedResourceList{ 45 | {Name: "name2", Namespace: "ns2", Kind: "Kind2", APIVersion: "v2"}, 46 | {Name: "xxxxx", Namespace: "xxxxx", Kind: "xxxxx", APIVersion: "xxxxx"}, 47 | }, 48 | expectedDiff: []*unstructured.Unstructured{ 49 | { 50 | Object: map[string]interface{}{ 51 | "metadata": map[string]interface{}{"name": "name1", "namespace": "ns1"}, 52 | "apiVersion": "v1", "kind": "Kind1", 53 | }, 54 | }, 55 | { 56 | Object: map[string]interface{}{ 57 | "metadata": map[string]interface{}{"name": "name3", "namespace": "ns3"}, 58 | "apiVersion": "v3", "kind": "Kind3", 59 | }, 60 | }, 61 | }, 62 | }, 63 | } 64 | for _, tt := range tests { 65 | t.Run(tt.name, func(t *testing.T) { 66 | actual, err := Diff(tt.oldList, tt.newList) 67 | if tt.expectErr { 68 | assert.Error(t, err, "") 69 | } else { 70 | assert.NoError(t, err) 71 | assert.Equal(t, tt.expectedDiff, actual) 72 | } 73 | }) 74 | } 75 | } 76 | 77 | func TestStatus_ToListUnstructured(t *testing.T) { 78 | testCases := []struct { 79 | name string 80 | resourceList komv1alpha1.AppliedResourceList 81 | expected []*unstructured.Unstructured 82 | expectErr bool 83 | }{ 84 | { 85 | name: "Convert valid resource list to unstructured", 86 | resourceList: komv1alpha1.AppliedResourceList{ 87 | {Name: "name1", Namespace: "ns1", Kind: "Kind1", APIVersion: "v1"}, 88 | {Name: "name2", Namespace: "ns2", Kind: "Kind2", APIVersion: "v2"}, 89 | }, 90 | expected: []*unstructured.Unstructured{ 91 | { 92 | Object: map[string]interface{}{ 93 | "metadata": map[string]interface{}{"name": "name1", "namespace": "ns1"}, 94 | "apiVersion": "v1", "kind": "Kind1", 95 | }, 96 | }, 97 | { 98 | Object: map[string]interface{}{ 99 | "metadata": map[string]interface{}{"name": "name2", "namespace": "ns2"}, 100 | "apiVersion": "v2", "kind": "Kind2", 101 | }, 102 | }, 103 | }, 104 | expectErr: false, 105 | }, 106 | { 107 | name: "Fail conversion when resource lacks kind", 108 | resourceList: komv1alpha1.AppliedResourceList{ 109 | {Name: "name1", Namespace: "ns1", Kind: "Kind1", APIVersion: "v1"}, 110 | // Omitting Kind to simulate a malformed resource 111 | {Name: "name2", Namespace: "ns2", APIVersion: "v2"}, 112 | }, 113 | expectErr: true, 114 | }, 115 | } 116 | 117 | for _, tc := range testCases { 118 | t.Run(tc.name, func(t *testing.T) { 119 | result, err := ToListUnstructured(tc.resourceList) 120 | if tc.expectErr { 121 | assert.Error(t, err) 122 | } else { 123 | assert.NoError(t, err) 124 | assert.Equal(t, tc.expected, result) 125 | } 126 | }) 127 | } 128 | } 129 | 130 | func TestStatus_ToUnstructured(t *testing.T) { 131 | testCases := []struct { 132 | name string 133 | resource komv1alpha1.AppliedResource 134 | expected *unstructured.Unstructured 135 | expectErr bool 136 | }{ 137 | { 138 | name: "Convert a valid resource to unstructured", 139 | resource: komv1alpha1.AppliedResource{Name: "name1", Namespace: "ns1", Kind: "Kind1", APIVersion: "v1"}, 140 | expected: &unstructured.Unstructured{ 141 | Object: map[string]interface{}{ 142 | "metadata": map[string]interface{}{"name": "name1", "namespace": "ns1"}, 143 | "apiVersion": "v1", "kind": "Kind1", 144 | }, 145 | }, 146 | }, 147 | { 148 | name: "Fail to convert resource with missing name", 149 | resource: komv1alpha1.AppliedResource{Name: "", Namespace: "ns1", Kind: "Kind1", APIVersion: "v1"}, 150 | expectErr: true, 151 | }, 152 | { 153 | name: "Fail to convert resource with missing namespace", 154 | resource: komv1alpha1.AppliedResource{Name: "name1", Namespace: "", Kind: "Kind1", APIVersion: "v1"}, 155 | expectErr: true, 156 | }, 157 | { 158 | name: "Fail to convert resource with missing kind", 159 | resource: komv1alpha1.AppliedResource{Name: "name1", Namespace: "ns1", Kind: "", APIVersion: "v1"}, 160 | expectErr: true, 161 | }, 162 | { 163 | name: "Fail to convert resource with missing API version", 164 | resource: komv1alpha1.AppliedResource{Name: "name1", Namespace: "ns1", Kind: "Kind1", APIVersion: ""}, 165 | expectErr: true, 166 | }, 167 | } 168 | for _, tc := range testCases { 169 | t.Run(tc.name, func(t *testing.T) { 170 | result, err := ToUnstructured(tc.resource) 171 | if tc.expectErr { 172 | assert.Error(t, err) 173 | } else { 174 | assert.NoError(t, err) 175 | assert.Equal(t, tc.expected, result) 176 | } 177 | }) 178 | } 179 | } 180 | 181 | func TestStatus_ToAppliedResource(t *testing.T) { 182 | testCases := []struct { 183 | name string 184 | input unstructured.Unstructured 185 | expected *komv1alpha1.AppliedResource 186 | expectErr bool 187 | }{ 188 | { 189 | name: "Convert a fully populated unstructured object", 190 | input: unstructured.Unstructured{ 191 | Object: map[string]interface{}{ 192 | "apiVersion": "v1", 193 | "kind": "Kind1", 194 | "metadata": map[string]interface{}{ 195 | "name": "name1", 196 | "namespace": "ns1", 197 | }, 198 | }, 199 | }, 200 | expected: &komv1alpha1.AppliedResource{ 201 | Name: "name1", 202 | Namespace: "ns1", 203 | Kind: "Kind1", 204 | APIVersion: "v1", 205 | }, 206 | expectErr: false, 207 | }, 208 | { 209 | name: "Fail to convert due to missing name", 210 | input: unstructured.Unstructured{ 211 | Object: map[string]interface{}{ 212 | "apiVersion": "v1", 213 | "kind": "Kind1", 214 | "metadata": map[string]interface{}{"namespace": "ns1"}, 215 | }, 216 | }, 217 | expectErr: true, 218 | }, 219 | { 220 | name: "Fail to convert due to missing namespace", 221 | input: unstructured.Unstructured{ 222 | Object: map[string]interface{}{ 223 | "apiVersion": "v1", 224 | "kind": "Kind1", 225 | "metadata": map[string]interface{}{"name": "name1"}, 226 | }, 227 | }, 228 | expectErr: true, 229 | }, 230 | { 231 | name: "Fail to convert due to missing kind", 232 | input: unstructured.Unstructured{ 233 | Object: map[string]interface{}{ 234 | "apiVersion": "v1", 235 | "metadata": map[string]interface{}{ 236 | "name": "name1", 237 | "namespace": "ns1", 238 | }, 239 | }, 240 | }, 241 | expectErr: true, 242 | }, 243 | { 244 | name: "Fail to convert due to missing apiVersion", 245 | input: unstructured.Unstructured{ 246 | Object: map[string]interface{}{ 247 | "kind": "Kind1", 248 | "metadata": map[string]interface{}{ 249 | "name": "name1", 250 | "namespace": "ns1", 251 | }, 252 | }, 253 | }, 254 | expectErr: true, 255 | }, 256 | } 257 | 258 | for _, tc := range testCases { 259 | t.Run(tc.name, func(t *testing.T) { 260 | result, err := ToAppliedResource(tc.input) 261 | if tc.expectErr { 262 | assert.Error(t, err) 263 | } else { 264 | assert.NoError(t, err) 265 | assert.Equal(t, tc.expected, result) 266 | } 267 | }) 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /charts/kom/crds/operatormanager-crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | annotations: 5 | controller-gen.kubebuilder.io/version: v0.14.0 6 | name: operatormanagers.kom.kkb0318.github.io 7 | spec: 8 | group: kom.kkb0318.github.io 9 | names: 10 | kind: OperatorManager 11 | listKind: OperatorManagerList 12 | plural: operatormanagers 13 | singular: operatormanager 14 | scope: Namespaced 15 | versions: 16 | - name: v1alpha1 17 | schema: 18 | openAPIV3Schema: 19 | description: OperatorManager is the Schema for the operatormanagers API 20 | properties: 21 | apiVersion: 22 | description: |- 23 | APIVersion defines the versioned schema of this representation of an object. 24 | Servers should convert recognized schemas to the latest internal value, and 25 | may reject unrecognized values. 26 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 27 | type: string 28 | kind: 29 | description: |- 30 | Kind is a string value representing the REST resource this object represents. 31 | Servers may infer this from the endpoint the client submits requests to. 32 | Cannot be updated. 33 | In CamelCase. 34 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 35 | type: string 36 | metadata: 37 | type: object 38 | spec: 39 | description: OperatorManagerSpec defines the desired state and configuration 40 | of an OperatorManager. 41 | properties: 42 | cleanup: 43 | description: |- 44 | Cleanup, when enabled, allows the OperatorManager to perform garbage collection 45 | of resources that are no longer needed or managed. 46 | type: boolean 47 | resource: 48 | description: Resource specifies the source repository (Helm or Git) 49 | for the operators to be managed. 50 | properties: 51 | git: 52 | description: |- 53 | Git specifies one or more Git repositories containing the operators. 54 | This field is optional and only needed if operators are to be sourced from Git repositories. 55 | items: 56 | description: Git defines the configuration for accessing a Git 57 | repository. 58 | properties: 59 | name: 60 | description: Name is a user-defined identifier for the resource. 61 | type: string 62 | namespace: 63 | description: |- 64 | Namespace is the Kubernetes namespace where the Helm repository resource is located. 65 | The default value depends on the GitOps tool used: "kom-system" for Flux and "argocd" for ArgoCD. 66 | type: string 67 | path: 68 | description: |- 69 | Path specifies the directory path within the Git repository that contains the desired resources. 70 | This allows for selective management of resources located in specific parts of the repository. 71 | type: string 72 | reference: 73 | description: |- 74 | Reference contains the reference information (such as branch, tag, or semver) for the Git repository. 75 | This allows for targeting specific versions or configurations of the resources within the repository. 76 | properties: 77 | type: 78 | description: |- 79 | Type indicates the method of versioning used in the repository, applicable only for Flux. 80 | Valid options are "branch", "semver", or "tag", allowing for different strategies of version management. 81 | type: string 82 | value: 83 | description: |- 84 | Value specifies the exact reference to track, such as the name of a branch, a semantic versioning pattern, or a tag. 85 | This allows for precise control over which version of the resources is deployed. 86 | type: string 87 | type: object 88 | url: 89 | description: Url is the URL of the Git repository. 90 | type: string 91 | type: object 92 | type: array 93 | helm: 94 | description: |- 95 | Helm specifies one or more Helm repositories containing the operators. 96 | This field is optional and only needed if operators are to be sourced from Helm repositories. 97 | items: 98 | description: |- 99 | Helm defines the configuration for accessing a Helm repository. 100 | Depending on the GitOps tool in use (Flux or ArgoCD), it corresponds to a HelmRepository CR or a Secret, respectively. 101 | properties: 102 | charts: 103 | description: Charts specifies the Helm charts within the 104 | repository to be managed. 105 | items: 106 | description: |- 107 | Chart defines the details of a Helm chart to be managed. 108 | Depending on the GitOps tool (Flux or ArgoCD), it corresponds to a HelmRelease CR or an Application CR, respectively. 109 | properties: 110 | name: 111 | description: Name is the name of the Helm chart. 112 | type: string 113 | values: 114 | description: Values specifies Helm values to be passed 115 | to helm template, defined as a map. 116 | x-kubernetes-preserve-unknown-fields: true 117 | version: 118 | description: Version specifies the version of the 119 | Helm chart to be deployed. 120 | type: string 121 | type: object 122 | type: array 123 | name: 124 | description: Name is a user-defined identifier for the resource. 125 | type: string 126 | namespace: 127 | description: |- 128 | Namespace is the Kubernetes namespace where the Helm repository resource is located. 129 | The default value depends on the GitOps tool used: "kom-system" for Flux and "argocd" for ArgoCD. 130 | type: string 131 | url: 132 | description: Url is the URL of the Helm repository. 133 | type: string 134 | type: object 135 | type: array 136 | type: object 137 | tool: 138 | description: |- 139 | Tool specifies the GitOps tool to be used. Users must set this field to either "flux" or "argo". 140 | This field is required and determines various default behaviors and configurations. 141 | type: string 142 | required: 143 | - cleanup 144 | - resource 145 | - tool 146 | type: object 147 | status: 148 | description: OperatorManagerStatus defines the observed state of OperatorManager 149 | properties: 150 | appliedResources: 151 | description: Inventory of applied resources 152 | items: 153 | description: Unique identifier for the resource, "namespace-name-kind-group-apiversion" 154 | properties: 155 | apiVersion: 156 | description: APIVersion of the resource, e.g., "apps/v1" 157 | type: string 158 | kind: 159 | description: Kind of the Kubernetes resource, e.g., Deployment, 160 | Service, etc. 161 | type: string 162 | name: 163 | description: Name of the resource 164 | type: string 165 | namespace: 166 | description: Namespace of the resource, if applicable 167 | type: string 168 | required: 169 | - apiVersion 170 | - kind 171 | - name 172 | type: object 173 | type: array 174 | type: object 175 | type: object 176 | served: true 177 | storage: true 178 | subresources: 179 | status: {} 180 | -------------------------------------------------------------------------------- /config/crd/bases/kom.kkb0318.github.io_operatormanagers.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.16.0 7 | name: operatormanagers.kom.kkb0318.github.io 8 | spec: 9 | group: kom.kkb0318.github.io 10 | names: 11 | kind: OperatorManager 12 | listKind: OperatorManagerList 13 | plural: operatormanagers 14 | singular: operatormanager 15 | scope: Namespaced 16 | versions: 17 | - name: v1alpha1 18 | schema: 19 | openAPIV3Schema: 20 | description: OperatorManager is the Schema for the operatormanagers 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: OperatorManagerSpec defines the desired state and configuration 41 | of an OperatorManager. 42 | properties: 43 | cleanup: 44 | description: |- 45 | Cleanup, when enabled, allows the OperatorManager to perform garbage collection 46 | of resources that are no longer needed or managed. 47 | type: boolean 48 | resource: 49 | description: Resource specifies the source repository (Helm or Git) 50 | for the operators to be managed. 51 | properties: 52 | git: 53 | description: |- 54 | Git specifies one or more Git repositories containing the operators. 55 | This field is optional and only needed if operators are to be sourced from Git repositories. 56 | items: 57 | description: Git defines the configuration for accessing a Git 58 | repository. 59 | properties: 60 | name: 61 | description: Name is a user-defined identifier for the resource. 62 | type: string 63 | namespace: 64 | description: |- 65 | Namespace is the Kubernetes namespace where the Helm repository resource is located. 66 | The default value depends on the GitOps tool used: "kom-system" for Flux and "argocd" for ArgoCD. 67 | type: string 68 | path: 69 | description: |- 70 | Path specifies the directory path within the Git repository that contains the desired resources. 71 | This allows for selective management of resources located in specific parts of the repository. 72 | type: string 73 | reference: 74 | description: |- 75 | Reference contains the reference information (such as branch, tag, or semver) for the Git repository. 76 | This allows for targeting specific versions or configurations of the resources within the repository. 77 | properties: 78 | type: 79 | description: |- 80 | Type indicates the method of versioning used in the repository, applicable only for Flux. 81 | Valid options are "branch", "semver", or "tag", allowing for different strategies of version management. 82 | type: string 83 | value: 84 | description: |- 85 | Value specifies the exact reference to track, such as the name of a branch, a semantic versioning pattern, or a tag. 86 | This allows for precise control over which version of the resources is deployed. 87 | type: string 88 | type: object 89 | url: 90 | description: Url is the URL of the Git repository. 91 | type: string 92 | type: object 93 | type: array 94 | helm: 95 | description: |- 96 | Helm specifies one or more Helm repositories containing the operators. 97 | This field is optional and only needed if operators are to be sourced from Helm repositories. 98 | items: 99 | description: |- 100 | Helm defines the configuration for accessing a Helm repository. 101 | Depending on the GitOps tool in use (Flux or ArgoCD), it corresponds to a HelmRepository CR or a Secret, respectively. 102 | properties: 103 | charts: 104 | description: Charts specifies the Helm charts within the 105 | repository to be managed. 106 | items: 107 | description: |- 108 | Chart defines the details of a Helm chart to be managed. 109 | Depending on the GitOps tool (Flux or ArgoCD), it corresponds to a HelmRelease CR or an Application CR, respectively. 110 | properties: 111 | name: 112 | description: Name is the name of the Helm chart. 113 | type: string 114 | values: 115 | description: Values specifies Helm values to be passed 116 | to helm template, defined as a map. 117 | x-kubernetes-preserve-unknown-fields: true 118 | version: 119 | description: Version specifies the version of the 120 | Helm chart to be deployed. 121 | type: string 122 | type: object 123 | type: array 124 | name: 125 | description: Name is a user-defined identifier for the resource. 126 | type: string 127 | namespace: 128 | description: |- 129 | Namespace is the Kubernetes namespace where the Helm repository resource is located. 130 | The default value depends on the GitOps tool used: "kom-system" for Flux and "argocd" for ArgoCD. 131 | type: string 132 | url: 133 | description: Url is the URL of the Helm repository. 134 | type: string 135 | type: object 136 | type: array 137 | type: object 138 | tool: 139 | description: |- 140 | Tool specifies the GitOps tool to be used. Users must set this field to either "flux" or "argo". 141 | This field is required and determines various default behaviors and configurations. 142 | type: string 143 | required: 144 | - cleanup 145 | - resource 146 | - tool 147 | type: object 148 | status: 149 | description: OperatorManagerStatus defines the observed state of OperatorManager 150 | properties: 151 | appliedResources: 152 | description: Inventory of applied resources 153 | items: 154 | description: Unique identifier for the resource, "namespace-name-kind-group-apiversion" 155 | properties: 156 | apiVersion: 157 | description: APIVersion of the resource, e.g., "apps/v1" 158 | type: string 159 | kind: 160 | description: Kind of the Kubernetes resource, e.g., Deployment, 161 | Service, etc. 162 | type: string 163 | name: 164 | description: Name of the resource 165 | type: string 166 | namespace: 167 | description: Namespace of the resource, if applicable 168 | type: string 169 | required: 170 | - apiVersion 171 | - kind 172 | - name 173 | type: object 174 | type: array 175 | type: object 176 | type: object 177 | served: true 178 | storage: true 179 | subresources: 180 | status: {} 181 | --------------------------------------------------------------------------------