├── .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 |
--------------------------------------------------------------------------------