├── .envrc ├── .tool-versions ├── config ├── webhook │ ├── manifests.yaml │ ├── kustomization.yaml │ ├── service.yaml │ └── kustomizeconfig.yaml ├── manager │ ├── kustomization.yaml │ └── manager.yaml ├── default │ ├── namespace.yaml │ ├── argo_config_namespace_patch.yaml │ ├── manager_image_patch.yaml │ ├── manager_prometheus_metrics_patch.yaml │ ├── manager_webhook_patch.yaml │ ├── webhookcainjection_patch.yaml │ ├── manager_auth_proxy_patch.yaml │ └── kustomization.yaml ├── samples │ └── addonmgr_v1alpha1_addon.yaml ├── rbac │ ├── auth_proxy_role_binding.yaml │ ├── leader_election_role_binding.yaml │ ├── auth_proxy_role.yaml │ ├── auth_proxy_service.yaml │ ├── kustomization.yaml │ ├── leader_election_role.yaml │ ├── role_binding.yaml │ └── role.yaml ├── argo │ ├── kustomization.yaml │ └── argo.yaml ├── crd │ ├── patches │ │ ├── cainjection_in_addons.yaml │ │ └── webhook_in_addons.yaml │ ├── kustomizeconfig.yaml │ └── kustomization.yaml └── certmanager │ ├── kustomizeconfig.yaml │ ├── kustomization.yaml │ └── certificate.yaml ├── hack ├── custom-boilerplate.go.txt ├── kind.cluster.yaml └── boilerplate.go.txt ├── api ├── addon │ ├── v1alpha1 │ │ ├── doc.go │ │ └── register.go │ └── const.go └── api-tests │ ├── suite_test.go │ ├── app_types_unit_test.go │ └── addon_types_test.go ├── docs ├── img │ └── addon-manager-arch.png └── examples │ └── eventrouter.yaml ├── pkg ├── client │ ├── clientset │ │ └── versioned │ │ │ ├── doc.go │ │ │ ├── fake │ │ │ ├── doc.go │ │ │ ├── register.go │ │ │ └── clientset_generated.go │ │ │ ├── typed │ │ │ └── addon │ │ │ │ └── v1alpha1 │ │ │ │ ├── generated_expansion.go │ │ │ │ ├── fake │ │ │ │ ├── doc.go │ │ │ │ ├── fake_addon_client.go │ │ │ │ └── fake_addon.go │ │ │ │ ├── doc.go │ │ │ │ ├── addon_client.go │ │ │ │ └── addon.go │ │ │ ├── scheme │ │ │ ├── doc.go │ │ │ └── register.go │ │ │ └── clientset.go │ ├── listers │ │ └── addon │ │ │ └── v1alpha1 │ │ │ ├── expansion_generated.go │ │ │ └── addon.go │ └── informers │ │ └── externalversions │ │ ├── internalinterfaces │ │ └── factory_interfaces.go │ │ ├── addon │ │ ├── v1alpha1 │ │ │ ├── interface.go │ │ │ └── addon.go │ │ └── interface.go │ │ ├── generic.go │ │ └── factory.go ├── common │ ├── interface.go │ ├── scheme.go │ ├── scheme_test.go │ ├── k8sutil.go │ ├── k8sutil_test.go │ ├── schemas_test.go │ ├── schemas.go │ └── helpers.go ├── addonctl │ └── addonctl_test.go ├── version │ └── version.go ├── workflows │ └── workflow_builder_test.go └── addon │ ├── addon_version_cache.go │ ├── addon_update.go │ └── addon_update_test.go ├── PROJECT ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── question.md │ ├── documentation_issue.md │ ├── feature_request.md │ └── bug_report.md ├── CONTRIBUTING.md ├── DEVELOPER.md ├── CODEOWNERS ├── dependabot.yml ├── CODE_OF_CONDUCT.md ├── workflows │ ├── push.yml │ ├── pr-gate.yml │ └── codeql.yml └── PULL_REQUEST_TEMPLATE.md ├── .editorconfig ├── .gitignore ├── .chglog ├── config.yml └── CHANGELOG.tpl.md ├── cmd └── addonctl │ └── main.go ├── Dockerfile ├── codecov.yml ├── controllers ├── controller_manager_setup.go ├── suite_test.go ├── workflow_controller.go ├── workflow_controller_test.go └── objects.go ├── main.go ├── test-bdd └── testutil │ ├── customresource.go │ ├── helpers.go │ └── addonresource.go ├── go.mod ├── .goreleaser.yml ├── test-load └── main.go └── Makefile /.envrc: -------------------------------------------------------------------------------- 1 | export GO111MODULE=on 2 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | golang 1.24.3 2 | -------------------------------------------------------------------------------- /config/webhook/manifests.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hack/custom-boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /api/addon/v1alpha1/doc.go: -------------------------------------------------------------------------------- 1 | // +k8s:deepcopy-gen=package 2 | // +groupName=addonmgr.keikoproj.io 3 | package v1alpha1 4 | -------------------------------------------------------------------------------- /docs/img/addon-manager-arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keikoproj/addon-manager/HEAD/docs/img/addon-manager-arch.png -------------------------------------------------------------------------------- /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manager.yaml 3 | apiVersion: kustomize.config.k8s.io/v1beta1 4 | kind: Kustomization 5 | -------------------------------------------------------------------------------- /config/default/namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: addon-manager 6 | name: system -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/doc.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | // This package has the automatically generated clientset. 4 | package versioned 5 | -------------------------------------------------------------------------------- /hack/kind.cluster.yaml: -------------------------------------------------------------------------------- 1 | kind: Cluster 2 | apiVersion: kind.x-k8s.io/v1alpha4 3 | nodes: 4 | - role: control-plane 5 | - role: worker 6 | - role: worker 7 | - role: worker 8 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/fake/doc.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | // This package has the automatically generated fake clientset. 4 | package fake 5 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/addon/v1alpha1/generated_expansion.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | package v1alpha1 4 | 5 | type AddonExpansion interface{} 6 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | version: "2" 2 | domain: keikoproj.io 3 | multigroup: true 4 | repo: github.com/keikoproj/addon-manager 5 | resources: 6 | - group: addonmgr 7 | version: v1alpha1 8 | kind: Addon 9 | -------------------------------------------------------------------------------- /config/samples/addonmgr_v1alpha1_addon.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: addonmgr.keikoproj.io/v1alpha1 2 | kind: Addon 3 | metadata: 4 | name: addon-sample 5 | spec: 6 | # Add fields here 7 | foo: bar 8 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/addon/v1alpha1/fake/doc.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | // Package fake has the automatically generated clients. 4 | package fake 5 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/scheme/doc.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | // This package contains the scheme of the automatically generated clientset. 4 | package scheme 5 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/addon/v1alpha1/doc.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | // This package has the automatically generated typed clients. 4 | package v1alpha1 5 | -------------------------------------------------------------------------------- /config/webhook/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manifests.yaml 3 | - service.yaml 4 | 5 | configurations: 6 | - kustomizeconfig.yaml 7 | apiVersion: kustomize.config.k8s.io/v1beta1 8 | kind: Kustomization 9 | -------------------------------------------------------------------------------- /config/webhook/service.yaml: -------------------------------------------------------------------------------- 1 | 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: webhook-service 6 | namespace: system 7 | spec: 8 | ports: 9 | - port: 443 10 | targetPort: 443 11 | selector: 12 | control-plane: addon-manager 13 | -------------------------------------------------------------------------------- /config/default/argo_config_namespace_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: workflow-controller-configmap 5 | namespace: system 6 | data: 7 | config: | 8 | namespace: addon-manager-system 9 | instanceID: addon-manager-workflow-controller 10 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: proxy-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: proxy-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: default 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: leader-election-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: Role 8 | name: leader-election-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: default 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/argo/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - argo.yaml 5 | labels: 6 | - includeSelectors: true 7 | pairs: 8 | app.kubernetes.io/managed-by: addonmgr.keikoproj.io 9 | app.kubernetes.io/name: addon-manager-argo-addon 10 | app.kubernetes.io/part-of: addon-manager-argo-addon 11 | -------------------------------------------------------------------------------- /config/default/manager_image_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller 5 | namespace: system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | # Change the value of image field below to your controller image URL 11 | - image: keikoproj/addon-manager:latest 12 | name: manager 13 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: proxy-role 5 | rules: 6 | - apiGroups: ["authentication.k8s.io"] 7 | resources: 8 | - tokenreviews 9 | verbs: ["create"] 10 | - apiGroups: ["authorization.k8s.io"] 11 | resources: 12 | - subjectaccessreviews 13 | verbs: ["create"] 14 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_addons.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | # CRD conversion requires k8s 1.16 or later. 3 | apiVersion: apiextensions.k8s.io/v1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | certmanager.k8s.io/inject-ca-from: $(NAMESPACE)/$(CERTIFICATENAME) 8 | name: addons.addonmgr.keikoproj.io 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Keiko Community Support 4 | url: https://github.com/keikoproj/community 5 | about: Please ask general questions about Keiko projects here 6 | - name: Keiko Security Issues 7 | url: https://github.com/keikoproj/addon-manager/security/policy 8 | about: Please report security vulnerabilities here 9 | -------------------------------------------------------------------------------- /pkg/client/listers/addon/v1alpha1/expansion_generated.go: -------------------------------------------------------------------------------- 1 | // Code generated by lister-gen. DO NOT EDIT. 2 | 3 | package v1alpha1 4 | 5 | // AddonListerExpansion allows custom methods to be added to 6 | // AddonLister. 7 | type AddonListerExpansion interface{} 8 | 9 | // AddonNamespaceListerExpansion allows custom methods to be added to 10 | // AddonNamespaceLister. 11 | type AddonNamespaceListerExpansion interface{} 12 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | ## How to report a bug 4 | 5 | * What did you do? (how to reproduce) 6 | * What did you see? (include logs and screenshots as appropriate) 7 | * What did you expect? 8 | 9 | ## How to contribute a bug fix 10 | 11 | * Open an issue and discuss it. 12 | * Create a pull request for your fix. 13 | 14 | ## How to suggest a new feature 15 | 16 | * Open an issue and discuss it. 17 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | annotations: 5 | prometheus.io/port: "8443" 6 | prometheus.io/scheme: https 7 | prometheus.io/scrape: "true" 8 | labels: 9 | control-plane: addon-manager 10 | name: metrics-service 11 | namespace: system 12 | spec: 13 | ports: 14 | - name: https 15 | port: 8443 16 | targetPort: https 17 | selector: 18 | control-plane: addon-manager 19 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Comment the following 3 lines if you want to disable 2 | # the auth proxy (https://github.com/brancz/kube-rbac-proxy) 3 | # which protects your /metrics endpoint. 4 | resources: 5 | - role.yaml 6 | - role_binding.yaml 7 | - leader_election_role.yaml 8 | - leader_election_role_binding.yaml 9 | - auth_proxy_service.yaml 10 | - auth_proxy_role.yaml 11 | - auth_proxy_role_binding.yaml 12 | apiVersion: kustomize.config.k8s.io/v1beta1 13 | kind: Kustomization 14 | -------------------------------------------------------------------------------- /config/certmanager/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This configuration is for teaching kustomize how to update name ref and var substitution 2 | nameReference: 3 | - kind: Issuer 4 | group: certmanager.k8s.io 5 | fieldSpecs: 6 | - kind: Certificate 7 | group: certmanager.k8s.io 8 | path: spec/issuerRef/name 9 | 10 | varReference: 11 | - kind: Certificate 12 | group: certmanager.k8s.io 13 | path: spec/commonName 14 | - kind: Certificate 15 | group: certmanager.k8s.io 16 | path: spec/dnsNames 17 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: leader-election-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - configmaps 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - create 16 | - update 17 | - patch 18 | - delete 19 | - apiGroups: 20 | - "" 21 | resources: 22 | - configmaps/status 23 | verbs: 24 | - get 25 | - update 26 | - patch 27 | -------------------------------------------------------------------------------- /.github/DEVELOPER.md: -------------------------------------------------------------------------------- 1 | # Developing 2 | 3 | Addon Manager was built using [kubebuilder](https://book.kubebuilder.io/). 4 | 5 | ## Prerequisites: 6 | * [Goreleaser](https://goreleaser.com/install/) 7 | 8 | ## Create a local cluster 9 | **Skip this is you already have a cluster** 10 | 11 | ### Prerequisites: 12 | * [kind](https://kind.sigs.k8s.io/docs/user/quick-start/#installation) 13 | 14 | Spin up a local cluster: `make kind-cluster` 15 | 16 | See [Kind cluster reference](https://book.kubebuilder.io/reference/kind.html) for more details 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | insert_final_newline = true 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | indent_style = space 12 | indent_size = 2 13 | 14 | [*.md] 15 | indent_size = 4 16 | trim_trailing_whitespace = false 17 | 18 | [{Makefile,go.mod,go.sum,*.go,.gitmodules}] 19 | indent_style = tab 20 | indent_size = 4 21 | 22 | [Dockerfile] 23 | indent_size = 4 24 | -------------------------------------------------------------------------------- /config/default/manager_prometheus_metrics_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch enables Prometheus scraping for the manager pod. 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: controller 6 | namespace: system 7 | spec: 8 | template: 9 | metadata: 10 | annotations: 11 | prometheus.io/scrape: 'true' 12 | spec: 13 | containers: 14 | # Expose the prometheus metrics on default port 15 | - name: manager 16 | ports: 17 | - containerPort: 8080 18 | name: metrics 19 | protocol: TCP 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 | group: apiextensions.k8s.io 8 | path: spec/conversion/webhookClientConfig/service/name 9 | 10 | namespace: 11 | - kind: CustomResourceDefinition 12 | group: apiextensions.k8s.io 13 | path: spec/conversion/webhookClientConfig/service/namespace 14 | create: false 15 | 16 | varReference: 17 | - path: metadata/annotations 18 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # PLEASE READ: 2 | 3 | # This is a comment. 4 | # Each line is a file pattern followed by one or more owners. 5 | 6 | # These owners will be the default owners for everything in 7 | # the repo. Unless a later match takes precedence, 8 | # review when someone opens a pull request. 9 | * @keikoproj/authorized-approvers 10 | 11 | # Admins own root and CI. 12 | .github/** @keikoproj/keiko-admins @keikoproj/keiko-maintainers @keikoproj/addon-manager-maintainers 13 | /* @keikoproj/keiko-admins @keikoproj/keiko-maintainers @keikoproj/addon-manager-maintainers 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | *.tar.gz 8 | 9 | # Test binary, build with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | coverage.html 15 | coverage.txt 16 | 17 | # Ignore xunit files 18 | **/junit.xml 19 | 20 | # Ignore project files 21 | .idea 22 | *.code-workspace 23 | .vscode/* 24 | 25 | bin/ 26 | .DS_Store 27 | *.yaml-e 28 | 29 | dist/ 30 | 31 | 32 | *.log 33 | .windsurfrules 34 | 35 | # Explicitly include test files 36 | !**/*_test.go 37 | .qodo 38 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ -------------------------------------------------------------------------------- /config/default/manager_webhook_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller 5 | namespace: system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: manager 11 | ports: 12 | - containerPort: 443 13 | name: webhook-server 14 | protocol: TCP 15 | volumeMounts: 16 | - mountPath: /tmp/k8s-webhook-server/serving-certs 17 | name: cert 18 | readOnly: true 19 | volumes: 20 | - name: cert 21 | secret: 22 | defaultMode: 420 23 | secretName: webhook-server-cert 24 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: manager-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: default 12 | namespace: system 13 | --- 14 | apiVersion: rbac.authorization.k8s.io/v1 15 | kind: RoleBinding 16 | metadata: 17 | name: rolebinding 18 | namespace: system 19 | roleRef: 20 | apiGroup: rbac.authorization.k8s.io 21 | kind: Role 22 | name: manager-role 23 | subjects: 24 | - kind: ServiceAccount 25 | name: default 26 | namespace: system 27 | -------------------------------------------------------------------------------- /config/default/webhookcainjection_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch add annotation to admission webhook config and 2 | # the variables $(NAMESPACE) and $(CERTIFICATENAME) will be substituted by kustomize. 3 | apiVersion: admissionregistration.k8s.io/v1 4 | kind: MutatingWebhookConfiguration 5 | metadata: 6 | name: mutating-webhook-configuration 7 | annotations: 8 | certmanager.k8s.io/inject-ca-from: $(NAMESPACE)/$(CERTIFICATENAME) 9 | --- 10 | apiVersion: admissionregistration.k8s.io/v1 11 | kind: ValidatingWebhookConfiguration 12 | metadata: 13 | name: validating-webhook-configuration 14 | annotations: 15 | certmanager.k8s.io/inject-ca-from: $(NAMESPACE)/$(CERTIFICATENAME) 16 | -------------------------------------------------------------------------------- /.chglog/config.yml: -------------------------------------------------------------------------------- 1 | style: github 2 | template: CHANGELOG.tpl.md 3 | info: 4 | title: CHANGELOG 5 | repository_url: https://github.com/keikoproj/addon-manager 6 | options: 7 | commits: 8 | # filters: 9 | # Type: 10 | # - feat 11 | # - fix 12 | # - perf 13 | # - refactor 14 | commit_groups: 15 | # title_maps: 16 | # feat: Features 17 | # fix: Bug Fixes 18 | # perf: Performance Improvements 19 | # refactor: Code Refactoring 20 | header: 21 | pattern: "^(\\w*)(?:\\(([\\w\\$\\.\\-\\*\\s]*)\\))?\\:\\s(.*)$" 22 | pattern_maps: 23 | - Type 24 | - Scope 25 | - Subject 26 | notes: 27 | keywords: 28 | - BREAKING CHANGE -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_addons.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables conversion webhook for CRD 2 | # CRD conversion requires k8s 1.16 or later. 3 | apiVersion: apiextensions.k8s.io/v1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | name: addons.addonmgr.keikoproj.io 7 | spec: 8 | conversion: 9 | strategy: Webhook 10 | webhookClientConfig: 11 | # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, 12 | # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) 13 | caBundle: Cg== 14 | service: 15 | namespace: system 16 | name: webhook-service 17 | path: /convert 18 | -------------------------------------------------------------------------------- /config/certmanager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - certificate.yaml 3 | 4 | # the following config is for teaching kustomize how to do var substitution 5 | vars: 6 | - fieldref: 7 | fieldPath: metadata.namespace 8 | name: NAMESPACE 9 | objref: 10 | kind: Service 11 | name: webhook-service 12 | version: v1 13 | - fieldref: {} 14 | name: CERTIFICATENAME 15 | objref: 16 | group: certmanager.k8s.io 17 | kind: Certificate 18 | name: serving-cert 19 | version: v1alpha1 20 | - fieldref: {} 21 | name: SERVICENAME 22 | objref: 23 | kind: Service 24 | name: webhook-service 25 | version: v1 26 | 27 | configurations: 28 | - kustomizeconfig.yaml 29 | apiVersion: kustomize.config.k8s.io/v1beta1 30 | kind: Kustomization 31 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/addon/v1alpha1/fake/fake_addon_client.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | package fake 4 | 5 | import ( 6 | v1alpha1 "github.com/keikoproj/addon-manager/pkg/client/clientset/versioned/typed/addon/v1alpha1" 7 | rest "k8s.io/client-go/rest" 8 | testing "k8s.io/client-go/testing" 9 | ) 10 | 11 | type FakeAddonmgrV1alpha1 struct { 12 | *testing.Fake 13 | } 14 | 15 | func (c *FakeAddonmgrV1alpha1) Addons(namespace string) v1alpha1.AddonInterface { 16 | return &FakeAddons{c, namespace} 17 | } 18 | 19 | // RESTClient returns a RESTClient that is used to communicate 20 | // with API server by this client implementation. 21 | func (c *FakeAddonmgrV1alpha1) RESTClient() rest.Interface { 22 | var ret *rest.RESTClient 23 | return ret 24 | } 25 | -------------------------------------------------------------------------------- /cmd/addonctl/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package main 16 | 17 | import "github.com/keikoproj/addon-manager/pkg/addonctl" 18 | 19 | func init() { 20 | 21 | } 22 | 23 | func main() { 24 | addonctl.Execute() 25 | } 26 | -------------------------------------------------------------------------------- /pkg/common/interface.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package common 16 | 17 | // Validator is a common interface for validators 18 | type Validator interface { 19 | Validate() (bool, error) 20 | ValidateDependencies() error 21 | } 22 | -------------------------------------------------------------------------------- /config/default/manager_auth_proxy_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch inject a sidecar container which is a HTTP proxy for the controller manager, 2 | # it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: controller 7 | namespace: system 8 | spec: 9 | template: 10 | spec: 11 | containers: 12 | - name: kube-rbac-proxy 13 | image: gcr.io/kubebuilder/kube-rbac-proxy:v0.13.1 14 | args: 15 | - "--secure-listen-address=0.0.0.0:8443" 16 | - "--upstream=http://127.0.0.1:8080/" 17 | - "--logtostderr=true" 18 | - "--v=10" 19 | ports: 20 | - containerPort: 8443 21 | name: https 22 | - name: manager 23 | args: 24 | - "--metrics-addr=127.0.0.1:8080" 25 | - "--enable-leader-election" 26 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | - package-ecosystem: "gomod" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | allow: 13 | - dependency-type: "direct" 14 | ignore: 15 | - dependency-name: "k8s.io*" 16 | update-types: ["version-update:semver-major", "version-update:semver-minor"] 17 | - dependency-name: "sigs.k8s.io*" 18 | update-types: ["version-update:semver-major", "version-update:semver-minor"] 19 | - dependency-name: "*" 20 | update-types: ["version-update:semver-major"] 21 | - package-ecosystem: "docker" 22 | directory: "/" 23 | schedule: 24 | interval: "weekly" 25 | ignore: 26 | - dependency-name: "golang" 27 | -------------------------------------------------------------------------------- /config/webhook/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # the following config is for teaching kustomize where to look at when substituting vars. 2 | # It requires kustomize v2.1.0 or newer to work properly. 3 | nameReference: 4 | - kind: Service 5 | version: v1 6 | fieldSpecs: 7 | - kind: MutatingWebhookConfiguration 8 | group: admissionregistration.k8s.io 9 | path: webhooks/clientConfig/service/name 10 | - kind: ValidatingWebhookConfiguration 11 | group: admissionregistration.k8s.io 12 | path: webhooks/clientConfig/service/name 13 | 14 | namespace: 15 | - kind: MutatingWebhookConfiguration 16 | group: admissionregistration.k8s.io 17 | path: webhooks/clientConfig/service/namespace 18 | create: true 19 | - kind: ValidatingWebhookConfiguration 20 | group: admissionregistration.k8s.io 21 | path: webhooks/clientConfig/service/namespace 22 | create: true 23 | 24 | varReference: 25 | - path: metadata/annotations 26 | -------------------------------------------------------------------------------- /config/certmanager/certificate.yaml: -------------------------------------------------------------------------------- 1 | # The following manifests contain a self-signed issuer CR and a certificate CR. 2 | # More document can be found at https://docs.cert-manager.io 3 | apiVersion: certmanager.k8s.io/v1alpha1 4 | kind: Issuer 5 | metadata: 6 | name: selfsigned-issuer 7 | namespace: system 8 | spec: 9 | selfSigned: {} 10 | --- 11 | apiVersion: certmanager.k8s.io/v1alpha1 12 | kind: Certificate 13 | metadata: 14 | name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml 15 | namespace: system 16 | spec: 17 | # $(SERVICENAME) and $(NAMESPACE) will be substituted by kustomize 18 | commonName: $(SERVICENAME).$(NAMESPACE).svc 19 | dnsNames: 20 | - $(SERVICENAME).$(NAMESPACE).svc.cluster.local 21 | issuerRef: 22 | kind: Issuer 23 | name: selfsigned-issuer 24 | secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize 25 | -------------------------------------------------------------------------------- /pkg/common/scheme.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | wfv1 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" 5 | addonmgrv1alpha1 "github.com/keikoproj/addon-manager/api/addon/v1alpha1" 6 | apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 7 | "k8s.io/apimachinery/pkg/runtime" 8 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 9 | ) 10 | 11 | var ( 12 | scheme = runtime.NewScheme() 13 | ) 14 | 15 | func init() { 16 | if err := clientgoscheme.AddToScheme(scheme); err != nil { 17 | panic(err) 18 | } 19 | 20 | if err := apiextensionsv1.AddToScheme(scheme); err != nil { 21 | panic(err) 22 | } 23 | 24 | if err := wfv1.AddToScheme(scheme); err != nil { 25 | panic(err) 26 | } 27 | 28 | if err := addonmgrv1alpha1.AddToScheme(scheme); err != nil { 29 | panic(err) 30 | } 31 | 32 | } 33 | 34 | func GetAddonMgrScheme() *runtime.Scheme { 35 | return scheme 36 | } 37 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=$BUILDPLATFORM golang:1.24 as builder 2 | 3 | ARG TAG 4 | ARG COMMIT 5 | ARG REPO_INFO 6 | ARG DATE 7 | ARG TARGETOS 8 | ARG TARGETARCH 9 | WORKDIR /workspace 10 | 11 | ADD go.mod . 12 | ADD go.sum . 13 | RUN go mod download 14 | 15 | COPY pkg/ pkg/ 16 | COPY api/ api/ 17 | COPY cmd/ cmd/ 18 | COPY controllers/ controllers/ 19 | COPY main.go main.go 20 | # Build 21 | RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH GO111MODULE=on go build -ldflags "-X 'github.com/keikoproj/addon-manager/pkg/version.GitCommit=${COMMIT}' -X 'github.com/keikoproj/addon-manager/pkg/version.BuildDate=${DATE}'" -a -o manager main.go 22 | 23 | # Use distroless as minimal base image to package the manager binary 24 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 25 | FROM gcr.io/distroless/static:nonroot AS distroless 26 | WORKDIR / 27 | COPY --from=builder /workspace/manager . 28 | USER nonroot:nonroot 29 | 30 | ENTRYPOINT ["/manager"] 31 | -------------------------------------------------------------------------------- /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/addonmgr.keikoproj.io_addons.yaml 6 | - bases/argoproj_v1alpha1_workflows.yaml 7 | # +kubebuilder:scaffold:crdkustomizeresource 8 | 9 | #patchesStrategicMerge: 10 | # [WEBHOOK] patches here are for enabling the conversion webhook for each CRD 11 | #- patches/webhook_in_addons.yaml 12 | # +kubebuilder:scaffold:crdkustomizewebhookpatch 13 | 14 | # [CAINJECTION] patches here are for enabling the CA injection for each CRD 15 | #- patches/cainjection_in_addons.yaml 16 | # +kubebuilder:scaffold:crdkustomizecainjectionpatch 17 | 18 | # the following config is for teaching kustomize how to do kustomization for CRDs. 19 | configurations: 20 | - kustomizeconfig.yaml 21 | apiVersion: kustomize.config.k8s.io/v1beta1 22 | kind: Kustomization 23 | -------------------------------------------------------------------------------- /pkg/addonctl/addonctl_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package addonctl 16 | 17 | // func TestAddonctlCreate(t *testing.T) { //dryrun 18 | // c := &cobra.Command{Use: "addonctl create"} 19 | // c.SetArgs([]string{"addon-test"}) 20 | 21 | // output, err := c.ExecuteC() 22 | // fmt.Println(output) 23 | // if err != nil { 24 | // t.Fatalf("Unexpected error: %v", err) 25 | // } 26 | // } 27 | -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/internalinterfaces/factory_interfaces.go: -------------------------------------------------------------------------------- 1 | // Code generated by informer-gen. DO NOT EDIT. 2 | 3 | package internalinterfaces 4 | 5 | import ( 6 | time "time" 7 | 8 | versioned "github.com/keikoproj/addon-manager/pkg/client/clientset/versioned" 9 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | runtime "k8s.io/apimachinery/pkg/runtime" 11 | cache "k8s.io/client-go/tools/cache" 12 | ) 13 | 14 | // NewInformerFunc takes versioned.Interface and time.Duration to return a SharedIndexInformer. 15 | type NewInformerFunc func(versioned.Interface, time.Duration) cache.SharedIndexInformer 16 | 17 | // SharedInformerFactory a small interface to allow for adding an informer without an import cycle 18 | type SharedInformerFactory interface { 19 | Start(stopCh <-chan struct{}) 20 | InformerFor(obj runtime.Object, newFunc NewInformerFunc) cache.SharedIndexInformer 21 | } 22 | 23 | // TweakListOptionsFunc is a function that transforms a v1.ListOptions. 24 | type TweakListOptionsFunc func(*v1.ListOptions) 25 | -------------------------------------------------------------------------------- /pkg/common/scheme_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package common 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/onsi/gomega" 21 | apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 22 | ) 23 | 24 | func TestGetAddonMgrScheme(t *testing.T) { 25 | g := gomega.NewGomegaWithT(t) 26 | scheme := GetAddonMgrScheme() 27 | 28 | // Verify that the scheme contains expected types 29 | g.Expect(scheme.Recognizes(apiextensionsv1.SchemeGroupVersion.WithKind("CustomResourceDefinition"))).To(gomega.BeTrue()) 30 | } 31 | -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/addon/v1alpha1/interface.go: -------------------------------------------------------------------------------- 1 | // Code generated by informer-gen. DO NOT EDIT. 2 | 3 | package v1alpha1 4 | 5 | import ( 6 | internalinterfaces "github.com/keikoproj/addon-manager/pkg/client/informers/externalversions/internalinterfaces" 7 | ) 8 | 9 | // Interface provides access to all the informers in this group version. 10 | type Interface interface { 11 | // Addons returns a AddonInformer. 12 | Addons() AddonInformer 13 | } 14 | 15 | type version struct { 16 | factory internalinterfaces.SharedInformerFactory 17 | namespace string 18 | tweakListOptions internalinterfaces.TweakListOptionsFunc 19 | } 20 | 21 | // New returns a new Interface. 22 | func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { 23 | return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} 24 | } 25 | 26 | // Addons returns a AddonInformer. 27 | func (v *version) Addons() AddonInformer { 28 | return &addonInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} 29 | } 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question or Support Request 3 | about: Ask a question or request support 4 | title: '[QUESTION] ' 5 | labels: question 6 | assignees: '' 7 | --- 8 | 9 | ## Question 10 | 11 | 12 | 13 | ## Context 14 | 15 | 16 | 17 | 18 | ## Environment (if relevant) 19 | 20 | 21 | - Version: 22 | - Kubernetes version: 23 | - Cloud Provider: 24 | - OS: 25 | 26 | ## Screenshots/Logs (if applicable) 27 | 28 | 29 | 30 | ## Related Documentation 31 | 32 | 33 | 34 | ## Search Terms 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | range: "70...90" 3 | round: down 4 | precision: 2 5 | status: 6 | project: 7 | default: 8 | target: 50% 9 | threshold: 1% 10 | informational: true 11 | paths: 12 | - "!pkg/client/" 13 | - "!api/addon/v1alpha1/zz_generated.*" 14 | patch: 15 | default: 16 | target: 50% 17 | informational: true 18 | 19 | comment: 20 | layout: "reach, diff, flags, files" 21 | behavior: default 22 | require_changes: false 23 | require_base: no 24 | require_head: no 25 | 26 | ignore: 27 | - "**/zz_generated.*" # generated code 28 | - "**/*_generated.go" # all generated code files 29 | - "bin" 30 | - "config" 31 | - "hack" 32 | - "pkg/addon/client" # generated code 33 | - "pkg/client/**" # generated client code 34 | - "**/apis/addon/v1alpha1/register.go" # boilerplate registration code 35 | - "**/apis/**/doc.go" # documentation files 36 | - "cmd/main.go" # main entry points with little testable logic 37 | - "cmd/addonctl/main.go" 38 | - "test-*/" # test directories themselves 39 | - "api/addon/v1alpha1/addon_types.go" # API types don't need extensive testing 40 | -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/addon/interface.go: -------------------------------------------------------------------------------- 1 | // Code generated by informer-gen. DO NOT EDIT. 2 | 3 | package addon 4 | 5 | import ( 6 | v1alpha1 "github.com/keikoproj/addon-manager/pkg/client/informers/externalversions/addon/v1alpha1" 7 | internalinterfaces "github.com/keikoproj/addon-manager/pkg/client/informers/externalversions/internalinterfaces" 8 | ) 9 | 10 | // Interface provides access to each of this group's versions. 11 | type Interface interface { 12 | // V1alpha1 provides access to shared informers for resources in V1alpha1. 13 | V1alpha1() v1alpha1.Interface 14 | } 15 | 16 | type group struct { 17 | factory internalinterfaces.SharedInformerFactory 18 | namespace string 19 | tweakListOptions internalinterfaces.TweakListOptionsFunc 20 | } 21 | 22 | // New returns a new Interface. 23 | func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { 24 | return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} 25 | } 26 | 27 | // V1alpha1 returns a new v1alpha1.Interface. 28 | func (g *group) V1alpha1() v1alpha1.Interface { 29 | return v1alpha1.New(g.factory, g.namespace, g.tweakListOptions) 30 | } 31 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Adds namespace to all resources. 2 | namespace: addon-manager-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: addon-manager- 10 | 11 | # Labels to add to all resources and selectors. 12 | #commonLabels: 13 | # someName: someValue 14 | 15 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in crd/kustomization.yaml 16 | #- ../webhook 17 | # [CERTMANAGER] To enable cert-manager, uncomment next line. 'WEBHOOK' components are required. 18 | #- ../certmanager 19 | 20 | resources: 21 | - namespace.yaml 22 | - ../crd 23 | - ../rbac 24 | - ../manager 25 | - ../argo 26 | 27 | # Protect the /metrics endpoint by putting it behind auth. 28 | # Only one of manager_auth_proxy_patch.yaml and 29 | # manager_prometheus_metrics_patch.yaml should be enabled. 30 | apiVersion: kustomize.config.k8s.io/v1beta1 31 | kind: Kustomization 32 | patches: 33 | - path: manager_image_patch.yaml 34 | - path: argo_config_namespace_patch.yaml 35 | - path: manager_auth_proxy_patch.yaml 36 | -------------------------------------------------------------------------------- /pkg/version/version.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package version 16 | 17 | import "fmt" 18 | 19 | // The below variables will be overrriden using ldflags set by goreleaser during the build process 20 | var ( 21 | // Version is the version string 22 | Version = "v0.9.0" 23 | 24 | // GitCommit is the git commit hash 25 | GitCommit = "NONE" 26 | 27 | // BuildDate is the date of the build 28 | BuildDate = "UNKNOWN" 29 | ) 30 | 31 | const versionStringFmt = `{"version": "%s", "gitCommit": "%s", "buildDate": "%s"}` 32 | 33 | // ToString returns the output of Version, GitCommit, BuildDate 34 | func ToString() string { 35 | return fmt.Sprintf(versionStringFmt, Version, GitCommit, BuildDate) 36 | } 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation_issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Documentation Issue 3 | about: Report issues with documentation or suggest improvements 4 | title: '[DOCS] ' 5 | labels: documentation 6 | assignees: '' 7 | --- 8 | 9 | ## Documentation Issue 10 | 11 | 12 | 13 | 14 | ## Page/Location 15 | 16 | 17 | 18 | 19 | ## Suggested Changes 20 | 21 | 22 | 23 | 24 | ## Additional Information 25 | 26 | 27 | 28 | 29 | ## Would you be willing to contribute this documentation improvement? 30 | 31 | 32 | - [ ] Yes, I can submit a PR with the changes 33 | - [ ] No, I'm not able to contribute documentation for this 34 | -------------------------------------------------------------------------------- /api/api-tests/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package apitests 16 | 17 | import ( 18 | "testing" 19 | 20 | . "github.com/onsi/ginkgo/v2" 21 | . "github.com/onsi/gomega" 22 | logf "sigs.k8s.io/controller-runtime/pkg/log" 23 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 24 | ) 25 | 26 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 27 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 28 | 29 | func TestAPIs(t *testing.T) { 30 | RegisterFailHandler(Fail) 31 | 32 | RunSpecs(t, "v1alpha1 Suite") 33 | } 34 | 35 | var _ = BeforeSuite(func() { 36 | logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) 37 | }) 38 | 39 | var _ = AfterSuite(func() { 40 | By("tearing down the test environment") 41 | }) 42 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | We welcome participation from individuals and groups of all backgrounds who want to benefit the broader open source community 4 | through participation in this project. We are dedicated to ensuring a productive, safe and educational experience for all. 5 | 6 | # Guidelines 7 | 8 | Be welcoming 9 | * Make it easy for new members to learn and contribute. Help them along the path. Don't make them jump through hoops. 10 | 11 | Be considerate 12 | * There is a live person at the other end of the Internet. Consider how your comments will affect them. It is often better to give a quick but useful reply than to delay to compose a more thorough reply. 13 | 14 | Be respectful 15 | * Not everyone is Linus Torvalds, and this is probably a good thing :) but everyone is deserving of respect and consideration for wanting to benefit the broader community. Criticize ideas but respect the person. Saying something positive before you criticize lets the other person know that your criticism is not personal. 16 | 17 | Be patient 18 | * We have diverse backgrounds. It will take time and effort to understand each others' points of view. Some of us have day jobs and other responsibilities and may take time to respond to requests. 19 | 20 | # Relevant References 21 | * http://contributor-covenant.org/version/1/4/code_of_conduct.md 22 | * http://contributor-covenant.org/ -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/fake/register.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | package fake 4 | 5 | import ( 6 | addonmgrv1alpha1 "github.com/keikoproj/addon-manager/api/addon/v1alpha1" 7 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | runtime "k8s.io/apimachinery/pkg/runtime" 9 | schema "k8s.io/apimachinery/pkg/runtime/schema" 10 | serializer "k8s.io/apimachinery/pkg/runtime/serializer" 11 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 12 | ) 13 | 14 | var scheme = runtime.NewScheme() 15 | var codecs = serializer.NewCodecFactory(scheme) 16 | 17 | var localSchemeBuilder = runtime.SchemeBuilder{ 18 | addonmgrv1alpha1.AddToScheme, 19 | } 20 | 21 | // AddToScheme adds all types of this clientset into the given scheme. This allows composition 22 | // of clientsets, like in: 23 | // 24 | // import ( 25 | // "k8s.io/client-go/kubernetes" 26 | // clientsetscheme "k8s.io/client-go/kubernetes/scheme" 27 | // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" 28 | // ) 29 | // 30 | // kclientset, _ := kubernetes.NewForConfig(c) 31 | // _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) 32 | // 33 | // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types 34 | // correctly. 35 | var AddToScheme = localSchemeBuilder.AddToScheme 36 | 37 | func init() { 38 | v1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"}) 39 | utilruntime.Must(AddToScheme(scheme)) 40 | } 41 | -------------------------------------------------------------------------------- /.chglog/CHANGELOG.tpl.md: -------------------------------------------------------------------------------- 1 | {{ if .Versions -}} 2 | 3 | ## [Unreleased] 4 | 5 | {{ if .Unreleased.CommitGroups -}} 6 | {{ range .Unreleased.CommitGroups -}} 7 | ### {{ .Title }} 8 | {{ range .Commits -}} 9 | - {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }} 10 | {{ end }} 11 | {{ end -}} 12 | {{ end -}} 13 | {{ end -}} 14 | 15 | {{ range .Versions }} 16 | 17 | ## {{ if .Tag.Previous }}[{{ .Tag.Name }}]{{ else }}{{ .Tag.Name }}{{ end }} - {{ datetime "2006-01-02" .Tag.Date }} 18 | {{ range .CommitGroups -}} 19 | ### {{ .Title }} 20 | {{ range .Commits -}} 21 | - {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }} 22 | {{ end }} 23 | {{ end -}} 24 | 25 | {{- if .RevertCommits -}} 26 | ### Reverts 27 | {{ range .RevertCommits -}} 28 | - {{ .Revert.Header }} 29 | {{ end }} 30 | {{ end -}} 31 | 32 | {{- if .MergeCommits -}} 33 | ### Pull Requests 34 | {{ range .MergeCommits -}} 35 | - {{ .Header }} 36 | {{ end }} 37 | {{ end -}} 38 | 39 | {{- if .NoteGroups -}} 40 | {{ range .NoteGroups -}} 41 | ### {{ .Title }} 42 | {{ range .Notes }} 43 | {{ .Body }} 44 | {{ end }} 45 | {{ end -}} 46 | {{ end -}} 47 | {{ end -}} 48 | 49 | {{- if .Versions }} 50 | [Unreleased]: {{ .Info.RepositoryURL }}/compare/{{ $latest := index .Versions 0 }}{{ $latest.Tag.Name }}...HEAD 51 | {{ range .Versions -}} 52 | {{ if .Tag.Previous -}} 53 | [{{ .Tag.Name }}]: {{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }} 54 | {{ end -}} 55 | {{ end -}} 56 | {{ end -}} -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an enhancement or new feature 4 | title: '[FEATURE] ' 5 | labels: enhancement 6 | assignees: '' 7 | --- 8 | 9 | ## Problem Statement 10 | 11 | 12 | 13 | 14 | ## Proposed Solution 15 | 16 | 17 | 18 | 19 | ## Alternatives Considered 20 | 21 | 22 | 23 | 24 | ## User Value 25 | 26 | 27 | 28 | 29 | ## Implementation Ideas 30 | 31 | 32 | 33 | 34 | ## Additional Context 35 | 36 | 37 | 38 | ## Would you be willing to contribute this feature? 39 | 40 | 41 | - [ ] Yes, I'd like to implement this feature 42 | - [ ] I could contribute partially 43 | - [ ] No, I'm not able to contribute code for this 44 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/scheme/register.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | package scheme 4 | 5 | import ( 6 | addonmgrv1alpha1 "github.com/keikoproj/addon-manager/api/addon/v1alpha1" 7 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | runtime "k8s.io/apimachinery/pkg/runtime" 9 | schema "k8s.io/apimachinery/pkg/runtime/schema" 10 | serializer "k8s.io/apimachinery/pkg/runtime/serializer" 11 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 12 | ) 13 | 14 | var Scheme = runtime.NewScheme() 15 | var Codecs = serializer.NewCodecFactory(Scheme) 16 | var ParameterCodec = runtime.NewParameterCodec(Scheme) 17 | var localSchemeBuilder = runtime.SchemeBuilder{ 18 | addonmgrv1alpha1.AddToScheme, 19 | } 20 | 21 | // AddToScheme adds all types of this clientset into the given scheme. This allows composition 22 | // of clientsets, like in: 23 | // 24 | // import ( 25 | // "k8s.io/client-go/kubernetes" 26 | // clientsetscheme "k8s.io/client-go/kubernetes/scheme" 27 | // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" 28 | // ) 29 | // 30 | // kclientset, _ := kubernetes.NewForConfig(c) 31 | // _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) 32 | // 33 | // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types 34 | // correctly. 35 | var AddToScheme = localSchemeBuilder.AddToScheme 36 | 37 | func init() { 38 | v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"}) 39 | utilruntime.Must(AddToScheme(Scheme)) 40 | } 41 | -------------------------------------------------------------------------------- /controllers/controller_manager_setup.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | addonapiv1 "github.com/keikoproj/addon-manager/api/addon" 8 | "github.com/keikoproj/addon-manager/pkg/addon" 9 | "github.com/keikoproj/addon-manager/pkg/common" 10 | "k8s.io/client-go/dynamic" 11 | "k8s.io/client-go/dynamic/dynamicinformer" 12 | "sigs.k8s.io/controller-runtime/pkg/manager" 13 | ) 14 | 15 | func New(mgr manager.Manager) error { 16 | versionCache := addon.NewAddonVersionCacheClient() 17 | dynClient := dynamic.NewForConfigOrDie(mgr.GetConfig()) 18 | nsInformers := dynamicinformer.NewFilteredDynamicSharedInformerFactory(dynClient, addonapiv1.AddonResyncPeriod, addonapiv1.ManagedNameSpace, nil) 19 | wfInf := nsInformers.ForResource(common.WorkflowGVR()).Informer() 20 | addonUpdater := addon.NewAddonUpdater(mgr.GetEventRecorderFor("addons"), mgr.GetClient(), versionCache, mgr.GetLogger()) 21 | if err := mgr.Add(manager.RunnableFunc(func(ctx context.Context) error { 22 | nsInformers.Start(ctx.Done()) 23 | nsInformers.WaitForCacheSync(ctx.Done()) 24 | return nil 25 | })); err != nil { 26 | return fmt.Errorf("failed to run informer sync: %w", err) 27 | } 28 | 29 | if _, err := NewAddonController(mgr, dynClient, wfInf, versionCache, addonUpdater); err != nil { 30 | return fmt.Errorf("failed to create addon controller: %w", err) 31 | } 32 | 33 | if err := NewWFController(mgr, dynClient, addonUpdater); err != nil { 34 | return fmt.Errorf("failed to create addon wf controller: %w", err) 35 | } 36 | 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /api/addon/const.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package addon 16 | 17 | import "time" 18 | 19 | // Addon constants 20 | const ( 21 | Group string = "addonmgr.keikoproj.io" 22 | Version string = "v1alpha1" 23 | APIVersion string = Group + "/" + Version 24 | AddonKind string = "Addon" 25 | AddonSingular string = "addon" 26 | AddonPlural string = "addons" 27 | AddonShortName string = "addon" 28 | AddonFullName string = AddonPlural + "." + Group 29 | 30 | ManagedNameSpace string = "addon-manager-system" 31 | 32 | AddonResyncPeriod = 20 * time.Minute 33 | CacheSyncTimeout = 5 * time.Minute 34 | 35 | FinalizerName = "delete.addonmgr.keikoproj.io" 36 | 37 | ResourceDefaultManageByLabel = "app.kubernetes.io/managed-by" 38 | ResourceDefaultOwnLabel = "app.kubernetes.io/name" 39 | ResourceDefaultPartLabel = "app.kubernetes.io/part-of" 40 | ResourceDefaultVersionLabel = "app.kubernetes.io/version" 41 | 42 | TTL = time.Duration(1) * time.Hour // 1 hour 43 | 44 | ) 45 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Report a bug in the project 4 | title: '[BUG] ' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | ## Bug Description 10 | 11 | 12 | 13 | ## Steps To Reproduce 14 | 15 | 16 | 17 | 1. Step one 18 | 2. Step two 19 | 3. Step three 20 | 21 | ## Expected Behavior 22 | 23 | 24 | 25 | ## Actual Behavior 26 | 27 | 28 | 29 | ## Screenshots/Logs 30 | 31 | 32 | 33 | ## Environment 34 | 35 | 36 | 37 | - Version: 38 | - Kubernetes version: 39 | - Cloud Provider: 40 | - Installation method: 41 | - OS: 42 | - Browser (if applicable): 43 | 44 | ## Additional Context 45 | 46 | 47 | 48 | 49 | ## Impact 50 | 51 | 52 | 53 | - [ ] Blocking (cannot proceed with work) 54 | - [ ] High (significant workaround needed) 55 | - [ ] Medium (minor workaround needed) 56 | - [ ] Low (inconvenience) 57 | 58 | ## Possible Solution 59 | 60 | 61 | -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/generic.go: -------------------------------------------------------------------------------- 1 | // Code generated by informer-gen. DO NOT EDIT. 2 | 3 | package externalversions 4 | 5 | import ( 6 | "fmt" 7 | 8 | v1alpha1 "github.com/keikoproj/addon-manager/api/addon/v1alpha1" 9 | schema "k8s.io/apimachinery/pkg/runtime/schema" 10 | cache "k8s.io/client-go/tools/cache" 11 | ) 12 | 13 | // GenericInformer is type of SharedIndexInformer which will locate and delegate to other 14 | // sharedInformers based on type 15 | type GenericInformer interface { 16 | Informer() cache.SharedIndexInformer 17 | Lister() cache.GenericLister 18 | } 19 | 20 | type genericInformer struct { 21 | informer cache.SharedIndexInformer 22 | resource schema.GroupResource 23 | } 24 | 25 | // Informer returns the SharedIndexInformer. 26 | func (f *genericInformer) Informer() cache.SharedIndexInformer { 27 | return f.informer 28 | } 29 | 30 | // Lister returns the GenericLister. 31 | func (f *genericInformer) Lister() cache.GenericLister { 32 | return cache.NewGenericLister(f.Informer().GetIndexer(), f.resource) 33 | } 34 | 35 | // ForResource gives generic access to a shared informer of the matching type 36 | // TODO extend this to unknown resources with a client pool 37 | func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) { 38 | switch resource { 39 | // Group=addonmgr.keikoproj.io, Version=v1alpha1 40 | case v1alpha1.SchemeGroupVersion.WithResource("addons"): 41 | return &genericInformer{resource: resource.GroupResource(), informer: f.Addonmgr().V1alpha1().Addons().Informer()}, nil 42 | 43 | } 44 | 45 | return nil, fmt.Errorf("no informer found for %v", resource) 46 | } 47 | -------------------------------------------------------------------------------- /pkg/common/k8sutil.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package common 16 | 17 | import ( 18 | "time" 19 | 20 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 21 | "k8s.io/apimachinery/pkg/runtime" 22 | "k8s.io/client-go/dynamic" 23 | "k8s.io/client-go/dynamic/dynamicinformer" 24 | "k8s.io/client-go/tools/cache" 25 | 26 | wfv1 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" 27 | ) 28 | 29 | func NewWorkflowInformer(dclient dynamic.Interface, ns string, resyncPeriod time.Duration) cache.SharedIndexInformer { 30 | var nsInformers = dynamicinformer.NewFilteredDynamicSharedInformerFactory(dclient, resyncPeriod, ns, nil) 31 | return nsInformers.ForResource(WorkflowGVR()).Informer() 32 | } 33 | 34 | // ToUnstructured converts an workflow to an Unstructured object 35 | func ToUnstructured(wf *wfv1.Workflow) (*unstructured.Unstructured, error) { 36 | obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(wf) 37 | if err != nil { 38 | return nil, err 39 | } 40 | un := &unstructured.Unstructured{Object: obj} 41 | // we need to add these values so that the `EventRecorder` does not error 42 | un.SetKind("Workflow") 43 | un.SetAPIVersion("argoproj.io/v1alpha1") 44 | return un, nil 45 | } 46 | -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | name: push 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | tags: 7 | - 'v[0-9]+.[0-9]+.[0-9]+' 8 | 9 | jobs: 10 | release: 11 | if: github.repository_owner == 'keikoproj' 12 | runs-on: ubuntu-latest 13 | env: 14 | flags: "" 15 | steps: 16 | - if: ${{ !startsWith(github.ref, 'refs/tags/v') }} 17 | run: echo "flags=--snapshot" >> $GITHUB_ENV 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | 23 | - name: Set up Go 24 | uses: actions/setup-go@v5 25 | with: 26 | go-version: 1.24 27 | cache: true 28 | 29 | - name: Download Kubebuilder 30 | run: | 31 | curl -L -o kubebuilder_2.3.2_linux_amd64.tar.gz https://github.com/kubernetes-sigs/kubebuilder/releases/download/v2.3.2/kubebuilder_2.3.2_linux_amd64.tar.gz 32 | tar -zxvf kubebuilder_2.3.2_linux_amd64.tar.gz 33 | sudo mv kubebuilder_2.3.2_linux_amd64 /usr/local/kubebuilder 34 | 35 | - name: Set up QEMU 36 | uses: docker/setup-qemu-action@v3 37 | with: 38 | platforms: arm64,amd64 39 | 40 | - name: Set up Docker Buildx 41 | id: buildx 42 | uses: docker/setup-buildx-action@v3 43 | with: 44 | install: true 45 | 46 | - name: Login to DockerHub 47 | uses: docker/login-action@v3 48 | with: 49 | username: ${{ secrets.DOCKER_USERNAME }} 50 | password: ${{ secrets.DOCKER_PASSWORD }} 51 | 52 | - name: Available platforms 53 | run: echo ${{ steps.buildx.outputs.platforms }} 54 | 55 | - name: Run GoReleaser 56 | uses: goreleaser/goreleaser-action@v6 57 | with: 58 | version: latest 59 | args: release --clean ${{ env.flags }} -f .goreleaser.yml 60 | env: 61 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 62 | -------------------------------------------------------------------------------- /pkg/common/k8sutil_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package common 16 | 17 | import ( 18 | "testing" 19 | "time" 20 | 21 | wfv1 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" 22 | "github.com/onsi/gomega" 23 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | "k8s.io/client-go/dynamic/fake" 25 | ) 26 | 27 | func TestToUnstructured(t *testing.T) { 28 | g := gomega.NewGomegaWithT(t) 29 | 30 | // Create a test workflow 31 | wf := &wfv1.Workflow{ 32 | ObjectMeta: metav1.ObjectMeta{ 33 | Name: "test-workflow", 34 | Namespace: "default", 35 | }, 36 | Spec: wfv1.WorkflowSpec{ 37 | Templates: []wfv1.Template{ 38 | { 39 | Name: "test-template", 40 | }, 41 | }, 42 | }, 43 | } 44 | 45 | // Convert to unstructured 46 | un, err := ToUnstructured(wf) 47 | g.Expect(err).To(gomega.BeNil()) 48 | g.Expect(un).ToNot(gomega.BeNil()) 49 | g.Expect(un.GetKind()).To(gomega.Equal("Workflow")) 50 | g.Expect(un.GetAPIVersion()).To(gomega.Equal("argoproj.io/v1alpha1")) 51 | g.Expect(un.GetName()).To(gomega.Equal("test-workflow")) 52 | } 53 | 54 | func TestNewWorkflowInformer(t *testing.T) { 55 | g := gomega.NewGomegaWithT(t) 56 | 57 | // Create a fake dynamic client 58 | dclient := fake.NewSimpleDynamicClient(GetAddonMgrScheme()) 59 | 60 | // Create the informer 61 | informer := NewWorkflowInformer(dclient, "default", 10*time.Minute) 62 | 63 | // Verify it's not nil 64 | g.Expect(informer).ToNot(gomega.BeNil()) 65 | } 66 | -------------------------------------------------------------------------------- /api/api-tests/app_types_unit_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package apitests 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/onsi/gomega" 21 | 22 | addonv1 "github.com/keikoproj/addon-manager/api/addon/v1alpha1" 23 | ) 24 | 25 | func TestAPIFunctions(t *testing.T) { 26 | g := gomega.NewGomegaWithT(t) 27 | tests := []struct { 28 | phase addonv1.ApplicationAssemblyPhase 29 | expected bool 30 | }{ 31 | { 32 | phase: addonv1.Succeeded, 33 | expected: true, 34 | }, 35 | { 36 | phase: addonv1.Pending, 37 | expected: false, 38 | }, 39 | } 40 | 41 | for _, tc := range tests { 42 | res := tc.phase.Completed() 43 | g.Expect(res).To(gomega.Equal(tc.expected)) 44 | } 45 | 46 | tests = []struct { 47 | phase addonv1.ApplicationAssemblyPhase 48 | expected bool 49 | }{ 50 | { 51 | phase: addonv1.Succeeded, 52 | expected: true, 53 | }, 54 | { 55 | phase: addonv1.Failed, 56 | expected: false, 57 | }, 58 | } 59 | 60 | for _, tc := range tests { 61 | res := tc.phase.Succeeded() 62 | g.Expect(res).To(gomega.Equal(tc.expected)) 63 | } 64 | 65 | tests = []struct { 66 | phase addonv1.ApplicationAssemblyPhase 67 | expected bool 68 | }{ 69 | { 70 | phase: addonv1.Deleting, 71 | expected: true, 72 | }, 73 | { 74 | phase: addonv1.Failed, 75 | expected: false, 76 | }, 77 | } 78 | 79 | for _, tc := range tests { 80 | res := tc.phase.Deleting() 81 | g.Expect(res).To(gomega.Equal(tc.expected)) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | ## What type of PR is this? 10 | 11 | 12 | - [ ] Bug fix (non-breaking change which fixes an issue) 13 | - [ ] Feature/Enhancement (non-breaking change which adds functionality) 14 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 15 | - [ ] Documentation update 16 | - [ ] Refactoring (no functional changes) 17 | - [ ] Performance improvement 18 | - [ ] Test updates 19 | - [ ] CI/CD related changes 20 | - [ ] Dependency upgrade 21 | 22 | ## Description 23 | 24 | 25 | ## Related issue(s) 26 | 27 | 28 | ## High-level overview of changes 29 | 33 | 34 | ## Testing performed 35 | 42 | 43 | ## Checklist 44 | 45 | 46 | - [ ] I've read the [CONTRIBUTING](/CONTRIBUTING.md) doc 47 | - [ ] I've added/updated tests that prove my fix is effective or that my feature works 48 | - [ ] I've added necessary documentation (if appropriate) 49 | - [ ] I've run `make test` locally and all tests pass 50 | - [ ] I've signed-off my commits with `git commit -s` for DCO verification 51 | - [ ] I've updated any relevant documentation 52 | - [ ] Code follows the style guidelines of this project 53 | 54 | ## Additional information 55 | 59 | -------------------------------------------------------------------------------- /pkg/common/schemas_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package common 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/onsi/gomega" 21 | ) 22 | 23 | func TestAddonGVR(t *testing.T) { 24 | g := gomega.NewGomegaWithT(t) 25 | gvr := AddonGVR() 26 | 27 | g.Expect(gvr.Group).To(gomega.Equal("addonmgr.keikoproj.io")) 28 | g.Expect(gvr.Version).To(gomega.Equal("v1alpha1")) 29 | g.Expect(gvr.Resource).To(gomega.Equal("addons")) 30 | } 31 | 32 | func TestCRDGVR(t *testing.T) { 33 | g := gomega.NewGomegaWithT(t) 34 | gvr := CRDGVR() 35 | 36 | g.Expect(gvr.Group).To(gomega.Equal("apiextensions.k8s.io")) 37 | g.Expect(gvr.Version).To(gomega.Equal("v1")) 38 | g.Expect(gvr.Resource).To(gomega.Equal("customresourcedefinitions")) 39 | } 40 | 41 | func TestSecretGVR(t *testing.T) { 42 | g := gomega.NewGomegaWithT(t) 43 | gvr := SecretGVR() 44 | 45 | g.Expect(gvr.Group).To(gomega.Equal("")) 46 | g.Expect(gvr.Version).To(gomega.Equal("v1")) 47 | g.Expect(gvr.Resource).To(gomega.Equal("secrets")) 48 | } 49 | 50 | func TestWorkflowGVR(t *testing.T) { 51 | g := gomega.NewGomegaWithT(t) 52 | gvr := WorkflowGVR() 53 | 54 | g.Expect(gvr.Group).To(gomega.Equal("argoproj.io")) 55 | g.Expect(gvr.Version).To(gomega.Equal("v1alpha1")) 56 | g.Expect(gvr.Resource).To(gomega.Equal("workflows")) 57 | } 58 | 59 | func TestWorkflowType(t *testing.T) { 60 | g := gomega.NewGomegaWithT(t) 61 | wf := WorkflowType() 62 | 63 | g.Expect(wf.GetKind()).To(gomega.Equal("Workflow")) 64 | g.Expect(wf.GetAPIVersion()).To(gomega.Equal("argoproj.io/v1alpha1")) 65 | } 66 | -------------------------------------------------------------------------------- /api/addon/v1alpha1/register.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package v1alpha1 16 | 17 | import ( 18 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 19 | runtime "k8s.io/apimachinery/pkg/runtime" 20 | "k8s.io/apimachinery/pkg/runtime/schema" 21 | ) 22 | 23 | // SchemeGroupVersion is group version used to register these objects 24 | var ( 25 | SchemeGroupVersion = schema.GroupVersion{Group: "addonmgr.keikoproj.io", Version: "v1alpha1"} 26 | AddonSchemaGroupVersionKind = schema.GroupVersionKind{Group: "addonmgr.keikoproj.io", Version: "v1alpha1", Kind: "Addon"} 27 | ) 28 | 29 | // Kind takes an unqualified kind and returns back a Group qualified GroupKind 30 | func Kind(kind string) schema.GroupKind { 31 | return SchemeGroupVersion.WithKind(kind).GroupKind() 32 | } 33 | 34 | // Resource takes an unqualified resource and returns a Group-qualified GroupResource. 35 | func Resource(resource string) schema.GroupResource { 36 | return SchemeGroupVersion.WithResource(resource).GroupResource() 37 | } 38 | 39 | var ( 40 | 41 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 42 | SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) 43 | 44 | // AddToScheme adds the types in this group-version to the given scheme. 45 | AddToScheme = SchemeBuilder.AddToScheme 46 | ) 47 | 48 | // addKnownTypes adds the set of types defined in this package to the supplied scheme. 49 | func addKnownTypes(scheme *runtime.Scheme) error { 50 | scheme.AddKnownTypes(SchemeGroupVersion, 51 | &Addon{}, 52 | &AddonList{}, 53 | ) 54 | metav1.AddToGroupVersion(scheme, SchemeGroupVersion) 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /pkg/common/schemas.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package common 16 | 17 | import ( 18 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 19 | "k8s.io/apimachinery/pkg/runtime/schema" 20 | ) 21 | 22 | // AddonGVR returns the schema representation of the addon resource 23 | func AddonGVR() schema.GroupVersionResource { 24 | return schema.GroupVersionResource{ 25 | Group: "addonmgr.keikoproj.io", 26 | Version: "v1alpha1", 27 | Resource: "addons", 28 | } 29 | } 30 | 31 | // CRDGVR returns the schema representation for customresourcedefinitions 32 | func CRDGVR() schema.GroupVersionResource { 33 | return schema.GroupVersionResource{ 34 | Group: "apiextensions.k8s.io", 35 | Version: "v1", 36 | Resource: "customresourcedefinitions", 37 | } 38 | } 39 | 40 | // SecretGVR returns the schema representation of the secret resource 41 | func SecretGVR() schema.GroupVersionResource { 42 | return schema.GroupVersionResource{ 43 | Group: "", 44 | Version: "v1", 45 | Resource: "secrets", 46 | } 47 | } 48 | 49 | // WorkflowGVR returns the schema representation of the workflow resource 50 | func WorkflowGVR() schema.GroupVersionResource { 51 | return schema.GroupVersionResource{ 52 | Group: "argoproj.io", 53 | Version: "v1alpha1", 54 | Resource: "workflows", 55 | } 56 | } 57 | 58 | // WorkflowType return an unstructured workflow type object 59 | func WorkflowType() *unstructured.Unstructured { 60 | wf := &unstructured.Unstructured{} 61 | wf.SetGroupVersionKind(schema.GroupVersionKind{ 62 | Kind: "Workflow", 63 | Group: "argoproj.io", 64 | Version: "v1alpha1", 65 | }) 66 | return wf 67 | } 68 | -------------------------------------------------------------------------------- /config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: service 5 | namespace: system 6 | labels: 7 | control-plane: addon-manager 8 | spec: 9 | selector: 10 | control-plane: addon-manager 11 | ports: 12 | - port: 8443 13 | --- 14 | apiVersion: apps/v1 15 | kind: Deployment 16 | metadata: 17 | name: controller 18 | namespace: system 19 | labels: 20 | control-plane: addon-manager 21 | spec: 22 | selector: 23 | matchLabels: 24 | control-plane: addon-manager 25 | replicas: 1 26 | template: 27 | metadata: 28 | labels: 29 | control-plane: addon-manager 30 | spec: 31 | containers: 32 | - command: 33 | - /manager 34 | args: 35 | - --enable-leader-election 36 | image: keikoproj/addon-manager:latest 37 | name: manager 38 | resources: 39 | requests: 40 | cpu: 100m 41 | memory: 20Mi 42 | terminationGracePeriodSeconds: 10 43 | --- 44 | apiVersion: v1 45 | kind: ServiceAccount 46 | metadata: 47 | name: workflow-installer-sa 48 | namespace: system 49 | --- 50 | apiVersion: rbac.authorization.k8s.io/v1 51 | kind: ClusterRole 52 | metadata: 53 | name: addon-workflow-cr 54 | rules: 55 | - apiGroups: ["rbac.authorization.k8s.io", ""] 56 | resources: ["namespaces", "clusterroles", "clusterrolebindings", "configmaps", "events", "pods", "serviceaccounts"] 57 | verbs: ["get", "watch", "list", "create", "update", "patch", "delete"] 58 | - apiGroups: ["apps", "extensions"] 59 | resources: ["deployments", "daemonsets", "statefulsets", "replicasets", "ingresses", "controllerrevisions", "customresourcedefinitions"] 60 | verbs: ["get", "watch", "list", "create", "update", "patch", "delete"] 61 | - apiGroups: ["batch"] 62 | resources: ["jobs", "cronjobs"] 63 | verbs: ["get", "watch", "list", "create", "update", "patch"] 64 | - apiGroups: ["*"] 65 | resources: ["*"] 66 | verbs: ["*"] 67 | --- 68 | apiVersion: rbac.authorization.k8s.io/v1 69 | kind: ClusterRoleBinding 70 | metadata: 71 | name: addon-workflow-crb 72 | roleRef: 73 | apiGroup: rbac.authorization.k8s.io 74 | kind: ClusterRole 75 | name: addon-workflow-cr 76 | subjects: 77 | - kind: ServiceAccount 78 | name: workflow-installer-sa 79 | namespace: system 80 | -------------------------------------------------------------------------------- /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 | - clusterroles 11 | - configmaps 12 | - events 13 | - namespaces 14 | - pods 15 | - serviceaccounts 16 | - services 17 | verbs: 18 | - create 19 | - get 20 | - list 21 | - patch 22 | - update 23 | - watch 24 | - apiGroups: 25 | - "" 26 | resources: 27 | - secrets 28 | verbs: 29 | - list 30 | - apiGroups: 31 | - addonmgr.keikoproj.io 32 | resources: 33 | - addons 34 | verbs: 35 | - create 36 | - delete 37 | - get 38 | - list 39 | - patch 40 | - update 41 | - watch 42 | - apiGroups: 43 | - addonmgr.keikoproj.io 44 | resources: 45 | - addons/status 46 | verbs: 47 | - get 48 | - patch 49 | - update 50 | - apiGroups: 51 | - apps 52 | resources: 53 | - daemonsets 54 | - deployments 55 | - replicasets 56 | - statefulsets 57 | verbs: 58 | - create 59 | - get 60 | - list 61 | - patch 62 | - update 63 | - watch 64 | - apiGroups: 65 | - batch 66 | resources: 67 | - cronjobs 68 | - jobs 69 | verbs: 70 | - create 71 | - get 72 | - list 73 | - patch 74 | - update 75 | - watch 76 | - apiGroups: 77 | - extensions 78 | resources: 79 | - daemonsets 80 | - deployments 81 | - ingresses 82 | - replicasets 83 | verbs: 84 | - create 85 | - get 86 | - list 87 | - patch 88 | - update 89 | - watch 90 | - apiGroups: 91 | - rbac.authorization.k8s.io 92 | resources: 93 | - clusterrolebindings 94 | - clusterroles 95 | verbs: 96 | - create 97 | - get 98 | - list 99 | - patch 100 | --- 101 | apiVersion: rbac.authorization.k8s.io/v1 102 | kind: Role 103 | metadata: 104 | name: manager-role 105 | namespace: system 106 | rules: 107 | - apiGroups: 108 | - argoproj.io 109 | resources: 110 | - workflows 111 | verbs: 112 | - create 113 | - delete 114 | - get 115 | - list 116 | - patch 117 | - update 118 | - watch 119 | - apiGroups: 120 | - coordination.k8s.io 121 | resources: 122 | - leases 123 | verbs: 124 | - create 125 | - delete 126 | - get 127 | - list 128 | - patch 129 | - update 130 | - watch 131 | -------------------------------------------------------------------------------- /.github/workflows/pr-gate.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | unit-test: 12 | if: github.repository_owner == 'keikoproj' 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Set up Go 18 | uses: actions/setup-go@v5 19 | with: 20 | go-version: 1.24 21 | 22 | - name: Download Kubebuilder 23 | run: | 24 | curl -L -o kubebuilder_2.3.2_linux_amd64.tar.gz https://github.com/kubernetes-sigs/kubebuilder/releases/download/v2.3.2/kubebuilder_2.3.2_linux_amd64.tar.gz 25 | tar -zxvf kubebuilder_2.3.2_linux_amd64.tar.gz 26 | sudo mv kubebuilder_2.3.2_linux_amd64 /usr/local/kubebuilder 27 | 28 | - name: Test 29 | run: | 30 | make cover 31 | 32 | - name: Upload coverage reports to Codecov 33 | uses: codecov/codecov-action@v5 34 | with: 35 | file: ./cover.out 36 | token: ${{ secrets.CODECOV_TOKEN }} 37 | 38 | buildx: 39 | if: github.repository_owner == 'keikoproj' 40 | runs-on: ubuntu-latest 41 | steps: 42 | - uses: actions/checkout@v4 43 | 44 | - name: Set up Go 45 | uses: actions/setup-go@v5 46 | with: 47 | go-version: 1.24 48 | 49 | - name: Download Kubebuilder 50 | run: | 51 | curl -L -o kubebuilder_2.3.2_linux_amd64.tar.gz https://github.com/kubernetes-sigs/kubebuilder/releases/download/v2.3.2/kubebuilder_2.3.2_linux_amd64.tar.gz 52 | tar -zxvf kubebuilder_2.3.2_linux_amd64.tar.gz 53 | sudo mv kubebuilder_2.3.2_linux_amd64 /usr/local/kubebuilder 54 | 55 | - name: Set up QEMU 56 | uses: docker/setup-qemu-action@v3 57 | with: 58 | platforms: arm64,amd64 59 | 60 | - name: Set up Docker Buildx 61 | id: buildx 62 | uses: docker/setup-buildx-action@v3 63 | with: 64 | install: true 65 | 66 | - name: Available platforms 67 | run: echo ${{ steps.buildx.outputs.platforms }} 68 | 69 | - name: Run GoReleaser 70 | uses: goreleaser/goreleaser-action@v6 71 | with: 72 | version: latest 73 | args: check -f .goreleaser.yml 74 | 75 | - name: Build 76 | run: | 77 | make docker-build 78 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/fake/clientset_generated.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | package fake 4 | 5 | import ( 6 | clientset "github.com/keikoproj/addon-manager/pkg/client/clientset/versioned" 7 | addonmgrv1alpha1 "github.com/keikoproj/addon-manager/pkg/client/clientset/versioned/typed/addon/v1alpha1" 8 | fakeaddonmgrv1alpha1 "github.com/keikoproj/addon-manager/pkg/client/clientset/versioned/typed/addon/v1alpha1/fake" 9 | "k8s.io/apimachinery/pkg/runtime" 10 | "k8s.io/apimachinery/pkg/watch" 11 | "k8s.io/client-go/discovery" 12 | fakediscovery "k8s.io/client-go/discovery/fake" 13 | "k8s.io/client-go/testing" 14 | ) 15 | 16 | // NewSimpleClientset returns a clientset that will respond with the provided objects. 17 | // It's backed by a very simple object tracker that processes creates, updates and deletions as-is, 18 | // without applying any validations and/or defaults. It shouldn't be considered a replacement 19 | // for a real clientset and is mostly useful in simple unit tests. 20 | func NewSimpleClientset(objects ...runtime.Object) *Clientset { 21 | o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder()) 22 | for _, obj := range objects { 23 | if err := o.Add(obj); err != nil { 24 | panic(err) 25 | } 26 | } 27 | 28 | cs := &Clientset{tracker: o} 29 | cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake} 30 | cs.AddReactor("*", "*", testing.ObjectReaction(o)) 31 | cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) { 32 | gvr := action.GetResource() 33 | ns := action.GetNamespace() 34 | watch, err := o.Watch(gvr, ns) 35 | if err != nil { 36 | return false, nil, err 37 | } 38 | return true, watch, nil 39 | }) 40 | 41 | return cs 42 | } 43 | 44 | // Clientset implements clientset.Interface. Meant to be embedded into a 45 | // struct to get a default implementation. This makes faking out just the method 46 | // you want to test easier. 47 | type Clientset struct { 48 | testing.Fake 49 | discovery *fakediscovery.FakeDiscovery 50 | tracker testing.ObjectTracker 51 | } 52 | 53 | func (c *Clientset) Discovery() discovery.DiscoveryInterface { 54 | return c.discovery 55 | } 56 | 57 | func (c *Clientset) Tracker() testing.ObjectTracker { 58 | return c.tracker 59 | } 60 | 61 | var ( 62 | _ clientset.Interface = &Clientset{} 63 | _ testing.FakeClient = &Clientset{} 64 | ) 65 | 66 | // AddonmgrV1alpha1 retrieves the AddonmgrV1alpha1Client 67 | func (c *Clientset) AddonmgrV1alpha1() addonmgrv1alpha1.AddonmgrV1alpha1Interface { 68 | return &fakeaddonmgrv1alpha1.FakeAddonmgrV1alpha1{Fake: &c.Fake} 69 | } 70 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package main 16 | 17 | import ( 18 | "flag" 19 | "os" 20 | 21 | _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" 22 | ctrl "sigs.k8s.io/controller-runtime" 23 | "sigs.k8s.io/controller-runtime/pkg/cache" 24 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 25 | "sigs.k8s.io/controller-runtime/pkg/metrics/server" 26 | 27 | "github.com/keikoproj/addon-manager/api/addon" 28 | "github.com/keikoproj/addon-manager/controllers" 29 | "github.com/keikoproj/addon-manager/pkg/common" 30 | "github.com/keikoproj/addon-manager/pkg/version" 31 | // +kubebuilder:scaffold:imports 32 | ) 33 | 34 | var ( 35 | setupLog = ctrl.Log.WithName("setup") 36 | debug bool 37 | metricsAddr string 38 | enableLeaderElection bool 39 | ) 40 | 41 | func init() { 42 | flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.") 43 | flag.BoolVar(&enableLeaderElection, "enable-leader-election", false, 44 | "Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager.") 45 | flag.BoolVar(&debug, "debug", false, "Debug logging") 46 | flag.Parse() 47 | } 48 | 49 | func main() { 50 | ctrl.SetLogger(zap.New(zap.UseDevMode(debug))) 51 | 52 | setupLog.Info(version.ToString()) 53 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 54 | Scheme: common.GetAddonMgrScheme(), 55 | Metrics: server.Options{ 56 | BindAddress: metricsAddr, 57 | }, 58 | LeaderElection: enableLeaderElection, 59 | LeaderElectionID: "addonmgr.keikoproj.io", 60 | Cache: cache.Options{ 61 | DefaultNamespaces: map[string]cache.Config{ 62 | addon.ManagedNameSpace: {}, 63 | }, 64 | }, 65 | }) 66 | if err != nil { 67 | setupLog.Error(err, "unable to start manager") 68 | os.Exit(1) 69 | } 70 | 71 | err = controllers.New(mgr) 72 | if err != nil { 73 | setupLog.Error(err, "unable to create controller", "controller", "Addon") 74 | os.Exit(1) 75 | } 76 | 77 | // +kubebuilder:scaffold:builder 78 | setupLog.Info("starting manager") 79 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 80 | setupLog.Error(err, "problem running manager") 81 | os.Exit(1) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/addon/v1alpha1/addon_client.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | package v1alpha1 4 | 5 | import ( 6 | "net/http" 7 | 8 | v1alpha1 "github.com/keikoproj/addon-manager/api/addon/v1alpha1" 9 | "github.com/keikoproj/addon-manager/pkg/client/clientset/versioned/scheme" 10 | rest "k8s.io/client-go/rest" 11 | ) 12 | 13 | type AddonmgrV1alpha1Interface interface { 14 | RESTClient() rest.Interface 15 | AddonsGetter 16 | } 17 | 18 | // AddonmgrV1alpha1Client is used to interact with features provided by the addonmgr.keikoproj.io group. 19 | type AddonmgrV1alpha1Client struct { 20 | restClient rest.Interface 21 | } 22 | 23 | func (c *AddonmgrV1alpha1Client) Addons(namespace string) AddonInterface { 24 | return newAddons(c, namespace) 25 | } 26 | 27 | // NewForConfig creates a new AddonmgrV1alpha1Client for the given config. 28 | // NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), 29 | // where httpClient was generated with rest.HTTPClientFor(c). 30 | func NewForConfig(c *rest.Config) (*AddonmgrV1alpha1Client, error) { 31 | config := *c 32 | if err := setConfigDefaults(&config); err != nil { 33 | return nil, err 34 | } 35 | httpClient, err := rest.HTTPClientFor(&config) 36 | if err != nil { 37 | return nil, err 38 | } 39 | return NewForConfigAndClient(&config, httpClient) 40 | } 41 | 42 | // NewForConfigAndClient creates a new AddonmgrV1alpha1Client for the given config and http client. 43 | // Note the http client provided takes precedence over the configured transport values. 44 | func NewForConfigAndClient(c *rest.Config, h *http.Client) (*AddonmgrV1alpha1Client, error) { 45 | config := *c 46 | if err := setConfigDefaults(&config); err != nil { 47 | return nil, err 48 | } 49 | client, err := rest.RESTClientForConfigAndClient(&config, h) 50 | if err != nil { 51 | return nil, err 52 | } 53 | return &AddonmgrV1alpha1Client{client}, nil 54 | } 55 | 56 | // NewForConfigOrDie creates a new AddonmgrV1alpha1Client for the given config and 57 | // panics if there is an error in the config. 58 | func NewForConfigOrDie(c *rest.Config) *AddonmgrV1alpha1Client { 59 | client, err := NewForConfig(c) 60 | if err != nil { 61 | panic(err) 62 | } 63 | return client 64 | } 65 | 66 | // New creates a new AddonmgrV1alpha1Client for the given RESTClient. 67 | func New(c rest.Interface) *AddonmgrV1alpha1Client { 68 | return &AddonmgrV1alpha1Client{c} 69 | } 70 | 71 | func setConfigDefaults(config *rest.Config) error { 72 | gv := v1alpha1.SchemeGroupVersion 73 | config.GroupVersion = &gv 74 | config.APIPath = "/apis" 75 | config.NegotiatedSerializer = scheme.Codecs.WithoutConversion() 76 | 77 | if config.UserAgent == "" { 78 | config.UserAgent = rest.DefaultKubernetesUserAgent() 79 | } 80 | 81 | return nil 82 | } 83 | 84 | // RESTClient returns a RESTClient that is used to communicate 85 | // with API server by this client implementation. 86 | func (c *AddonmgrV1alpha1Client) RESTClient() rest.Interface { 87 | if c == nil { 88 | return nil 89 | } 90 | return c.restClient 91 | } 92 | -------------------------------------------------------------------------------- /pkg/client/listers/addon/v1alpha1/addon.go: -------------------------------------------------------------------------------- 1 | // Code generated by lister-gen. DO NOT EDIT. 2 | 3 | package v1alpha1 4 | 5 | import ( 6 | v1alpha1 "github.com/keikoproj/addon-manager/api/addon/v1alpha1" 7 | "k8s.io/apimachinery/pkg/api/errors" 8 | "k8s.io/apimachinery/pkg/labels" 9 | "k8s.io/client-go/tools/cache" 10 | ) 11 | 12 | // AddonLister helps list Addons. 13 | // All objects returned here must be treated as read-only. 14 | type AddonLister interface { 15 | // List lists all Addons in the indexer. 16 | // Objects returned here must be treated as read-only. 17 | List(selector labels.Selector) (ret []*v1alpha1.Addon, err error) 18 | // Addons returns an object that can list and get Addons. 19 | Addons(namespace string) AddonNamespaceLister 20 | AddonListerExpansion 21 | } 22 | 23 | // addonLister implements the AddonLister interface. 24 | type addonLister struct { 25 | indexer cache.Indexer 26 | } 27 | 28 | // NewAddonLister returns a new AddonLister. 29 | func NewAddonLister(indexer cache.Indexer) AddonLister { 30 | return &addonLister{indexer: indexer} 31 | } 32 | 33 | // List lists all Addons in the indexer. 34 | func (s *addonLister) List(selector labels.Selector) (ret []*v1alpha1.Addon, err error) { 35 | err = cache.ListAll(s.indexer, selector, func(m interface{}) { 36 | ret = append(ret, m.(*v1alpha1.Addon)) 37 | }) 38 | return ret, err 39 | } 40 | 41 | // Addons returns an object that can list and get Addons. 42 | func (s *addonLister) Addons(namespace string) AddonNamespaceLister { 43 | return addonNamespaceLister{indexer: s.indexer, namespace: namespace} 44 | } 45 | 46 | // AddonNamespaceLister helps list and get Addons. 47 | // All objects returned here must be treated as read-only. 48 | type AddonNamespaceLister interface { 49 | // List lists all Addons in the indexer for a given namespace. 50 | // Objects returned here must be treated as read-only. 51 | List(selector labels.Selector) (ret []*v1alpha1.Addon, err error) 52 | // Get retrieves the Addon from the indexer for a given namespace and name. 53 | // Objects returned here must be treated as read-only. 54 | Get(name string) (*v1alpha1.Addon, error) 55 | AddonNamespaceListerExpansion 56 | } 57 | 58 | // addonNamespaceLister implements the AddonNamespaceLister 59 | // interface. 60 | type addonNamespaceLister struct { 61 | indexer cache.Indexer 62 | namespace string 63 | } 64 | 65 | // List lists all Addons in the indexer for a given namespace. 66 | func (s addonNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.Addon, err error) { 67 | err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { 68 | ret = append(ret, m.(*v1alpha1.Addon)) 69 | }) 70 | return ret, err 71 | } 72 | 73 | // Get retrieves the Addon from the indexer for a given namespace and name. 74 | func (s addonNamespaceLister) Get(name string) (*v1alpha1.Addon, error) { 75 | obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) 76 | if err != nil { 77 | return nil, err 78 | } 79 | if !exists { 80 | return nil, errors.NewNotFound(v1alpha1.Resource("addon"), name) 81 | } 82 | return obj.(*v1alpha1.Addon), nil 83 | } 84 | -------------------------------------------------------------------------------- /test-bdd/testutil/customresource.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package testutil 16 | 17 | import ( 18 | "context" 19 | "io" 20 | "os" 21 | 22 | apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 23 | apiextcs "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" 24 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 26 | "k8s.io/apimachinery/pkg/util/json" 27 | "k8s.io/apimachinery/pkg/util/yaml" 28 | ) 29 | 30 | // CreateCRD creates the CRD parsed from the path given 31 | func CreateCRD(kubeClient apiextcs.Interface, relativePath string) error { 32 | ctx := context.TODO() 33 | CRD, err := parseCRDYaml(relativePath) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | _, err = kubeClient.ApiextensionsV1().CustomResourceDefinitions().Get(ctx, CRD.Name, metav1.GetOptions{}) 39 | 40 | if err == nil { 41 | err = KubectlApply(relativePath) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | } else { 47 | _, err = kubeClient.ApiextensionsV1().CustomResourceDefinitions().Create(ctx, CRD, metav1.CreateOptions{}) 48 | if err != nil { 49 | return err 50 | } 51 | } 52 | 53 | return nil 54 | } 55 | 56 | // DeleteCRD deletes the CRD parsed from the path given 57 | func DeleteCRD(kubeClient apiextcs.Interface, relativePath string) error { 58 | ctx := context.TODO() 59 | CRD, err := parseCRDYaml(relativePath) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | if err := kubeClient.ApiextensionsV1().CustomResourceDefinitions().Delete(ctx, CRD.Name, metav1.DeleteOptions{}); err != nil { 65 | return err 66 | } 67 | 68 | return nil 69 | } 70 | 71 | func parseCRDYaml(relativePath string) (*apiextensions.CustomResourceDefinition, error) { 72 | var manifest *os.File 73 | var err error 74 | 75 | var crd apiextensions.CustomResourceDefinition 76 | if manifest, err = PathToOSFile(relativePath); err != nil { 77 | return nil, err 78 | } 79 | 80 | decoder := yaml.NewYAMLOrJSONDecoder(manifest, 100) 81 | for { 82 | var out unstructured.Unstructured 83 | err = decoder.Decode(&out) 84 | if err != nil { 85 | // this would indicate it's malformed YAML. 86 | break 87 | } 88 | 89 | if out.GetKind() == "CustomResourceDefinition" { 90 | var marshaled []byte 91 | marshaled, err = out.MarshalJSON() 92 | json.Unmarshal(marshaled, &crd) 93 | break 94 | } 95 | } 96 | 97 | if err != io.EOF && err != nil { 98 | return nil, err 99 | } 100 | return &crd, nil 101 | } 102 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "master" ] 20 | schedule: 21 | - cron: '32 23 * * 5' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'go' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Use only 'java' to analyze code written in Java, Kotlin or both 38 | # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both 39 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 40 | 41 | steps: 42 | - name: Checkout repository 43 | uses: actions/checkout@v4 44 | 45 | # Initializes the CodeQL tools for scanning. 46 | - name: Initialize CodeQL 47 | uses: github/codeql-action/init@v3 48 | with: 49 | languages: ${{ matrix.language }} 50 | # If you wish to specify custom queries, you can do so here or in a config file. 51 | # By default, queries listed here will override any specified in a config file. 52 | # Prefix the list here with "+" to use these queries and those in the config file. 53 | 54 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 55 | # queries: security-extended,security-and-quality 56 | 57 | 58 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 59 | # If this step fails, then you should remove it and run the build manually (see below) 60 | - name: Autobuild 61 | uses: github/codeql-action/autobuild@v3 62 | 63 | # ℹ️ Command-line programs to run using the OS shell. 64 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 65 | 66 | # If the Autobuild fails above, remove it and uncomment the following three lines. 67 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 68 | 69 | # - run: | 70 | # echo "Run, Build Application using script" 71 | # ./location_of_script_within_repo/buildscript.sh 72 | 73 | - name: Perform CodeQL Analysis 74 | uses: github/codeql-action/analyze@v3 75 | with: 76 | category: "/language:${{matrix.language}}" 77 | -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/addon/v1alpha1/addon.go: -------------------------------------------------------------------------------- 1 | // Code generated by informer-gen. DO NOT EDIT. 2 | 3 | package v1alpha1 4 | 5 | import ( 6 | "context" 7 | time "time" 8 | 9 | addonv1alpha1 "github.com/keikoproj/addon-manager/api/addon/v1alpha1" 10 | versioned "github.com/keikoproj/addon-manager/pkg/client/clientset/versioned" 11 | internalinterfaces "github.com/keikoproj/addon-manager/pkg/client/informers/externalversions/internalinterfaces" 12 | v1alpha1 "github.com/keikoproj/addon-manager/pkg/client/listers/addon/v1alpha1" 13 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | runtime "k8s.io/apimachinery/pkg/runtime" 15 | watch "k8s.io/apimachinery/pkg/watch" 16 | cache "k8s.io/client-go/tools/cache" 17 | ) 18 | 19 | // AddonInformer provides access to a shared informer and lister for 20 | // Addons. 21 | type AddonInformer interface { 22 | Informer() cache.SharedIndexInformer 23 | Lister() v1alpha1.AddonLister 24 | } 25 | 26 | type addonInformer struct { 27 | factory internalinterfaces.SharedInformerFactory 28 | tweakListOptions internalinterfaces.TweakListOptionsFunc 29 | namespace string 30 | } 31 | 32 | // NewAddonInformer constructs a new informer for Addon type. 33 | // Always prefer using an informer factory to get a shared informer instead of getting an independent 34 | // one. This reduces memory footprint and number of connections to the server. 35 | func NewAddonInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { 36 | return NewFilteredAddonInformer(client, namespace, resyncPeriod, indexers, nil) 37 | } 38 | 39 | // NewFilteredAddonInformer constructs a new informer for Addon type. 40 | // Always prefer using an informer factory to get a shared informer instead of getting an independent 41 | // one. This reduces memory footprint and number of connections to the server. 42 | func NewFilteredAddonInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { 43 | return cache.NewSharedIndexInformer( 44 | &cache.ListWatch{ 45 | ListFunc: func(options v1.ListOptions) (runtime.Object, error) { 46 | if tweakListOptions != nil { 47 | tweakListOptions(&options) 48 | } 49 | return client.AddonmgrV1alpha1().Addons(namespace).List(context.TODO(), options) 50 | }, 51 | WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { 52 | if tweakListOptions != nil { 53 | tweakListOptions(&options) 54 | } 55 | return client.AddonmgrV1alpha1().Addons(namespace).Watch(context.TODO(), options) 56 | }, 57 | }, 58 | &addonv1alpha1.Addon{}, 59 | resyncPeriod, 60 | indexers, 61 | ) 62 | } 63 | 64 | func (f *addonInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { 65 | return NewFilteredAddonInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) 66 | } 67 | 68 | func (f *addonInformer) Informer() cache.SharedIndexInformer { 69 | return f.factory.InformerFor(&addonv1alpha1.Addon{}, f.defaultInformer) 70 | } 71 | 72 | func (f *addonInformer) Lister() v1alpha1.AddonLister { 73 | return v1alpha1.NewAddonLister(f.Informer().GetIndexer()) 74 | } 75 | -------------------------------------------------------------------------------- /controllers/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package controllers 16 | 17 | import ( 18 | "context" 19 | "path/filepath" 20 | "testing" 21 | 22 | "github.com/go-logr/logr" 23 | . "github.com/onsi/ginkgo/v2" 24 | . "github.com/onsi/gomega" 25 | "k8s.io/client-go/kubernetes/scheme" 26 | ctrl "sigs.k8s.io/controller-runtime" 27 | "sigs.k8s.io/controller-runtime/pkg/client" 28 | "sigs.k8s.io/controller-runtime/pkg/envtest" 29 | logf "sigs.k8s.io/controller-runtime/pkg/log" 30 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 31 | 32 | wfv1 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" 33 | addonmgrv1alpha1 "github.com/keikoproj/addon-manager/api/addon/v1alpha1" 34 | // +kubebuilder:scaffold:imports 35 | ) 36 | 37 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 38 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 39 | 40 | var ( 41 | k8sClient client.Client 42 | testEnv *envtest.Environment 43 | log logr.Logger 44 | ctx context.Context 45 | cancel context.CancelFunc 46 | ) 47 | 48 | func TestAPIs(t *testing.T) { 49 | RegisterFailHandler(Fail) 50 | 51 | RunSpecs(t, "Controller Suite") 52 | } 53 | 54 | var _ = BeforeSuite(func() { 55 | log = zap.New(zap.UseDevMode(true), zap.WriteTo(GinkgoWriter)) 56 | logf.SetLogger(log) 57 | 58 | ctx, cancel = context.WithCancel(context.TODO()) 59 | 60 | By("bootstrapping test environment") 61 | testEnv = &envtest.Environment{ 62 | CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, 63 | ErrorIfCRDPathMissing: true, 64 | } 65 | 66 | cfg, err := testEnv.Start() 67 | Expect(err).ToNot(HaveOccurred()) 68 | Expect(cfg).ToNot(BeNil()) 69 | 70 | err = addonmgrv1alpha1.AddToScheme(scheme.Scheme) 71 | Expect(err).NotTo(HaveOccurred()) 72 | err = wfv1.AddToScheme(scheme.Scheme) 73 | Expect(err).NotTo(HaveOccurred()) 74 | 75 | // +kubebuilder:scaffold:scheme 76 | 77 | By("starting reconciler and manager") 78 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) 79 | Expect(err).ToNot(HaveOccurred()) 80 | Expect(k8sClient).ToNot(BeNil()) 81 | 82 | mgr, err := ctrl.NewManager(cfg, ctrl.Options{ 83 | Scheme: scheme.Scheme, 84 | LeaderElection: false, 85 | }) 86 | Expect(err).ToNot(HaveOccurred()) 87 | Expect(mgr).ToNot(BeNil()) 88 | 89 | err = New(mgr) 90 | Expect(err).ToNot(HaveOccurred()) 91 | 92 | go func() { 93 | defer GinkgoRecover() 94 | Expect(mgr.Start(ctx)).ToNot(HaveOccurred(), "failed to run manager") 95 | }() 96 | }) 97 | 98 | var _ = AfterSuite(func() { 99 | cancel() 100 | By("tearing down the test environment") 101 | err := testEnv.Stop() 102 | Expect(err).ToNot(HaveOccurred()) 103 | }) 104 | -------------------------------------------------------------------------------- /pkg/workflows/workflow_builder_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package workflows 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/onsi/gomega" 21 | ) 22 | 23 | // Verify the default workflow after calling Build() 24 | func TestWorkflowBuilder(t *testing.T) { 25 | g := gomega.NewGomegaWithT(t) 26 | 27 | builder := New() 28 | wf := builder.Build() 29 | 30 | g.Expect(wf.GetAPIVersion()).To(gomega.Equal("argoproj.io/v1alpha1")) 31 | g.Expect(wf.GetKind()).To(gomega.Equal("Workflow")) 32 | g.Expect(wf.GetGenerateName()).To(gomega.Equal("-")) 33 | 34 | spec := wf.UnstructuredContent()["spec"].(map[string]interface{}) 35 | g.Expect(spec).NotTo(gomega.BeNil()) 36 | g.Expect(spec["entrypoint"]).To(gomega.Equal("entry")) 37 | g.Expect(spec["serviceAccountName"]).To(gomega.Equal("addon-manager-workflow-installer-sa")) 38 | 39 | templates := spec["templates"].([]map[string]interface{}) 40 | g.Expect(templates).To(gomega.HaveLen(2)) 41 | 42 | g.Expect(templates[0]).To(gomega.HaveKeyWithValue("name", "entry")) 43 | g.Expect(templates[1]).To(gomega.HaveKeyWithValue("name", "submit")) 44 | 45 | submitTemplateContainer := templates[1]["container"].(map[string]interface{}) 46 | g.Expect(submitTemplateContainer["args"]).To(gomega.Equal([]string{"kubectl apply -f /tmp/doc"})) 47 | g.Expect(submitTemplateContainer["command"]).To(gomega.Equal([]string{"sh", "-c"})) 48 | g.Expect(submitTemplateContainer["image"]).To(gomega.Equal(defaultSubmitContainerImage)) 49 | 50 | submitTemplateInputs := templates[1]["inputs"].(map[string]interface{}) 51 | g.Expect(submitTemplateInputs["parameters"]).To(gomega.Equal(make([]map[string]interface{}, 0))) 52 | g.Expect(submitTemplateInputs["artifacts"]).To(gomega.Equal(make([]map[string]interface{}, 0))) 53 | } 54 | 55 | func TestDeleteWorkflowBuilder(t *testing.T) { 56 | g := gomega.NewGomegaWithT(t) 57 | 58 | builder := New() 59 | wf := builder.Delete().Build() 60 | 61 | g.Expect(wf.GetAPIVersion()).To(gomega.Equal("argoproj.io/v1alpha1")) 62 | g.Expect(wf.GetKind()).To(gomega.Equal("Workflow")) 63 | g.Expect(wf.GetGenerateName()).To(gomega.Equal("-")) 64 | 65 | spec := wf.UnstructuredContent()["spec"].(map[string]interface{}) 66 | g.Expect(spec).NotTo(gomega.BeNil()) 67 | g.Expect(spec["entrypoint"]).To(gomega.Equal("entry")) 68 | g.Expect(spec["serviceAccountName"]).To(gomega.Equal("addon-manager-workflow-installer-sa")) 69 | 70 | templates := spec["templates"].([]map[string]interface{}) 71 | g.Expect(templates).To(gomega.HaveLen(2)) 72 | g.Expect(templates[0]).To(gomega.HaveKeyWithValue("name", "delete-wf")) 73 | g.Expect(templates[1]).To(gomega.HaveKeyWithValue("name", "delete-ns")) 74 | 75 | deleteWfTemplateSteps := templates[0]["steps"].([][]map[string]interface{}) 76 | g.Expect(deleteWfTemplateSteps[0][0]["name"]).To(gomega.Equal("delete-ns")) 77 | g.Expect(deleteWfTemplateSteps[0][0]["template"]).To(gomega.Equal("delete-ns")) 78 | 79 | deleteNSContainer := templates[1]["container"].(map[string]interface{}) 80 | g.Expect(deleteNSContainer["args"]).To(gomega.Equal([]string{"kubectl delete all -n {{workflow.parameters.namespace}} --all"})) 81 | g.Expect(deleteNSContainer["command"]).To(gomega.Equal([]string{"sh", "-c"})) 82 | g.Expect(deleteNSContainer["image"]).To(gomega.Equal(defaultSubmitContainerImage)) 83 | } 84 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/clientset.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | package versioned 4 | 5 | import ( 6 | "fmt" 7 | "net/http" 8 | 9 | addonmgrv1alpha1 "github.com/keikoproj/addon-manager/pkg/client/clientset/versioned/typed/addon/v1alpha1" 10 | discovery "k8s.io/client-go/discovery" 11 | rest "k8s.io/client-go/rest" 12 | flowcontrol "k8s.io/client-go/util/flowcontrol" 13 | ) 14 | 15 | type Interface interface { 16 | Discovery() discovery.DiscoveryInterface 17 | AddonmgrV1alpha1() addonmgrv1alpha1.AddonmgrV1alpha1Interface 18 | } 19 | 20 | // Clientset contains the clients for groups. Each group has exactly one 21 | // version included in a Clientset. 22 | type Clientset struct { 23 | *discovery.DiscoveryClient 24 | addonmgrV1alpha1 *addonmgrv1alpha1.AddonmgrV1alpha1Client 25 | } 26 | 27 | // AddonmgrV1alpha1 retrieves the AddonmgrV1alpha1Client 28 | func (c *Clientset) AddonmgrV1alpha1() addonmgrv1alpha1.AddonmgrV1alpha1Interface { 29 | return c.addonmgrV1alpha1 30 | } 31 | 32 | // Discovery retrieves the DiscoveryClient 33 | func (c *Clientset) Discovery() discovery.DiscoveryInterface { 34 | if c == nil { 35 | return nil 36 | } 37 | return c.DiscoveryClient 38 | } 39 | 40 | // NewForConfig creates a new Clientset for the given config. 41 | // If config's RateLimiter is not set and QPS and Burst are acceptable, 42 | // NewForConfig will generate a rate-limiter in configShallowCopy. 43 | // NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), 44 | // where httpClient was generated with rest.HTTPClientFor(c). 45 | func NewForConfig(c *rest.Config) (*Clientset, error) { 46 | configShallowCopy := *c 47 | 48 | if configShallowCopy.UserAgent == "" { 49 | configShallowCopy.UserAgent = rest.DefaultKubernetesUserAgent() 50 | } 51 | 52 | // share the transport between all clients 53 | httpClient, err := rest.HTTPClientFor(&configShallowCopy) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | return NewForConfigAndClient(&configShallowCopy, httpClient) 59 | } 60 | 61 | // NewForConfigAndClient creates a new Clientset for the given config and http client. 62 | // Note the http client provided takes precedence over the configured transport values. 63 | // If config's RateLimiter is not set and QPS and Burst are acceptable, 64 | // NewForConfigAndClient will generate a rate-limiter in configShallowCopy. 65 | func NewForConfigAndClient(c *rest.Config, httpClient *http.Client) (*Clientset, error) { 66 | configShallowCopy := *c 67 | if configShallowCopy.RateLimiter == nil && configShallowCopy.QPS > 0 { 68 | if configShallowCopy.Burst <= 0 { 69 | return nil, fmt.Errorf("burst is required to be greater than 0 when RateLimiter is not set and QPS is set to greater than 0") 70 | } 71 | configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(configShallowCopy.QPS, configShallowCopy.Burst) 72 | } 73 | 74 | var cs Clientset 75 | var err error 76 | cs.addonmgrV1alpha1, err = addonmgrv1alpha1.NewForConfigAndClient(&configShallowCopy, httpClient) 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfigAndClient(&configShallowCopy, httpClient) 82 | if err != nil { 83 | return nil, err 84 | } 85 | return &cs, nil 86 | } 87 | 88 | // NewForConfigOrDie creates a new Clientset for the given config and 89 | // panics if there is an error in the config. 90 | func NewForConfigOrDie(c *rest.Config) *Clientset { 91 | cs, err := NewForConfig(c) 92 | if err != nil { 93 | panic(err) 94 | } 95 | return cs 96 | } 97 | 98 | // New creates a new Clientset for the given RESTClient. 99 | func New(c rest.Interface) *Clientset { 100 | var cs Clientset 101 | cs.addonmgrV1alpha1 = addonmgrv1alpha1.New(c) 102 | 103 | cs.DiscoveryClient = discovery.NewDiscoveryClient(c) 104 | return &cs 105 | } 106 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/keikoproj/addon-manager 2 | 3 | go 1.24.3 4 | 5 | require ( 6 | github.com/Masterminds/semver/v3 v3.3.1 7 | github.com/argoproj/argo-workflows/v3 v3.6.10 8 | github.com/go-logr/logr v1.4.2 9 | github.com/onsi/ginkgo/v2 v2.23.4 10 | github.com/onsi/gomega v1.37.0 11 | github.com/pkg/errors v0.9.1 12 | github.com/spf13/cobra v1.9.1 13 | golang.org/x/net v0.39.0 14 | gopkg.in/yaml.v3 v3.0.1 15 | k8s.io/api v0.32.4 16 | k8s.io/apiextensions-apiserver v0.32.3 17 | k8s.io/apimachinery v0.32.4 18 | k8s.io/client-go v0.32.4 19 | sigs.k8s.io/controller-runtime v0.20.4 20 | ) 21 | 22 | require ( 23 | github.com/beorn7/perks v1.0.1 // indirect 24 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 25 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 26 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 27 | github.com/evanphx/json-patch/v5 v5.9.11 // indirect 28 | github.com/fsnotify/fsnotify v1.8.0 // indirect 29 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 30 | github.com/go-logr/zapr v1.3.0 // indirect 31 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 32 | github.com/go-openapi/jsonreference v0.21.0 // indirect 33 | github.com/go-openapi/swag v0.23.0 // indirect 34 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 35 | github.com/gogo/protobuf v1.3.2 // indirect 36 | github.com/golang/protobuf v1.5.4 // indirect 37 | github.com/google/btree v1.1.3 // indirect 38 | github.com/google/gnostic-models v0.6.9 // indirect 39 | github.com/google/go-cmp v0.7.0 // indirect 40 | github.com/google/gofuzz v1.2.0 // indirect 41 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect 42 | github.com/google/uuid v1.6.0 // indirect 43 | github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect 44 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 45 | github.com/josharian/intern v1.0.0 // indirect 46 | github.com/json-iterator/go v1.1.12 // indirect 47 | github.com/klauspost/compress v1.18.0 // indirect 48 | github.com/mailru/easyjson v0.7.7 // indirect 49 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 50 | github.com/modern-go/reflect2 v1.0.2 // indirect 51 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 52 | github.com/prometheus/client_golang v1.21.1 // indirect 53 | github.com/prometheus/client_model v0.6.1 // indirect 54 | github.com/prometheus/common v0.62.0 // indirect 55 | github.com/prometheus/procfs v0.15.1 // indirect 56 | github.com/sirupsen/logrus v1.9.3 // indirect 57 | github.com/spf13/pflag v1.0.6 // indirect 58 | github.com/x448/float16 v0.8.4 // indirect 59 | go.uber.org/automaxprocs v1.6.0 // indirect 60 | go.uber.org/multierr v1.11.0 // indirect 61 | go.uber.org/zap v1.27.0 // indirect 62 | golang.org/x/oauth2 v0.28.0 // indirect 63 | golang.org/x/sync v0.13.0 // indirect 64 | golang.org/x/sys v0.32.0 // indirect 65 | golang.org/x/term v0.31.0 // indirect 66 | golang.org/x/text v0.24.0 // indirect 67 | golang.org/x/time v0.11.0 // indirect 68 | golang.org/x/tools v0.31.0 // indirect 69 | gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect 70 | google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb // indirect 71 | google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect 72 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect 73 | google.golang.org/grpc v1.71.1 // 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 | k8s.io/klog/v2 v2.130.1 // indirect 78 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect 79 | k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect 80 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 81 | sigs.k8s.io/randfill v1.0.0 // indirect 82 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect 83 | sigs.k8s.io/yaml v1.4.0 // indirect 84 | ) 85 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | project_name: addon-manager 3 | before: 4 | hooks: 5 | - go mod tidy 6 | - go mod download 7 | - go generate ./... 8 | - go fmt ./... 9 | - go vet ./... 10 | builds: 11 | - 12 | id: 'manager' 13 | main: ./main.go 14 | binary: manager 15 | 16 | ldflags: 17 | - -X github.com/keikoproj/addon-manager/pkg/version.BuildDate={{.Date}} 18 | - -X github.com/keikoproj/addon-manager/pkg/version.GitCommit={{.ShortCommit}} 19 | - -X github.com/keikoproj/addon-manager/pkg/version.Version=v{{.Version}} 20 | 21 | env: 22 | - CGO_ENABLED=0 23 | - GO111MODULE=on 24 | 25 | goos: 26 | - linux 27 | 28 | goarch: 29 | - amd64 30 | - arm64 31 | 32 | - 33 | id: 'addonctl' 34 | binary: addonctl 35 | main: ./cmd/addonctl/main.go 36 | 37 | ldflags: 38 | - -X github.com/keikoproj/addon-manager/pkg/version.BuildDate={{.Date}} 39 | - -X github.com/keikoproj/addon-manager/pkg/version.GitCommit={{.ShortCommit}} 40 | - -X github.com/keikoproj/addon-manager/pkg/version.Version=v{{.Version}} 41 | 42 | env: 43 | - CGO_ENABLED=0 44 | - GO111MODULE=on 45 | 46 | goos: 47 | - linux 48 | - windows 49 | - darwin 50 | goarch: 51 | - amd64 52 | - arm64 53 | ignore: 54 | - goos: windows 55 | goarch: arm64 56 | archives: 57 | - id: addonctl-archive 58 | name_template: >- 59 | {{ .ProjectName }}_{{ .Binary }}_{{ .Version }}_{{ title .Os }}_ 60 | {{- if eq .Arch "amd64" }}x86_64 61 | {{- else if eq .Arch "386" }}i386 62 | {{- else }}{{ .Arch }}{{ end }} 63 | {{- if .Arm }}v{{ .Arm }}{{ end }} 64 | {{- if .Mips }}_{{ .Mips }}{{ end }} 65 | ids: 66 | - addonctl 67 | 68 | changelog: 69 | sort: asc 70 | filters: 71 | exclude: 72 | - '^docs:' 73 | - '^test:' 74 | checksum: 75 | name_template: 'checksums.txt' 76 | snapshot: 77 | version_template: "{{ .Major }}.{{ .Minor }}-dev-{{ .ShortCommit }}" 78 | 79 | dockers: 80 | - goos: linux 81 | goarch: amd64 82 | goarm: '' 83 | use: buildx 84 | ids: 85 | - manager 86 | image_templates: 87 | - "keikoproj/addon-manager:v{{ .Version }}-amd64" 88 | build_flag_templates: 89 | - "--pull" 90 | - "--label=org.opencontainers.image.created={{.Date}}" 91 | - "--label=org.opencontainers.image.name={{.ProjectName}}" 92 | - "--label=org.opencontainers.image.revision={{.FullCommit}}" 93 | - "--label=org.opencontainers.image.version={{.Version}}" 94 | - "--platform=linux/amd64" 95 | - "--build-arg=COMMIT={{.ShortCommit}}" 96 | - "--build-arg=DATE={{.Date}}" 97 | extra_files: 98 | - go.mod 99 | - go.sum 100 | - main.go 101 | - pkg/ 102 | - api/ 103 | - cmd/ 104 | - controllers/ 105 | - goos: linux 106 | goarch: arm64 107 | goarm: '' 108 | use: buildx 109 | ids: 110 | - manager 111 | image_templates: 112 | - "keikoproj/addon-manager:v{{ .Version }}-arm64" 113 | build_flag_templates: 114 | - "--pull" 115 | - "--label=org.opencontainers.image.created={{.Date}}" 116 | - "--label=org.opencontainers.image.name={{.ProjectName}}" 117 | - "--label=org.opencontainers.image.revision={{.FullCommit}}" 118 | - "--label=org.opencontainers.image.version={{.Version}}" 119 | - "--platform=linux/arm64" 120 | - "--build-arg=COMMIT={{.ShortCommit}}" 121 | - "--build-arg=DATE={{.Date}}" 122 | extra_files: 123 | - go.mod 124 | - go.sum 125 | - main.go 126 | - pkg/ 127 | - api/ 128 | - cmd/ 129 | - controllers/ 130 | docker_manifests: 131 | - name_template: "keikoproj/addon-manager:v{{ .Version }}" 132 | image_templates: 133 | - "keikoproj/addon-manager:v{{ .Version }}-amd64" 134 | - "keikoproj/addon-manager:v{{ .Version }}-arm64" 135 | -------------------------------------------------------------------------------- /controllers/workflow_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package controllers 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | 21 | wfv1 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" 22 | "github.com/go-logr/logr" 23 | addonapiv1 "github.com/keikoproj/addon-manager/api/addon" 24 | pkgaddon "github.com/keikoproj/addon-manager/pkg/addon" 25 | apierrors "k8s.io/apimachinery/pkg/api/errors" 26 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 | "k8s.io/client-go/dynamic" 28 | ctrl "sigs.k8s.io/controller-runtime" 29 | "sigs.k8s.io/controller-runtime/pkg/client" 30 | "sigs.k8s.io/controller-runtime/pkg/controller" 31 | "sigs.k8s.io/controller-runtime/pkg/manager" 32 | ) 33 | 34 | const ( 35 | wfcontroller = "addon-manager-wf-controller" 36 | ) 37 | 38 | type WorkflowReconciler struct { 39 | client client.Client 40 | dynClient dynamic.Interface 41 | log logr.Logger 42 | addonUpdater *pkgaddon.AddonUpdater 43 | } 44 | 45 | func NewWFController(mgr manager.Manager, dynClient dynamic.Interface, addonUpdater *pkgaddon.AddonUpdater) error { 46 | r := &WorkflowReconciler{ 47 | client: mgr.GetClient(), 48 | dynClient: dynClient, 49 | log: ctrl.Log.WithName(wfcontroller), 50 | addonUpdater: addonUpdater, 51 | } 52 | 53 | return ctrl.NewControllerManagedBy(mgr). 54 | For(&wfv1.Workflow{}). 55 | WithOptions(controller.Options{CacheSyncTimeout: addonapiv1.CacheSyncTimeout}). 56 | Complete(r) 57 | } 58 | 59 | // +kubebuilder:rbac:groups=argoproj.io,resources=workflows,namespace=system,verbs=get;list;watch;create;update;patch;delete 60 | 61 | func (r *WorkflowReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 62 | defer func() { 63 | if err := recover(); err != nil { 64 | r.log.Info(fmt.Sprintf("Error: Panic occurred when reconciling %s due to %v", req.String(), err)) 65 | } 66 | }() 67 | wfobj := &wfv1.Workflow{} 68 | err := r.client.Get(ctx, req.NamespacedName, wfobj) 69 | if apierrors.IsNotFound(err) { 70 | return ctrl.Result{}, nil 71 | } else if err != nil { 72 | return ctrl.Result{}, fmt.Errorf("failed to get workflow %s: %#v", req, err) 73 | } 74 | 75 | // Resource is being deleted, skip reconciling. 76 | if !wfobj.ObjectMeta.DeletionTimestamp.IsZero() { 77 | r.log.Info("workflow ", wfobj.GetNamespace(), wfobj.GetName(), " is being deleted skip reconciling") 78 | return ctrl.Result{}, nil 79 | } 80 | 81 | r.log.Info("reconciling", "request", req, " workflow ", wfobj.Name) 82 | if len(string(wfobj.Status.Phase)) == 0 { 83 | r.log.Info("workflow ", wfobj.GetNamespace(), wfobj.GetName(), " status", " is empty") 84 | return ctrl.Result{}, nil 85 | } 86 | 87 | owner := metav1.GetControllerOf(wfobj) 88 | if owner == nil { 89 | err := fmt.Errorf("workflow %s/%s has no owner", wfobj.GetNamespace(), wfobj.GetName()) 90 | r.log.Error(err, wfobj.GetNamespace(), wfobj.GetName(), " owner is empty") 91 | return ctrl.Result{}, err 92 | } 93 | if owner.Kind != "Addon" { 94 | r.log.Info("workflow ", wfobj.GetNamespace(), wfobj.GetName(), " owner ", owner.Kind, " is not an addon") 95 | return ctrl.Result{}, nil 96 | } 97 | 98 | err = r.addonUpdater.UpdateAddonStatusLifecycleFromWorkflow(ctx, wfobj.GetNamespace(), owner.Name, wfobj) 99 | if err != nil { 100 | return ctrl.Result{}, err 101 | } 102 | return ctrl.Result{}, nil 103 | } 104 | -------------------------------------------------------------------------------- /test-bdd/testutil/helpers.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package testutil 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "os" 21 | "os/exec" 22 | "path/filepath" 23 | "strings" 24 | 25 | "github.com/pkg/errors" 26 | "gopkg.in/yaml.v3" 27 | corev1 "k8s.io/api/core/v1" 28 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 30 | "k8s.io/client-go/dynamic" 31 | 32 | "github.com/keikoproj/addon-manager/pkg/common" 33 | ) 34 | 35 | // PathToOSFile takes a relatice path and returns the full path on the OS 36 | func PathToOSFile(relativPath string) (*os.File, error) { 37 | path, err := filepath.Abs(relativPath) 38 | if err != nil { 39 | return nil, errors.Wrap(err, fmt.Sprintf("failed generate absolut file path of %s", relativPath)) 40 | } 41 | 42 | manifest, err := os.Open(path) 43 | if err != nil { 44 | return nil, errors.Wrap(err, fmt.Sprintf("failed to open file %s", path)) 45 | } 46 | 47 | return manifest, nil 48 | } 49 | 50 | // KubectlApply runs 'kubectl apply -f ' with the given path 51 | func KubectlApply(manifestRelativePath string) error { 52 | kubectlBinaryPath, err := exec.LookPath("kubectl") 53 | if err != nil { 54 | return err 55 | } 56 | 57 | path, err := filepath.Abs(manifestRelativePath) 58 | if err != nil { 59 | return errors.Wrap(err, fmt.Sprintf("failed generate absolut file path of %s", manifestRelativePath)) 60 | } 61 | 62 | applyArgs := []string{"apply", "-f", path} 63 | cmd := exec.Command(kubectlBinaryPath, applyArgs...) 64 | fmt.Printf("Executing: %v %v", kubectlBinaryPath, applyArgs) 65 | 66 | err = cmd.Start() 67 | if err != nil { 68 | return errors.Wrap(err, fmt.Sprintf("Could not exec kubectl: ")) 69 | } 70 | 71 | err = cmd.Wait() 72 | if err != nil { 73 | return errors.Wrap(err, fmt.Sprintf("Command resulted in error: ")) 74 | } 75 | 76 | return nil 77 | } 78 | 79 | func isNodeReady(n corev1.Node) bool { 80 | for _, condition := range n.Status.Conditions { 81 | if condition.Type == "Ready" { 82 | if condition.Status == "True" { 83 | return true 84 | } 85 | } 86 | } 87 | return false 88 | } 89 | 90 | // ConcatonateList joins lists to strings delimited with `delimiter` 91 | func ConcatonateList(list []string, delimiter string) string { 92 | return strings.Trim(strings.Join(strings.Fields(fmt.Sprint(list)), delimiter), "[]") 93 | } 94 | 95 | // ReadFile reads the raw content from a file path 96 | func ReadFile(path string) ([]byte, error) { 97 | f, err := os.ReadFile(path) 98 | if err != nil { 99 | fmt.Printf("failed to read file %v", path) 100 | return nil, err 101 | } 102 | return f, nil 103 | } 104 | 105 | // CRDExists returns true if a schema with the given name was found 106 | func CRDExists(kubeClient dynamic.Interface, name string) bool { 107 | ctx := context.TODO() 108 | CRDSchema := common.CRDGVR() 109 | _, err := kubeClient.Resource(CRDSchema).Get(ctx, name, metav1.GetOptions{}) 110 | if err != nil { 111 | fmt.Println(err) 112 | return false 113 | } 114 | return true 115 | } 116 | 117 | // ParseCustomResourceYaml parsed the YAMl for a CRD into an Unstructured object 118 | func ParseCustomResourceYaml(raw string) (*unstructured.Unstructured, error) { 119 | var err error 120 | cr := unstructured.Unstructured{} 121 | data := []byte(raw) 122 | err = yaml.Unmarshal(data, &cr.Object) 123 | if err != nil { 124 | fmt.Println(err) 125 | return &cr, err 126 | } 127 | return &cr, nil 128 | } 129 | -------------------------------------------------------------------------------- /pkg/addon/addon_version_cache.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package addon 16 | 17 | import ( 18 | "sync" 19 | 20 | "github.com/Masterminds/semver/v3" 21 | addonmgrv1alpha1 "github.com/keikoproj/addon-manager/api/addon/v1alpha1" 22 | ) 23 | 24 | // VersionCacheClient interface clients must implement for addon version cache. 25 | type VersionCacheClient interface { 26 | AddVersion(Version) 27 | GetVersions(pkgName string) map[string]Version 28 | GetVersion(pkgName, pkgVersion string) *Version 29 | HasVersionName(name string) (bool, *Version) 30 | RemoveVersion(pkgName, pkgVersion string) 31 | RemoveVersions(pkgName string) 32 | GetAllVersions() map[string]map[string]Version 33 | } 34 | 35 | // Version data that will be cached 36 | type Version struct { 37 | Name string 38 | Namespace string 39 | addonmgrv1alpha1.PackageSpec 40 | PkgPhase addonmgrv1alpha1.ApplicationAssemblyPhase 41 | } 42 | 43 | type cached struct { 44 | sync.RWMutex 45 | addons map[string]map[string]Version 46 | } 47 | 48 | // NewAddonVersionCacheClient returns a new instance of VersionCacheClient 49 | func NewAddonVersionCacheClient() VersionCacheClient { 50 | return &cached{ 51 | addons: make(map[string]map[string]Version), 52 | } 53 | } 54 | 55 | func (c *cached) AddVersion(v Version) { 56 | c.Lock() 57 | defer c.Unlock() 58 | 59 | _, ok := c.addons[v.PkgName] 60 | if !ok { 61 | mm := make(map[string]Version) 62 | c.addons[v.PkgName] = mm 63 | } 64 | c.addons[v.PkgName][v.PkgVersion] = v 65 | } 66 | 67 | func (c *cached) GetVersions(pkgName string) map[string]Version { 68 | c.RLock() 69 | defer c.RUnlock() 70 | 71 | m, ok := c.addons[pkgName] 72 | if !ok { 73 | return nil 74 | } 75 | 76 | return c.copyVersionMap(m) 77 | } 78 | 79 | func (c *cached) GetVersion(pkgName, pkgVersion string) *Version { 80 | var vmap = c.GetVersions(pkgName) 81 | 82 | if vmap == nil { 83 | return nil 84 | } 85 | 86 | v, ok := vmap[pkgVersion] 87 | if !ok { 88 | return c.resolveVersion(vmap, pkgVersion) 89 | } 90 | 91 | return &v 92 | } 93 | 94 | func (c *cached) RemoveVersion(pkgName, pkgVersion string) { 95 | c.Lock() 96 | defer c.Unlock() 97 | 98 | if _, ok := c.addons[pkgName][pkgVersion]; ok { 99 | // Remove version 100 | delete(c.addons[pkgName], pkgVersion) 101 | } 102 | } 103 | 104 | func (c *cached) RemoveVersions(pkgName string) { 105 | c.Lock() 106 | defer c.Unlock() 107 | 108 | if _, ok := c.addons[pkgName]; ok { 109 | // Remove all versions 110 | delete(c.addons, pkgName) 111 | } 112 | } 113 | 114 | func (c *cached) GetAllVersions() map[string]map[string]Version { 115 | return c.deepCopy() 116 | } 117 | 118 | func (c *cached) HasVersionName(name string) (bool, *Version) { 119 | vvmap := c.GetAllVersions() 120 | 121 | for _, vmap := range vvmap { 122 | for _, version := range vmap { 123 | if version.Name == name { 124 | return true, &version 125 | } 126 | } 127 | } 128 | 129 | return false, nil 130 | } 131 | 132 | func (c *cached) resolveVersion(m map[string]Version, pkgVersion string) *Version { 133 | // Assume pkgVersion may be a semantic package description 134 | ct, err := semver.NewConstraint(pkgVersion) 135 | if err != nil { 136 | // Package version is not a constraint 137 | return nil 138 | } 139 | 140 | for key := range m { 141 | sv, err := semver.NewVersion(key) 142 | if err != nil { 143 | // Cannot parse cached map version 144 | continue 145 | } 146 | 147 | if ct.Check(sv) { 148 | v := m[key] 149 | return &v 150 | } 151 | } 152 | 153 | return nil 154 | } 155 | 156 | func (c *cached) deepCopy() map[string]map[string]Version { 157 | c.RLock() 158 | defer c.RUnlock() 159 | cacheCopy := make(map[string]map[string]Version) 160 | 161 | for key, val := range c.addons { 162 | cacheCopy[key] = c.copyVersionMap(val) 163 | } 164 | 165 | return cacheCopy 166 | } 167 | 168 | func (c *cached) copyVersionMap(m map[string]Version) map[string]Version { 169 | var vmap = make(map[string]Version) 170 | 171 | // copy map by assigning elements to new map 172 | for key, value := range m { 173 | vmap[key] = value 174 | } 175 | 176 | return vmap 177 | } 178 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/addon/v1alpha1/fake/fake_addon.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | package fake 4 | 5 | import ( 6 | "context" 7 | 8 | v1alpha1 "github.com/keikoproj/addon-manager/api/addon/v1alpha1" 9 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | labels "k8s.io/apimachinery/pkg/labels" 11 | schema "k8s.io/apimachinery/pkg/runtime/schema" 12 | types "k8s.io/apimachinery/pkg/types" 13 | watch "k8s.io/apimachinery/pkg/watch" 14 | testing "k8s.io/client-go/testing" 15 | ) 16 | 17 | // FakeAddons implements AddonInterface 18 | type FakeAddons struct { 19 | Fake *FakeAddonmgrV1alpha1 20 | ns string 21 | } 22 | 23 | var addonsResource = schema.GroupVersionResource{Group: "addonmgr.keikoproj.io", Version: "v1alpha1", Resource: "addons"} 24 | 25 | var addonsKind = schema.GroupVersionKind{Group: "addonmgr.keikoproj.io", Version: "v1alpha1", Kind: "Addon"} 26 | 27 | // Get takes name of the addon, and returns the corresponding addon object, and an error if there is any. 28 | func (c *FakeAddons) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.Addon, err error) { 29 | obj, err := c.Fake. 30 | Invokes(testing.NewGetAction(addonsResource, c.ns, name), &v1alpha1.Addon{}) 31 | 32 | if obj == nil { 33 | return nil, err 34 | } 35 | return obj.(*v1alpha1.Addon), err 36 | } 37 | 38 | // List takes label and field selectors, and returns the list of Addons that match those selectors. 39 | func (c *FakeAddons) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.AddonList, err error) { 40 | obj, err := c.Fake. 41 | Invokes(testing.NewListAction(addonsResource, addonsKind, c.ns, opts), &v1alpha1.AddonList{}) 42 | 43 | if obj == nil { 44 | return nil, err 45 | } 46 | 47 | label, _, _ := testing.ExtractFromListOptions(opts) 48 | if label == nil { 49 | label = labels.Everything() 50 | } 51 | list := &v1alpha1.AddonList{ListMeta: obj.(*v1alpha1.AddonList).ListMeta} 52 | for _, item := range obj.(*v1alpha1.AddonList).Items { 53 | if label.Matches(labels.Set(item.Labels)) { 54 | list.Items = append(list.Items, item) 55 | } 56 | } 57 | return list, err 58 | } 59 | 60 | // Watch returns a watch.Interface that watches the requested addons. 61 | func (c *FakeAddons) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { 62 | return c.Fake. 63 | InvokesWatch(testing.NewWatchAction(addonsResource, c.ns, opts)) 64 | 65 | } 66 | 67 | // Create takes the representation of a addon and creates it. Returns the server's representation of the addon, and an error, if there is any. 68 | func (c *FakeAddons) Create(ctx context.Context, addon *v1alpha1.Addon, opts v1.CreateOptions) (result *v1alpha1.Addon, err error) { 69 | obj, err := c.Fake. 70 | Invokes(testing.NewCreateAction(addonsResource, c.ns, addon), &v1alpha1.Addon{}) 71 | 72 | if obj == nil { 73 | return nil, err 74 | } 75 | return obj.(*v1alpha1.Addon), err 76 | } 77 | 78 | // Update takes the representation of a addon and updates it. Returns the server's representation of the addon, and an error, if there is any. 79 | func (c *FakeAddons) Update(ctx context.Context, addon *v1alpha1.Addon, opts v1.UpdateOptions) (result *v1alpha1.Addon, err error) { 80 | obj, err := c.Fake. 81 | Invokes(testing.NewUpdateAction(addonsResource, c.ns, addon), &v1alpha1.Addon{}) 82 | 83 | if obj == nil { 84 | return nil, err 85 | } 86 | return obj.(*v1alpha1.Addon), err 87 | } 88 | 89 | // UpdateStatus was generated because the type contains a Status member. 90 | // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). 91 | func (c *FakeAddons) UpdateStatus(ctx context.Context, addon *v1alpha1.Addon, opts v1.UpdateOptions) (*v1alpha1.Addon, error) { 92 | obj, err := c.Fake. 93 | Invokes(testing.NewUpdateSubresourceAction(addonsResource, "status", c.ns, addon), &v1alpha1.Addon{}) 94 | 95 | if obj == nil { 96 | return nil, err 97 | } 98 | return obj.(*v1alpha1.Addon), err 99 | } 100 | 101 | // Delete takes name of the addon and deletes it. Returns an error if one occurs. 102 | func (c *FakeAddons) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { 103 | _, err := c.Fake. 104 | Invokes(testing.NewDeleteActionWithOptions(addonsResource, c.ns, name, opts), &v1alpha1.Addon{}) 105 | 106 | return err 107 | } 108 | 109 | // DeleteCollection deletes a collection of objects. 110 | func (c *FakeAddons) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { 111 | action := testing.NewDeleteCollectionAction(addonsResource, c.ns, listOpts) 112 | 113 | _, err := c.Fake.Invokes(action, &v1alpha1.AddonList{}) 114 | return err 115 | } 116 | 117 | // Patch applies the patch and returns the patched addon. 118 | func (c *FakeAddons) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.Addon, err error) { 119 | obj, err := c.Fake. 120 | Invokes(testing.NewPatchSubresourceAction(addonsResource, c.ns, name, pt, data, subresources...), &v1alpha1.Addon{}) 121 | 122 | if obj == nil { 123 | return nil, err 124 | } 125 | return obj.(*v1alpha1.Addon), err 126 | } 127 | -------------------------------------------------------------------------------- /config/argo/argo.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: argo 5 | namespace: system 6 | --- 7 | apiVersion: rbac.authorization.k8s.io/v1 8 | kind: Role 9 | metadata: 10 | name: argo-role 11 | namespace: system 12 | rules: 13 | - apiGroups: 14 | - coordination.k8s.io 15 | resources: 16 | - leases 17 | verbs: 18 | - create 19 | - get 20 | - update 21 | - apiGroups: 22 | - "" 23 | resources: 24 | - pods 25 | - pods/exec 26 | verbs: 27 | - create 28 | - get 29 | - list 30 | - watch 31 | - update 32 | - patch 33 | - delete 34 | - apiGroups: 35 | - "" 36 | resources: 37 | - configmaps 38 | verbs: 39 | - get 40 | - watch 41 | - list 42 | - apiGroups: 43 | - "" 44 | resources: 45 | - persistentvolumeclaims 46 | - persistentvolumeclaims/finalizers 47 | verbs: 48 | - create 49 | - update 50 | - delete 51 | - get 52 | - apiGroups: 53 | - argoproj.io 54 | resources: 55 | - workflows 56 | - workflows/finalizers 57 | - workflowtasksets 58 | - workflowtasksets/finalizers 59 | verbs: 60 | - get 61 | - list 62 | - watch 63 | - update 64 | - patch 65 | - delete 66 | - create 67 | - apiGroups: 68 | - argoproj.io 69 | resources: 70 | - workflowtemplates 71 | - workflowtemplates/finalizers 72 | verbs: 73 | - get 74 | - list 75 | - watch 76 | - apiGroups: 77 | - argoproj.io 78 | resources: 79 | - workflowtaskresults 80 | verbs: 81 | - list 82 | - watch 83 | - deletecollection 84 | - apiGroups: 85 | - "" 86 | resources: 87 | - serviceaccounts 88 | verbs: 89 | - get 90 | - list 91 | - apiGroups: 92 | - "" 93 | resources: 94 | - secrets 95 | verbs: 96 | - get 97 | - apiGroups: 98 | - argoproj.io 99 | resources: 100 | - cronworkflows 101 | - cronworkflows/finalizers 102 | verbs: 103 | - get 104 | - list 105 | - watch 106 | - update 107 | - patch 108 | - delete 109 | - apiGroups: 110 | - "" 111 | resources: 112 | - events 113 | verbs: 114 | - create 115 | - patch 116 | - apiGroups: 117 | - policy 118 | resources: 119 | - poddisruptionbudgets 120 | verbs: 121 | - create 122 | - get 123 | - delete 124 | --- 125 | apiVersion: rbac.authorization.k8s.io/v1 126 | kind: RoleBinding 127 | metadata: 128 | name: argo-binding 129 | namespace: system 130 | roleRef: 131 | apiGroup: rbac.authorization.k8s.io 132 | kind: Role 133 | name: argo-role 134 | subjects: 135 | - kind: ServiceAccount 136 | name: argo 137 | --- 138 | apiVersion: v1 139 | kind: ConfigMap 140 | metadata: 141 | name: workflow-controller-configmap 142 | namespace: system 143 | data: 144 | namespace: addon-manager-system 145 | instanceID: addon-manager-workflow-controller 146 | --- 147 | apiVersion: scheduling.k8s.io/v1 148 | kind: PriorityClass 149 | metadata: 150 | name: workflow-controller 151 | value: 1000000 152 | --- 153 | apiVersion: apps/v1 154 | kind: Deployment 155 | metadata: 156 | name: workflow-controller 157 | namespace: system 158 | spec: 159 | selector: 160 | matchLabels: 161 | app: addon-manager-workflow-controller 162 | template: 163 | metadata: 164 | labels: 165 | app: addon-manager-workflow-controller 166 | spec: 167 | containers: 168 | - args: 169 | - --configmap 170 | - addon-manager-workflow-controller-configmap 171 | - --executor-image 172 | - quay.io/argoproj/argoexec:v3.4.8 173 | - --namespaced 174 | command: 175 | - workflow-controller 176 | env: 177 | - name: LEADER_ELECTION_IDENTITY 178 | valueFrom: 179 | fieldRef: 180 | apiVersion: v1 181 | fieldPath: metadata.name 182 | image: quay.io/argoproj/workflow-controller:v3.4.8 183 | livenessProbe: 184 | failureThreshold: 3 185 | httpGet: 186 | path: /healthz 187 | port: 6060 188 | initialDelaySeconds: 90 189 | periodSeconds: 60 190 | timeoutSeconds: 30 191 | name: workflow-controller 192 | ports: 193 | - containerPort: 9090 194 | name: metrics 195 | - containerPort: 6060 196 | securityContext: 197 | allowPrivilegeEscalation: false 198 | capabilities: 199 | drop: 200 | - ALL 201 | readOnlyRootFilesystem: true 202 | runAsNonRoot: true 203 | nodeSelector: 204 | kubernetes.io/os: linux 205 | priorityClassName: workflow-controller 206 | securityContext: 207 | runAsNonRoot: true 208 | serviceAccountName: argo 209 | --- 210 | apiVersion: addonmgr.keikoproj.io/v1alpha1 211 | kind: Addon 212 | metadata: 213 | name: argo-addon 214 | namespace: system 215 | spec: 216 | pkgName: addon-argo-workflow 217 | pkgVersion: v3.4.8 218 | pkgType: composite 219 | pkgDescription: "Argo Workflow Controller for Addon Controller." 220 | params: 221 | namespace: addon-manager-system 222 | selector: 223 | matchLabels: 224 | app.kubernetes.io/name: addon-manager-argo-addon 225 | app.kubernetes.io/part-of: addon-manager-argo-addon 226 | app.kubernetes.io/managed-by: addonmgr.keikoproj.io 227 | -------------------------------------------------------------------------------- /pkg/common/helpers.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package common 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | "strings" 21 | "time" 22 | 23 | wfv1versioned "github.com/argoproj/argo-workflows/v3/pkg/client/clientset/versioned" 24 | addonv1versioned "github.com/keikoproj/addon-manager/pkg/client/clientset/versioned" 25 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 26 | "k8s.io/apimachinery/pkg/runtime" 27 | "k8s.io/client-go/rest" 28 | 29 | wfv1 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" 30 | addonv1 "github.com/keikoproj/addon-manager/api/addon/v1alpha1" 31 | ) 32 | 33 | // ContainsString helper function to check string in a slice of strings. 34 | func ContainsString(slice []string, s string) bool { 35 | for _, item := range slice { 36 | if item == s { 37 | return true 38 | } 39 | } 40 | return false 41 | } 42 | 43 | // RemoveString helper function to remove a string in a slice of strings. 44 | func RemoveString(slice []string, s string) (result []string) { 45 | for _, item := range slice { 46 | if item == s { 47 | continue 48 | } 49 | result = append(result, item) 50 | } 51 | return 52 | } 53 | 54 | // GetCurrentTimestamp -- get current timestamp in millisecond 55 | func GetCurrentTimestamp() int64 { 56 | return time.Now().UnixMilli() 57 | } 58 | 59 | // IsExpired --- check if reached ttl time 60 | func IsExpired(startTime int64, ttlTime int64) bool { 61 | if GetCurrentTimestamp()-startTime >= ttlTime { 62 | return true 63 | } 64 | return false 65 | } 66 | 67 | // NewWFClient -- declare new workflow client 68 | func NewWFClient(cfg *rest.Config) wfv1versioned.Interface { 69 | cli, err := wfv1versioned.NewForConfig(cfg) 70 | if err != nil { 71 | return nil 72 | } 73 | return cli 74 | } 75 | 76 | // NewAddonClient - declare new addon client 77 | func NewAddonClient(cfg *rest.Config) addonv1versioned.Interface { 78 | cli, err := addonv1versioned.NewForConfig(cfg) 79 | if err != nil { 80 | return nil 81 | } 82 | return cli 83 | } 84 | 85 | func WorkFlowFromUnstructured(un *unstructured.Unstructured) (*wfv1.Workflow, error) { 86 | var wf wfv1.Workflow 87 | err := FromUnstructuredObj(un, &wf) 88 | return &wf, err 89 | } 90 | 91 | func FromUnstructured(un *unstructured.Unstructured) (*addonv1.Addon, error) { 92 | var addon addonv1.Addon 93 | err := FromUnstructuredObj(un, &addon) 94 | return &addon, err 95 | } 96 | 97 | // FromUnstructuredObj convert unstructured to objects 98 | func FromUnstructuredObj(un *unstructured.Unstructured, v interface{}) error { 99 | err := runtime.DefaultUnstructuredConverter.FromUnstructured(un.Object, v) 100 | if err != nil { 101 | if err.Error() == "cannot convert int64 to v1alpha1.AnyString" { 102 | data, err := json.Marshal(un) 103 | if err != nil { 104 | return err 105 | } 106 | return json.Unmarshal(data, v) 107 | } 108 | return err 109 | } 110 | return nil 111 | } 112 | 113 | func ConvertWorkflowPhaseToAddonPhase(lifecycle addonv1.LifecycleStep, phase wfv1.WorkflowPhase) addonv1.ApplicationAssemblyPhase { 114 | 115 | switch phase { 116 | case wfv1.WorkflowPending, wfv1.WorkflowRunning: 117 | if lifecycle == addonv1.Delete { 118 | return addonv1.Deleting 119 | } 120 | return addonv1.Pending 121 | case wfv1.WorkflowSucceeded: 122 | if lifecycle == addonv1.Delete { 123 | return addonv1.DeleteSucceeded 124 | } 125 | return addonv1.Succeeded 126 | case wfv1.WorkflowFailed, wfv1.WorkflowError: 127 | if lifecycle == addonv1.Delete { 128 | return addonv1.DeleteFailed 129 | } 130 | return addonv1.Failed 131 | default: 132 | return "" 133 | } 134 | } 135 | 136 | // ExtractChecksumAndLifecycleStep extracts the checksum and lifecycle step from the workflow name 137 | func ExtractChecksumAndLifecycleStep(addonWorkflowName string) (string, addonv1.LifecycleStep, error) { 138 | // addonWorkflowName is of the form ---wf 139 | // e.g. my-addon-prereqs-12345678-wf 140 | wfParts := strings.Split(addonWorkflowName, "-") 141 | if len(wfParts) < 4 || strings.TrimSpace(wfParts[len(wfParts)-1]) != "wf" { 142 | return "", "", fmt.Errorf("invalid workflow name %s", addonWorkflowName) 143 | } 144 | 145 | var checksum = strings.TrimSpace(wfParts[len(wfParts)-2]) 146 | var lifecycle addonv1.LifecycleStep 147 | switch strings.TrimSpace(wfParts[len(wfParts)-3]) { 148 | case "prereqs": 149 | lifecycle = addonv1.Prereqs 150 | case "install": 151 | lifecycle = addonv1.Install 152 | case "validate": 153 | lifecycle = addonv1.Validate 154 | case "delete": 155 | lifecycle = addonv1.Delete 156 | default: 157 | return "", "", fmt.Errorf("invalid lifecycle in workflow name %s", addonWorkflowName) 158 | } 159 | 160 | return checksum, lifecycle, nil 161 | } 162 | -------------------------------------------------------------------------------- /controllers/workflow_controller_test.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | wfv1 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" 8 | v1 "k8s.io/api/core/v1" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "k8s.io/apimachinery/pkg/types" 11 | 12 | logrtesting "github.com/go-logr/logr/testr" 13 | addonmgrv1alpha1 "github.com/keikoproj/addon-manager/api/addon/v1alpha1" 14 | pkgaddon "github.com/keikoproj/addon-manager/pkg/addon" 15 | "github.com/onsi/gomega" 16 | "k8s.io/apimachinery/pkg/runtime" 17 | dynfake "k8s.io/client-go/dynamic/fake" 18 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 19 | "k8s.io/client-go/tools/record" 20 | controllerruntime "sigs.k8s.io/controller-runtime" 21 | "sigs.k8s.io/controller-runtime/pkg/client/fake" 22 | ) 23 | 24 | var ( 25 | testScheme = runtime.NewScheme() 26 | rcdr = record.NewBroadcasterForTests(1*time.Second).NewRecorder(testScheme, v1.EventSource{Component: "addons"}) 27 | ) 28 | 29 | func init() { 30 | _ = addonmgrv1alpha1.AddToScheme(testScheme) 31 | _ = wfv1.AddToScheme(testScheme) 32 | _ = clientgoscheme.AddToScheme(testScheme) 33 | } 34 | 35 | func TestWorkflowReconciler_Reconcile(t *testing.T) { 36 | g := gomega.NewGomegaWithT(t) 37 | 38 | fakeCli := fake.NewClientBuilder().WithScheme(testScheme).Build() 39 | dynFakeCli := dynfake.NewSimpleDynamicClient(testScheme) 40 | testLog := logrtesting.New(t) 41 | addonUpdater := pkgaddon.NewAddonUpdater(rcdr, fakeCli, pkgaddon.NewAddonVersionCacheClient(), testLog) 42 | 43 | r := &WorkflowReconciler{ 44 | client: fakeCli, 45 | dynClient: dynFakeCli, 46 | log: testLog, 47 | addonUpdater: addonUpdater, 48 | } 49 | 50 | res, err := r.Reconcile(ctx, controllerruntime.Request{ 51 | NamespacedName: types.NamespacedName{Namespace: "default", Name: "test"}}) 52 | g.Expect(err).To(gomega.BeNil()) 53 | g.Expect(res).To(gomega.Equal(controllerruntime.Result{})) 54 | } 55 | 56 | func TestWorkflowReconciler_Reconcile_EmptyPhase(t *testing.T) { 57 | g := gomega.NewGomegaWithT(t) 58 | 59 | wf := &wfv1.Workflow{ 60 | ObjectMeta: metav1.ObjectMeta{ 61 | Name: "test", 62 | Namespace: "default", 63 | OwnerReferences: nil, 64 | }, 65 | } 66 | 67 | fakeCli := fake.NewClientBuilder().WithScheme(testScheme).WithRuntimeObjects(wf).Build() 68 | dynFakeCli := dynfake.NewSimpleDynamicClient(testScheme) 69 | testLog := logrtesting.New(t) 70 | addonUpdater := pkgaddon.NewAddonUpdater(rcdr, fakeCli, pkgaddon.NewAddonVersionCacheClient(), testLog) 71 | 72 | r := &WorkflowReconciler{ 73 | client: fakeCli, 74 | dynClient: dynFakeCli, 75 | log: testLog, 76 | addonUpdater: addonUpdater, 77 | } 78 | 79 | res, err := r.Reconcile(ctx, controllerruntime.Request{ 80 | NamespacedName: types.NamespacedName{Namespace: "default", Name: "test"}}) 81 | g.Expect(err).To(gomega.BeNil()) 82 | g.Expect(res).To(gomega.Equal(controllerruntime.Result{})) 83 | } 84 | 85 | func TestWorkflowReconciler_Reconcile_OwnerRefEmpty(t *testing.T) { 86 | g := gomega.NewGomegaWithT(t) 87 | 88 | wf := &wfv1.Workflow{ 89 | ObjectMeta: metav1.ObjectMeta{ 90 | Name: "test", 91 | Namespace: "default", 92 | OwnerReferences: nil, 93 | }, 94 | Status: wfv1.WorkflowStatus{ 95 | Phase: wfv1.WorkflowSucceeded, 96 | }, 97 | } 98 | 99 | fakeCli := fake.NewClientBuilder().WithScheme(testScheme).WithRuntimeObjects(wf).Build() 100 | dynFakeCli := dynfake.NewSimpleDynamicClient(testScheme) 101 | testLog := logrtesting.New(t) 102 | addonUpdater := pkgaddon.NewAddonUpdater(rcdr, fakeCli, pkgaddon.NewAddonVersionCacheClient(), testLog) 103 | 104 | r := &WorkflowReconciler{ 105 | client: fakeCli, 106 | dynClient: dynFakeCli, 107 | log: testLog, 108 | addonUpdater: addonUpdater, 109 | } 110 | 111 | res, err := r.Reconcile(ctx, controllerruntime.Request{ 112 | NamespacedName: types.NamespacedName{Namespace: "default", Name: "test"}}) 113 | g.Expect(err).To(gomega.HaveOccurred()) 114 | g.Expect(err).To(gomega.MatchError("workflow default/test has no owner")) 115 | g.Expect(res).To(gomega.Equal(controllerruntime.Result{})) 116 | } 117 | 118 | func TestWorkflowReconciler_Reconcile_OwnerrefNotAddon(t *testing.T) { 119 | g := gomega.NewGomegaWithT(t) 120 | 121 | wf := &wfv1.Workflow{ 122 | ObjectMeta: metav1.ObjectMeta{ 123 | Name: "test", 124 | Namespace: "default", 125 | OwnerReferences: []metav1.OwnerReference{ 126 | {Kind: "Pod", Name: "test", UID: "test", APIVersion: "v1", Controller: &[]bool{true}[0]}, 127 | }, 128 | }, 129 | Status: wfv1.WorkflowStatus{ 130 | Phase: wfv1.WorkflowSucceeded, 131 | }, 132 | } 133 | 134 | fakeCli := fake.NewClientBuilder().WithScheme(testScheme).WithRuntimeObjects(wf).Build() 135 | dynFakeCli := dynfake.NewSimpleDynamicClient(testScheme) 136 | testLog := logrtesting.New(t) 137 | addonUpdater := pkgaddon.NewAddonUpdater(rcdr, fakeCli, pkgaddon.NewAddonVersionCacheClient(), testLog) 138 | 139 | r := &WorkflowReconciler{ 140 | client: fakeCli, 141 | dynClient: dynFakeCli, 142 | log: testLog, 143 | addonUpdater: addonUpdater, 144 | } 145 | 146 | res, err := r.Reconcile(ctx, controllerruntime.Request{ 147 | NamespacedName: types.NamespacedName{Namespace: "default", Name: "test"}}) 148 | g.Expect(err).ToNot(gomega.HaveOccurred()) 149 | g.Expect(res).To(gomega.Equal(controllerruntime.Result{})) 150 | } 151 | -------------------------------------------------------------------------------- /test-bdd/testutil/addonresource.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package testutil 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "io" 21 | "sync" 22 | 23 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 25 | "k8s.io/apimachinery/pkg/util/json" 26 | "k8s.io/apimachinery/pkg/util/yaml" 27 | "k8s.io/client-go/dynamic" 28 | 29 | "github.com/keikoproj/addon-manager/pkg/common" 30 | ) 31 | 32 | var addonGroupSchema = common.AddonGVR() 33 | 34 | // CreateAddon parses the raw data from the path into an Unstructured object (Addon) and submits and returns that object 35 | func CreateAddon(kubeClient dynamic.Interface, relativePath string, nameSuffix string) (*unstructured.Unstructured, error) { 36 | ctx := context.TODO() 37 | addon, err := parseAddonYaml(relativePath) 38 | if err != nil { 39 | return addon, err 40 | } 41 | 42 | name := addon.GetName() 43 | namespace := addon.GetNamespace() 44 | 45 | if nameSuffix != "" { 46 | addon.SetName(name + nameSuffix) 47 | name = addon.GetName() 48 | } 49 | 50 | // make sure the addonGroupScheme is valid if failing 51 | addonObject, err := kubeClient.Resource(addonGroupSchema).Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) 52 | 53 | if err == nil { 54 | resourceVersion := addonObject.GetResourceVersion() 55 | addon.SetResourceVersion(resourceVersion) 56 | _, err = kubeClient.Resource(addonGroupSchema).Namespace(namespace).Update(ctx, addon, metav1.UpdateOptions{}) 57 | if err != nil { 58 | return addon, err 59 | } 60 | 61 | } else { 62 | _, err = kubeClient.Resource(addonGroupSchema).Namespace(namespace).Create(ctx, addon, metav1.CreateOptions{}) 63 | if err != nil { 64 | return addon, err 65 | } 66 | } 67 | return addon, nil 68 | } 69 | 70 | // DeleteAddon deletes the Addon using the name and namespace parsed from the raw data at the given path 71 | func DeleteAddon(kubeClient dynamic.Interface, relativePath string) (*unstructured.Unstructured, error) { 72 | ctx := context.TODO() 73 | addon, err := parseAddonYaml(relativePath) 74 | if err != nil { 75 | return addon, err 76 | } 77 | name := addon.GetName() 78 | namespace := addon.GetNamespace() 79 | 80 | if err := kubeClient.Resource(addonGroupSchema).Namespace(namespace).Delete(ctx, name, metav1.DeleteOptions{}); err != nil { 81 | return addon, err 82 | } 83 | 84 | return addon, nil 85 | } 86 | 87 | func parseAddonYaml(relativePath string) (*unstructured.Unstructured, error) { 88 | var err error 89 | 90 | var addon *unstructured.Unstructured 91 | 92 | if _, err = PathToOSFile(relativePath); err != nil { 93 | return nil, err 94 | } 95 | 96 | fileData, err := ReadFile(relativePath) 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewReader(fileData), 100) 102 | for { 103 | var out unstructured.Unstructured 104 | err = decoder.Decode(&out) 105 | if err != nil { 106 | // this would indicate it's malformed YAML. 107 | break 108 | } 109 | 110 | if out.GetKind() == "Addon" { 111 | var marshaled []byte 112 | marshaled, err = out.MarshalJSON() 113 | json.Unmarshal(marshaled, &addon) 114 | break 115 | } 116 | } 117 | 118 | if err != io.EOF && err != nil { 119 | return nil, err 120 | } 121 | return addon, nil 122 | } 123 | 124 | func CreateLoadTestsAddon(lock *sync.Mutex, kubeClient dynamic.Interface, relativePath string, nameSuffix string) (*unstructured.Unstructured, error) { 125 | lock.Lock() 126 | defer lock.Unlock() 127 | 128 | ctx := context.TODO() 129 | addon, err := parseAddonYaml(relativePath) 130 | if err != nil { 131 | return addon, err 132 | } 133 | 134 | name := addon.GetName() 135 | namespace := addon.GetNamespace() 136 | 137 | if nameSuffix != "" { 138 | addon.SetName(name + nameSuffix) 139 | name = addon.GetName() 140 | } 141 | 142 | // make sure the addonGroupScheme is valid if failing 143 | addonObject, err := kubeClient.Resource(addonGroupSchema).Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) 144 | 145 | pkgName, pkgVersion, ns := name, "v"+nameSuffix, "addon-event-router-ns"+nameSuffix 146 | unstructured.SetNestedField(addon.UnstructuredContent(), pkgName, "spec", "pkgName") 147 | unstructured.SetNestedField(addon.UnstructuredContent(), pkgVersion, "spec", "pkgVersion") 148 | unstructured.SetNestedField(addon.UnstructuredContent(), ns, "spec", "params", "namespace") 149 | 150 | if err == nil { 151 | resourceVersion := addonObject.GetResourceVersion() 152 | addon.SetResourceVersion(resourceVersion) 153 | _, err = kubeClient.Resource(addonGroupSchema).Namespace(namespace).Update(ctx, addon, metav1.UpdateOptions{}) 154 | if err != nil { 155 | return addon, err 156 | } 157 | 158 | } else { 159 | _, err = kubeClient.Resource(addonGroupSchema).Namespace(namespace).Create(ctx, addon, metav1.CreateOptions{}) 160 | if err != nil { 161 | return addon, err 162 | } 163 | } 164 | return addon, nil 165 | } 166 | -------------------------------------------------------------------------------- /test-load/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package main 16 | 17 | import ( 18 | "bufio" 19 | "bytes" 20 | "context" 21 | "fmt" 22 | "io" 23 | "os" 24 | "os/exec" 25 | "strconv" 26 | "strings" 27 | "sync" 28 | "time" 29 | 30 | "github.com/keikoproj/addon-manager/pkg/common" 31 | "github.com/keikoproj/addon-manager/test-bdd/testutil" 32 | "k8s.io/client-go/dynamic" 33 | "sigs.k8s.io/controller-runtime/pkg/client/config" 34 | 35 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 36 | ) 37 | 38 | const ( 39 | numberOfRoutines = 10 40 | ) 41 | 42 | func main() { 43 | s, e := os.Getenv("LOADTEST_START_NUMBER"), os.Getenv("LOADTEST_END_NUMBER") 44 | x, _ := strconv.Atoi(s) 45 | y, _ := strconv.Atoi(e) 46 | fmt.Printf("start = %d end = %d", x, y) 47 | 48 | costsummary, err := os.Create("summary.txt") 49 | if err != nil { 50 | return 51 | } 52 | dataWriter := bufio.NewWriter(costsummary) 53 | 54 | stop := make(chan bool) 55 | mgrPid := os.Getenv("MANAGER_PID") 56 | ctrlPid := os.Getenv("WFCTRL_PID") 57 | go func(mgrPid, ctrlPid string, writer *bufio.Writer) { 58 | for { 59 | select { 60 | case <-stop: 61 | return 62 | default: 63 | fmt.Printf("\n every 2 minutes collecting data for mgr %s wfctrl %s", mgrPid, ctrlPid) 64 | Summary(mgrPid, ctrlPid, writer) 65 | time.Sleep(2 * time.Minute) 66 | } 67 | } 68 | }(mgrPid, ctrlPid, dataWriter) 69 | 70 | var wg sync.WaitGroup 71 | wg.Add(numberOfRoutines) 72 | lock := &sync.Mutex{} 73 | 74 | cfg := config.GetConfigOrDie() 75 | dynClient := dynamic.NewForConfigOrDie(cfg) 76 | ctx := context.TODO() 77 | var addonName string 78 | var addonNamespace string 79 | var relativeAddonPath = "docs/examples/eventrouter.yaml" 80 | var addonGroupSchema = common.AddonGVR() 81 | 82 | for i := 1; i <= numberOfRoutines; i++ { 83 | go func(i int, lock *sync.Mutex) { 84 | defer wg.Done() 85 | for j := i * 100; j < i*100+200; j++ { 86 | addon, err := testutil.CreateLoadTestsAddon(lock, dynClient, relativeAddonPath, fmt.Sprintf("-%d", j)) 87 | if err != nil { 88 | fmt.Printf("\n\n create addon failure err %v", err) 89 | time.Sleep(1 * time.Second) 90 | continue 91 | } 92 | 93 | addonName = addon.GetName() 94 | addonNamespace = addon.GetNamespace() 95 | for x := 0; x <= 500; x++ { 96 | a, err := dynClient.Resource(addonGroupSchema).Namespace(addonNamespace).Get(ctx, addonName, metav1.GetOptions{}) 97 | if a == nil || err != nil || a.UnstructuredContent()["status"] == nil { 98 | fmt.Printf("\n\n retry get addon status %v get ", err) 99 | time.Sleep(1 * time.Second) 100 | continue 101 | } 102 | break 103 | } 104 | } 105 | }(i, lock) 106 | } 107 | wg.Wait() 108 | stop <- true 109 | costsummary.Close() 110 | 111 | } 112 | 113 | // capture cpu/memory/addons number every 3 mintues 114 | func Summary(managerPID, wfctrlPID string, datawriter *bufio.Writer) error { 115 | 116 | kubectlCmd := exec.Command("kubectl", "-n", "addon-manager-system", "get", "addons") 117 | wcCmd := exec.Command("wc", "-l") 118 | 119 | //make a pipe 120 | reader, writer := io.Pipe() 121 | var buf bytes.Buffer 122 | 123 | //set the output of "cat" command to pipe writer 124 | kubectlCmd.Stdout = writer 125 | //set the input of the "wc" command pipe reader 126 | 127 | wcCmd.Stdin = reader 128 | 129 | //cache the output of "wc" to memory 130 | wcCmd.Stdout = &buf 131 | 132 | //start to execute "cat" command 133 | kubectlCmd.Start() 134 | 135 | //start to execute "wc" command 136 | wcCmd.Start() 137 | 138 | //waiting for "cat" command complete and close the writer 139 | kubectlCmd.Wait() 140 | writer.Close() 141 | 142 | //waiting for the "wc" command complete and close the reader 143 | wcCmd.Wait() 144 | reader.Close() 145 | 146 | AddonsNum := buf.String() 147 | fmt.Printf("\n addons number %s\n", AddonsNum) 148 | 149 | //cmd = fmt.Sprintf("ps -p %s -o %%cpu,%%mem", managerPID) 150 | cmd := exec.Command("ps", "-p", managerPID, "-o", "%cpu,%mem") 151 | fmt.Printf("manager cpu cmd %v", *cmd) 152 | managerUsage, err := cmd.Output() 153 | if err != nil { 154 | fmt.Printf("failed to collect addonmanager cpu/mem usage. %v", err) 155 | return err 156 | } 157 | //fmt.Printf("\n addonmanager cpu/mem usage %s ", managerUsage) 158 | 159 | cmd = exec.Command("ps", "-p", wfctrlPID, "-o", "%cpu,%mem") 160 | wfControllerUsage, err := cmd.Output() 161 | if err != nil { 162 | fmt.Printf("failed to collect addonmanager cpu/mem usage. %v", err) 163 | return err 164 | } 165 | //fmt.Printf("workflow controller cpu/mem usage %s ", wfControllerUsage) 166 | fmt.Printf("addons number %s addonmanager cpu/mem usage %s controller cpu/mem usage %s", AddonsNum, managerUsage, wfControllerUsage) 167 | oneline := fmt.Sprintf("", strings.TrimSpace(AddonsNum), strings.TrimSuffix(string(managerUsage), "\n"), strings.TrimSuffix(string(wfControllerUsage), "\n")) 168 | datawriter.WriteString(oneline + "\n#############\n") 169 | datawriter.Flush() 170 | return nil 171 | } 172 | -------------------------------------------------------------------------------- /pkg/addon/addon_update.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package addon 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "sync" 21 | 22 | wfv1 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" 23 | "github.com/keikoproj/addon-manager/pkg/common" 24 | "k8s.io/client-go/tools/record" 25 | "k8s.io/client-go/util/retry" 26 | 27 | "github.com/go-logr/logr" 28 | addonmgrv1alpha1 "github.com/keikoproj/addon-manager/api/addon/v1alpha1" 29 | "k8s.io/apimachinery/pkg/types" 30 | "sigs.k8s.io/controller-runtime/pkg/client" 31 | ) 32 | 33 | type AddonUpdater struct { 34 | client client.Client 35 | log logr.Logger 36 | versionCache VersionCacheClient 37 | recorder record.EventRecorder 38 | statusMap map[string]*sync.Mutex 39 | } 40 | 41 | func NewAddonUpdater(recorder record.EventRecorder, cli client.Client, versionCache VersionCacheClient, logger logr.Logger) *AddonUpdater { 42 | return &AddonUpdater{ 43 | client: cli, 44 | versionCache: versionCache, 45 | recorder: recorder, 46 | statusMap: make(map[string]*sync.Mutex), 47 | log: logger.WithName("addon-updater"), 48 | } 49 | } 50 | 51 | func (c *AddonUpdater) UpdateStatus(ctx context.Context, log logr.Logger, addon *addonmgrv1alpha1.Addon) error { 52 | addonName := types.NamespacedName{Name: addon.Name, Namespace: addon.Namespace} 53 | m := c.getStatusMutex(addonName.Name) 54 | m.Lock() 55 | defer m.Unlock() 56 | 57 | err := retry.RetryOnConflict(retry.DefaultRetry, func() error { 58 | // Get the latest version of Addon before attempting update 59 | currentAddon := &addonmgrv1alpha1.Addon{} 60 | err := c.client.Get(ctx, addonName, currentAddon) 61 | if err != nil { 62 | return err 63 | } 64 | addon.Status.DeepCopyInto(¤tAddon.Status) 65 | return c.client.Status().Update(ctx, currentAddon) 66 | }) 67 | if err != nil { 68 | log.Error(err, "Addon status could not be updated.") 69 | c.recorder.Event(addon, "Warning", "Failed", fmt.Sprintf("Addon %s/%s status could not be updated. %v", addon.Namespace, addon.Name, err)) 70 | return err 71 | } 72 | 73 | log.Info("successfully updated addon statuses", "prereqs_status", addon.GetPrereqStatus(), "installed_status", addon.GetInstallStatus()) 74 | 75 | // Always update the version cache 76 | c.addAddonToCache(log, addon) 77 | 78 | return nil 79 | } 80 | 81 | func (c *AddonUpdater) getStatusMutex(addonName string) *sync.Mutex { 82 | m, ok := c.statusMap[addonName] 83 | if !ok { 84 | m = &sync.Mutex{} 85 | c.statusMap[addonName] = m 86 | } 87 | return m 88 | } 89 | 90 | func (c *AddonUpdater) removeStatusWaitGroup(addonName string) { 91 | delete(c.statusMap, addonName) 92 | } 93 | 94 | func (c *AddonUpdater) getExistingAddon(ctx context.Context, namespace, name string) (*addonmgrv1alpha1.Addon, error) { 95 | addonName := types.NamespacedName{Name: name, Namespace: namespace} 96 | currentAddon := &addonmgrv1alpha1.Addon{} 97 | err := c.client.Get(ctx, addonName, currentAddon) 98 | if err != nil { 99 | return nil, err 100 | } 101 | return currentAddon, nil 102 | } 103 | 104 | func (c *AddonUpdater) addAddonToCache(log logr.Logger, addon *addonmgrv1alpha1.Addon) { 105 | var version = Version{ 106 | Name: addon.GetName(), 107 | Namespace: addon.GetNamespace(), 108 | PackageSpec: addon.GetPackageSpec(), 109 | PkgPhase: addon.GetInstallStatus(), 110 | } 111 | c.versionCache.AddVersion(version) 112 | log.Info("Adding version cache", "phase", version.PkgPhase) 113 | } 114 | 115 | // UpdateAddonStatusLifecycleFromWorkflow updates the status of the addon 116 | func (c *AddonUpdater) UpdateAddonStatusLifecycleFromWorkflow(ctx context.Context, namespace, addonName string, wf *wfv1.Workflow) error { 117 | existingAddon, err := c.getExistingAddon(ctx, namespace, addonName) 118 | if err != nil { 119 | return err 120 | } 121 | 122 | if existingAddon.Status.Lifecycle.Installed.Completed() { 123 | // If the addon is already installed, we don't want to update the status 124 | return nil 125 | } 126 | 127 | checksum, lifecycle, err := common.ExtractChecksumAndLifecycleStep(wf.GetName()) 128 | if err != nil { 129 | return err 130 | } 131 | 132 | if existingAddon.GetFormattedWorkflowName(lifecycle) != wf.GetName() { 133 | return nil 134 | } 135 | 136 | if existingAddon.CalculateChecksum() != checksum { 137 | return nil 138 | } 139 | 140 | phase := common.ConvertWorkflowPhaseToAddonPhase(lifecycle, wf.Status.Phase) 141 | reason := "" 142 | 143 | if phase == "" { 144 | return nil 145 | } 146 | 147 | if phase.Failed() { 148 | reason = wf.Status.Message 149 | } 150 | 151 | if err := existingAddon.SetStatusByLifecyleStep(lifecycle, phase, reason); err != nil { 152 | return fmt.Errorf("failed to update prereqs status. %w", err) 153 | } 154 | 155 | return c.UpdateStatus(ctx, c.log, existingAddon) 156 | } 157 | 158 | func (c *AddonUpdater) RemoveFromCache(addonName string) { 159 | // Remove version from cache 160 | if ok, v := c.versionCache.HasVersionName(addonName); ok { 161 | c.versionCache.RemoveVersion(v.PkgName, v.PkgVersion) 162 | } 163 | // Remove addon from waitgroup map 164 | c.removeStatusWaitGroup(addonName) 165 | } 166 | -------------------------------------------------------------------------------- /controllers/objects.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | addonmgrv1alpha1 "github.com/keikoproj/addon-manager/api/addon/v1alpha1" 8 | "sigs.k8s.io/controller-runtime/pkg/client" 9 | 10 | appsv1 "k8s.io/api/apps/v1" 11 | batchv1 "k8s.io/api/batch/v1" 12 | corev1 "k8s.io/api/core/v1" 13 | "k8s.io/apimachinery/pkg/labels" 14 | ) 15 | 16 | func ObserveService(cli client.Client, namespace string, selector labels.Selector) ([]addonmgrv1alpha1.ObjectStatus, error) { 17 | services := &corev1.ServiceList{} 18 | err := cli.List(context.TODO(), services, &client.ListOptions{ 19 | LabelSelector: selector, 20 | Namespace: namespace, 21 | }) 22 | if err != nil { 23 | return nil, fmt.Errorf("failed to list services %v", err) 24 | } 25 | 26 | res := []addonmgrv1alpha1.ObjectStatus{} 27 | for _, service := range services.Items { 28 | if service.ObjectMeta.Namespace == namespace { 29 | res = append(res, addonmgrv1alpha1.ObjectStatus{ 30 | Kind: "Service", 31 | Group: "", 32 | Name: service.GetName(), 33 | Link: service.GetSelfLink(), 34 | }) 35 | } 36 | } 37 | return res, nil 38 | } 39 | 40 | func ObserveJob(cli client.Client, namespace string, selector labels.Selector) ([]addonmgrv1alpha1.ObjectStatus, error) { 41 | jobs := &batchv1.JobList{} 42 | err := cli.List(context.TODO(), jobs, &client.ListOptions{ 43 | LabelSelector: selector, 44 | Namespace: namespace, 45 | }) 46 | if err != nil { 47 | return nil, fmt.Errorf("failed to list cronjobs %v", err) 48 | } 49 | 50 | res := []addonmgrv1alpha1.ObjectStatus{} 51 | for _, job := range jobs.Items { 52 | res = append(res, addonmgrv1alpha1.ObjectStatus{ 53 | Kind: "Job", 54 | Group: "batch/v1", 55 | Name: job.GetName(), 56 | Link: job.GetSelfLink(), 57 | }) 58 | } 59 | return res, nil 60 | } 61 | 62 | func ObserveCronJob(cli client.Client, namespace string, selector labels.Selector) ([]addonmgrv1alpha1.ObjectStatus, error) { 63 | cronJobs := &batchv1.CronJobList{} 64 | err := cli.List(context.TODO(), cronJobs, &client.ListOptions{ 65 | LabelSelector: selector, 66 | Namespace: namespace, 67 | }) 68 | if err != nil { 69 | return nil, fmt.Errorf("failed to list cronjobs %v", err) 70 | } 71 | 72 | res := []addonmgrv1alpha1.ObjectStatus{} 73 | for _, cronJob := range cronJobs.Items { 74 | res = append(res, addonmgrv1alpha1.ObjectStatus{ 75 | Kind: "CronJob", 76 | Group: "batch/v1", 77 | Name: cronJob.GetName(), 78 | Link: cronJob.GetSelfLink(), 79 | }) 80 | } 81 | return res, nil 82 | } 83 | 84 | func ObserveDeployment(cli client.Client, namespace string, selector labels.Selector) ([]addonmgrv1alpha1.ObjectStatus, error) { 85 | deployments := &appsv1.DeploymentList{} 86 | err := cli.List(context.TODO(), deployments, &client.ListOptions{ 87 | LabelSelector: selector}) 88 | if err != nil { 89 | return nil, err 90 | } 91 | 92 | res := []addonmgrv1alpha1.ObjectStatus{} 93 | for _, deployment := range deployments.Items { 94 | res = append(res, addonmgrv1alpha1.ObjectStatus{ 95 | Kind: "Deployment", 96 | Group: "apps/v1", 97 | Name: deployment.GetName(), 98 | Link: deployment.GetSelfLink(), 99 | }) 100 | } 101 | return res, nil 102 | } 103 | 104 | func ObserveDaemonSet(cli client.Client, namespace string, selector labels.Selector) ([]addonmgrv1alpha1.ObjectStatus, error) { 105 | daemonSets := &appsv1.DaemonSetList{} 106 | err := cli.List(context.TODO(), daemonSets, &client.ListOptions{ 107 | LabelSelector: selector}) 108 | if err != nil { 109 | return nil, err 110 | } 111 | 112 | res := []addonmgrv1alpha1.ObjectStatus{} 113 | for _, deployment := range daemonSets.Items { 114 | res = append(res, addonmgrv1alpha1.ObjectStatus{ 115 | Kind: "DaemonSet", 116 | Group: "apps/v1", 117 | Name: deployment.GetName(), 118 | Link: deployment.GetSelfLink(), 119 | }) 120 | } 121 | return res, nil 122 | } 123 | 124 | func ObserveReplicaSet(cli client.Client, namespace string, selector labels.Selector) ([]addonmgrv1alpha1.ObjectStatus, error) { 125 | replicaSets := &appsv1.ReplicaSetList{} 126 | err := cli.List(context.TODO(), replicaSets, &client.ListOptions{ 127 | LabelSelector: selector}) 128 | if err != nil { 129 | return nil, err 130 | } 131 | 132 | res := []addonmgrv1alpha1.ObjectStatus{} 133 | for _, replicaSet := range replicaSets.Items { 134 | res = append(res, addonmgrv1alpha1.ObjectStatus{ 135 | Kind: "ReplicaSe", 136 | Group: "apps/v1", 137 | Name: replicaSet.GetName(), 138 | Link: replicaSet.GetSelfLink(), 139 | }) 140 | } 141 | return res, nil 142 | } 143 | 144 | func ObserveStatefulSet(cli client.Client, name string, selector labels.Selector) ([]addonmgrv1alpha1.ObjectStatus, error) { 145 | statefulSets := &appsv1.StatefulSetList{} 146 | err := cli.List(context.TODO(), statefulSets, &client.ListOptions{ 147 | LabelSelector: selector}) 148 | if err != nil { 149 | return nil, err 150 | } 151 | 152 | res := []addonmgrv1alpha1.ObjectStatus{} 153 | for _, statefulSet := range statefulSets.Items { 154 | res = append(res, addonmgrv1alpha1.ObjectStatus{ 155 | Kind: "StatefulSet", 156 | Group: "apps/v1", 157 | Name: statefulSet.GetName(), 158 | Link: statefulSet.GetSelfLink(), 159 | }) 160 | } 161 | return res, nil 162 | } 163 | 164 | func ObserveNamespace(cli client.Client, name string, selector labels.Selector) ([]addonmgrv1alpha1.ObjectStatus, error) { 165 | namespaces := &corev1.NamespaceList{} 166 | err := cli.List(context.TODO(), namespaces, &client.ListOptions{ 167 | LabelSelector: selector}) 168 | if err != nil { 169 | return nil, err 170 | } 171 | 172 | res := []addonmgrv1alpha1.ObjectStatus{} 173 | for _, namespace := range namespaces.Items { 174 | if namespace.Name == name { 175 | res = append(res, addonmgrv1alpha1.ObjectStatus{ 176 | Kind: "Namespace", 177 | Group: "", 178 | Name: namespace.GetName(), 179 | Link: namespace.GetSelfLink(), 180 | }) 181 | } 182 | } 183 | return res, nil 184 | } 185 | -------------------------------------------------------------------------------- /docs/examples/eventrouter.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: addonmgr.keikoproj.io/v1alpha1 2 | kind: Addon 3 | metadata: 4 | name: event-router 5 | namespace: addon-manager-system 6 | spec: 7 | pkgName: event-router 8 | pkgVersion: v0.2 9 | pkgType: composite 10 | pkgDescription: "Event router" 11 | params: 12 | namespace: addon-event-router-ns 13 | context: 14 | clusterName: "cluster-name" 15 | clusterRegion: us-west-2 16 | lifecycle: 17 | prereqs: 18 | template: | 19 | apiVersion: argoproj.io/v1alpha1 20 | kind: Workflow 21 | metadata: 22 | name: wf-prereq- 23 | spec: 24 | activeDeadlineSeconds: 600 25 | entrypoint: entry 26 | serviceAccountName: addon-manager-workflow-installer-sa 27 | templates: 28 | - name: entry 29 | retryStrategy: 30 | limit: 2 31 | retryPolicy: "Always" 32 | steps: 33 | - - name: prereq-namespace 34 | template: submit-ns 35 | - name: prereq-serviceaccount 36 | template: submit-sa 37 | - name: prereq-configmap 38 | template: submit-cm 39 | - name: prereq-clusterrole 40 | template: submit-cr 41 | - name: prereq-clusterrolebinding 42 | template: submit-crb 43 | 44 | - name: submit-ns 45 | resource: 46 | action: apply 47 | manifest: | 48 | apiVersion: v1 49 | kind: Namespace 50 | metadata: 51 | name: "{{workflow.parameters.namespace}}" 52 | - name: submit-sa 53 | resource: 54 | action: apply 55 | manifest: | 56 | apiVersion: v1 57 | kind: ServiceAccount 58 | metadata: 59 | name: event-router-sa 60 | namespace: "{{workflow.parameters.namespace}}" 61 | - name: submit-cm 62 | resource: 63 | action: apply 64 | manifest: | 65 | apiVersion: v1 66 | kind: ConfigMap 67 | metadata: 68 | name: event-router-cm 69 | namespace: "{{workflow.parameters.namespace}}" 70 | data: 71 | config.json: |- 72 | { 73 | "sink": "stdout" 74 | } 75 | - name: submit-cr 76 | resource: 77 | action: apply 78 | manifest: | 79 | apiVersion: rbac.authorization.k8s.io/v1 80 | kind: ClusterRole 81 | metadata: 82 | name: event-router-cr 83 | rules: 84 | - apiGroups: [""] 85 | resources: ["events"] 86 | verbs: ["get", "watch", "list"] 87 | - name: submit-crb 88 | resource: 89 | action: apply 90 | manifest: | 91 | apiVersion: rbac.authorization.k8s.io/v1 92 | kind: ClusterRoleBinding 93 | metadata: 94 | name: event-router-crb 95 | roleRef: 96 | apiGroup: rbac.authorization.k8s.io 97 | kind: ClusterRole 98 | name: event-router-cr 99 | subjects: 100 | - kind: ServiceAccount 101 | name: event-router-sa 102 | namespace: "{{workflow.parameters.namespace}}" 103 | install: 104 | template: | 105 | apiVersion: argoproj.io/v1alpha1 106 | kind: Workflow 107 | metadata: 108 | name: wf-install- 109 | spec: 110 | activeDeadlineSeconds: 600 111 | entrypoint: entry 112 | serviceAccountName: addon-manager-workflow-installer-sa 113 | templates: 114 | - name: entry 115 | retryStrategy: 116 | limit: 2 117 | retryPolicy: "Always" 118 | steps: 119 | - - name: install-deployment 120 | template: submit 121 | 122 | - name: submit 123 | resource: 124 | action: apply 125 | manifest: | 126 | apiVersion: apps/v1 127 | kind: Deployment 128 | metadata: 129 | name: event-router 130 | namespace: "{{workflow.parameters.namespace}}" 131 | labels: 132 | app: event-router 133 | spec: 134 | replicas: 1 135 | selector: 136 | matchLabels: 137 | app: event-router 138 | template: 139 | metadata: 140 | labels: 141 | app: event-router 142 | spec: 143 | containers: 144 | - name: kube-event-router 145 | image: gcr.io/heptio-images/eventrouter:v0.2 146 | imagePullPolicy: IfNotPresent 147 | volumeMounts: 148 | - name: config-volume 149 | mountPath: /etc/eventrouter 150 | serviceAccount: event-router-sa 151 | volumes: 152 | - name: config-volume 153 | configMap: 154 | name: event-router-cm 155 | delete: 156 | template: | 157 | apiVersion: argoproj.io/v1alpha1 158 | kind: Workflow 159 | metadata: 160 | name: er-delete- 161 | spec: 162 | entrypoint: delete-wf 163 | serviceAccountName: addon-manager-workflow-installer-sa 164 | 165 | templates: 166 | - name: delete-wf 167 | retryStrategy: 168 | limit: 2 169 | retryPolicy: "Always" 170 | steps: 171 | - - name: delete-ns 172 | template: delete-ns 173 | 174 | - name: delete-ns 175 | container: 176 | image: expert360/kubectl-awscli:v1.11.2 177 | command: [sh, -c] 178 | args: ["kubectl delete all -n {{workflow.parameters.namespace}} --all"] 179 | -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/factory.go: -------------------------------------------------------------------------------- 1 | // Code generated by informer-gen. DO NOT EDIT. 2 | 3 | package externalversions 4 | 5 | import ( 6 | reflect "reflect" 7 | sync "sync" 8 | time "time" 9 | 10 | versioned "github.com/keikoproj/addon-manager/pkg/client/clientset/versioned" 11 | addon "github.com/keikoproj/addon-manager/pkg/client/informers/externalversions/addon" 12 | internalinterfaces "github.com/keikoproj/addon-manager/pkg/client/informers/externalversions/internalinterfaces" 13 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | runtime "k8s.io/apimachinery/pkg/runtime" 15 | schema "k8s.io/apimachinery/pkg/runtime/schema" 16 | cache "k8s.io/client-go/tools/cache" 17 | ) 18 | 19 | // SharedInformerOption defines the functional option type for SharedInformerFactory. 20 | type SharedInformerOption func(*sharedInformerFactory) *sharedInformerFactory 21 | 22 | type sharedInformerFactory struct { 23 | client versioned.Interface 24 | namespace string 25 | tweakListOptions internalinterfaces.TweakListOptionsFunc 26 | lock sync.Mutex 27 | defaultResync time.Duration 28 | customResync map[reflect.Type]time.Duration 29 | 30 | informers map[reflect.Type]cache.SharedIndexInformer 31 | // startedInformers is used for tracking which informers have been started. 32 | // This allows Start() to be called multiple times safely. 33 | startedInformers map[reflect.Type]bool 34 | } 35 | 36 | // WithCustomResyncConfig sets a custom resync period for the specified informer types. 37 | func WithCustomResyncConfig(resyncConfig map[v1.Object]time.Duration) SharedInformerOption { 38 | return func(factory *sharedInformerFactory) *sharedInformerFactory { 39 | for k, v := range resyncConfig { 40 | factory.customResync[reflect.TypeOf(k)] = v 41 | } 42 | return factory 43 | } 44 | } 45 | 46 | // WithTweakListOptions sets a custom filter on all listers of the configured SharedInformerFactory. 47 | func WithTweakListOptions(tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerOption { 48 | return func(factory *sharedInformerFactory) *sharedInformerFactory { 49 | factory.tweakListOptions = tweakListOptions 50 | return factory 51 | } 52 | } 53 | 54 | // WithNamespace limits the SharedInformerFactory to the specified namespace. 55 | func WithNamespace(namespace string) SharedInformerOption { 56 | return func(factory *sharedInformerFactory) *sharedInformerFactory { 57 | factory.namespace = namespace 58 | return factory 59 | } 60 | } 61 | 62 | // NewSharedInformerFactory constructs a new instance of sharedInformerFactory for all namespaces. 63 | func NewSharedInformerFactory(client versioned.Interface, defaultResync time.Duration) SharedInformerFactory { 64 | return NewSharedInformerFactoryWithOptions(client, defaultResync) 65 | } 66 | 67 | // NewFilteredSharedInformerFactory constructs a new instance of sharedInformerFactory. 68 | // Listers obtained via this SharedInformerFactory will be subject to the same filters 69 | // as specified here. 70 | // Deprecated: Please use NewSharedInformerFactoryWithOptions instead 71 | func NewFilteredSharedInformerFactory(client versioned.Interface, defaultResync time.Duration, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerFactory { 72 | return NewSharedInformerFactoryWithOptions(client, defaultResync, WithNamespace(namespace), WithTweakListOptions(tweakListOptions)) 73 | } 74 | 75 | // NewSharedInformerFactoryWithOptions constructs a new instance of a SharedInformerFactory with additional options. 76 | func NewSharedInformerFactoryWithOptions(client versioned.Interface, defaultResync time.Duration, options ...SharedInformerOption) SharedInformerFactory { 77 | factory := &sharedInformerFactory{ 78 | client: client, 79 | namespace: v1.NamespaceAll, 80 | defaultResync: defaultResync, 81 | informers: make(map[reflect.Type]cache.SharedIndexInformer), 82 | startedInformers: make(map[reflect.Type]bool), 83 | customResync: make(map[reflect.Type]time.Duration), 84 | } 85 | 86 | // Apply all options 87 | for _, opt := range options { 88 | factory = opt(factory) 89 | } 90 | 91 | return factory 92 | } 93 | 94 | // Start initializes all requested informers. 95 | func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) { 96 | f.lock.Lock() 97 | defer f.lock.Unlock() 98 | 99 | for informerType, informer := range f.informers { 100 | if !f.startedInformers[informerType] { 101 | go informer.Run(stopCh) 102 | f.startedInformers[informerType] = true 103 | } 104 | } 105 | } 106 | 107 | // WaitForCacheSync waits for all started informers' cache were synced. 108 | func (f *sharedInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool { 109 | informers := func() map[reflect.Type]cache.SharedIndexInformer { 110 | f.lock.Lock() 111 | defer f.lock.Unlock() 112 | 113 | informers := map[reflect.Type]cache.SharedIndexInformer{} 114 | for informerType, informer := range f.informers { 115 | if f.startedInformers[informerType] { 116 | informers[informerType] = informer 117 | } 118 | } 119 | return informers 120 | }() 121 | 122 | res := map[reflect.Type]bool{} 123 | for informType, informer := range informers { 124 | res[informType] = cache.WaitForCacheSync(stopCh, informer.HasSynced) 125 | } 126 | return res 127 | } 128 | 129 | // InternalInformerFor returns the SharedIndexInformer for obj using an internal 130 | // client. 131 | func (f *sharedInformerFactory) InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer { 132 | f.lock.Lock() 133 | defer f.lock.Unlock() 134 | 135 | informerType := reflect.TypeOf(obj) 136 | informer, exists := f.informers[informerType] 137 | if exists { 138 | return informer 139 | } 140 | 141 | resyncPeriod, exists := f.customResync[informerType] 142 | if !exists { 143 | resyncPeriod = f.defaultResync 144 | } 145 | 146 | informer = newFunc(f.client, resyncPeriod) 147 | f.informers[informerType] = informer 148 | 149 | return informer 150 | } 151 | 152 | // SharedInformerFactory provides shared informers for resources in all known 153 | // API group versions. 154 | type SharedInformerFactory interface { 155 | internalinterfaces.SharedInformerFactory 156 | ForResource(resource schema.GroupVersionResource) (GenericInformer, error) 157 | WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool 158 | 159 | Addonmgr() addon.Interface 160 | } 161 | 162 | func (f *sharedInformerFactory) Addonmgr() addon.Interface { 163 | return addon.New(f, f.namespace, f.tweakListOptions) 164 | } 165 | -------------------------------------------------------------------------------- /api/api-tests/addon_types_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package apitests 16 | 17 | import ( 18 | "fmt" 19 | 20 | . "github.com/onsi/ginkgo/v2" 21 | . "github.com/onsi/gomega" 22 | 23 | addonmgrv1alpha1 "github.com/keikoproj/addon-manager/api/addon/v1alpha1" 24 | fakeAddonCli "github.com/keikoproj/addon-manager/pkg/client/clientset/versioned/fake" 25 | "golang.org/x/net/context" 26 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 | "k8s.io/apimachinery/pkg/runtime" 28 | ) 29 | 30 | var wfSpecTemplate = ` 31 | apiVersion: argoproj.io/v1alpha1 32 | kind: Workflow 33 | metadata: 34 | generateName: scripts-python- 35 | spec: 36 | entrypoint: python-script-example 37 | templates: 38 | - name: python-script-example 39 | steps: 40 | - - name: generate 41 | template: gen-random-int 42 | - - name: print 43 | template: print-message 44 | arguments: 45 | parameters: 46 | - name: message 47 | value: "{{steps.generate.outputs.result}}" 48 | 49 | - name: gen-random-int 50 | script: 51 | image: python:alpine3.6 52 | command: [python] 53 | source: | 54 | import random 55 | i = random.randint(1, 100) 56 | print(i) 57 | - name: print-message 58 | inputs: 59 | parameters: 60 | - name: message 61 | container: 62 | image: alpine:latest 63 | command: [sh, -c] 64 | args: ["echo result was: {{inputs.parameters.message}}"] 65 | ` 66 | 67 | // These tests are written in BDD-style using Ginkgo framework. Refer to 68 | // http://onsi.github.io/ginkgo to learn more. 69 | 70 | var _ = Describe("Addon", func() { 71 | 72 | BeforeEach(func() { 73 | // Add any setup steps that needs to be executed before each test 74 | }) 75 | 76 | AfterEach(func() { 77 | // Add any teardown steps that needs to be executed after each test 78 | }) 79 | 80 | // Add Tests for OpenAPI validation (or additional CRD features) specified in 81 | // your API definition. 82 | // Avoid adding tests for vanilla CRUD operations because they would 83 | // test Kubernetes API server, which isn't the goal here. 84 | Context("Create API", func() { 85 | 86 | It("should create an object successfully", func() { 87 | namespace := "default" 88 | adddonName := "foo" 89 | created := &addonmgrv1alpha1.Addon{ 90 | ObjectMeta: metav1.ObjectMeta{ 91 | Name: adddonName, 92 | Namespace: namespace, 93 | }, 94 | Spec: addonmgrv1alpha1.AddonSpec{ 95 | PackageSpec: addonmgrv1alpha1.PackageSpec{ 96 | PkgName: "my-addon", 97 | PkgVersion: "1.0.0", 98 | PkgType: addonmgrv1alpha1.HelmPkg, 99 | PkgDescription: "", 100 | PkgDeps: map[string]string{"core/A": "*", "core/B": "v1.0.0"}, 101 | }, 102 | Selector: metav1.LabelSelector{ 103 | MatchLabels: map[string]string{ 104 | "app": "my-app", 105 | }, 106 | }, 107 | Params: addonmgrv1alpha1.AddonParams{ 108 | Namespace: "foo-ns", 109 | Context: addonmgrv1alpha1.ClusterContext{ 110 | ClusterName: "foo-cluster", 111 | ClusterRegion: "foo-region", 112 | AdditionalConfigs: map[string]addonmgrv1alpha1.FlexString{ 113 | "additional": "config", 114 | }, 115 | }, 116 | Data: map[string]addonmgrv1alpha1.FlexString{ 117 | "foo-param": "val", 118 | }, 119 | }, 120 | Lifecycle: addonmgrv1alpha1.LifecycleWorkflowSpec{ 121 | Prereqs: addonmgrv1alpha1.WorkflowType{ 122 | NamePrefix: "my-prereqs", 123 | Template: wfSpecTemplate, 124 | }, 125 | Install: addonmgrv1alpha1.WorkflowType{ 126 | Template: wfSpecTemplate, 127 | }, 128 | Delete: addonmgrv1alpha1.WorkflowType{ 129 | Template: wfSpecTemplate, 130 | }, 131 | }, 132 | }, 133 | } 134 | 135 | apiCli := fakeAddonCli.NewSimpleClientset([]runtime.Object{}...) 136 | ctx := context.TODO() 137 | By("creating an API obj") 138 | created, err := apiCli.AddonmgrV1alpha1().Addons(namespace).Create(ctx, created, metav1.CreateOptions{}) 139 | Expect(err).To(BeNil()) 140 | Expect(created).NotTo(BeNil()) 141 | 142 | fetched, err := apiCli.AddonmgrV1alpha1().Addons(namespace).Get(ctx, adddonName, metav1.GetOptions{}) 143 | Expect(err).To(BeNil()) 144 | Expect(fetched).To(Equal(created)) 145 | 146 | By("Checking expected fetched values") 147 | pkgSpec := fetched.GetPackageSpec() 148 | Expect(pkgSpec).To(Equal(fetched.Spec.PackageSpec)) 149 | 150 | addonParams := fetched.GetAllAddonParameters() 151 | paramsMap := map[string]string{ 152 | "namespace": "foo-ns", 153 | "clusterName": "foo-cluster", 154 | "clusterRegion": "foo-region", 155 | "additional": "config", 156 | "foo-param": "val", 157 | } 158 | 159 | Expect(addonParams).To(HaveLen(len(paramsMap))) 160 | for name := range paramsMap { 161 | Expect(addonParams[name]).To(Equal(paramsMap[name])) 162 | } 163 | 164 | checksum := fetched.CalculateChecksum() 165 | Expect(checksum).To(Equal("4a77025d")) 166 | 167 | // Update status checksum 168 | fetched.Status.Checksum = checksum 169 | 170 | wfName := fetched.GetFormattedWorkflowName(addonmgrv1alpha1.Install) 171 | Expect(wfName).To(Equal(fmt.Sprintf("foo-install-%s-wf", checksum))) 172 | 173 | By("updating labels") 174 | updated := fetched.DeepCopy() 175 | updated.Labels = map[string]string{"hello": "world"} 176 | updated, err = apiCli.AddonmgrV1alpha1().Addons(namespace).Update(ctx, updated, metav1.UpdateOptions{}) 177 | Expect(err).To(BeNil()) 178 | Expect(updated).NotTo(BeNil()) 179 | 180 | fetched, err = apiCli.AddonmgrV1alpha1().Addons(namespace).Get(ctx, adddonName, metav1.GetOptions{}) 181 | Expect(err).To(BeNil()) 182 | Expect(fetched).To(Equal(updated)) 183 | 184 | By("deleting the created object") 185 | err = apiCli.AddonmgrV1alpha1().Addons(namespace).Delete(ctx, adddonName, metav1.DeleteOptions{}) 186 | Expect(err).To(BeNil()) 187 | }) 188 | 189 | }) 190 | 191 | }) 192 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/addon/v1alpha1/addon.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | package v1alpha1 4 | 5 | import ( 6 | "context" 7 | "time" 8 | 9 | v1alpha1 "github.com/keikoproj/addon-manager/api/addon/v1alpha1" 10 | scheme "github.com/keikoproj/addon-manager/pkg/client/clientset/versioned/scheme" 11 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | types "k8s.io/apimachinery/pkg/types" 13 | watch "k8s.io/apimachinery/pkg/watch" 14 | rest "k8s.io/client-go/rest" 15 | ) 16 | 17 | // AddonsGetter has a method to return a AddonInterface. 18 | // A group's client should implement this interface. 19 | type AddonsGetter interface { 20 | Addons(namespace string) AddonInterface 21 | } 22 | 23 | // AddonInterface has methods to work with Addon resources. 24 | type AddonInterface interface { 25 | Create(ctx context.Context, addon *v1alpha1.Addon, opts v1.CreateOptions) (*v1alpha1.Addon, error) 26 | Update(ctx context.Context, addon *v1alpha1.Addon, opts v1.UpdateOptions) (*v1alpha1.Addon, error) 27 | UpdateStatus(ctx context.Context, addon *v1alpha1.Addon, opts v1.UpdateOptions) (*v1alpha1.Addon, error) 28 | Delete(ctx context.Context, name string, opts v1.DeleteOptions) error 29 | DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error 30 | Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.Addon, error) 31 | List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.AddonList, error) 32 | Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) 33 | Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.Addon, err error) 34 | AddonExpansion 35 | } 36 | 37 | // addons implements AddonInterface 38 | type addons struct { 39 | client rest.Interface 40 | ns string 41 | } 42 | 43 | // newAddons returns a Addons 44 | func newAddons(c *AddonmgrV1alpha1Client, namespace string) *addons { 45 | return &addons{ 46 | client: c.RESTClient(), 47 | ns: namespace, 48 | } 49 | } 50 | 51 | // Get takes name of the addon, and returns the corresponding addon object, and an error if there is any. 52 | func (c *addons) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.Addon, err error) { 53 | result = &v1alpha1.Addon{} 54 | err = c.client.Get(). 55 | Namespace(c.ns). 56 | Resource("addons"). 57 | Name(name). 58 | VersionedParams(&options, scheme.ParameterCodec). 59 | Do(ctx). 60 | Into(result) 61 | return 62 | } 63 | 64 | // List takes label and field selectors, and returns the list of Addons that match those selectors. 65 | func (c *addons) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.AddonList, err error) { 66 | var timeout time.Duration 67 | if opts.TimeoutSeconds != nil { 68 | timeout = time.Duration(*opts.TimeoutSeconds) * time.Second 69 | } 70 | result = &v1alpha1.AddonList{} 71 | err = c.client.Get(). 72 | Namespace(c.ns). 73 | Resource("addons"). 74 | VersionedParams(&opts, scheme.ParameterCodec). 75 | Timeout(timeout). 76 | Do(ctx). 77 | Into(result) 78 | return 79 | } 80 | 81 | // Watch returns a watch.Interface that watches the requested addons. 82 | func (c *addons) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { 83 | var timeout time.Duration 84 | if opts.TimeoutSeconds != nil { 85 | timeout = time.Duration(*opts.TimeoutSeconds) * time.Second 86 | } 87 | opts.Watch = true 88 | return c.client.Get(). 89 | Namespace(c.ns). 90 | Resource("addons"). 91 | VersionedParams(&opts, scheme.ParameterCodec). 92 | Timeout(timeout). 93 | Watch(ctx) 94 | } 95 | 96 | // Create takes the representation of a addon and creates it. Returns the server's representation of the addon, and an error, if there is any. 97 | func (c *addons) Create(ctx context.Context, addon *v1alpha1.Addon, opts v1.CreateOptions) (result *v1alpha1.Addon, err error) { 98 | result = &v1alpha1.Addon{} 99 | err = c.client.Post(). 100 | Namespace(c.ns). 101 | Resource("addons"). 102 | VersionedParams(&opts, scheme.ParameterCodec). 103 | Body(addon). 104 | Do(ctx). 105 | Into(result) 106 | return 107 | } 108 | 109 | // Update takes the representation of a addon and updates it. Returns the server's representation of the addon, and an error, if there is any. 110 | func (c *addons) Update(ctx context.Context, addon *v1alpha1.Addon, opts v1.UpdateOptions) (result *v1alpha1.Addon, err error) { 111 | result = &v1alpha1.Addon{} 112 | err = c.client.Put(). 113 | Namespace(c.ns). 114 | Resource("addons"). 115 | Name(addon.Name). 116 | VersionedParams(&opts, scheme.ParameterCodec). 117 | Body(addon). 118 | Do(ctx). 119 | Into(result) 120 | return 121 | } 122 | 123 | // UpdateStatus was generated because the type contains a Status member. 124 | // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). 125 | func (c *addons) UpdateStatus(ctx context.Context, addon *v1alpha1.Addon, opts v1.UpdateOptions) (result *v1alpha1.Addon, err error) { 126 | result = &v1alpha1.Addon{} 127 | err = c.client.Put(). 128 | Namespace(c.ns). 129 | Resource("addons"). 130 | Name(addon.Name). 131 | SubResource("status"). 132 | VersionedParams(&opts, scheme.ParameterCodec). 133 | Body(addon). 134 | Do(ctx). 135 | Into(result) 136 | return 137 | } 138 | 139 | // Delete takes name of the addon and deletes it. Returns an error if one occurs. 140 | func (c *addons) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { 141 | return c.client.Delete(). 142 | Namespace(c.ns). 143 | Resource("addons"). 144 | Name(name). 145 | Body(&opts). 146 | Do(ctx). 147 | Error() 148 | } 149 | 150 | // DeleteCollection deletes a collection of objects. 151 | func (c *addons) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { 152 | var timeout time.Duration 153 | if listOpts.TimeoutSeconds != nil { 154 | timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second 155 | } 156 | return c.client.Delete(). 157 | Namespace(c.ns). 158 | Resource("addons"). 159 | VersionedParams(&listOpts, scheme.ParameterCodec). 160 | Timeout(timeout). 161 | Body(&opts). 162 | Do(ctx). 163 | Error() 164 | } 165 | 166 | // Patch applies the patch and returns the patched addon. 167 | func (c *addons) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.Addon, err error) { 168 | result = &v1alpha1.Addon{} 169 | err = c.client.Patch(pt). 170 | Namespace(c.ns). 171 | Resource("addons"). 172 | Name(name). 173 | SubResource(subresources...). 174 | VersionedParams(&opts, scheme.ParameterCodec). 175 | Body(data). 176 | Do(ctx). 177 | Into(result) 178 | return 179 | } 180 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Image URL to use all building/pushing image targets 2 | IMG ?= keikoproj/addon-manager:latest 3 | # ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. 4 | ENVTEST_K8S_VERSION = 1.32.0 5 | 6 | KUBERNETES_LOCAL_CLUSTER_VERSION ?= --image=kindest/node:v1.32.0 7 | GIT_COMMIT := $(shell git rev-parse --short HEAD) 8 | BUILD_DATE := $(shell date -u +%Y-%m-%dT%H:%M:%SZ) 9 | PKGS := $(shell go list ./...|grep -v test-) 10 | MODULE := $(shell go list -m) 11 | 12 | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) 13 | ifeq (,$(shell go env GOBIN)) 14 | GOBIN=$(shell go env GOPATH)/bin 15 | else 16 | GOBIN=$(shell go env GOBIN) 17 | endif 18 | 19 | LOADTEST_TIMEOUT ?= "60m" 20 | LOADTEST_START_NUMBER ?= 1 21 | LOADTEST_END_NUMBER ?= 2000 22 | 23 | .EXPORT_ALL_VARIABLES: 24 | GO111MODULE=on 25 | 26 | all: test manager addonctl 27 | 28 | # Run tests 29 | .PHONY: test 30 | test: manifests generate fmt vet envtest ## Run tests. 31 | KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test -race -v $(PKGS) -coverprofile cover.out 32 | 33 | .PHONY: cover 34 | cover: test 35 | go tool cover -func=cover.out -o coverage.txt 36 | go tool cover -html=cover.out -o coverage.html 37 | @cat coverage.txt 38 | @echo "Run 'open coverage.html' to view coverage report." 39 | 40 | # Run E2E tests 41 | bdd: clean fmt vet deploy 42 | go test -timeout 5m -v ./test-bdd/... 43 | 44 | loadtest: fmt vet deploy 45 | go test -timeout $(LOADTEST_TIMEOUT) -startnumber $(LOADTEST_START_NUMBER) -endnumber $(LOADTEST_END_NUMBER) -v ./test-load/... 46 | 47 | # Build manager binary 48 | manager: generate fmt vet 49 | go build -race -o bin/manager main.go 50 | 51 | # Build addonctl binary 52 | addonctl: generate fmt vet 53 | go build -race -o bin/addonctl cmd/addonctl/main.go 54 | 55 | # Run against the configured Kubernetes cluster in ~/.kube/config 56 | run: generate fmt vet 57 | go run ./main.go 58 | 59 | # Install CRDs into a cluster 60 | install: manifests 61 | kubectl apply -f config/crd/bases 62 | 63 | # Deploy controller in the configured Kubernetes cluster in ~/.kube/config 64 | deploy: install 65 | kubectl kustomize config/default | kubectl apply -f - 66 | 67 | clean: 68 | @echo "Cleaning up addons and deployments..." 69 | kubectl delete addons -n addon-manager-system --all --wait=true --timeout=60s || true 70 | @for addon in $(kubectl get addons -n addon-manager-system -o jsonpath='{.items[*].metadata.name}'); do kubectl patch addon ${addon} -n addon-manager-system -p '{"metadata":{"finalizers":null}}' --type=merge; done 71 | @kubectl kustomize config/default | kubectl delete -f - 2> /dev/null || true 72 | 73 | kind-cluster-config: 74 | export KUBECONFIG=$$(kind export kubeconfig --name="kind") 75 | 76 | kind-cluster: 77 | kind create cluster --config hack/kind.cluster.yaml --name="kind" $(KUBERNETES_LOCAL_CLUSTER_VERSION) 78 | kind load docker-image ${IMG} 79 | 80 | kind-cluster-delete: kind-cluster-config 81 | kind delete cluster 82 | 83 | # Generate manifests e.g. CRD, RBAC etc. 84 | .PHONY: manifests 85 | manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. 86 | $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases 87 | 88 | # Run go fmt against code 89 | fmt: 90 | go fmt ./... 91 | 92 | # Run go vet against code 93 | vet: 94 | go vet ./... 95 | 96 | # Generate code 97 | .PHONY: generate 98 | generate: controller-gen types ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. 99 | $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="$(MODULE)" 100 | 101 | # generates many other files (listers, informers, client etc). 102 | api/addon/v1alpha1/zz_generated.deepcopy.go: $(TYPES) 103 | ln -s . v1 104 | $(CODE_GENERATOR_GEN)/generate-groups.sh \ 105 | "deepcopy,client,informer,lister" \ 106 | github.com/keikoproj/addon-manager/pkg/client github.com/keikoproj/addon-manager/api\ 107 | addon:v1alpha1 \ 108 | --go-header-file ./hack/custom-boilerplate.go.txt 109 | rm -rf v1 110 | 111 | .PHONY: types 112 | types: api/addon/v1alpha1/zz_generated.deepcopy.go 113 | 114 | # Build the docker image 115 | docker-build: manager 116 | docker build --build-arg COMMIT=${GIT_COMMIT} --build-arg DATE=${BUILD_DATE} -t ${IMG} . 117 | @echo "updating kustomize image patch file for manager resource" 118 | sed -i'' -e 's@image: .*@image: '"${IMG}"'@' ./config/default/manager_image_patch.yaml 119 | 120 | # Push the docker image 121 | docker-push: 122 | docker push ${IMG} 123 | 124 | release: 125 | goreleaser release --clean 126 | 127 | snapshot: 128 | goreleaser release --clean --snapshot 129 | 130 | code-generator: 131 | ifeq (, $(shell which code-generator)) 132 | @{ \ 133 | set -e ;\ 134 | CONTROLLER_GEN_TMP_DIR=$$(mktemp -d) ;\ 135 | cd $$CONTROLLER_GEN_TMP_DIR ;\ 136 | curl -L -o code-generator.zip https://github.com/kubernetes/code-generator/archive/refs/tags/v0.28.2.zip ;\ 137 | unzip code-generator.zip ;\ 138 | mv code-generator-0.28.2 $(GOPATH)/bin/ ;\ 139 | rm -rf code-generator.zip ;\ 140 | } 141 | CODE_GENERATOR_GEN=$(GOBIN)/code-generator-0.28.2 142 | else 143 | CODE_GENERATOR_GEN=$(shell which code-generator) 144 | endif 145 | 146 | ##@ Build Dependencies 147 | 148 | ## Location to install dependencies to 149 | LOCALBIN ?= $(shell pwd)/bin 150 | $(LOCALBIN): 151 | mkdir -p $(LOCALBIN) 152 | 153 | ## Tool Binaries 154 | KUSTOMIZE ?= $(LOCALBIN)/kustomize-$(KUSTOMIZE_VERSION) 155 | CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen-$(CONTROLLER_TOOLS_VERSION) 156 | ENVTEST ?= $(LOCALBIN)/setup-envtest-$(ENVTEST_VERSION) 157 | 158 | ## Tool Versions 159 | KUSTOMIZE_VERSION ?= v5.3.0 160 | CONTROLLER_TOOLS_VERSION ?= v0.17.2 161 | ENVTEST_VERSION ?= release-0.20 162 | 163 | .PHONY: kustomize 164 | kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. 165 | $(KUSTOMIZE): $(LOCALBIN) 166 | $(call go-install-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v5,$(KUSTOMIZE_VERSION)) 167 | 168 | .PHONY: controller-gen 169 | controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. 170 | $(CONTROLLER_GEN): $(LOCALBIN) 171 | $(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen,$(CONTROLLER_TOOLS_VERSION)) 172 | 173 | .PHONY: envtest 174 | envtest: $(ENVTEST) ## Download setup-envtest locally if necessary. 175 | $(ENVTEST): $(LOCALBIN) 176 | $(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest,$(ENVTEST_VERSION)) 177 | 178 | # go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist 179 | # $1 - target path with name of binary (ideally with version) 180 | # $2 - package url which can be installed 181 | # $3 - specific version of package 182 | define go-install-tool 183 | @[ -f $(1) ] || { \ 184 | set -e; \ 185 | package=$(2)@$(3) ;\ 186 | echo "Downloading $${package}" ;\ 187 | GOBIN=$(LOCALBIN) go install $${package} ;\ 188 | mv "$$(echo "$(1)" | sed "s/-$(3)$$//")" $(1) ;\ 189 | } 190 | endef 191 | -------------------------------------------------------------------------------- /pkg/addon/addon_update_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package addon 16 | 17 | import ( 18 | "context" 19 | "testing" 20 | "time" 21 | 22 | wfv1 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" 23 | "github.com/keikoproj/addon-manager/pkg/workflows" 24 | v1 "k8s.io/api/core/v1" 25 | "k8s.io/apimachinery/pkg/types" 26 | "k8s.io/client-go/tools/record" 27 | ctrl "sigs.k8s.io/controller-runtime" 28 | "sigs.k8s.io/controller-runtime/pkg/client/fake" 29 | 30 | addonmgrv1alpha1 "github.com/keikoproj/addon-manager/api/addon/v1alpha1" 31 | "github.com/onsi/gomega" 32 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 33 | "k8s.io/apimachinery/pkg/runtime" 34 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 35 | "sigs.k8s.io/controller-runtime/pkg/client" 36 | ) 37 | 38 | var ( 39 | scheme = runtime.NewScheme() 40 | fakeRcdr = record.NewBroadcasterForTests(1*time.Second).NewRecorder(scheme, v1.EventSource{Component: "addons"}) 41 | ctx = context.TODO() 42 | ) 43 | 44 | func init() { 45 | _ = addonmgrv1alpha1.AddToScheme(scheme) 46 | _ = wfv1.AddToScheme(scheme) 47 | _ = clientgoscheme.AddToScheme(scheme) 48 | } 49 | 50 | func TestUpdateAddonStatusLifecycleFromWorkflow(t *testing.T) { 51 | g := gomega.NewGomegaWithT(t) 52 | 53 | testNamespace := "default" 54 | testAddonName := "test-addon" 55 | 56 | testAddon := &addonmgrv1alpha1.Addon{ 57 | ObjectMeta: metav1.ObjectMeta{ 58 | Name: testAddonName, 59 | Namespace: testNamespace, 60 | }, 61 | Spec: addonmgrv1alpha1.AddonSpec{ 62 | PackageSpec: addonmgrv1alpha1.PackageSpec{ 63 | PkgName: "test-addon", 64 | }, 65 | Params: addonmgrv1alpha1.AddonParams{ 66 | Namespace: "test-addon-ns", 67 | }, 68 | }, 69 | Status: addonmgrv1alpha1.AddonStatus{ 70 | Lifecycle: addonmgrv1alpha1.AddonStatusLifecycle{}, 71 | Resources: []addonmgrv1alpha1.ObjectStatus{}, 72 | }, 73 | } 74 | 75 | fakeCli := fake.NewClientBuilder().WithScheme(scheme).WithStatusSubresource(testAddon).Build() 76 | updater := NewAddonUpdater(fakeRcdr, fakeCli, NewAddonVersionCacheClient(), ctrl.Log.WithName("test")) 77 | 78 | err := updater.client.Create(ctx, testAddon, &client.CreateOptions{}) 79 | g.Expect(err).ToNot(gomega.HaveOccurred()) 80 | 81 | existingAddon, err := updater.getExistingAddon(ctx, testNamespace, testAddonName) 82 | g.Expect(err).ToNot(gomega.HaveOccurred()) 83 | g.Expect(existingAddon).ToNot(gomega.BeNil()) 84 | 85 | var wfSucceeded = &wfv1.Workflow{ 86 | TypeMeta: metav1.TypeMeta{ 87 | Kind: "Workflow", 88 | APIVersion: "argoproj.io/v1alpha1", 89 | }, 90 | ObjectMeta: metav1.ObjectMeta{ 91 | Name: "test-addon-install-9375dca7-wf", 92 | Namespace: testNamespace, 93 | Labels: map[string]string{ 94 | workflows.WfInstanceIdLabelKey: workflows.WfInstanceId, 95 | }, 96 | }, 97 | Status: wfv1.WorkflowStatus{ 98 | Phase: wfv1.WorkflowSucceeded, 99 | }, 100 | } 101 | 102 | err = updater.UpdateAddonStatusLifecycleFromWorkflow(ctx, testNamespace, testAddonName, wfSucceeded) 103 | g.Expect(err).ToNot(gomega.HaveOccurred()) 104 | 105 | err = updater.client.Get(ctx, types.NamespacedName{Namespace: testNamespace, Name: testAddonName}, testAddon) 106 | g.Expect(err).ToNot(gomega.HaveOccurred()) 107 | g.Expect(testAddon.Status.Lifecycle.Installed).To(gomega.Equal(addonmgrv1alpha1.Succeeded)) 108 | } 109 | 110 | func TestUpdateAddonStatusLifecycleFromWorkflow_InvalidChecksum(t *testing.T) { 111 | g := gomega.NewGomegaWithT(t) 112 | 113 | testNamespace := "default" 114 | testAddonName := "test-addon" 115 | 116 | fakeCli := fake.NewClientBuilder().WithScheme(scheme).Build() 117 | 118 | updater := NewAddonUpdater(fakeRcdr, fakeCli, NewAddonVersionCacheClient(), ctrl.Log.WithName("test")) 119 | testAddon := &addonmgrv1alpha1.Addon{ 120 | ObjectMeta: metav1.ObjectMeta{ 121 | Name: testAddonName, 122 | Namespace: testNamespace, 123 | }, 124 | Spec: addonmgrv1alpha1.AddonSpec{ 125 | PackageSpec: addonmgrv1alpha1.PackageSpec{ 126 | PkgName: "test-addon", 127 | }, 128 | Params: addonmgrv1alpha1.AddonParams{ 129 | Namespace: "test-addon-ns", 130 | }, 131 | }, 132 | Status: addonmgrv1alpha1.AddonStatus{ 133 | Lifecycle: addonmgrv1alpha1.AddonStatusLifecycle{}, 134 | Resources: []addonmgrv1alpha1.ObjectStatus{}, 135 | }, 136 | } 137 | err := updater.client.Create(ctx, testAddon, &client.CreateOptions{}) 138 | g.Expect(err).ToNot(gomega.HaveOccurred()) 139 | 140 | existingAddon, err := updater.getExistingAddon(ctx, testNamespace, testAddonName) 141 | g.Expect(err).ToNot(gomega.HaveOccurred()) 142 | g.Expect(existingAddon).ToNot(gomega.BeNil()) 143 | 144 | var wfSucceeded = &wfv1.Workflow{ 145 | TypeMeta: metav1.TypeMeta{ 146 | Kind: "Workflow", 147 | APIVersion: "argoproj.io/v1alpha1", 148 | }, 149 | ObjectMeta: metav1.ObjectMeta{ 150 | Name: "test-addon-prereqs-123456-wf", 151 | Namespace: testNamespace, 152 | Labels: map[string]string{ 153 | workflows.WfInstanceIdLabelKey: workflows.WfInstanceId, 154 | }, 155 | }, 156 | Status: wfv1.WorkflowStatus{ 157 | Phase: wfv1.WorkflowSucceeded, 158 | }, 159 | } 160 | 161 | err = updater.UpdateAddonStatusLifecycleFromWorkflow(ctx, testNamespace, testAddonName, wfSucceeded) 162 | g.Expect(err).ToNot(gomega.HaveOccurred()) 163 | } 164 | 165 | func TestUpdateAddonStatusLifecycleFromWorkflow_DeleteFailed(t *testing.T) { 166 | g := gomega.NewGomegaWithT(t) 167 | 168 | testNamespace := "default" 169 | testAddonName := "test-addon" 170 | 171 | testAddon := &addonmgrv1alpha1.Addon{ 172 | ObjectMeta: metav1.ObjectMeta{ 173 | Name: testAddonName, 174 | Namespace: testNamespace, 175 | }, 176 | Spec: addonmgrv1alpha1.AddonSpec{ 177 | PackageSpec: addonmgrv1alpha1.PackageSpec{ 178 | PkgName: "test-addon", 179 | }, 180 | Params: addonmgrv1alpha1.AddonParams{ 181 | Namespace: "test-addon-ns", 182 | }, 183 | }, 184 | Status: addonmgrv1alpha1.AddonStatus{ 185 | Lifecycle: addonmgrv1alpha1.AddonStatusLifecycle{}, 186 | Resources: []addonmgrv1alpha1.ObjectStatus{}, 187 | }, 188 | } 189 | 190 | fakeCli := fake.NewClientBuilder().WithScheme(scheme).WithStatusSubresource(testAddon).Build() 191 | updater := NewAddonUpdater(fakeRcdr, fakeCli, NewAddonVersionCacheClient(), ctrl.Log.WithName("test")) 192 | 193 | err := updater.client.Create(ctx, testAddon, &client.CreateOptions{}) 194 | g.Expect(err).ToNot(gomega.HaveOccurred()) 195 | 196 | existingAddon, err := updater.getExistingAddon(ctx, testNamespace, testAddonName) 197 | g.Expect(err).ToNot(gomega.HaveOccurred()) 198 | g.Expect(existingAddon).ToNot(gomega.BeNil()) 199 | 200 | var wfDelete = &wfv1.Workflow{ 201 | TypeMeta: metav1.TypeMeta{ 202 | Kind: "Workflow", 203 | APIVersion: "argoproj.io/v1alpha1", 204 | }, 205 | ObjectMeta: metav1.ObjectMeta{ 206 | Name: "test-addon-delete-9375dca7-wf", 207 | Namespace: testNamespace, 208 | Labels: map[string]string{ 209 | workflows.WfInstanceIdLabelKey: workflows.WfInstanceId, 210 | }, 211 | }, 212 | Status: wfv1.WorkflowStatus{ 213 | Phase: wfv1.WorkflowError, 214 | }, 215 | } 216 | 217 | err = updater.UpdateAddonStatusLifecycleFromWorkflow(ctx, testNamespace, testAddonName, wfDelete) 218 | g.Expect(err).ToNot(gomega.HaveOccurred()) 219 | 220 | err = updater.client.Get(ctx, types.NamespacedName{Namespace: testNamespace, Name: testAddonName}, testAddon) 221 | g.Expect(err).ToNot(gomega.HaveOccurred()) 222 | g.Expect(testAddon.Status.Lifecycle.Installed).To(gomega.Equal(addonmgrv1alpha1.DeleteFailed)) 223 | } 224 | --------------------------------------------------------------------------------