├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .golangci.yaml ├── .goreleaser.yml ├── LICENSE ├── README.md ├── cmd └── helmify │ ├── flags.go │ ├── main.go │ └── version.go ├── examples ├── README.md ├── app │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ │ ├── _helpers.tpl │ │ ├── batch-job.yaml │ │ ├── cron-job.yaml │ │ ├── daemonset.yaml │ │ ├── deployment.yaml │ │ ├── my-config-props.yaml │ │ ├── my-config.yaml │ │ ├── my-sample-pv-claim.yaml │ │ ├── my-secret-ca.yaml │ │ ├── my-secret-vars.yaml │ │ ├── myapp-ingress.yaml │ │ ├── myapp-lb-service.yaml │ │ ├── myapp-pdb.yaml │ │ ├── myapp-service.yaml │ │ ├── nginx.yaml │ │ └── statefulset.yaml │ └── values.yaml └── operator │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ ├── _helpers.tpl │ ├── cephvolume-crd.yaml │ ├── configmap-vars.yaml │ ├── deployment.yaml │ ├── leader-election-rbac.yaml │ ├── manager-aggregated-rbac.yaml │ ├── manager-config.yaml │ ├── manager-rbac.yaml │ ├── manifestcephvolume-crd.yaml │ ├── metrics-reader-rbac.yaml │ ├── metrics-service.yaml │ ├── mutating-webhook-configuration.yaml │ ├── proxy-rbac.yaml │ ├── pvc-lim.yaml │ ├── secret-ca.yaml │ ├── secret-registry-credentials.yaml │ ├── secret-vars.yaml │ ├── selfsigned-issuer.yaml │ ├── serviceaccount.yaml │ ├── serving-cert.yaml │ ├── validating-webhook-configuration.yaml │ └── webhook-service.yaml │ └── values.yaml ├── go.mod ├── go.sum ├── internal └── test_utils.go ├── pkg ├── app │ ├── app.go │ ├── app_e2e_test.go │ └── context.go ├── cluster │ └── domain.go ├── config │ ├── config.go │ └── config_test.go ├── decoder │ ├── decoder.go │ └── decoder_test.go ├── file │ └── reader.go ├── format │ ├── fix_quotes.go │ ├── fix_quotes_test.go │ ├── trailing_whitespaces.go │ └── trailing_whitespaces_test.go ├── helm │ ├── chart.go │ ├── doc.go │ └── init.go ├── helmify │ ├── model.go │ ├── values.go │ └── values_test.go ├── metadata │ ├── metadata.go │ └── metadata_test.go ├── processor │ ├── configmap │ │ ├── configmap.go │ │ └── configmap_test.go │ ├── crd │ │ ├── crd.go │ │ └── crd_test.go │ ├── daemonset │ │ ├── daemonset.go │ │ └── daemonset_test.go │ ├── default.go │ ├── default_test.go │ ├── deployment │ │ ├── deployment.go │ │ └── deployment_test.go │ ├── doc.go │ ├── job │ │ ├── cron.go │ │ ├── cron_test.go │ │ ├── job.go │ │ └── job_test.go │ ├── meta.go │ ├── meta_test.go │ ├── pod │ │ ├── pod.go │ │ └── pod_test.go │ ├── poddisruptionbudget │ │ ├── pdb.go │ │ └── pdb_test.go │ ├── rbac │ │ ├── clusterrolebinding.go │ │ ├── clusterrolebinding_test.go │ │ ├── role.go │ │ ├── role_test.go │ │ ├── rolebinding.go │ │ ├── rolebinding_test.go │ │ ├── serviceaccount.go │ │ └── serviceaccount_test.go │ ├── secret │ │ ├── secret.go │ │ └── secret_test.go │ ├── security-context │ │ ├── container_security_context.go │ │ └── container_security_context_test.go │ ├── service │ │ ├── ingress.go │ │ ├── ingress_test.go │ │ ├── service.go │ │ └── service_test.go │ ├── statefulset │ │ └── statefulset.go │ ├── storage │ │ ├── pvc.go │ │ └── pvc_test.go │ └── webhook │ │ ├── cert.go │ │ ├── cert_test.go │ │ ├── issuer.go │ │ ├── issuer_test.go │ │ ├── mutating.go │ │ ├── mutating_test.go │ │ ├── validating.go │ │ └── validating_test.go └── yaml │ ├── yaml.go │ └── yaml_test.go └── test_data ├── dir ├── another_dir │ └── stateful_set.yaml ├── config.yaml ├── deployment.yaml ├── secret.yaml ├── service.yaml └── storage.yaml ├── k8s-operator-ci.yaml ├── k8s-operator-kustomize.output └── sample-app.yaml /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | # GO tests 17 | - name: Set up Go 18 | uses: actions/setup-go@v4 19 | with: 20 | go-version: '1.21' 21 | cache: false 22 | 23 | - name: Fmt 24 | run: | 25 | # Run gofmt in "diff" mode to check for unformatted code 26 | UNFORMATTED_FILES=$(gofmt -l .) 27 | # Check if any files are unformatted 28 | if [[ -n "$UNFORMATTED_FILES" ]]; then 29 | echo "::error::The following Go files are not formatted correctly:" 30 | echo "$UNFORMATTED_FILES" # List unformatted files in the log 31 | echo "::error::Please format your Go code by running \`go fmt ./...\` and commit the changes." 32 | exit 1 # Fail the check 33 | else 34 | echo "All Go files are properly formatted." 35 | fi 36 | - name: Vet 37 | run: go vet ./... 38 | 39 | - name: Test 40 | run: go test ./... 41 | 42 | - name: golangci-lint 43 | uses: golangci/golangci-lint-action@v3 44 | with: 45 | version: v1.54 46 | # Generate example charts 47 | - name: Generate example charts 48 | run: | 49 | cat test_data/sample-app.yaml | go run ./cmd/helmify examples/app 50 | cat test_data/k8s-operator-kustomize.output | go run ./cmd/helmify examples/operator 51 | - name: Check that chart examples were commited 52 | run: | 53 | if [[ -n "$(git status --porcelain)" ]]; then 54 | # Capture the list of uncommitted files 55 | UNCOMMITTED_FILES=$(git status --porcelain) 56 | echo "::error::Chart examples generation step has uncommitted changes: $UNCOMMITTED_FILES 57 | Please run following commands and commit the results: 58 | - \`cat test_data/sample-app.yaml | go run ./cmd/helmify examples/app\` 59 | - \`cat test_data/k8s-operator-kustomize.output | go run ./cmd/helmify examples/operator\`" 60 | exit 1 61 | else 62 | echo "Chart examples generation check passed. No uncommitted changes." 63 | fi 64 | # Dry-run generated charts in cluster 65 | - name: Install k8s cluster 66 | uses: helm/kind-action@v1.4.0 67 | - name: Install certs 68 | run: kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.1.1/cert-manager.yaml 69 | 70 | - name: Generate operator ci chart 71 | run: cat test_data/k8s-operator-ci.yaml | go run ./cmd/helmify examples/operator-ci 72 | - name: Fill operator ci secrets 73 | run: sed -i 's/""/"abc"/' ./examples/operator-ci/values.yaml 74 | - name: Dry-run operator in k8s cluster 75 | run: helm template ./examples/operator-ci -n operator-ns --create-namespace | kubectl apply --dry-run=server -f - 76 | 77 | - name: Generate app chart 78 | run: cat test_data/sample-app.yaml | go run ./cmd/helmify examples/app 79 | - name: Fill app secrets 80 | run: sed -i 's/""/"abc"/' ./examples/app/values.yaml 81 | - name: Dry-run app in k8s cluster 82 | run: helm template ./examples/app -n app-ns --create-namespace | kubectl apply --dry-run=server -f - 83 | 84 | # Validate charts with Kubeconform 85 | - name: Install Kubeconform 86 | run: go install github.com/yannh/kubeconform/cmd/kubeconform@v0.6.1 87 | 88 | - name: Validate app 89 | run: helm template ./examples/app -n app-ns --create-namespace | kubeconform -schema-location 'https://raw.githubusercontent.com/kubernetes/kubernetes/master/api/openapi-spec/v3/apis__apiextensions.k8s.io__v1_openapi.json' -strict 90 | 91 | - name: Generate operator example chart 92 | run: cat test_data/k8s-operator-kustomize.output | go run ./cmd/helmify examples/operator 93 | - name: Fill operator example secrets 94 | run: sed -i 's/""/"abc"/' ./examples/operator/values.yaml 95 | - name: Validate example operator 96 | run: helm template ./examples/operator -n operator-ns --create-namespace | kubeconform -schema-location 'https://raw.githubusercontent.com/kubernetes/kubernetes/master/api/openapi-spec/v3/apis__apiextensions.k8s.io__v1_openapi.json' -strict 97 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Go Binaries 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | goreleaser: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: actions/setup-go@v3 14 | with: 15 | go-version: '1.21' 16 | - name: Run GoReleaser 17 | uses: goreleaser/goreleaser-action@v2 18 | with: 19 | version: v1.26.2 20 | args: release --rm-dist 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | bin 9 | testbin/* 10 | 11 | # Test binary, build with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Kubernetes Generated files - skip generated files, except for vendored files 18 | 19 | !vendor/**/zz_generated.* 20 | 21 | # editor and IDE paraphernalia 22 | .idea 23 | *.swp 24 | *.swo 25 | *~ 26 | # VSCode devcontainer/codespace 27 | .devcontainer/ 28 | 29 | dist/ 30 | 31 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | run: 2 | tests: false 3 | skip-dirs: 4 | - internal 5 | go: 1.21 6 | 7 | fix: true 8 | 9 | linters-settings: 10 | maligned: 11 | suggest-new: true 12 | staticcheck: 13 | go: "1.21" 14 | checks: [ "all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022","-SA6005","-SA1019" ] 15 | gocritic: 16 | disabled-checks: 17 | - captLocal 18 | - commentFormatting 19 | revive: 20 | rules: 21 | - name: var-naming 22 | disabled: true 23 | nolintlint: 24 | require-explanation: true 25 | 26 | linters: 27 | disable-all: true 28 | enable: 29 | - asasalint # check for pass []any as any in variadic func(...any) 30 | - asciicheck # simple linter to check that your code does not contain non-ASCII identifiers 31 | - errchkjson # checks types passed to the json encoding functions 32 | - errorlint # errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13 33 | - exportloopref # checks for pointers to enclosing loop variables 34 | - gocritic # provides diagnostics that check for bugs, performance and style issues 35 | - revive # fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint 36 | - nolintlint # requiresw to write notes why linter is disabled 37 | - errcheck 38 | - staticcheck 39 | - typecheck 40 | - unused 41 | - govet 42 | - ineffassign 43 | - gosimple 44 | - durationcheck 45 | - errchkjson 46 | - errorlint 47 | - makezero 48 | - exportloopref 49 | - errchkjson 50 | - prealloc 51 | - deadcode 52 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: helmify 2 | before: 3 | hooks: 4 | - go mod tidy 5 | builds: 6 | - main: ./cmd/helmify 7 | env: 8 | - CGO_ENABLED=0 9 | goos: 10 | - linux 11 | - windows 12 | - darwin 13 | ignore: 14 | - goos: darwin 15 | goarch: 386 16 | - goos: darwin 17 | goarch: arm 18 | - goos: windows 19 | goarch: arm 20 | - goos: windows 21 | goarch: arm64 22 | archives: 23 | - 24 | name_template: >- 25 | {{ .ProjectName }}_ 26 | {{- title .Os }}_ 27 | {{- if eq .Arch "amd64" }}x86_64 28 | {{- else if eq .Arch "386" }}i386 29 | {{- else }}{{ .Arch }}{{ end }} 30 | format_overrides: 31 | - goos: windows 32 | format: zip 33 | files: 34 | - none* 35 | checksum: 36 | name_template: 'checksums.txt' 37 | brews: 38 | - 39 | tap: 40 | owner: arttor 41 | name: homebrew-tap 42 | commit_author: 43 | name: arttor 44 | email: torubarov-a-a@yandex.ru 45 | commit_msg_template: "Brew formula update for {{ .ProjectName }} version {{ .Tag }}" 46 | folder: Formula 47 | homepage: "https://github.com/arttor/helmify" 48 | description: "Creates Helm chart from Kubernetes yaml." 49 | license: "MIT" 50 | test: | 51 | system "#{bin}/helmify --version" 52 | install: | 53 | bin.install "helmify" 54 | release: 55 | github: 56 | owner: arttor 57 | name: helmify -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 arttor 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cmd/helmify/flags.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/arttor/helmify/pkg/config" 11 | ) 12 | 13 | const helpText = `Helmify parses kubernetes resources from std.in and converts it to a Helm chart. 14 | 15 | Example 1: 'kustomize build | helmify mychart' 16 | - will create 'mychart' directory with Helm chart from kustomize output. 17 | 18 | Example 2: 'cat my-app.yaml | helmify mychart' 19 | - will create 'mychart' directory with Helm chart from yaml file. 20 | 21 | Example 3: 'helmify -f ./test_data/dir mychart' 22 | - will scan directory ./test_data/dir for files with k8s manifests and create 'mychart' directory with Helm chart. 23 | 24 | Example 4: 'helmify -f ./test_data/dir -r mychart' 25 | - will scan directory ./test_data/dir recursively and create 'mychart' directory with Helm chart. 26 | 27 | Example 5: 'helmify -f ./test_data/dir -f ./test_data/sample-app.yaml -f ./test_data/dir/another_dir mychart' 28 | - will scan provided multiple files and directories and create 'mychart' directory with Helm chart. 29 | 30 | Example 6: 'awk 'FNR==1 && NR!=1 {print "---"}{print}' /my_directory/*.yaml | helmify mychart' 31 | - will create 'mychart' directory with Helm chart from all yaml files in my_directory directory. 32 | 33 | Usage: 34 | helmify [flags] CHART_NAME - CHART_NAME is optional. Default is 'chart'. Can be a directory, e.g. 'deploy/charts/mychart'. 35 | 36 | Flags: 37 | ` 38 | 39 | type arrayFlags []string 40 | 41 | func (i *arrayFlags) String() string { 42 | if i == nil || len(*i) == 0 { 43 | return "" 44 | } 45 | return strings.Join(*i, ", ") 46 | } 47 | 48 | func (i *arrayFlags) Set(value string) error { 49 | *i = append(*i, value) 50 | return nil 51 | } 52 | 53 | // ReadFlags command-line flags into app config. 54 | func ReadFlags() config.Config { 55 | files := arrayFlags{} 56 | result := config.Config{} 57 | var h, help, version, crd, preservens bool 58 | flag.BoolVar(&h, "h", false, "Print help. Example: helmify -h") 59 | flag.BoolVar(&help, "help", false, "Print help. Example: helmify -help") 60 | flag.BoolVar(&version, "version", false, "Print helmify version. Example: helmify -version") 61 | flag.BoolVar(&result.Verbose, "v", false, "Enable verbose output (print WARN & INFO). Example: helmify -v") 62 | flag.BoolVar(&result.VeryVerbose, "vv", false, "Enable very verbose output. Same as verbose but with DEBUG. Example: helmify -vv") 63 | flag.BoolVar(&crd, "crd-dir", false, "Enable crd install into 'crds' directory.\nWarning: CRDs placed in 'crds' directory will not be templated by Helm.\nSee https://helm.sh/docs/chart_best_practices/custom_resource_definitions/#some-caveats-and-explanations\nExample: helmify -crd-dir") 64 | flag.BoolVar(&result.ImagePullSecrets, "image-pull-secrets", false, "Allows the user to use existing secrets as imagePullSecrets in values.yaml") 65 | flag.BoolVar(&result.GenerateDefaults, "generate-defaults", false, "Allows the user to add empty placeholders for typical customization options in values.yaml. Currently covers: topology constraints, node selectors, tolerances") 66 | flag.BoolVar(&result.CertManagerAsSubchart, "cert-manager-as-subchart", false, "Allows the user to add cert-manager as a subchart") 67 | flag.StringVar(&result.CertManagerVersion, "cert-manager-version", "v1.12.2", "Allows the user to specify cert-manager subchart version. Only useful with cert-manager-as-subchart.") 68 | flag.BoolVar(&result.CertManagerInstallCRD, "cert-manager-install-crd", true, "Allows the user to install cert-manager CRD. Only useful with cert-manager-as-subchart.") 69 | flag.BoolVar(&result.FilesRecursively, "r", false, "Scan dirs from -f option recursively") 70 | flag.BoolVar(&result.OriginalName, "original-name", false, "Use the object's original name instead of adding the chart's release name as the common prefix.") 71 | flag.Var(&files, "f", "File or directory containing k8s manifests") 72 | flag.BoolVar(&preservens, "preserve-ns", false, "Use the object's original namespace instead of adding all the resources to a common namespace") 73 | flag.BoolVar(&result.AddWebhookOption, "add-webhook-option", false, "Allows the user to add webhook option in values.yaml") 74 | 75 | flag.Parse() 76 | if h || help { 77 | fmt.Print(helpText) 78 | flag.PrintDefaults() 79 | os.Exit(0) 80 | } 81 | if version { 82 | printVersion() 83 | os.Exit(0) 84 | } 85 | name := flag.Arg(0) 86 | if name != "" { 87 | result.ChartName = filepath.Base(name) 88 | result.ChartDir = filepath.Dir(name) 89 | } 90 | if crd { 91 | result.Crd = crd 92 | } 93 | if preservens { 94 | result.PreserveNs = true 95 | } 96 | result.Files = files 97 | return result 98 | } 99 | -------------------------------------------------------------------------------- /cmd/helmify/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/arttor/helmify/pkg/app" 7 | "github.com/sirupsen/logrus" 8 | ) 9 | 10 | func main() { 11 | conf := ReadFlags() 12 | stat, err := os.Stdin.Stat() 13 | if err != nil { 14 | logrus.WithError(err).Error("stdin error") 15 | os.Exit(1) 16 | } 17 | if len(conf.Files) == 0 && (stat.Mode()&os.ModeCharDevice) != 0 { 18 | logrus.Error("no data piped in stdin") 19 | os.Exit(1) 20 | } 21 | if err = app.Start(os.Stdin, conf); err != nil { 22 | logrus.WithError(err).Error("helmify finished with error") 23 | os.Exit(1) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /cmd/helmify/version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // these information will be collected when build, by `-ldflags "-X main.version=0.1"`. 8 | var ( 9 | version = "development" 10 | date = "not set" 11 | commit = "not set" 12 | ) 13 | 14 | func printVersion() { 15 | fmt.Printf("Version: %s\n", version) 16 | fmt.Printf("Build Time: %s\n", date) 17 | fmt.Printf("Git Commit: %s\n", commit) 18 | } 19 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | `app` - helm chart generated by helmify from [test_data/sample-app.yaml](https://github.com/arttor/helmify/blob/main/test_data/sample-app.yaml). 3 | Represents typical k8s app with Deployment, Service, ConfigMap, Secret. 4 | Generated with: `cat test_data/sample-app.yaml | go run ./cmd/helmify examples/app` 5 | 6 | `operator` - helm chart generated by helmify from [test_data/k8s-operator-kustomize.output](https://github.com/arttor/helmify/blob/main/test_data/k8s-operator-kustomize.output). 7 | Represents typical k8s operator build with [Operator-SDK](https://github.com/operator-framework/operator-sdk) or [Kubebuilder](https://github.com/kubernetes-sigs/kubebuilder). 8 | Generated with: `cat test_data/k8s-operator-kustomize.output | go run ./cmd/helmify examples/operator` -------------------------------------------------------------------------------- /examples/app/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /examples/app/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: app 3 | description: A Helm chart for Kubernetes 4 | # A chart can be either an 'application' or a 'library' chart. 5 | # 6 | # Application charts are a collection of templates that can be packaged into versioned archives 7 | # to be deployed. 8 | # 9 | # Library charts provide useful utilities or functions for the chart developer. They're included as 10 | # a dependency of application charts to inject those utilities and functions into the rendering 11 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 12 | type: application 13 | # This is the chart version. This version number should be incremented each time you make changes 14 | # to the chart and its templates, including the app version. 15 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 16 | version: 0.1.0 17 | # This is the version number of the application being deployed. This version number should be 18 | # incremented each time you make changes to the application. Versions are not expected to 19 | # follow Semantic Versioning. They should reflect the version the application is using. 20 | # It is recommended to use it with quotes. 21 | appVersion: "0.1.0" 22 | -------------------------------------------------------------------------------- /examples/app/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "app.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "app.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "app.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "app.labels" -}} 37 | helm.sh/chart: {{ include "app.chart" . }} 38 | {{ include "app.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "app.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "app.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "app.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "app.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /examples/app/templates/batch-job.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: Job 3 | metadata: 4 | name: {{ include "app.fullname" . }}-batch-job 5 | labels: 6 | {{- include "app.labels" . | nindent 4 }} 7 | spec: 8 | backoffLimit: {{ .Values.batchJob.backoffLimit }} 9 | template: 10 | spec: 11 | containers: 12 | - command: 13 | - perl 14 | - -Mbignum=bpi 15 | - -wle 16 | - print bpi(2000) 17 | env: 18 | - name: KUBERNETES_CLUSTER_DOMAIN 19 | value: {{ quote .Values.kubernetesClusterDomain }} 20 | image: {{ .Values.batchJob.pi.image.repository }}:{{ .Values.batchJob.pi.image.tag 21 | | default .Chart.AppVersion }} 22 | name: pi 23 | resources: {} 24 | restartPolicy: Never 25 | -------------------------------------------------------------------------------- /examples/app/templates/cron-job.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: CronJob 3 | metadata: 4 | name: {{ include "app.fullname" . }}-cron-job 5 | labels: 6 | {{- include "app.labels" . | nindent 4 }} 7 | spec: 8 | jobTemplate: 9 | spec: 10 | template: 11 | spec: 12 | containers: 13 | - command: 14 | - /bin/sh 15 | - -c 16 | - date; echo Hello from the Kubernetes cluster 17 | env: 18 | - name: KUBERNETES_CLUSTER_DOMAIN 19 | value: {{ quote .Values.kubernetesClusterDomain }} 20 | image: {{ .Values.cronJob.hello.image.repository }}:{{ .Values.cronJob.hello.image.tag 21 | | default .Chart.AppVersion }} 22 | imagePullPolicy: {{ .Values.cronJob.hello.imagePullPolicy }} 23 | name: hello 24 | resources: {} 25 | restartPolicy: OnFailure 26 | schedule: {{ .Values.cronJob.schedule | quote }} 27 | -------------------------------------------------------------------------------- /examples/app/templates/daemonset.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: DaemonSet 3 | metadata: 4 | name: {{ include "app.fullname" . }}-fluentd-elasticsearch 5 | labels: 6 | k8s-app: fluentd-logging 7 | {{- include "app.labels" . | nindent 4 }} 8 | spec: 9 | selector: 10 | matchLabels: 11 | name: fluentd-elasticsearch 12 | {{- include "app.selectorLabels" . | nindent 6 }} 13 | template: 14 | metadata: 15 | labels: 16 | name: fluentd-elasticsearch 17 | {{- include "app.selectorLabels" . | nindent 8 }} 18 | spec: 19 | containers: 20 | - env: 21 | - name: KUBERNETES_CLUSTER_DOMAIN 22 | value: {{ quote .Values.kubernetesClusterDomain }} 23 | image: {{ .Values.fluentdElasticsearch.fluentdElasticsearch.image.repository }}:{{ 24 | .Values.fluentdElasticsearch.fluentdElasticsearch.image.tag | default .Chart.AppVersion 25 | }} 26 | name: fluentd-elasticsearch 27 | resources: {{- toYaml .Values.fluentdElasticsearch.fluentdElasticsearch.resources 28 | | nindent 10 }} 29 | volumeMounts: 30 | - mountPath: /var/log 31 | name: varlog 32 | - mountPath: /var/lib/docker/containers 33 | name: varlibdockercontainers 34 | readOnly: true 35 | terminationGracePeriodSeconds: 30 36 | tolerations: 37 | - effect: NoSchedule 38 | key: node-role.kubernetes.io/master 39 | operator: Exists 40 | volumes: 41 | - hostPath: 42 | path: /var/log 43 | name: varlog 44 | - hostPath: 45 | path: /var/lib/docker/containers 46 | name: varlibdockercontainers 47 | -------------------------------------------------------------------------------- /examples/app/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "app.fullname" . }}-myapp 5 | labels: 6 | app: myapp 7 | {{- include "app.labels" . | nindent 4 }} 8 | spec: 9 | replicas: {{ .Values.myapp.replicas }} 10 | revisionHistoryLimit: {{ .Values.myapp.revisionHistoryLimit }} 11 | selector: 12 | matchLabels: 13 | app: myapp 14 | {{- include "app.selectorLabels" . | nindent 6 }} 15 | template: 16 | metadata: 17 | labels: 18 | app: myapp 19 | {{- include "app.selectorLabels" . | nindent 8 }} 20 | spec: 21 | containers: 22 | - args: {{- toYaml .Values.myapp.app.args | nindent 8 }} 23 | command: 24 | - /manager 25 | env: 26 | - name: VAR1 27 | valueFrom: 28 | secretKeyRef: 29 | key: VAR1 30 | name: {{ include "app.fullname" . }}-my-secret-vars 31 | - name: VAR2 32 | valueFrom: 33 | secretKeyRef: 34 | key: VAR2 35 | name: {{ include "app.fullname" . }}-my-secret-vars 36 | - name: APP_NAME 37 | valueFrom: 38 | fieldRef: 39 | fieldPath: metadata.labels['app.kubernetes.io/name'] 40 | - name: INSTANCE_NAME 41 | valueFrom: 42 | fieldRef: 43 | fieldPath: metadata.labels['app.kubernetes.io/instance'] 44 | - name: KUBERNETES_CLUSTER_DOMAIN 45 | value: {{ quote .Values.kubernetesClusterDomain }} 46 | image: {{ .Values.myapp.app.image.repository }}:{{ .Values.myapp.app.image.tag 47 | | default .Chart.AppVersion }} 48 | livenessProbe: 49 | httpGet: 50 | path: /healthz 51 | port: 8081 52 | initialDelaySeconds: 15 53 | periodSeconds: 20 54 | name: app 55 | readinessProbe: 56 | httpGet: 57 | path: /readyz 58 | port: 8081 59 | initialDelaySeconds: 5 60 | periodSeconds: 10 61 | resources: {{- toYaml .Values.myapp.app.resources | nindent 10 }} 62 | securityContext: {{- toYaml .Values.myapp.app.containerSecurityContext | nindent 63 | 10 }} 64 | volumeMounts: 65 | - mountPath: /my_config.properties 66 | name: manager-config 67 | subPath: my_config.properties 68 | - mountPath: /my.ca 69 | name: secret-volume 70 | - mountPath: /etc/props 71 | name: props 72 | - mountPath: /usr/share/nginx/html 73 | name: sample-pv-storage 74 | - args: {{- toYaml .Values.myapp.proxySidecar.args | nindent 8 }} 75 | env: 76 | - name: KUBERNETES_CLUSTER_DOMAIN 77 | value: {{ quote .Values.kubernetesClusterDomain }} 78 | image: {{ .Values.myapp.proxySidecar.image.repository }}:{{ .Values.myapp.proxySidecar.image.tag 79 | | default .Chart.AppVersion }} 80 | name: proxy-sidecar 81 | ports: 82 | - containerPort: 8443 83 | name: https 84 | resources: {} 85 | initContainers: 86 | - command: 87 | - /bin/sh 88 | - -c 89 | - echo 'Initializing container...' 90 | env: 91 | - name: KUBERNETES_CLUSTER_DOMAIN 92 | value: {{ quote .Values.kubernetesClusterDomain }} 93 | image: {{ .Values.myapp.initContainer.image.repository }}:{{ .Values.myapp.initContainer.image.tag 94 | | default .Chart.AppVersion }} 95 | name: init-container 96 | resources: {} 97 | nodeSelector: {{- toYaml .Values.myapp.nodeSelector | nindent 8 }} 98 | securityContext: {{- toYaml .Values.myapp.podSecurityContext | nindent 8 }} 99 | terminationGracePeriodSeconds: 10 100 | volumes: 101 | - configMap: 102 | name: {{ include "app.fullname" . }}-my-config 103 | name: manager-config 104 | - configMap: 105 | name: {{ include "app.fullname" . }}-my-config-props 106 | name: props 107 | - name: secret-volume 108 | secret: 109 | secretName: {{ include "app.fullname" . }}-my-secret-ca 110 | - name: sample-pv-storage 111 | persistentVolumeClaim: 112 | claimName: {{ include "app.fullname" . }}-my-sample-pv-claim 113 | -------------------------------------------------------------------------------- /examples/app/templates/my-config-props.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{ include "app.fullname" . }}-my-config-props 5 | labels: 6 | {{- include "app.labels" . | nindent 4 }} 7 | data: 8 | my.prop1: {{ .Values.myConfigProps.myProp1 | quote }} 9 | my.prop2: {{ .Values.myConfigProps.myProp2 | quote }} 10 | my.prop3: {{ .Values.myConfigProps.myProp3 | quote }} 11 | myval.yaml: {{ .Values.myConfigProps.myvalYaml | toYaml | indent 1 }} 12 | -------------------------------------------------------------------------------- /examples/app/templates/my-config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{ include "app.fullname" . }}-my-config 5 | labels: 6 | {{- include "app.labels" . | nindent 4 }} 7 | immutable: true 8 | data: 9 | dummyconfigmapkey: {{ .Values.myConfig.dummyconfigmapkey | quote }} 10 | my_config.properties: | 11 | health.healthProbeBindAddress={{ .Values.myConfig.myConfigProperties.health.healthProbeBindAddress | quote }} 12 | metrics.bindAddress={{ .Values.myConfig.myConfigProperties.metrics.bindAddress | quote }} 13 | -------------------------------------------------------------------------------- /examples/app/templates/my-sample-pv-claim.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolumeClaim 3 | metadata: 4 | name: {{ include "app.fullname" . }}-my-sample-pv-claim 5 | labels: 6 | {{- include "app.labels" . | nindent 4 }} 7 | spec: 8 | accessModes: 9 | - ReadWriteOnce 10 | resources: 11 | limits: 12 | storage: {{ .Values.pvc.mySamplePvClaim.storageLimit | quote }} 13 | requests: 14 | storage: {{ .Values.pvc.mySamplePvClaim.storageRequest | quote }} 15 | storageClassName: {{ .Values.pvc.mySamplePvClaim.storageClass | quote }} 16 | -------------------------------------------------------------------------------- /examples/app/templates/my-secret-ca.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: {{ include "app.fullname" . }}-my-secret-ca 5 | labels: 6 | {{- include "app.labels" . | nindent 4 }} 7 | data: 8 | ca.crt: {{ required "mySecretCa.caCrt is required" .Values.mySecretCa.caCrt | b64enc 9 | | quote }} 10 | type: opaque 11 | -------------------------------------------------------------------------------- /examples/app/templates/my-secret-vars.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: {{ include "app.fullname" . }}-my-secret-vars 5 | labels: 6 | {{- include "app.labels" . | nindent 4 }} 7 | data: 8 | ELASTIC_FOOBAR_HUNTER123_MEOWTOWN_VERIFY: {{ required "mySecretVars.elasticFoobarHunter123MeowtownVerify is required" .Values.mySecretVars.elasticFoobarHunter123MeowtownVerify | b64enc 9 | | quote }} 10 | VAR1: {{ required "mySecretVars.var1 is required" .Values.mySecretVars.var1 | b64enc 11 | | quote }} 12 | VAR2: {{ required "mySecretVars.var2 is required" .Values.mySecretVars.var2 | b64enc 13 | | quote }} 14 | stringData: 15 | str: {{ required "mySecretVars.str is required" .Values.mySecretVars.str | quote 16 | }} 17 | type: opaque 18 | -------------------------------------------------------------------------------- /examples/app/templates/myapp-ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: {{ include "app.fullname" . }}-myapp-ingress 5 | labels: 6 | {{- include "app.labels" . | nindent 4 }} 7 | annotations: 8 | nginx.ingress.kubernetes.io/rewrite-target: / 9 | spec: 10 | rules: 11 | - http: 12 | paths: 13 | - backend: 14 | service: 15 | name: '{{ include "app.fullname" . }}-myapp-service' 16 | port: 17 | number: 8443 18 | path: /testpath 19 | pathType: Prefix 20 | -------------------------------------------------------------------------------- /examples/app/templates/myapp-lb-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "app.fullname" . }}-myapp-lb-service 5 | labels: 6 | app: myapp 7 | {{- include "app.labels" . | nindent 4 }} 8 | spec: 9 | type: {{ .Values.myappLbService.type }} 10 | selector: 11 | app: myapp 12 | {{- include "app.selectorLabels" . | nindent 4 }} 13 | ports: 14 | {{- .Values.myappLbService.ports | toYaml | nindent 2 }} 15 | loadBalancerSourceRanges: 16 | {{- .Values.myappLbService.loadBalancerSourceRanges | toYaml | nindent 2 }} 17 | -------------------------------------------------------------------------------- /examples/app/templates/myapp-pdb.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: policy/v1 2 | kind: PodDisruptionBudget 3 | metadata: 4 | name: {{ include "app.fullname" . }}-myapp-pdb 5 | labels: 6 | app: nginx 7 | {{- include "app.labels" . | nindent 4 }} 8 | spec: 9 | minAvailable: {{ .Values.myappPdb.minAvailable }} 10 | maxUnavailable: {{ .Values.myappPdb.maxUnavailable }} 11 | selector: 12 | matchLabels: 13 | app: nginx 14 | {{- include "app.selectorLabels" . | nindent 6 }} 15 | -------------------------------------------------------------------------------- /examples/app/templates/myapp-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "app.fullname" . }}-myapp-service 5 | labels: 6 | app: myapp 7 | {{- include "app.labels" . | nindent 4 }} 8 | spec: 9 | type: {{ .Values.myappService.type }} 10 | selector: 11 | app: myapp 12 | {{- include "app.selectorLabels" . | nindent 4 }} 13 | ports: 14 | {{- .Values.myappService.ports | toYaml | nindent 2 }} 15 | -------------------------------------------------------------------------------- /examples/app/templates/nginx.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "app.fullname" . }}-nginx 5 | labels: 6 | app: nginx 7 | {{- include "app.labels" . | nindent 4 }} 8 | spec: 9 | type: {{ .Values.nginx.type }} 10 | selector: 11 | app: nginx 12 | {{- include "app.selectorLabels" . | nindent 4 }} 13 | ports: 14 | {{- .Values.nginx.ports | toYaml | nindent 2 }} 15 | -------------------------------------------------------------------------------- /examples/app/templates/statefulset.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: {{ include "app.fullname" . }}-web 5 | labels: 6 | {{- include "app.labels" . | nindent 4 }} 7 | spec: 8 | replicas: {{ .Values.web.replicas }} 9 | selector: 10 | matchLabels: 11 | app: nginx 12 | serviceName: {{ include "app.fullname" . }}-nginx 13 | template: 14 | metadata: 15 | labels: 16 | app: nginx 17 | spec: 18 | containers: 19 | - env: 20 | - name: KUBERNETES_CLUSTER_DOMAIN 21 | value: {{ quote .Values.kubernetesClusterDomain }} 22 | image: {{ .Values.web.nginx.image.repository }}:{{ .Values.web.nginx.image.tag 23 | | default .Chart.AppVersion }} 24 | name: nginx 25 | ports: 26 | - containerPort: 80 27 | name: web 28 | resources: {} 29 | volumeMounts: 30 | - mountPath: /usr/share/nginx/html 31 | name: www 32 | updateStrategy: {} 33 | volumeClaimTemplates: 34 | - metadata: 35 | creationTimestamp: null 36 | name: www 37 | spec: 38 | accessModes: 39 | - ReadWriteOnce 40 | resources: {{ .Values.web.volumeClaims.www | toYaml | nindent 8 }} 41 | -------------------------------------------------------------------------------- /examples/app/values.yaml: -------------------------------------------------------------------------------- 1 | batchJob: 2 | backoffLimit: 4 3 | pi: 4 | image: 5 | repository: perl 6 | tag: 5.34.0 7 | cronJob: 8 | hello: 9 | image: 10 | repository: busybox 11 | tag: "1.28" 12 | imagePullPolicy: IfNotPresent 13 | schedule: '* * * * *' 14 | fluentdElasticsearch: 15 | fluentdElasticsearch: 16 | image: 17 | repository: quay.io/fluentd_elasticsearch/fluentd 18 | tag: v2.5.2 19 | resources: 20 | limits: 21 | memory: 200Mi 22 | requests: 23 | cpu: 100m 24 | memory: 200Mi 25 | kubernetesClusterDomain: cluster.local 26 | myConfig: 27 | dummyconfigmapkey: dummyconfigmapvalue 28 | myConfigProperties: 29 | health: 30 | healthProbeBindAddress: "8081" 31 | metrics: 32 | bindAddress: 127.0.0.1:8080 33 | myConfigProps: 34 | myProp1: "1" 35 | myProp2: val 1 36 | myProp3: "true" 37 | myvalYaml: |- 38 | apiVersion: clickhouse.altinity.com/v1 39 | kind: ClickHouseInstallationTemplate 40 | metadata: 41 | name: default-oneperhost-pod-template 42 | spec: 43 | templates: 44 | podTemplates: 45 | - name: default-oneperhost-pod-template 46 | distribution: "OnePerHost" 47 | mySecretCa: 48 | caCrt: "" 49 | mySecretVars: 50 | elasticFoobarHunter123MeowtownVerify: "" 51 | str: "" 52 | var1: "" 53 | var2: "" 54 | myapp: 55 | app: 56 | args: 57 | - --health-probe-bind-address=:8081 58 | - --metrics-bind-address=127.0.0.1:8080 59 | - --leader-elect 60 | containerSecurityContext: 61 | allowPrivilegeEscalation: false 62 | image: 63 | repository: controller 64 | tag: latest 65 | resources: 66 | limits: 67 | cpu: 100m 68 | memory: 30Mi 69 | requests: 70 | cpu: 100m 71 | memory: 20Mi 72 | initContainer: 73 | image: 74 | repository: bash 75 | tag: latest 76 | nodeSelector: 77 | region: east 78 | type: user-node 79 | podSecurityContext: 80 | fsGroup: 20000 81 | runAsNonRoot: true 82 | runAsUser: 65532 83 | proxySidecar: 84 | args: 85 | - --secure-listen-address=0.0.0.0:8443 86 | - --v=10 87 | image: 88 | repository: gcr.io/kubebuilder/kube-rbac-proxy 89 | tag: v0.8.0 90 | replicas: 3 91 | revisionHistoryLimit: 5 92 | myappLbService: 93 | loadBalancerSourceRanges: 94 | - 10.0.0.0/8 95 | ports: 96 | - name: https 97 | port: 8443 98 | targetPort: https 99 | type: LoadBalancer 100 | myappPdb: 101 | minAvailable: 2 102 | myappService: 103 | ports: 104 | - name: https 105 | port: 8443 106 | targetPort: https 107 | type: ClusterIP 108 | nginx: 109 | ports: 110 | - name: web 111 | port: 80 112 | targetPort: 0 113 | type: ClusterIP 114 | pvc: 115 | mySamplePvClaim: 116 | storageClass: manual 117 | storageLimit: 5Gi 118 | storageRequest: 3Gi 119 | web: 120 | nginx: 121 | image: 122 | repository: registry.k8s.io/nginx-slim 123 | tag: "0.8" 124 | replicas: 2 125 | volumeClaims: 126 | www: 127 | requests: 128 | storage: 1Gi 129 | -------------------------------------------------------------------------------- /examples/operator/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /examples/operator/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: operator 3 | description: A Helm chart for Kubernetes 4 | # A chart can be either an 'application' or a 'library' chart. 5 | # 6 | # Application charts are a collection of templates that can be packaged into versioned archives 7 | # to be deployed. 8 | # 9 | # Library charts provide useful utilities or functions for the chart developer. They're included as 10 | # a dependency of application charts to inject those utilities and functions into the rendering 11 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 12 | type: application 13 | # This is the chart version. This version number should be incremented each time you make changes 14 | # to the chart and its templates, including the app version. 15 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 16 | version: 0.1.0 17 | # This is the version number of the application being deployed. This version number should be 18 | # incremented each time you make changes to the application. Versions are not expected to 19 | # follow Semantic Versioning. They should reflect the version the application is using. 20 | # It is recommended to use it with quotes. 21 | appVersion: "0.1.0" 22 | -------------------------------------------------------------------------------- /examples/operator/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "operator.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "operator.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "operator.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "operator.labels" -}} 37 | helm.sh/chart: {{ include "operator.chart" . }} 38 | {{ include "operator.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "operator.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "operator.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "operator.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "operator.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /examples/operator/templates/configmap-vars.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{ include "operator.fullname" . }}-configmap-vars 5 | labels: 6 | {{- include "operator.labels" . | nindent 4 }} 7 | data: 8 | VAR4: {{ .Values.configmapVars.var4 | quote }} 9 | -------------------------------------------------------------------------------- /examples/operator/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "operator.fullname" . }}-controller-manager 5 | labels: 6 | control-plane: controller-manager 7 | {{- include "operator.labels" . | nindent 4 }} 8 | spec: 9 | replicas: {{ .Values.controllerManager.replicas }} 10 | strategy: 11 | rollingUpdate: 12 | maxSurge: {{ .Values.controllerManager.strategy.rollingUpdate.maxSurge | quote 13 | }} 14 | maxUnavailable: {{ .Values.controllerManager.strategy.rollingUpdate.maxUnavailable 15 | | quote }} 16 | type: {{ .Values.controllerManager.strategy.type | quote }} 17 | selector: 18 | matchLabels: 19 | control-plane: controller-manager 20 | {{- include "operator.selectorLabels" . | nindent 6 }} 21 | template: 22 | metadata: 23 | labels: 24 | control-plane: controller-manager 25 | {{- include "operator.selectorLabels" . | nindent 8 }} 26 | spec: 27 | containers: 28 | - args: {{- toYaml .Values.controllerManager.kubeRbacProxy.args | nindent 8 }} 29 | env: 30 | - name: KUBERNETES_CLUSTER_DOMAIN 31 | value: {{ quote .Values.kubernetesClusterDomain }} 32 | image: {{ .Values.controllerManager.kubeRbacProxy.image.repository }}:{{ .Values.controllerManager.kubeRbacProxy.image.tag 33 | | default .Chart.AppVersion }} 34 | name: kube-rbac-proxy 35 | ports: 36 | - containerPort: 8443 37 | name: https 38 | resources: {} 39 | - args: {{- toYaml .Values.controllerManager.manager.args | nindent 8 }} 40 | command: 41 | - /manager 42 | env: 43 | - name: VAR1 44 | valueFrom: 45 | secretKeyRef: 46 | key: VAR1 47 | name: {{ include "operator.fullname" . }}-secret-vars 48 | - name: VAR2 49 | value: {{ quote .Values.controllerManager.manager.env.var2 }} 50 | - name: VAR3_MY_ENV 51 | value: {{ quote .Values.controllerManager.manager.env.var3MyEnv }} 52 | - name: VAR4 53 | valueFrom: 54 | configMapKeyRef: 55 | key: VAR4 56 | name: {{ include "operator.fullname" . }}-configmap-vars 57 | - name: VAR5 58 | valueFrom: 59 | fieldRef: 60 | fieldPath: metadata.namespace 61 | - name: VAR6 62 | valueFrom: 63 | resourceFieldRef: 64 | divisor: "0" 65 | resource: limits.cpu 66 | - name: KUBERNETES_CLUSTER_DOMAIN 67 | value: {{ quote .Values.kubernetesClusterDomain }} 68 | image: {{ .Values.controllerManager.manager.image.repository }}:{{ .Values.controllerManager.manager.image.tag 69 | | default .Chart.AppVersion }} 70 | imagePullPolicy: {{ .Values.controllerManager.manager.imagePullPolicy }} 71 | livenessProbe: 72 | httpGet: 73 | path: /healthz 74 | port: 8081 75 | initialDelaySeconds: 15 76 | periodSeconds: 20 77 | name: manager 78 | readinessProbe: 79 | httpGet: 80 | path: /readyz 81 | port: 8081 82 | initialDelaySeconds: 5 83 | periodSeconds: 10 84 | resources: {{- toYaml .Values.controllerManager.manager.resources | nindent 10 85 | }} 86 | securityContext: {{- toYaml .Values.controllerManager.manager.containerSecurityContext 87 | | nindent 10 }} 88 | volumeMounts: 89 | - mountPath: /controller_manager_config.yaml 90 | name: manager-config 91 | subPath: controller_manager_config.yaml 92 | - mountPath: /my.ca 93 | name: secret-volume 94 | imagePullSecrets: 95 | - name: {{ include "operator.fullname" . }}-secret-registry-credentials 96 | nodeSelector: {{- toYaml .Values.controllerManager.nodeSelector | nindent 8 }} 97 | securityContext: {{- toYaml .Values.controllerManager.podSecurityContext | nindent 98 | 8 }} 99 | serviceAccountName: {{ include "operator.fullname" . }}-controller-manager 100 | terminationGracePeriodSeconds: 10 101 | topologySpreadConstraints: 102 | - matchLabelKeys: 103 | - app 104 | - pod-template-hash 105 | maxSkew: 1 106 | topologyKey: kubernetes.io/hostname 107 | whenUnsatisfiable: DoNotSchedule 108 | volumes: 109 | - configMap: 110 | name: {{ include "operator.fullname" . }}-manager-config 111 | name: manager-config 112 | - name: secret-volume 113 | secret: 114 | secretName: {{ include "operator.fullname" . }}-secret-ca 115 | -------------------------------------------------------------------------------- /examples/operator/templates/leader-election-rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: Role 3 | metadata: 4 | name: {{ include "operator.fullname" . }}-leader-election-role 5 | labels: 6 | {{- include "operator.labels" . | nindent 4 }} 7 | rules: 8 | - apiGroups: 9 | - "" 10 | resources: 11 | - configmaps 12 | verbs: 13 | - get 14 | - list 15 | - watch 16 | - create 17 | - update 18 | - patch 19 | - delete 20 | - apiGroups: 21 | - coordination.k8s.io 22 | resources: 23 | - leases 24 | verbs: 25 | - get 26 | - list 27 | - watch 28 | - create 29 | - update 30 | - patch 31 | - delete 32 | - apiGroups: 33 | - "" 34 | resources: 35 | - events 36 | verbs: 37 | - create 38 | - patch 39 | --- 40 | apiVersion: rbac.authorization.k8s.io/v1 41 | kind: RoleBinding 42 | metadata: 43 | name: {{ include "operator.fullname" . }}-leader-election-rolebinding 44 | labels: 45 | {{- include "operator.labels" . | nindent 4 }} 46 | roleRef: 47 | apiGroup: rbac.authorization.k8s.io 48 | kind: Role 49 | name: '{{ include "operator.fullname" . }}-leader-election-role' 50 | subjects: 51 | - kind: ServiceAccount 52 | name: '{{ include "operator.fullname" . }}-controller-manager' 53 | namespace: '{{ .Release.Namespace }}' 54 | -------------------------------------------------------------------------------- /examples/operator/templates/manager-aggregated-rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: {{ include "operator.fullname" . }}-manager-aggregated-role 5 | labels: 6 | {{- include "operator.labels" . | nindent 4 }} 7 | aggregationRule: 8 | clusterRoleSelectors: 9 | - matchExpressions: 10 | - key: app.kubernetes.io/instance 11 | operator: In 12 | values: 13 | - my-operator 14 | rules: [] 15 | -------------------------------------------------------------------------------- /examples/operator/templates/manager-config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{ include "operator.fullname" . }}-manager-config 5 | labels: 6 | {{- include "operator.labels" . | nindent 4 }} 7 | data: 8 | controller_manager_config.yaml: {{ .Values.managerConfig.controllerManagerConfigYaml 9 | | toYaml | indent 1 }} 10 | dummyconfigmapkey: {{ .Values.managerConfig.dummyconfigmapkey | quote }} 11 | -------------------------------------------------------------------------------- /examples/operator/templates/manager-rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: {{ include "operator.fullname" . }}-manager-role 5 | labels: 6 | {{- include "operator.labels" . | nindent 4 }} 7 | rules: 8 | - apiGroups: 9 | - "" 10 | resources: 11 | - pods 12 | verbs: 13 | - get 14 | - list 15 | - apiGroups: 16 | - "" 17 | resources: 18 | - pods/exec 19 | verbs: 20 | - create 21 | - get 22 | - apiGroups: 23 | - test.example.com 24 | resources: 25 | - cephvolumes 26 | verbs: 27 | - create 28 | - delete 29 | - get 30 | - list 31 | - patch 32 | - update 33 | - watch 34 | - apiGroups: 35 | - test.example.com 36 | resources: 37 | - cephvolumes/finalizers 38 | verbs: 39 | - update 40 | - apiGroups: 41 | - test.example.com 42 | resources: 43 | - cephvolumes/status 44 | verbs: 45 | - get 46 | - patch 47 | - update 48 | - apiGroups: 49 | - test.example.com 50 | resources: 51 | - manifestcephvolumes 52 | verbs: 53 | - create 54 | - delete 55 | - get 56 | - list 57 | - patch 58 | - update 59 | - watch 60 | - apiGroups: 61 | - test.example.com 62 | resources: 63 | - manifestcephvolumes/finalizers 64 | verbs: 65 | - update 66 | - apiGroups: 67 | - test.example.com 68 | resources: 69 | - manifestcephvolumes/status 70 | verbs: 71 | - get 72 | - patch 73 | - update 74 | - apiGroups: 75 | - test.example.com 76 | resources: 77 | - storagetypes 78 | verbs: 79 | - get 80 | - list 81 | - watch 82 | - apiGroups: 83 | - test.example.com 84 | resources: 85 | - storagetypes/status 86 | verbs: 87 | - get 88 | --- 89 | apiVersion: rbac.authorization.k8s.io/v1 90 | kind: ClusterRoleBinding 91 | metadata: 92 | name: {{ include "operator.fullname" . }}-manager-rolebinding 93 | labels: 94 | {{- include "operator.labels" . | nindent 4 }} 95 | roleRef: 96 | apiGroup: rbac.authorization.k8s.io 97 | kind: ClusterRole 98 | name: '{{ include "operator.fullname" . }}-manager-role' 99 | subjects: 100 | - kind: ServiceAccount 101 | name: '{{ include "operator.fullname" . }}-controller-manager' 102 | namespace: '{{ .Release.Namespace }}' 103 | -------------------------------------------------------------------------------- /examples/operator/templates/manifestcephvolume-crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: manifestcephvolumes.test.example.com 5 | annotations: 6 | cert-manager.io/inject-ca-from: '{{ .Release.Namespace }}/{{ include "operator.fullname" 7 | . }}-serving-cert' 8 | labels: 9 | {{- include "operator.labels" . | nindent 4 }} 10 | spec: 11 | conversion: 12 | strategy: Webhook 13 | webhook: 14 | clientConfig: 15 | service: 16 | name: '{{ include "operator.fullname" . }}-webhook-service' 17 | namespace: '{{ .Release.Namespace }}' 18 | path: /convert 19 | conversionReviewVersions: 20 | - v1 21 | group: test.example.com 22 | names: 23 | kind: ManifestCephVolume 24 | listKind: ManifestCephVolumeList 25 | plural: manifestcephvolumes 26 | singular: manifestcephvolume 27 | scope: Namespaced 28 | versions: 29 | - additionalPrinterColumns: 30 | - description: Ceph RBD pool name 31 | jsonPath: .spec.poolName 32 | name: PoolName 33 | type: string 34 | - description: Sync interval in seconds 35 | jsonPath: .spec.interval 36 | name: Interval 37 | type: string 38 | - description: Last update time 39 | jsonPath: .status.lastUpdate 40 | name: LastUpdate 41 | type: string 42 | name: v1alpha1 43 | schema: 44 | openAPIV3Schema: 45 | description: ManifestCephVolume monitors given ceph pool and manifests containing 46 | volumes as CephVolume CR 47 | properties: 48 | apiVersion: 49 | description: 'APIVersion defines the versioned schema of this representation 50 | of an object. Servers should convert recognized schemas to the latest 51 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 52 | type: string 53 | kind: 54 | description: 'Kind is a string value representing the REST resource this 55 | object represents. Servers may infer this from the endpoint the client 56 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 57 | type: string 58 | metadata: 59 | type: object 60 | spec: 61 | description: ManifestCephVolumeSpec defines the desired state of ManifestCephVolume 62 | properties: 63 | interval: 64 | description: Interval - Ceph pool polling interval 65 | format: int32 66 | minimum: 60 67 | type: integer 68 | poolName: 69 | description: PoolName name of Ceph RBD pool to get volumes 70 | type: string 71 | required: 72 | - interval 73 | type: object 74 | status: 75 | description: ManifestCephVolumeStatus defines the observed state of ManifestCephVolume 76 | properties: 77 | lastUpdate: 78 | description: LastUpdate - time of last successful volumes update 79 | format: date-time 80 | type: string 81 | type: object 82 | type: object 83 | served: true 84 | storage: true 85 | subresources: 86 | status: {} 87 | status: 88 | acceptedNames: 89 | kind: "" 90 | plural: "" 91 | conditions: [] 92 | storedVersions: [] 93 | -------------------------------------------------------------------------------- /examples/operator/templates/metrics-reader-rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: {{ include "operator.fullname" . }}-metrics-reader 5 | labels: 6 | {{- include "operator.labels" . | nindent 4 }} 7 | rules: 8 | - nonResourceURLs: 9 | - /metrics 10 | verbs: 11 | - get 12 | -------------------------------------------------------------------------------- /examples/operator/templates/metrics-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "operator.fullname" . }}-controller-manager-metrics-service 5 | labels: 6 | control-plane: controller-manager 7 | {{- include "operator.labels" . | nindent 4 }} 8 | spec: 9 | type: {{ .Values.metricsService.type }} 10 | selector: 11 | control-plane: controller-manager 12 | {{- include "operator.selectorLabels" . | nindent 4 }} 13 | ports: 14 | {{- .Values.metricsService.ports | toYaml | nindent 2 }} 15 | -------------------------------------------------------------------------------- /examples/operator/templates/mutating-webhook-configuration.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: admissionregistration.k8s.io/v1 2 | kind: MutatingWebhookConfiguration 3 | metadata: 4 | name: {{ include "operator.fullname" . }}-mutating-webhook-configuration 5 | annotations: 6 | cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ include "operator.fullname" . }}-serving-cert 7 | labels: 8 | {{- include "operator.labels" . | nindent 4 }} 9 | webhooks: 10 | - admissionReviewVersions: 11 | - v1 12 | clientConfig: 13 | service: 14 | name: '{{ include "operator.fullname" . }}-webhook-service' 15 | namespace: '{{ .Release.Namespace }}' 16 | path: /mutate-ceph-example-com-v1-mycluster 17 | failurePolicy: Fail 18 | name: mmycluster.kb.io 19 | rules: 20 | - apiGroups: 21 | - test.example.com 22 | apiVersions: 23 | - v1 24 | operations: 25 | - CREATE 26 | - UPDATE 27 | resources: 28 | - myclusters 29 | sideEffects: None 30 | -------------------------------------------------------------------------------- /examples/operator/templates/proxy-rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: {{ include "operator.fullname" . }}-proxy-role 5 | labels: 6 | {{- include "operator.labels" . | nindent 4 }} 7 | rules: 8 | - apiGroups: 9 | - authentication.k8s.io 10 | resources: 11 | - tokenreviews 12 | verbs: 13 | - create 14 | - apiGroups: 15 | - authorization.k8s.io 16 | resources: 17 | - subjectaccessreviews 18 | verbs: 19 | - create 20 | --- 21 | apiVersion: rbac.authorization.k8s.io/v1 22 | kind: ClusterRoleBinding 23 | metadata: 24 | name: {{ include "operator.fullname" . }}-proxy-rolebinding 25 | labels: 26 | {{- include "operator.labels" . | nindent 4 }} 27 | roleRef: 28 | apiGroup: rbac.authorization.k8s.io 29 | kind: ClusterRole 30 | name: '{{ include "operator.fullname" . }}-proxy-role' 31 | subjects: 32 | - kind: ServiceAccount 33 | name: '{{ include "operator.fullname" . }}-controller-manager' 34 | namespace: '{{ .Release.Namespace }}' 35 | -------------------------------------------------------------------------------- /examples/operator/templates/pvc-lim.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolumeClaim 3 | metadata: 4 | name: {{ include "operator.fullname" . }}-pvc-lim 5 | labels: 6 | {{- include "operator.labels" . | nindent 4 }} 7 | spec: 8 | accessModes: 9 | - ReadWriteOnce 10 | resources: 11 | requests: 12 | storage: {{ .Values.pvc.pvcLim.storageRequest | quote }} 13 | storageClassName: {{ .Values.pvc.pvcLim.storageClass | quote }} 14 | -------------------------------------------------------------------------------- /examples/operator/templates/secret-ca.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: {{ include "operator.fullname" . }}-secret-ca 5 | labels: 6 | {{- include "operator.labels" . | nindent 4 }} 7 | data: 8 | ca.crt: {{ required "secretCa.caCrt is required" .Values.secretCa.caCrt | b64enc 9 | | quote }} 10 | type: opaque 11 | -------------------------------------------------------------------------------- /examples/operator/templates/secret-registry-credentials.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: {{ include "operator.fullname" . }}-secret-registry-credentials 5 | labels: 6 | {{- include "operator.labels" . | nindent 4 }} 7 | data: 8 | .dockerconfigjson: {{ required "secretRegistryCredentials.dockerconfigjson is required" 9 | .Values.secretRegistryCredentials.dockerconfigjson | b64enc | quote }} 10 | type: kubernetes.io/dockerconfigjson 11 | -------------------------------------------------------------------------------- /examples/operator/templates/secret-vars.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: {{ include "operator.fullname" . }}-secret-vars 5 | labels: 6 | {{- include "operator.labels" . | nindent 4 }} 7 | data: 8 | VAR1: {{ required "secretVars.var1 is required" .Values.secretVars.var1 | b64enc 9 | | quote }} 10 | VAR2: {{ required "secretVars.var2 is required" .Values.secretVars.var2 | b64enc 11 | | quote }} 12 | type: opaque 13 | -------------------------------------------------------------------------------- /examples/operator/templates/selfsigned-issuer.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: cert-manager.io/v1 2 | kind: Issuer 3 | metadata: 4 | name: {{ include "operator.fullname" . }}-selfsigned-issuer 5 | labels: 6 | {{- include "operator.labels" . | nindent 4 }} 7 | spec: 8 | selfSigned: {} 9 | -------------------------------------------------------------------------------- /examples/operator/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: {{ include "operator.fullname" . }}-controller-manager 5 | labels: 6 | {{- include "operator.labels" . | nindent 4 }} 7 | annotations: 8 | {{- toYaml .Values.controllerManager.serviceAccount.annotations | nindent 4 }} 9 | -------------------------------------------------------------------------------- /examples/operator/templates/serving-cert.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: cert-manager.io/v1 2 | kind: Certificate 3 | metadata: 4 | name: {{ include "operator.fullname" . }}-serving-cert 5 | labels: 6 | {{- include "operator.labels" . | nindent 4 }} 7 | spec: 8 | dnsNames: 9 | - '{{ include "operator.fullname" . }}-webhook-service.{{ .Release.Namespace }}.svc' 10 | - '{{ include "operator.fullname" . }}-webhook-service.{{ .Release.Namespace }}.svc.{{ 11 | .Values.kubernetesClusterDomain }}' 12 | issuerRef: 13 | kind: Issuer 14 | name: '{{ include "operator.fullname" . }}-selfsigned-issuer' 15 | secretName: webhook-server-cert 16 | -------------------------------------------------------------------------------- /examples/operator/templates/validating-webhook-configuration.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: admissionregistration.k8s.io/v1 2 | kind: ValidatingWebhookConfiguration 3 | metadata: 4 | name: {{ include "operator.fullname" . }}-validating-webhook-configuration 5 | annotations: 6 | cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ include "operator.fullname" . }}-serving-cert 7 | labels: 8 | {{- include "operator.labels" . | nindent 4 }} 9 | webhooks: 10 | - admissionReviewVersions: 11 | - v1 12 | - v1beta1 13 | clientConfig: 14 | service: 15 | name: '{{ include "operator.fullname" . }}-webhook-service' 16 | namespace: '{{ .Release.Namespace }}' 17 | path: /validate-ceph-example-com-v1alpha1-volume 18 | failurePolicy: Fail 19 | name: vvolume.kb.io 20 | rules: 21 | - apiGroups: 22 | - test.example.com 23 | apiVersions: 24 | - v1alpha1 25 | operations: 26 | - CREATE 27 | - UPDATE 28 | resources: 29 | - volumes 30 | sideEffects: None 31 | -------------------------------------------------------------------------------- /examples/operator/templates/webhook-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "operator.fullname" . }}-webhook-service 5 | labels: 6 | {{- include "operator.labels" . | nindent 4 }} 7 | spec: 8 | type: {{ .Values.webhookService.type }} 9 | selector: 10 | control-plane: controller-manager 11 | {{- include "operator.selectorLabels" . | nindent 4 }} 12 | ports: 13 | {{- .Values.webhookService.ports | toYaml | nindent 2 }} 14 | -------------------------------------------------------------------------------- /examples/operator/values.yaml: -------------------------------------------------------------------------------- 1 | configmapVars: 2 | var4: value for var4 3 | controllerManager: 4 | kubeRbacProxy: 5 | args: 6 | - --secure-listen-address=0.0.0.0:8443 7 | - --upstream=http://127.0.0.1:8080/ 8 | - --logtostderr=true 9 | - --v=10 10 | image: 11 | repository: gcr.io/kubebuilder/kube-rbac-proxy 12 | tag: v0.8.0 13 | manager: 14 | args: 15 | - --health-probe-bind-address=:8081 16 | - --metrics-bind-address=127.0.0.1:8080 17 | - --leader-elect 18 | containerSecurityContext: 19 | allowPrivilegeEscalation: false 20 | capabilities: 21 | drop: 22 | - ALL 23 | privileged: false 24 | readOnlyRootFilesystem: true 25 | runAsNonRoot: true 26 | runAsUser: 65532 27 | seccompProfile: 28 | type: RuntimeDefault 29 | env: 30 | var2: ciao 31 | var3MyEnv: ciao 32 | image: 33 | repository: controller 34 | tag: latest 35 | imagePullPolicy: Always 36 | resources: 37 | limits: 38 | cpu: 100m 39 | memory: 30Mi 40 | requests: 41 | cpu: 100m 42 | memory: 20Mi 43 | nodeSelector: 44 | region: east 45 | type: user-node 46 | podSecurityContext: 47 | runAsNonRoot: true 48 | replicas: 1 49 | serviceAccount: 50 | annotations: 51 | k8s.acme.org/some-meta-data: ACME Inc. 52 | strategy: 53 | rollingUpdate: 54 | maxSurge: 25% 55 | maxUnavailable: 25% 56 | type: RollingUpdate 57 | kubernetesClusterDomain: cluster.local 58 | managerConfig: 59 | controllerManagerConfigYaml: |- 60 | apiVersion: controller-runtime.sigs.k8s.io/v1alpha1 61 | kind: ControllerManagerConfig 62 | health: 63 | healthProbeBindAddress: :8081 64 | metrics: 65 | bindAddress: 127.0.0.1:8080 66 | webhook: 67 | port: 9443 68 | leaderElection: 69 | leaderElect: true 70 | resourceName: 3a2e09e9.example.com 71 | rook: 72 | namespace: rook-ceph 73 | toolboxPodLabel: rook-ceph-tools 74 | dummyconfigmapkey: dummyconfigmapvalue 75 | metricsService: 76 | ports: 77 | - name: https 78 | port: 8443 79 | targetPort: https 80 | type: ClusterIP 81 | pvc: 82 | pvcLim: 83 | storageClass: cust1-mypool-lim 84 | storageRequest: 2Gi 85 | secretCa: 86 | caCrt: "" 87 | secretRegistryCredentials: 88 | dockerconfigjson: "" 89 | secretVars: 90 | var1: "" 91 | var2: "" 92 | webhookService: 93 | ports: 94 | - port: 443 95 | targetPort: 9443 96 | type: ClusterIP 97 | -------------------------------------------------------------------------------- /internal/test_utils.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 5 | "k8s.io/apimachinery/pkg/runtime/serializer/yaml" 6 | ) 7 | 8 | const ( 9 | nsYaml = `apiVersion: v1 10 | kind: Namespace 11 | metadata: 12 | labels: 13 | control-plane: controller-manager 14 | name: my-operator-system` 15 | TestNsName = "my-operator-system" 16 | ) 17 | 18 | // TestNs k8s namespace object example. 19 | var TestNs = GenerateObj(nsYaml) 20 | 21 | // GenerateObj generates unstructured form yaml string. 22 | func GenerateObj(objYaml string) *unstructured.Unstructured { 23 | obj := unstructured.Unstructured{} 24 | dec := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme) 25 | _, _, err := dec.Decode([]byte(objYaml), nil, &obj) 26 | if err != nil { 27 | panic(err) 28 | } 29 | return &obj 30 | } 31 | -------------------------------------------------------------------------------- /pkg/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | 10 | "github.com/arttor/helmify/pkg/file" 11 | "github.com/arttor/helmify/pkg/processor/job" 12 | "github.com/arttor/helmify/pkg/processor/poddisruptionbudget" 13 | "github.com/arttor/helmify/pkg/processor/statefulset" 14 | 15 | "github.com/sirupsen/logrus" 16 | 17 | "github.com/arttor/helmify/pkg/config" 18 | "github.com/arttor/helmify/pkg/decoder" 19 | "github.com/arttor/helmify/pkg/helm" 20 | "github.com/arttor/helmify/pkg/processor" 21 | "github.com/arttor/helmify/pkg/processor/configmap" 22 | "github.com/arttor/helmify/pkg/processor/crd" 23 | "github.com/arttor/helmify/pkg/processor/daemonset" 24 | "github.com/arttor/helmify/pkg/processor/deployment" 25 | "github.com/arttor/helmify/pkg/processor/rbac" 26 | "github.com/arttor/helmify/pkg/processor/secret" 27 | "github.com/arttor/helmify/pkg/processor/service" 28 | "github.com/arttor/helmify/pkg/processor/storage" 29 | "github.com/arttor/helmify/pkg/processor/webhook" 30 | ) 31 | 32 | // Start - application entrypoint for processing input to a Helm chart. 33 | func Start(stdin io.Reader, config config.Config) error { 34 | err := config.Validate() 35 | if err != nil { 36 | return err 37 | } 38 | setLogLevel(config) 39 | ctx, cancelFunc := context.WithCancel(context.Background()) 40 | defer cancelFunc() 41 | done := make(chan os.Signal, 1) 42 | signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) 43 | go func() { 44 | <-done 45 | logrus.Debug("Received termination, signaling shutdown") 46 | cancelFunc() 47 | }() 48 | appCtx := New(config, helm.NewOutput()) 49 | appCtx = appCtx.WithProcessors( 50 | configmap.New(), 51 | crd.New(), 52 | daemonset.New(), 53 | deployment.New(), 54 | statefulset.New(), 55 | storage.New(), 56 | service.New(), 57 | service.NewIngress(), 58 | rbac.ClusterRoleBinding(), 59 | rbac.Role(), 60 | rbac.RoleBinding(), 61 | rbac.ServiceAccount(), 62 | secret.New(), 63 | webhook.Issuer(), 64 | webhook.Certificate(), 65 | webhook.ValidatingWebhook(), 66 | webhook.MutatingWebhook(), 67 | job.NewCron(), 68 | job.NewJob(), 69 | poddisruptionbudget.New(), 70 | ).WithDefaultProcessor(processor.Default()) 71 | if len(config.Files) != 0 { 72 | file.Walk(config.Files, config.FilesRecursively, func(filename string, fileReader io.Reader) { 73 | objects := decoder.Decode(ctx.Done(), fileReader) 74 | for obj := range objects { 75 | appCtx.Add(obj, filename) 76 | } 77 | }) 78 | } else { 79 | objects := decoder.Decode(ctx.Done(), stdin) 80 | for obj := range objects { 81 | appCtx.Add(obj, "") 82 | } 83 | } 84 | 85 | return appCtx.CreateHelm(ctx.Done()) 86 | } 87 | 88 | func setLogLevel(config config.Config) { 89 | logrus.SetLevel(logrus.ErrorLevel) 90 | if config.Verbose { 91 | logrus.SetLevel(logrus.InfoLevel) 92 | } 93 | if config.VeryVerbose { 94 | logrus.SetLevel(logrus.DebugLevel) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /pkg/app/app_e2e_test.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "bufio" 5 | "os" 6 | "testing" 7 | 8 | "github.com/arttor/helmify/pkg/config" 9 | "github.com/stretchr/testify/assert" 10 | "helm.sh/helm/v3/pkg/action" 11 | ) 12 | 13 | const ( 14 | operatorChartName = "test-operator" 15 | appChartName = "test-app" 16 | ) 17 | 18 | func TestOperator(t *testing.T) { 19 | file, err := os.Open("../../test_data/k8s-operator-kustomize.output") 20 | assert.NoError(t, err) 21 | 22 | objects := bufio.NewReader(file) 23 | err = Start(objects, config.Config{ChartName: operatorChartName}) 24 | assert.NoError(t, err) 25 | 26 | t.Cleanup(func() { 27 | err = os.RemoveAll(operatorChartName) 28 | assert.NoError(t, err) 29 | }) 30 | 31 | helmLint := action.NewLint() 32 | helmLint.Strict = true 33 | helmLint.Namespace = "test-ns" 34 | result := helmLint.Run([]string{operatorChartName}, nil) 35 | for _, err = range result.Errors { 36 | assert.NoError(t, err) 37 | } 38 | } 39 | 40 | func TestApp(t *testing.T) { 41 | file, err := os.Open("../../test_data/sample-app.yaml") 42 | assert.NoError(t, err) 43 | 44 | objects := bufio.NewReader(file) 45 | err = Start(objects, config.Config{ChartName: appChartName}) 46 | assert.NoError(t, err) 47 | 48 | t.Cleanup(func() { 49 | err = os.RemoveAll(appChartName) 50 | assert.NoError(t, err) 51 | }) 52 | 53 | helmLint := action.NewLint() 54 | helmLint.Strict = true 55 | helmLint.Namespace = "test-ns" 56 | result := helmLint.Run([]string{appChartName}, nil) 57 | for _, err = range result.Errors { 58 | assert.NoError(t, err) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /pkg/app/context.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "github.com/arttor/helmify/pkg/config" 5 | "github.com/arttor/helmify/pkg/helmify" 6 | "github.com/arttor/helmify/pkg/metadata" 7 | "github.com/sirupsen/logrus" 8 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 9 | ) 10 | 11 | // appContext helm processing context. Stores processed objects. 12 | type appContext struct { 13 | processors []helmify.Processor 14 | defaultProcessor helmify.Processor 15 | output helmify.Output 16 | config config.Config 17 | appMeta *metadata.Service 18 | objects []*unstructured.Unstructured 19 | fileNames []string 20 | } 21 | 22 | // New returns context with config set. 23 | func New(config config.Config, output helmify.Output) *appContext { 24 | return &appContext{ 25 | config: config, 26 | appMeta: metadata.New(config), 27 | output: output, 28 | } 29 | } 30 | 31 | // WithProcessors add processors to the context and returns it. 32 | func (c *appContext) WithProcessors(processors ...helmify.Processor) *appContext { 33 | c.processors = append(c.processors, processors...) 34 | return c 35 | } 36 | 37 | // WithDefaultProcessor add defaultProcessor for unknown resources to the context and returns it. 38 | func (c *appContext) WithDefaultProcessor(processor helmify.Processor) *appContext { 39 | c.defaultProcessor = processor 40 | return c 41 | } 42 | 43 | // Add k8s object to app context. 44 | func (c *appContext) Add(obj *unstructured.Unstructured, filename string) { 45 | // we need to add all objects before start processing only to define app metadata. 46 | c.appMeta.Load(obj) 47 | c.objects = append(c.objects, obj) 48 | c.fileNames = append(c.fileNames, filename) 49 | } 50 | 51 | // CreateHelm creates helm chart from context k8s objects. 52 | func (c *appContext) CreateHelm(stop <-chan struct{}) error { 53 | logrus.WithFields(logrus.Fields{ 54 | "ChartName": c.appMeta.ChartName(), 55 | "Namespace": c.appMeta.Namespace(), 56 | }).Info("creating a chart") 57 | var templates []helmify.Template 58 | var filenames []string 59 | for i, obj := range c.objects { 60 | template, err := c.process(obj) 61 | if err != nil { 62 | return err 63 | } 64 | if template != nil { 65 | templates = append(templates, template) 66 | filename := template.Filename() 67 | if c.fileNames[i] != "" { 68 | filename = c.fileNames[i] 69 | } 70 | filenames = append(filenames, filename) 71 | } 72 | select { 73 | case <-stop: 74 | return nil 75 | default: 76 | } 77 | } 78 | return c.output.Create(c.config.ChartDir, c.config.ChartName, c.config.Crd, c.config.CertManagerAsSubchart, c.config.CertManagerVersion, c.config.CertManagerInstallCRD, templates, filenames) 79 | } 80 | 81 | func (c *appContext) process(obj *unstructured.Unstructured) (helmify.Template, error) { 82 | for _, p := range c.processors { 83 | if processed, result, err := p.Process(c.appMeta, obj); processed { 84 | if err != nil { 85 | return nil, err 86 | } 87 | logrus.WithFields(logrus.Fields{ 88 | "ApiVersion": obj.GetAPIVersion(), 89 | "Kind": obj.GetKind(), 90 | "Name": obj.GetName(), 91 | }).Debug("processed") 92 | return result, nil 93 | } 94 | } 95 | if c.defaultProcessor == nil { 96 | logrus.WithFields(logrus.Fields{ 97 | "ApiVersion": obj.GetAPIVersion(), 98 | "Kind": obj.GetKind(), 99 | "Name": obj.GetName(), 100 | }).Warn("Skipping: no suitable processor for resource.") 101 | return nil, nil 102 | } 103 | _, t, err := c.defaultProcessor.Process(c.appMeta, obj) 104 | return t, err 105 | } 106 | -------------------------------------------------------------------------------- /pkg/cluster/domain.go: -------------------------------------------------------------------------------- 1 | package cluster 2 | 3 | const ( 4 | DefaultDomain = "cluster.local" 5 | DomainKey = "kubernetesClusterDomain" 6 | DomainEnv = "KUBERNETES_CLUSTER_DOMAIN" 7 | ) 8 | -------------------------------------------------------------------------------- /pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/sirupsen/logrus" 7 | "k8s.io/apimachinery/pkg/util/validation" 8 | ) 9 | 10 | // defaultChartName - default name for a helm chart directory. 11 | const defaultChartName = "chart" 12 | 13 | // Config for Helmify application. 14 | type Config struct { 15 | // ChartName name of the Helm chart and its base directory where Chart.yaml is located. 16 | ChartName string 17 | // ChartDir - optional path to chart dir. Full chart path will be: ChartDir/ChartName/Chart.yaml. 18 | ChartDir string 19 | // Verbose set true to see WARN and INFO logs. 20 | Verbose bool 21 | // VeryVerbose set true to see WARN, INFO, and DEBUG logs. 22 | VeryVerbose bool 23 | // crd-dir set true to enable crd folder. 24 | Crd bool 25 | // ImagePullSecrets flag 26 | ImagePullSecrets bool 27 | // GenerateDefaults enables the generation of empty values placeholders for common customization options of helm chart 28 | // current generated values: tolerances, node selectors, topology constraints 29 | GenerateDefaults bool 30 | // CertManagerAsSubchart enables the generation of a subchart for cert-manager 31 | CertManagerAsSubchart bool 32 | // CertManagerVersion sets cert-manager version in dependency 33 | CertManagerVersion string 34 | // CertManagerVersion enables installation of cert-manager CRD 35 | CertManagerInstallCRD bool 36 | // Files - directories or files with k8s manifests 37 | Files []string 38 | // FilesRecursively read Files recursively 39 | FilesRecursively bool 40 | // OriginalName retains Kubernetes resource's original name 41 | OriginalName bool 42 | // PreserveNs retains the namespaces on the Kubernetes manifests 43 | PreserveNs bool 44 | // AddWebhookOption enables the generation of a webhook option in values.yamlß 45 | AddWebhookOption bool 46 | } 47 | 48 | func (c *Config) Validate() error { 49 | if c.ChartName == "" { 50 | logrus.Infof("Chart name is not set. Using default name '%s", defaultChartName) 51 | c.ChartName = defaultChartName 52 | } 53 | err := validation.IsDNS1123Subdomain(c.ChartName) 54 | if err != nil { 55 | for _, e := range err { 56 | logrus.Errorf("Invalid chart name %s", e) 57 | } 58 | return fmt.Errorf("invalid chart name %s", c.ChartName) 59 | } 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /pkg/config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestConfig_Validate(t *testing.T) { 10 | type fields struct { 11 | ChartName string 12 | Verbose bool 13 | VeryVerbose bool 14 | } 15 | tests := []struct { 16 | name string 17 | fields fields 18 | wantErr bool 19 | }{ 20 | {name: "valid", fields: fields{ChartName: ""}, wantErr: false}, 21 | {name: "valid", fields: fields{ChartName: "my.chart123"}, wantErr: false}, 22 | {name: "valid", fields: fields{ChartName: "my-chart123"}, wantErr: false}, 23 | {name: "invalid", fields: fields{ChartName: "my_chart123"}, wantErr: true}, 24 | {name: "invalid", fields: fields{ChartName: "my char123t"}, wantErr: true}, 25 | } 26 | for _, tt := range tests { 27 | t.Run(tt.name, func(t *testing.T) { 28 | c := &Config{ 29 | ChartName: tt.fields.ChartName, 30 | Verbose: tt.fields.Verbose, 31 | VeryVerbose: tt.fields.VeryVerbose, 32 | } 33 | if err := c.Validate(); (err != nil) != tt.wantErr { 34 | t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr) 35 | } 36 | }) 37 | } 38 | t.Run("chart name not set", func(t *testing.T) { 39 | c := &Config{} 40 | err := c.Validate() 41 | assert.NoError(t, err) 42 | assert.Equal(t, defaultChartName, c.ChartName) 43 | }) 44 | t.Run("chart name set", func(t *testing.T) { 45 | c := &Config{ChartName: "test"} 46 | err := c.Validate() 47 | assert.NoError(t, err) 48 | assert.Equal(t, "test", c.ChartName) 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /pkg/decoder/decoder.go: -------------------------------------------------------------------------------- 1 | package decoder 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | 7 | "github.com/sirupsen/logrus" 8 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 9 | "k8s.io/apimachinery/pkg/runtime" 10 | "k8s.io/apimachinery/pkg/runtime/serializer/yaml" 11 | yamlutil "k8s.io/apimachinery/pkg/util/yaml" 12 | ) 13 | 14 | const ( 15 | yamlDecoderBufferSize = 100 16 | decoderResultChannelBufferSize = 1 17 | ) 18 | 19 | // Decode - reads bytes stream of k8s yaml manifests and decodes it to k8s unstructured objects. 20 | // Non-blocking function. Sends results into buffered channel. Closes channel on io.EOF. 21 | func Decode(stop <-chan struct{}, reader io.Reader) <-chan *unstructured.Unstructured { 22 | decoder := yamlutil.NewYAMLOrJSONDecoder(reader, yamlDecoderBufferSize) 23 | res := make(chan *unstructured.Unstructured, decoderResultChannelBufferSize) 24 | go func() { 25 | defer close(res) 26 | logrus.Debug("Start processing...") 27 | for { 28 | select { 29 | case <-stop: 30 | logrus.Debug("Exiting: received stop signal") 31 | return 32 | default: 33 | } 34 | var rawObj runtime.RawExtension 35 | err := decoder.Decode(&rawObj) 36 | if errors.Is(err, io.EOF) { 37 | logrus.Debug("EOF received. Finishing input objects decoding.") 38 | return 39 | } 40 | if err != nil { 41 | logrus.WithError(err).Error("unable to decode yaml from input") 42 | continue 43 | } 44 | obj, _, err := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme).Decode(rawObj.Raw, nil, nil) 45 | if err != nil { 46 | logrus.WithError(err).Error("unable to decode yaml") 47 | continue 48 | } 49 | unstructuredMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) 50 | if err != nil { 51 | logrus.WithError(err).Error("unable to map yaml to k8s unstructured") 52 | continue 53 | } 54 | object := &unstructured.Unstructured{Object: unstructuredMap} 55 | logrus.WithFields(logrus.Fields{ 56 | "ApiVersion": object.GetAPIVersion(), 57 | "Kind": object.GetKind(), 58 | "Name": object.GetName(), 59 | }).Debug("decoded") 60 | res <- object 61 | } 62 | }() 63 | return res 64 | } 65 | -------------------------------------------------------------------------------- /pkg/decoder/decoder_test.go: -------------------------------------------------------------------------------- 1 | package decoder 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | const ( 11 | validObjects2 = `apiVersion: v1 12 | kind: Service 13 | metadata: 14 | name: my-operator-webhook-service 15 | namespace: my-operator-system 16 | spec: 17 | ports: 18 | - port: 443 19 | targetPort: 9443 20 | selector: 21 | control-plane: controller-manager 22 | --- 23 | apiVersion: v1 24 | kind: Namespace 25 | metadata: 26 | labels: 27 | control-plane: controller-manager 28 | name: my-operator-system 29 | ` 30 | validObjects2withInvalid = `ajrcmq84xpru038um9q8 31 | wqprux934ur8wcnqwp8urxqwrxuqweruncw 32 | --- 33 | apiVersion: v1 34 | kind: Service 35 | metadata: 36 | name: my-operator-webhook-service 37 | namespace: my-operator-system 38 | spec: 39 | ports: 40 | - port: 443 41 | targetPort: 9443 42 | selector: 43 | control-plane: controller-manager 44 | --- 45 | --- 46 | --- 47 | 8umx9284ru 82q983y49q 48 | q 3408tuqw8e 49 | q 49tuqw[fa iwfaowoewihfe4hf 50 | --- 51 | apiVersion: v1 52 | kind: Namespace 53 | metadata: 54 | labels: 55 | control-plane: controller-manager 56 | name: my-operator-system 57 | --- 58 | apiVersion: v1 59 | metadata: 60 | labels: 61 | ` 62 | validObjects0 = `--- 63 | --- 64 | --- 65 | ` 66 | ) 67 | 68 | func TestDecodeOk(t *testing.T) { 69 | reader := strings.NewReader(validObjects2) 70 | stop := make(chan struct{}) 71 | objects := Decode(stop, reader) 72 | i := 0 73 | for range objects { 74 | i++ 75 | } 76 | assert.Equal(t, 2, i, "decoded two objects") 77 | } 78 | 79 | func TestDecodeEmptyObj(t *testing.T) { 80 | reader := strings.NewReader(validObjects0) 81 | stop := make(chan struct{}) 82 | objects := Decode(stop, reader) 83 | i := 0 84 | for range objects { 85 | i++ 86 | } 87 | assert.Equal(t, 0, i, "decoded none objects") 88 | } 89 | 90 | func TestDecodeInvalidObj(t *testing.T) { 91 | reader := strings.NewReader(validObjects2withInvalid) 92 | stop := make(chan struct{}) 93 | objects := Decode(stop, reader) 94 | i := 0 95 | for range objects { 96 | i++ 97 | } 98 | assert.Equal(t, 2, i, "decoded 2 valid objects") 99 | } 100 | -------------------------------------------------------------------------------- /pkg/file/reader.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | "io" 6 | "io/fs" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | func Walk(paths []string, recursively bool, walkFunc func(filename string, r io.Reader)) { 12 | 13 | for _, path := range paths { 14 | info, err := os.Stat(path) 15 | if err != nil { 16 | logrus.Warnf("no such file or directory %q: %v", path, err) 17 | continue 18 | } 19 | // handle single file file: 20 | if !info.IsDir() { 21 | file, err := os.Open(path) 22 | if err != nil { 23 | logrus.Warnf("unable to open file %q: %v", file.Name(), err) 24 | continue 25 | } 26 | walkFunc(info.Name(), file) 27 | err = file.Close() 28 | if err != nil { 29 | logrus.Warnf("unable to close file %q: %v", file.Name(), err) 30 | } 31 | continue 32 | } 33 | // handle directory non-recursively: 34 | if !recursively { 35 | dir, err := os.Open(path) 36 | if err != nil { 37 | logrus.Warnf("unable to open directory %q: %v", dir.Name(), err) 38 | continue 39 | } 40 | files, err := dir.ReadDir(0) 41 | if err != nil { 42 | logrus.Warnf("unable to read directory %q: %v", dir.Name(), err) 43 | continue 44 | } 45 | for _, f := range files { 46 | if f.IsDir() { 47 | continue 48 | } 49 | file, err := os.Open(filepath.Join(path, f.Name())) 50 | if err != nil { 51 | logrus.Warnf("unable to open file %q: %v", file.Name(), err) 52 | continue 53 | } 54 | walkFunc(f.Name(), file) 55 | err = file.Close() 56 | if err != nil { 57 | logrus.Warnf("unable to close file %q: %v", file.Name(), err) 58 | } 59 | continue 60 | } 61 | continue 62 | } 63 | // handle directory recursively: 64 | err = filepath.WalkDir(path, func(path string, d fs.DirEntry, err error) error { 65 | if err != nil { 66 | return err 67 | } 68 | if d.IsDir() { 69 | return nil 70 | } 71 | file, err := os.Open(path) 72 | if err != nil { 73 | return err 74 | } 75 | walkFunc(d.Name(), file) 76 | err = file.Close() 77 | if err != nil { 78 | logrus.Warnf("unable to close file %q: %v", file.Name(), err) 79 | } 80 | return nil 81 | }) 82 | if err != nil { 83 | logrus.Warnf("unable to open %q: %v", info.Name(), err) 84 | continue 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /pkg/format/fix_quotes.go: -------------------------------------------------------------------------------- 1 | package format 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // FixUnterminatedQuotes check for Unterminated Quotes in helm templated strings 8 | // See https://github.com/arttor/helmify/issues/12 9 | func FixUnterminatedQuotes(in string) string { 10 | sb := strings.Builder{} 11 | hasUntermQuotes := false 12 | lines := strings.Split(in, "\n") 13 | for i, line := range lines { 14 | if hasUntermQuotes { 15 | line = " " + strings.TrimSpace(line) 16 | hasUntermQuotes = false 17 | } else { 18 | hasUntermQuotes = strings.Count(line, "\"")%2 != 0 19 | } 20 | sb.WriteString(line) 21 | if !hasUntermQuotes && i != len(lines)-1 { 22 | sb.WriteString("\n") 23 | } 24 | } 25 | return sb.String() 26 | } 27 | -------------------------------------------------------------------------------- /pkg/format/fix_quotes_test.go: -------------------------------------------------------------------------------- 1 | package format 2 | 3 | import "testing" 4 | 5 | func TestFixUnterminatedQuotes(t *testing.T) { 6 | tests := []struct { 7 | name string 8 | in string 9 | want string 10 | }{ 11 | { 12 | name: "remove line break for unterminated quotes", 13 | in: `apiVersion: v1 14 | kind: Secret 15 | metadata: 16 | name: {{ include "app.fullname" . }}-my-secret-vars 17 | labels: 18 | {{- include "app.labels" . | nindent 4 }} 19 | data: 20 | ELASTIC_FOOBAR_HUNTER123_MEOWTOWN_VERIFY: {{ required "mySecretVars.elasticFoobarHunter123MeowtownVerify 21 | is required" .Values.mySecretVars.elasticFoobarHunter123MeowtownVerify | b64enc 22 | | quote }} 23 | VAR1: {{ required "mySecretVars.var1 is required" .Values.mySecretVars.var1 | b64enc 24 | | quote }} 25 | VAR2: {{ required "mySecretVars.var2 is required" .Values.mySecretVars.var2 | b64enc 26 | | quote }} 27 | stringData: 28 | str: {{ required "mySecretVars.str is required" .Values.mySecretVars.str | quote 29 | }} 30 | type: opaque`, 31 | want: `apiVersion: v1 32 | kind: Secret 33 | metadata: 34 | name: {{ include "app.fullname" . }}-my-secret-vars 35 | labels: 36 | {{- include "app.labels" . | nindent 4 }} 37 | data: 38 | ELASTIC_FOOBAR_HUNTER123_MEOWTOWN_VERIFY: {{ required "mySecretVars.elasticFoobarHunter123MeowtownVerify is required" .Values.mySecretVars.elasticFoobarHunter123MeowtownVerify | b64enc 39 | | quote }} 40 | VAR1: {{ required "mySecretVars.var1 is required" .Values.mySecretVars.var1 | b64enc 41 | | quote }} 42 | VAR2: {{ required "mySecretVars.var2 is required" .Values.mySecretVars.var2 | b64enc 43 | | quote }} 44 | stringData: 45 | str: {{ required "mySecretVars.str is required" .Values.mySecretVars.str | quote 46 | }} 47 | type: opaque`, 48 | }, 49 | } 50 | for _, tt := range tests { 51 | t.Run(tt.name, func(t *testing.T) { 52 | if got := FixUnterminatedQuotes(tt.in); got != tt.want { 53 | t.Errorf("FixUnterminatedQuotes() = %v, want %v", got, tt.want) 54 | } 55 | }) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /pkg/format/trailing_whitespaces.go: -------------------------------------------------------------------------------- 1 | package format 2 | 3 | import "regexp" 4 | 5 | var removeWhitespace = regexp.MustCompile(`(\s+)(\n|$)`) 6 | 7 | func RemoveTrailingWhitespaces(in string) string { 8 | return removeWhitespace.ReplaceAllString(in, "$2") 9 | } 10 | -------------------------------------------------------------------------------- /pkg/format/trailing_whitespaces_test.go: -------------------------------------------------------------------------------- 1 | package format 2 | 3 | import "testing" 4 | 5 | func TestRemoveTrailingWhitespaces(t *testing.T) { 6 | tests := []struct { 7 | name string 8 | in string 9 | want string 10 | }{ 11 | { 12 | name: "", 13 | in: `abc `, 14 | want: `abc`, 15 | }, 16 | { 17 | name: "", 18 | in: `abc 19 | edf`, 20 | want: `abc 21 | edf`, 22 | }, 23 | { 24 | name: "", 25 | in: `abc 26 | edf `, 27 | want: `abc 28 | edf`, 29 | }, 30 | { 31 | name: "", 32 | in: `abc . 33 | edf .`, 34 | want: `abc . 35 | edf .`, 36 | }, 37 | } 38 | for _, tt := range tests { 39 | t.Run(tt.name, func(t *testing.T) { 40 | if got := RemoveTrailingWhitespaces(tt.in); got != tt.want { 41 | t.Errorf("RemoveTrailingWhitespaces() = %v, want %v", got, tt.want) 42 | } 43 | }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /pkg/helm/chart.go: -------------------------------------------------------------------------------- 1 | package helm 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/arttor/helmify/pkg/cluster" 10 | "github.com/arttor/helmify/pkg/helmify" 11 | 12 | "github.com/sirupsen/logrus" 13 | 14 | "sigs.k8s.io/yaml" 15 | ) 16 | 17 | // NewOutput creates interface to dump processed input to filesystem in Helm chart format. 18 | func NewOutput() helmify.Output { 19 | return &output{} 20 | } 21 | 22 | type output struct{} 23 | 24 | // Create a helm chart in the current directory: 25 | // chartName/ 26 | // 27 | // ├── .helmignore # Contains patterns to ignore when packaging Helm charts. 28 | // ├── Chart.yaml # Information about your chart 29 | // ├── values.yaml # The default values for your templates 30 | // └── templates/ # The template files 31 | // └── _helpers.tp # Helm default template partials 32 | // 33 | // Overwrites existing values.yaml and templates in templates dir on every run. 34 | func (o output) Create(chartDir, chartName string, crd bool, certManagerAsSubchart bool, certManagerVersion string, certManagerInstallCRD bool, templates []helmify.Template, filenames []string) error { 35 | err := initChartDir(chartDir, chartName, crd, certManagerAsSubchart, certManagerVersion) 36 | if err != nil { 37 | return err 38 | } 39 | // group templates into files 40 | files := map[string][]helmify.Template{} 41 | values := helmify.Values{} 42 | values[cluster.DomainKey] = cluster.DefaultDomain 43 | for i, template := range templates { 44 | file := files[filenames[i]] 45 | file = append(file, template) 46 | files[filenames[i]] = file 47 | err = values.Merge(template.Values()) 48 | if err != nil { 49 | return err 50 | } 51 | } 52 | cDir := filepath.Join(chartDir, chartName) 53 | for filename, tpls := range files { 54 | err = overwriteTemplateFile(filename, cDir, crd, tpls) 55 | if err != nil { 56 | return err 57 | } 58 | } 59 | err = overwriteValuesFile(cDir, values, certManagerAsSubchart, certManagerInstallCRD) 60 | if err != nil { 61 | return err 62 | } 63 | return nil 64 | } 65 | 66 | func overwriteTemplateFile(filename, chartDir string, crd bool, templates []helmify.Template) error { 67 | // pull in crd-dir setting and siphon crds into folder 68 | var subdir string 69 | if strings.Contains(filename, "crd") && crd { 70 | subdir = "crds" 71 | // create "crds" if not exists 72 | if _, err := os.Stat(filepath.Join(chartDir, "crds")); os.IsNotExist(err) { 73 | err = os.MkdirAll(filepath.Join(chartDir, "crds"), 0750) 74 | if err != nil { 75 | return fmt.Errorf("%w: unable create crds dir", err) 76 | } 77 | } 78 | } else { 79 | subdir = "templates" 80 | } 81 | file := filepath.Join(chartDir, subdir, filename) 82 | f, err := os.OpenFile(file, os.O_APPEND|os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) 83 | if err != nil { 84 | return fmt.Errorf("%w: unable to open %s", err, file) 85 | } 86 | defer f.Close() 87 | for i, t := range templates { 88 | logrus.WithField("file", file).Debug("writing a template into") 89 | err = t.Write(f) 90 | if err != nil { 91 | return fmt.Errorf("%w: unable to write into %s", err, file) 92 | } 93 | if i != len(templates)-1 { 94 | _, err = f.Write([]byte("\n---\n")) 95 | if err != nil { 96 | return fmt.Errorf("%w: unable to write into %s", err, file) 97 | } 98 | } 99 | } 100 | if len(templates) != 0 { 101 | _, err = f.Write([]byte("\n")) 102 | if err != nil { 103 | return fmt.Errorf("%w: unable to write newline into %s", err, file) 104 | } 105 | } 106 | logrus.WithField("file", file).Info("overwritten") 107 | return nil 108 | } 109 | 110 | func overwriteValuesFile(chartDir string, values helmify.Values, certManagerAsSubchart bool, certManagerInstallCRD bool) error { 111 | if certManagerAsSubchart { 112 | _, err := values.Add(certManagerInstallCRD, "certmanager", "installCRDs") 113 | if err != nil { 114 | return fmt.Errorf("%w: unable to add cert-manager.installCRDs", err) 115 | } 116 | 117 | _, err = values.Add(true, "certmanager", "enabled") 118 | if err != nil { 119 | return fmt.Errorf("%w: unable to add cert-manager.enabled", err) 120 | } 121 | } 122 | res, err := yaml.Marshal(values) 123 | if err != nil { 124 | return fmt.Errorf("%w: unable to write marshal values.yaml", err) 125 | } 126 | 127 | file := filepath.Join(chartDir, "values.yaml") 128 | err = os.WriteFile(file, res, 0600) 129 | if err != nil { 130 | return fmt.Errorf("%w: unable to write values.yaml", err) 131 | } 132 | logrus.WithField("file", file).Info("overwritten") 133 | return nil 134 | } 135 | -------------------------------------------------------------------------------- /pkg/helm/doc.go: -------------------------------------------------------------------------------- 1 | // Package helm contains code for writing templates to a filesystem as Helm chart. 2 | package helm 3 | -------------------------------------------------------------------------------- /pkg/helmify/model.go: -------------------------------------------------------------------------------- 1 | package helmify 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/arttor/helmify/pkg/config" 7 | 8 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 9 | ) 10 | 11 | // Processor - converts k8s object to helm template. 12 | // Implement this interface and register it to a context to support a new k8s resource conversion. 13 | type Processor interface { 14 | // Process - converts k8s object to Helm template. 15 | // return false if not able to process given object type. 16 | Process(appMeta AppMetadata, unstructured *unstructured.Unstructured) (bool, Template, error) 17 | } 18 | 19 | // Template - represents Helm template in 'templates' directory. 20 | type Template interface { 21 | // Filename - returns template filename 22 | Filename() string 23 | // Values - returns set of values used in template 24 | Values() Values 25 | // Write - writes helm template into given writer 26 | Write(writer io.Writer) error 27 | } 28 | 29 | // Output - converts Template into helm chart on disk. 30 | type Output interface { 31 | Create(chartName, chartDir string, Crd bool, certManagerAsSubchart bool, certManagerVersion string, certManagerInstallCRD bool, templates []Template, filenames []string) error 32 | } 33 | 34 | // AppMetadata handle common information about K8s objects in the chart. 35 | type AppMetadata interface { 36 | // Namespace returns app namespace. 37 | Namespace() string 38 | // ChartName returns chart name 39 | ChartName() string 40 | // TemplatedName converts object name to templated Helm name. 41 | // Example: "my-app-service1" -> "{{ include "chart.fullname" . }}-service1" 42 | // "my-app-secret" -> "{{ include "chart.fullname" . }}-secret" 43 | // etc... 44 | TemplatedName(objName string) string 45 | // TemplatedString converts a string to templated string with chart name. 46 | TemplatedString(str string) string 47 | // TrimName trims common prefix from object name if exists. 48 | // We trim common prefix because helm already using release for this purpose. 49 | TrimName(objName string) string 50 | 51 | Config() config.Config 52 | } 53 | -------------------------------------------------------------------------------- /pkg/helmify/values.go: -------------------------------------------------------------------------------- 1 | package helmify 2 | 3 | import ( 4 | "dario.cat/mergo" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/iancoleman/strcase" 10 | 11 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 12 | ) 13 | 14 | // Values - represents helm template values.yaml. 15 | type Values map[string]interface{} 16 | 17 | // Merge given values with current instance. 18 | func (v *Values) Merge(values Values) error { 19 | if err := mergo.Merge(v, values, mergo.WithAppendSlice); err != nil { 20 | return fmt.Errorf("%w: unable to merge helm values", err) 21 | } 22 | return nil 23 | } 24 | 25 | // Add - adds given value to values and returns its helm template representation {{ .Values. }} 26 | func (v *Values) Add(value interface{}, name ...string) (string, error) { 27 | name = toCamelCase(name) 28 | switch val := value.(type) { 29 | case int: 30 | value = int64(val) 31 | case int8: 32 | value = int64(val) 33 | case int16: 34 | value = int64(val) 35 | case int32: 36 | value = int64(val) 37 | } 38 | 39 | err := unstructured.SetNestedField(*v, value, name...) 40 | if err != nil { 41 | return "", fmt.Errorf("%w: unable to set value: %v", err, name) 42 | } 43 | _, isString := value.(string) 44 | if isString { 45 | return "{{ .Values." + strings.Join(name, ".") + " | quote }}", nil 46 | } 47 | _, isSlice := value.([]interface{}) 48 | if isSlice { 49 | spaces := strconv.Itoa(len(name) * 2) 50 | return "{{ toYaml .Values." + strings.Join(name, ".") + " | nindent " + spaces + " }}", nil 51 | } 52 | return "{{ .Values." + strings.Join(name, ".") + " }}", nil 53 | } 54 | 55 | // AddYaml - adds given value to values and returns its helm template representation as Yaml {{ .Values. | toYaml | indent i }} 56 | // indent <= 0 will be omitted. 57 | func (v *Values) AddYaml(value interface{}, indent int, newLine bool, name ...string) (string, error) { 58 | name = toCamelCase(name) 59 | err := unstructured.SetNestedField(*v, value, name...) 60 | if err != nil { 61 | return "", fmt.Errorf("%w: unable to set value: %v", err, name) 62 | } 63 | if indent > 0 { 64 | if newLine { 65 | return "{{ .Values." + strings.Join(name, ".") + fmt.Sprintf(" | toYaml | nindent %d }}", indent), nil 66 | } 67 | return "{{ .Values." + strings.Join(name, ".") + fmt.Sprintf(" | toYaml | indent %d }}", indent), nil 68 | } 69 | return "{{ .Values." + strings.Join(name, ".") + " | toYaml }}", nil 70 | } 71 | 72 | // AddSecret - adds empty value to values and returns its helm template representation {{ required "" .Values. }}. 73 | // Set toBase64=true for Secret data to be base64 encoded and set false for Secret stringData. 74 | func (v *Values) AddSecret(toBase64 bool, name ...string) (string, error) { 75 | name = toCamelCase(name) 76 | nameStr := strings.Join(name, ".") 77 | err := unstructured.SetNestedField(*v, "", name...) 78 | if err != nil { 79 | return "", fmt.Errorf("%w: unable to set value: %v", err, nameStr) 80 | } 81 | res := fmt.Sprintf(`{{ required "%[1]s is required" .Values.%[1]s`, nameStr) 82 | if toBase64 { 83 | res += " | b64enc" 84 | } 85 | return res + " | quote }}", err 86 | } 87 | 88 | func toCamelCase(name []string) []string { 89 | for i, n := range name { 90 | camelCase := strcase.ToLowerCamel(n) 91 | if n == strings.ToUpper(n) { 92 | camelCase = strcase.ToLowerCamel(strings.ToLower(n)) 93 | } 94 | name[i] = camelCase 95 | } 96 | return name 97 | } 98 | -------------------------------------------------------------------------------- /pkg/helmify/values_test.go: -------------------------------------------------------------------------------- 1 | package helmify 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestValues_Add(t *testing.T) { 10 | t.Run("quote func added for string values", func(t *testing.T) { 11 | testVal := Values{} 12 | res, err := testVal.Add("abc", "a", "b") 13 | assert.NoError(t, err) 14 | assert.Contains(t, res, "quote") 15 | }) 16 | t.Run("quote func not added for not string values", func(t *testing.T) { 17 | testVal := Values{} 18 | res, err := testVal.Add(int64(1), "a", "b") 19 | assert.NoError(t, err) 20 | assert.NotContains(t, res, "quote") 21 | res, err = testVal.Add(true, "a", "b") 22 | assert.NoError(t, err) 23 | assert.NotContains(t, res, "quote") 24 | res, err = testVal.Add(420.69, "a", "b") 25 | assert.NoError(t, err) 26 | assert.NotContains(t, res, "quote") 27 | }) 28 | t.Run("name path is dot formatted", func(t *testing.T) { 29 | testVal := Values{} 30 | res, err := testVal.Add(int64(1), "a", "b") 31 | assert.NoError(t, err) 32 | assert.Contains(t, res, " .Values.a.b ") 33 | }) 34 | t.Run("snake names camel cased", func(t *testing.T) { 35 | testVal := Values{} 36 | snake := "my_name" 37 | camel := "myName" 38 | res, err := testVal.Add(420.69, snake) 39 | assert.NoError(t, err) 40 | assert.NotContains(t, res, snake) 41 | assert.Contains(t, res, camel) 42 | }) 43 | t.Run("upper snake names camel cased", func(t *testing.T) { 44 | testVal := Values{} 45 | upSnake := "MY_NAME" 46 | camel := "myName" 47 | res, err := testVal.Add(420.69, upSnake) 48 | assert.NoError(t, err) 49 | assert.NotContains(t, res, upSnake) 50 | assert.Contains(t, res, camel) 51 | }) 52 | t.Run("kebab names camel cased", func(t *testing.T) { 53 | testVal := Values{} 54 | kebab := "my-name" 55 | camel := "myName" 56 | res, err := testVal.Add(420.69, kebab) 57 | assert.NoError(t, err) 58 | assert.NotContains(t, res, kebab) 59 | assert.Contains(t, res, camel) 60 | }) 61 | t.Run("dot names camel cased", func(t *testing.T) { 62 | testVal := Values{} 63 | dot := "my.name" 64 | camel := "myName" 65 | res, err := testVal.Add(420.69, dot) 66 | assert.NoError(t, err) 67 | assert.NotContains(t, res, dot) 68 | assert.Contains(t, res, camel) 69 | }) 70 | } 71 | func TestValues_AddSecret(t *testing.T) { 72 | t.Run("add base64 enc secret", func(t *testing.T) { 73 | testVal := Values{} 74 | res, err := testVal.AddSecret(true, "a", "b") 75 | assert.NoError(t, err) 76 | assert.Contains(t, res, "b64enc") 77 | }) 78 | t.Run("add not encoded secret", func(t *testing.T) { 79 | testVal := Values{} 80 | res, err := testVal.AddSecret(false, "a", "b") 81 | assert.NoError(t, err) 82 | assert.NotContains(t, res, "b64enc") 83 | }) 84 | } 85 | -------------------------------------------------------------------------------- /pkg/metadata/metadata.go: -------------------------------------------------------------------------------- 1 | package metadata 2 | 3 | import ( 4 | "fmt" 5 | "github.com/arttor/helmify/pkg/config" 6 | "strings" 7 | 8 | "github.com/arttor/helmify/pkg/helmify" 9 | "github.com/sirupsen/logrus" 10 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 11 | "k8s.io/apimachinery/pkg/runtime/schema" 12 | ) 13 | 14 | const nameTeml = `{{ include "%s.fullname" . }}-%s` 15 | 16 | var nsGVK = schema.GroupVersionKind{ 17 | Group: "", 18 | Version: "v1", 19 | Kind: "Namespace", 20 | } 21 | 22 | var crdGVK = schema.GroupVersionKind{ 23 | Group: "apiextensions.k8s.io", 24 | Version: "v1", 25 | Kind: "CustomResourceDefinition", 26 | } 27 | 28 | func New(conf config.Config) *Service { 29 | return &Service{names: make(map[string]struct{}), conf: conf} 30 | } 31 | 32 | type Service struct { 33 | commonPrefix string 34 | namespace string 35 | names map[string]struct{} 36 | conf config.Config 37 | } 38 | 39 | func (a *Service) Config() config.Config { 40 | return a.conf 41 | } 42 | 43 | // TrimName - tries to trim app common prefix for object name if detected. 44 | // If no common prefix - returns name as it is. 45 | // It is better to trim common prefix because Helm also adds release name as common prefix. 46 | func (a *Service) TrimName(objName string) string { 47 | trimmed := strings.TrimPrefix(objName, a.commonPrefix) 48 | trimmed = strings.TrimLeft(trimmed, "-./_ ") 49 | if trimmed == "" { 50 | return objName 51 | } 52 | return trimmed 53 | } 54 | 55 | var _ helmify.AppMetadata = &Service{} 56 | 57 | // Load processed objects one-by-one before actual processing to define app namespace, name common prefix and 58 | // other app meta information. 59 | func (a *Service) Load(obj *unstructured.Unstructured) { 60 | a.names[obj.GetName()] = struct{}{} 61 | a.commonPrefix = detectCommonPrefix(obj, a.commonPrefix) 62 | objNs := extractAppNamespace(obj) 63 | if objNs == "" { 64 | return 65 | } 66 | if a.namespace != "" && a.namespace != objNs { 67 | logrus.Warnf("Two different namespaces for app detected: %s and %s. Resulted char will have single namespace.", objNs, a.namespace) 68 | } 69 | a.namespace = objNs 70 | } 71 | 72 | // Namespace returns detected app namespace. 73 | func (a *Service) Namespace() string { 74 | return a.namespace 75 | } 76 | 77 | // ChartName returns ChartName. 78 | func (a *Service) ChartName() string { 79 | return a.conf.ChartName 80 | } 81 | 82 | // TemplatedName - converts object name to its Helm templated representation. 83 | // Adds chart fullname prefix from _helpers.tpl 84 | func (a *Service) TemplatedName(name string) string { 85 | if a.conf.OriginalName { 86 | return name 87 | } 88 | _, contains := a.names[name] 89 | if !contains { 90 | // template only app objects 91 | return name 92 | } 93 | name = a.TrimName(name) 94 | return fmt.Sprintf(nameTeml, a.conf.ChartName, name) 95 | } 96 | 97 | func (a *Service) TemplatedString(str string) string { 98 | name := a.TrimName(str) 99 | return fmt.Sprintf(nameTeml, a.conf.ChartName, name) 100 | } 101 | 102 | func extractAppNamespace(obj *unstructured.Unstructured) string { 103 | if obj.GroupVersionKind() == nsGVK { 104 | return obj.GetName() 105 | } 106 | return obj.GetNamespace() 107 | } 108 | 109 | func detectCommonPrefix(obj *unstructured.Unstructured, prevName string) string { 110 | if obj.GroupVersionKind() == crdGVK || obj.GroupVersionKind() == nsGVK { 111 | return prevName 112 | } 113 | if prevName == "" { 114 | return obj.GetName() 115 | } 116 | return commonPrefix(obj.GetName(), prevName) 117 | } 118 | 119 | func commonPrefix(one, two string) string { 120 | runes1 := []rune(one) 121 | runes2 := []rune(two) 122 | min := len(runes1) 123 | if min > len(runes2) { 124 | min = len(runes2) 125 | } 126 | for i := 0; i < min; i++ { 127 | if runes1[i] != runes2[i] { 128 | return string(runes1[:i]) 129 | } 130 | } 131 | return string(runes1[:min]) 132 | } 133 | -------------------------------------------------------------------------------- /pkg/metadata/metadata_test.go: -------------------------------------------------------------------------------- 1 | package metadata 2 | 3 | import ( 4 | "fmt" 5 | "github.com/arttor/helmify/pkg/config" 6 | "testing" 7 | 8 | "github.com/arttor/helmify/internal" 9 | "github.com/stretchr/testify/assert" 10 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 11 | ) 12 | 13 | const res = `apiVersion: v1 14 | kind: Secret 15 | metadata: 16 | name: %s 17 | namespace: %s` 18 | 19 | func Test_commonPrefix(t *testing.T) { 20 | type args struct { 21 | left, right string 22 | } 23 | tests := []struct { 24 | name string 25 | args args 26 | want string 27 | }{ 28 | { 29 | name: "left is a prefix of right", 30 | args: args{left: "test", right: "testimony"}, 31 | want: "test", 32 | }, 33 | { 34 | name: "common prefix", 35 | args: args{left: "testimony", right: "testicle"}, 36 | want: "testi", 37 | }, 38 | { 39 | name: "no common", 40 | args: args{left: "testimony", right: "abc"}, 41 | want: "", 42 | }, 43 | { 44 | name: "right is empty", 45 | args: args{left: "testimony", right: ""}, 46 | want: "", 47 | }, 48 | { 49 | name: "left is empty", 50 | args: args{left: "", right: "abc"}, 51 | want: "", 52 | }, 53 | { 54 | name: "both are empty", 55 | args: args{left: "", right: ""}, 56 | want: "", 57 | }, 58 | { 59 | name: "unicode", 60 | args: args{left: "багет", right: "багаж"}, 61 | want: "баг", 62 | }, 63 | } 64 | for _, tt := range tests { 65 | t.Run(tt.name, func(t *testing.T) { 66 | if got := commonPrefix(tt.args.left, tt.args.right); got != tt.want { 67 | t.Errorf("commonPrefix() = %v, want %v", got, tt.want) 68 | } 69 | }) 70 | } 71 | } 72 | 73 | func Test_Service(t *testing.T) { 74 | t.Run("load ns from object", func(t *testing.T) { 75 | obj := createRes("name", "ns") 76 | testSvc := New(config.Config{}) 77 | testSvc.Load(obj) 78 | assert.Equal(t, "ns", testSvc.Namespace()) 79 | testSvc.Load(internal.TestNs) 80 | assert.Equal(t, internal.TestNsName, testSvc.Namespace()) 81 | }) 82 | t.Run("get chart name", func(t *testing.T) { 83 | testSvc := New(config.Config{ChartName: "name"}) 84 | assert.Equal(t, "name", testSvc.ChartName()) 85 | }) 86 | t.Run("trim common prefix abc", func(t *testing.T) { 87 | testSvc := New(config.Config{}) 88 | testSvc.Load(createRes("abc-name1", "ns")) 89 | testSvc.Load(createRes("abc-name2", "ns")) 90 | testSvc.Load(createRes("abc-service", "ns")) 91 | 92 | assert.Equal(t, "name1", testSvc.TrimName("abc-name1")) 93 | assert.Equal(t, "name2", testSvc.TrimName("abc-name2")) 94 | assert.Equal(t, "service", testSvc.TrimName("abc-service")) 95 | }) 96 | t.Run("trim common prefix: no common", func(t *testing.T) { 97 | testSvc := New(config.Config{}) 98 | testSvc.Load(createRes("name1", "ns")) 99 | testSvc.Load(createRes("abc", "ns")) 100 | testSvc.Load(createRes("service", "ns")) 101 | 102 | assert.Equal(t, "name1", testSvc.TrimName("name1")) 103 | assert.Equal(t, "abc", testSvc.TrimName("abc")) 104 | assert.Equal(t, "service", testSvc.TrimName("service")) 105 | }) 106 | t.Run("template name", func(t *testing.T) { 107 | testSvc := New(config.Config{ChartName: "chart-name"}) 108 | testSvc.Load(createRes("abc", "ns")) 109 | templated := testSvc.TemplatedName("abc") 110 | assert.Equal(t, `{{ include "chart-name.fullname" . }}-abc`, templated) 111 | }) 112 | t.Run("template name: not process unknown name", func(t *testing.T) { 113 | testSvc := New(config.Config{ChartName: "chart-name"}) 114 | testSvc.Load(createRes("abc", "ns")) 115 | assert.Equal(t, "qwe", testSvc.TemplatedName("qwe")) 116 | assert.NotEqual(t, "abc", testSvc.TemplatedName("abc")) 117 | }) 118 | } 119 | 120 | func createRes(name, ns string) *unstructured.Unstructured { 121 | objYaml := fmt.Sprintf(res, name, ns) 122 | return internal.GenerateObj(objYaml) 123 | } 124 | -------------------------------------------------------------------------------- /pkg/processor/configmap/configmap_test.go: -------------------------------------------------------------------------------- 1 | package configmap 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/arttor/helmify/pkg/metadata" 7 | 8 | "github.com/arttor/helmify/internal" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | const ( 13 | strConfigmap = `apiVersion: v1 14 | kind: ConfigMap 15 | metadata: 16 | name: my-operator-manager-config 17 | namespace: my-operator-system 18 | data: 19 | dummyconfigmapkey: dummyconfigmapvalue 20 | controller_manager_config.yaml: | 21 | apiVersion: controller-runtime.sigs.k8s.io/v1alpha1 22 | kind: ControllerManagerConfig 23 | health: 24 | healthProbeBindAddress: :8081` 25 | ) 26 | 27 | func Test_configMap_Process(t *testing.T) { 28 | var testInstance configMap 29 | 30 | t.Run("processed", func(t *testing.T) { 31 | obj := internal.GenerateObj(strConfigmap) 32 | processed, _, err := testInstance.Process(&metadata.Service{}, obj) 33 | assert.NoError(t, err) 34 | assert.Equal(t, true, processed) 35 | }) 36 | t.Run("skipped", func(t *testing.T) { 37 | obj := internal.TestNs 38 | processed, _, err := testInstance.Process(&metadata.Service{}, obj) 39 | assert.NoError(t, err) 40 | assert.Equal(t, false, processed) 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /pkg/processor/crd/crd.go: -------------------------------------------------------------------------------- 1 | package crd 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/sirupsen/logrus" 7 | "io" 8 | "strings" 9 | 10 | v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 11 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 12 | "k8s.io/apimachinery/pkg/runtime" 13 | "k8s.io/apimachinery/pkg/runtime/schema" 14 | "sigs.k8s.io/yaml" 15 | 16 | "github.com/arttor/helmify/pkg/helmify" 17 | yamlformat "github.com/arttor/helmify/pkg/yaml" 18 | ) 19 | 20 | const crdTeml = `apiVersion: apiextensions.k8s.io/v1 21 | kind: CustomResourceDefinition 22 | metadata: 23 | name: %[1]s 24 | %[3]s 25 | labels: 26 | %[4]s 27 | {{- include "%[2]s.labels" . | nindent 4 }} 28 | spec: 29 | %[5]s 30 | status: 31 | acceptedNames: 32 | kind: "" 33 | plural: "" 34 | conditions: [] 35 | storedVersions: []` 36 | 37 | var crdGVC = schema.GroupVersionKind{ 38 | Group: "apiextensions.k8s.io", 39 | Version: "v1", 40 | Kind: "CustomResourceDefinition", 41 | } 42 | 43 | // New creates processor for k8s CustomResourceDefinition resource. 44 | func New() helmify.Processor { 45 | return &crd{} 46 | } 47 | 48 | type crd struct{} 49 | 50 | // Process k8s CustomResourceDefinition object into template. Returns false if not capable of processing given resource type. 51 | func (c crd) Process(appMeta helmify.AppMetadata, obj *unstructured.Unstructured) (bool, helmify.Template, error) { 52 | if obj.GroupVersionKind() != crdGVC { 53 | return false, nil, nil 54 | } 55 | name, ok, err := unstructured.NestedString(obj.Object, "spec", "names", "singular") 56 | if err != nil || !ok { 57 | return true, nil, fmt.Errorf("%w: unable to create crd template", err) 58 | } 59 | if appMeta.Config().Crd { 60 | logrus.WithField("crd", name).Info("put CRD under crds dir without templating") 61 | // do not template CRDs when placed to crds dir 62 | res, err := yaml.Marshal(obj) 63 | if err != nil { 64 | return true, nil, fmt.Errorf("%w: unable to create crd template", err) 65 | } 66 | return true, &result{ 67 | name: name + "-crd.yaml", 68 | data: res, 69 | }, nil 70 | } 71 | 72 | var labels, annotations string 73 | if len(obj.GetAnnotations()) != 0 { 74 | a := obj.GetAnnotations() 75 | certName := a["cert-manager.io/inject-ca-from"] 76 | if certName != "" { 77 | certName = strings.TrimPrefix(certName, appMeta.Namespace()+"/") 78 | certName = appMeta.TrimName(certName) 79 | a["cert-manager.io/inject-ca-from"] = fmt.Sprintf(`{{ .Release.Namespace }}/{{ include "%[1]s.fullname" . }}-%[2]s`, appMeta.ChartName(), certName) 80 | } 81 | annotations, err = yamlformat.Marshal(map[string]interface{}{"annotations": a}, 2) 82 | if err != nil { 83 | return true, nil, err 84 | } 85 | } 86 | if len(obj.GetLabels()) != 0 { 87 | l := obj.GetLabels() 88 | // provided by Helm 89 | delete(l, "app.kubernetes.io/name") 90 | delete(l, "app.kubernetes.io/instance") 91 | delete(l, "app.kubernetes.io/version") 92 | delete(l, "app.kubernetes.io/managed-by") 93 | delete(l, "helm.sh/chart") 94 | if len(l) != 0 { 95 | labels, err = yamlformat.Marshal(l, 4) 96 | if err != nil { 97 | return true, nil, err 98 | } 99 | labels = strings.Trim(labels, "\n") 100 | } 101 | } 102 | 103 | specUnstr, ok, err := unstructured.NestedMap(obj.Object, "spec") 104 | if err != nil || !ok { 105 | return true, nil, fmt.Errorf("%w: unable to create crd template", err) 106 | } 107 | 108 | spec := v1.CustomResourceDefinitionSpec{} 109 | err = runtime.DefaultUnstructuredConverter.FromUnstructured(specUnstr, &spec) 110 | if err != nil { 111 | return true, nil, fmt.Errorf("%w: unable to cast to crd spec", err) 112 | } 113 | 114 | if spec.Conversion != nil { 115 | conv := spec.Conversion 116 | if conv.Strategy == v1.WebhookConverter { 117 | wh := conv.Webhook 118 | if wh != nil && wh.ClientConfig != nil && wh.ClientConfig.Service != nil { 119 | wh.ClientConfig.Service.Name = appMeta.TemplatedName(wh.ClientConfig.Service.Name) 120 | wh.ClientConfig.Service.Namespace = strings.ReplaceAll(wh.ClientConfig.Service.Namespace, appMeta.Namespace(), `{{ .Release.Namespace }}`) 121 | } 122 | } 123 | } 124 | 125 | specYaml, _ := yaml.Marshal(spec) 126 | specYaml = yamlformat.Indent(specYaml, 2) 127 | specYaml = bytes.TrimRight(specYaml, "\n ") 128 | 129 | res := fmt.Sprintf(crdTeml, obj.GetName(), appMeta.ChartName(), annotations, labels, string(specYaml)) 130 | res = strings.ReplaceAll(res, "\n\n", "\n") 131 | 132 | return true, &result{ 133 | name: name + "-crd.yaml", 134 | data: []byte(res), 135 | }, nil 136 | } 137 | 138 | type result struct { 139 | name string 140 | data []byte 141 | } 142 | 143 | func (r *result) Filename() string { 144 | return r.name 145 | } 146 | 147 | func (r *result) Values() helmify.Values { 148 | return helmify.Values{} 149 | } 150 | 151 | func (r *result) Write(writer io.Writer) error { 152 | _, err := writer.Write(r.data) 153 | return err 154 | } 155 | -------------------------------------------------------------------------------- /pkg/processor/crd/crd_test.go: -------------------------------------------------------------------------------- 1 | package crd 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/arttor/helmify/pkg/metadata" 7 | 8 | "github.com/arttor/helmify/internal" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | const ( 13 | strCRD = `apiVersion: apiextensions.k8s.io/v1 14 | kind: CustomResourceDefinition 15 | metadata: 16 | annotations: 17 | cert-manager.io/inject-ca-from: my-operator-system/my-operator-serving-cert 18 | creationTimestamp: null 19 | name: cephvolumes.test.example.com 20 | labels: 21 | example: true 22 | spec: 23 | group: test.example.com 24 | names: 25 | kind: CephVolume 26 | listKind: CephVolumeList 27 | plural: cephvolumes 28 | singular: cephvolume 29 | scope: Namespaced 30 | ` 31 | ) 32 | 33 | func Test_crd_Process(t *testing.T) { 34 | var testInstance crd 35 | 36 | t.Run("processed", func(t *testing.T) { 37 | obj := internal.GenerateObj(strCRD) 38 | processed, _, err := testInstance.Process(&metadata.Service{}, obj) 39 | assert.NoError(t, err) 40 | assert.Equal(t, true, processed) 41 | }) 42 | t.Run("skipped", func(t *testing.T) { 43 | obj := internal.TestNs 44 | processed, _, err := testInstance.Process(&metadata.Service{}, obj) 45 | assert.NoError(t, err) 46 | assert.Equal(t, false, processed) 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /pkg/processor/daemonset/daemonset.go: -------------------------------------------------------------------------------- 1 | package daemonset 2 | 3 | import ( 4 | "fmt" 5 | "github.com/arttor/helmify/pkg/processor/pod" 6 | "io" 7 | "strings" 8 | "text/template" 9 | 10 | "github.com/arttor/helmify/pkg/helmify" 11 | "github.com/arttor/helmify/pkg/processor" 12 | yamlformat "github.com/arttor/helmify/pkg/yaml" 13 | "github.com/iancoleman/strcase" 14 | appsv1 "k8s.io/api/apps/v1" 15 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 16 | "k8s.io/apimachinery/pkg/runtime" 17 | "k8s.io/apimachinery/pkg/runtime/schema" 18 | ) 19 | 20 | var daemonsetGVC = schema.GroupVersionKind{ 21 | Group: "apps", 22 | Version: "v1", 23 | Kind: "DaemonSet", 24 | } 25 | 26 | var daemonsetTempl, _ = template.New("daemonset").Parse( 27 | `{{- .Meta }} 28 | spec: 29 | selector: 30 | {{ .Selector }} 31 | template: 32 | metadata: 33 | labels: 34 | {{ .PodLabels }} 35 | {{- .PodAnnotations }} 36 | spec: 37 | {{ .Spec }}`) 38 | 39 | const selectorTempl = `%[1]s 40 | {{- include "%[2]s.selectorLabels" . | nindent 6 }} 41 | %[3]s` 42 | 43 | // New creates processor for k8s Daemonset resource. 44 | func New() helmify.Processor { 45 | return &daemonset{} 46 | } 47 | 48 | type daemonset struct{} 49 | 50 | // Process k8s Daemonset object into template. Returns false if not capable of processing given resource type. 51 | func (d daemonset) Process(appMeta helmify.AppMetadata, obj *unstructured.Unstructured) (bool, helmify.Template, error) { 52 | if obj.GroupVersionKind() != daemonsetGVC { 53 | return false, nil, nil 54 | } 55 | dae := appsv1.DaemonSet{} 56 | err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &dae) 57 | if err != nil { 58 | return true, nil, fmt.Errorf("%w: unable to cast to daemonset", err) 59 | } 60 | meta, err := processor.ProcessObjMeta(appMeta, obj) 61 | if err != nil { 62 | return true, nil, err 63 | } 64 | 65 | values := helmify.Values{} 66 | 67 | name := appMeta.TrimName(obj.GetName()) 68 | 69 | matchLabels, err := yamlformat.Marshal(map[string]interface{}{"matchLabels": dae.Spec.Selector.MatchLabels}, 0) 70 | if err != nil { 71 | return true, nil, err 72 | } 73 | matchExpr := "" 74 | if dae.Spec.Selector.MatchExpressions != nil { 75 | matchExpr, err = yamlformat.Marshal(map[string]interface{}{"matchExpressions": dae.Spec.Selector.MatchExpressions}, 0) 76 | if err != nil { 77 | return true, nil, err 78 | } 79 | } 80 | selector := fmt.Sprintf(selectorTempl, matchLabels, appMeta.ChartName(), matchExpr) 81 | selector = strings.Trim(selector, " \n") 82 | selector = string(yamlformat.Indent([]byte(selector), 4)) 83 | 84 | podLabels, err := yamlformat.Marshal(dae.Spec.Template.ObjectMeta.Labels, 8) 85 | if err != nil { 86 | return true, nil, err 87 | } 88 | podLabels += fmt.Sprintf("\n {{- include \"%s.selectorLabels\" . | nindent 8 }}", appMeta.ChartName()) 89 | 90 | podAnnotations := "" 91 | if len(dae.Spec.Template.ObjectMeta.Annotations) != 0 { 92 | podAnnotations, err = yamlformat.Marshal(map[string]interface{}{"annotations": dae.Spec.Template.ObjectMeta.Annotations}, 6) 93 | if err != nil { 94 | return true, nil, err 95 | } 96 | 97 | podAnnotations = "\n" + podAnnotations 98 | } 99 | 100 | nameCamel := strcase.ToLowerCamel(name) 101 | specMap, podValues, err := pod.ProcessSpec(nameCamel, appMeta, dae.Spec.Template.Spec) 102 | if err != nil { 103 | return true, nil, err 104 | } 105 | err = values.Merge(podValues) 106 | if err != nil { 107 | return true, nil, err 108 | } 109 | 110 | spec, err := yamlformat.Marshal(specMap, 6) 111 | if err != nil { 112 | return true, nil, err 113 | } 114 | spec = strings.ReplaceAll(spec, "'", "") 115 | 116 | return true, &result{ 117 | values: values, 118 | data: struct { 119 | Meta string 120 | Selector string 121 | PodLabels string 122 | PodAnnotations string 123 | Spec string 124 | }{ 125 | Meta: meta, 126 | Selector: selector, 127 | PodLabels: podLabels, 128 | PodAnnotations: podAnnotations, 129 | Spec: spec, 130 | }, 131 | }, nil 132 | } 133 | 134 | type result struct { 135 | data struct { 136 | Meta string 137 | Selector string 138 | PodLabels string 139 | PodAnnotations string 140 | Spec string 141 | } 142 | values helmify.Values 143 | } 144 | 145 | func (r *result) Filename() string { 146 | return "daemonset.yaml" 147 | } 148 | 149 | func (r *result) Values() helmify.Values { 150 | return r.values 151 | } 152 | 153 | func (r *result) Write(writer io.Writer) error { 154 | return daemonsetTempl.Execute(writer, r.data) 155 | } 156 | -------------------------------------------------------------------------------- /pkg/processor/daemonset/daemonset_test.go: -------------------------------------------------------------------------------- 1 | package daemonset 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/arttor/helmify/pkg/metadata" 7 | 8 | "github.com/arttor/helmify/internal" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | const ( 13 | strDepl = `apiVersion: apps/v1 14 | kind: DaemonSet 15 | metadata: 16 | name: fluentd-elasticsearch 17 | namespace: kube-system 18 | labels: 19 | k8s-app: fluentd-logging 20 | spec: 21 | selector: 22 | matchLabels: 23 | name: fluentd-elasticsearch 24 | template: 25 | metadata: 26 | labels: 27 | name: fluentd-elasticsearch 28 | spec: 29 | tolerations: 30 | # this toleration is to have the daemonset runnable on master nodes 31 | # remove it if your masters can't run pods 32 | - key: node-role.kubernetes.io/master 33 | operator: Exists 34 | effect: NoSchedule 35 | containers: 36 | - name: fluentd-elasticsearch 37 | image: quay.io/fluentd_elasticsearch/fluentd:v2.5.2 38 | resources: 39 | limits: 40 | memory: 200Mi 41 | requests: 42 | cpu: 100m 43 | memory: 200Mi 44 | volumeMounts: 45 | - name: varlog 46 | mountPath: /var/log 47 | - name: varlibdockercontainers 48 | mountPath: /var/lib/docker/containers 49 | readOnly: true 50 | terminationGracePeriodSeconds: 30 51 | volumes: 52 | - name: varlog 53 | hostPath: 54 | path: /var/log 55 | - name: varlibdockercontainers 56 | hostPath: 57 | path: /var/lib/docker/containers 58 | ` 59 | ) 60 | 61 | func Test_daemonset_Process(t *testing.T) { 62 | var testInstance daemonset 63 | 64 | t.Run("processed", func(t *testing.T) { 65 | obj := internal.GenerateObj(strDepl) 66 | processed, _, err := testInstance.Process(&metadata.Service{}, obj) 67 | assert.NoError(t, err) 68 | assert.Equal(t, true, processed) 69 | }) 70 | t.Run("skipped", func(t *testing.T) { 71 | obj := internal.TestNs 72 | processed, _, err := testInstance.Process(&metadata.Service{}, obj) 73 | assert.NoError(t, err) 74 | assert.Equal(t, false, processed) 75 | }) 76 | } 77 | -------------------------------------------------------------------------------- /pkg/processor/default.go: -------------------------------------------------------------------------------- 1 | package processor 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/arttor/helmify/pkg/helmify" 7 | yamlformat "github.com/arttor/helmify/pkg/yaml" 8 | "github.com/sirupsen/logrus" 9 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 10 | "k8s.io/apimachinery/pkg/runtime/schema" 11 | ) 12 | 13 | var nsGVK = schema.GroupVersionKind{ 14 | Group: "", 15 | Version: "v1", 16 | Kind: "Namespace", 17 | } 18 | 19 | // Default default processor for unknown resources. 20 | func Default() helmify.Processor { 21 | return &dft{} 22 | } 23 | 24 | type dft struct{} 25 | 26 | // Process unknown resource to a helm template. Default processor just templates obj name and adds helm annotations. 27 | func (d dft) Process(appMeta helmify.AppMetadata, obj *unstructured.Unstructured) (bool, helmify.Template, error) { 28 | if obj.GroupVersionKind() == nsGVK { 29 | // Skip namespaces from processing because namespace will be handled by Helm. 30 | return true, nil, nil 31 | } 32 | logrus.WithFields(logrus.Fields{ 33 | "ApiVersion": obj.GetAPIVersion(), 34 | "Kind": obj.GetKind(), 35 | "Name": obj.GetName(), 36 | }).Warn("Unsupported resource: using default processor.") 37 | name := appMeta.TrimName(obj.GetName()) 38 | 39 | meta, err := ProcessObjMeta(appMeta, obj) 40 | if err != nil { 41 | return true, nil, err 42 | } 43 | delete(obj.Object, "apiVersion") 44 | delete(obj.Object, "kind") 45 | delete(obj.Object, "metadata") 46 | 47 | body, err := yamlformat.Marshal(obj.Object, 0) 48 | if err != nil { 49 | return true, nil, err 50 | } 51 | return true, &defaultResult{ 52 | data: []byte(meta + "\n" + body), 53 | name: name, 54 | }, nil 55 | } 56 | 57 | type defaultResult struct { 58 | data []byte 59 | name string 60 | } 61 | 62 | func (r *defaultResult) Filename() string { 63 | return r.name + ".yaml" 64 | } 65 | 66 | func (r *defaultResult) Values() helmify.Values { 67 | return helmify.Values{} 68 | } 69 | 70 | func (r *defaultResult) Write(writer io.Writer) error { 71 | _, err := writer.Write(r.data) 72 | return err 73 | } 74 | -------------------------------------------------------------------------------- /pkg/processor/default_test.go: -------------------------------------------------------------------------------- 1 | package processor 2 | 3 | import ( 4 | "github.com/arttor/helmify/pkg/config" 5 | "testing" 6 | 7 | "github.com/arttor/helmify/internal" 8 | "github.com/arttor/helmify/pkg/metadata" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | const pvcYaml = `apiVersion: v1 13 | kind: PersistentVolumeClaim 14 | metadata: 15 | name: my-operator-pvc-lim 16 | spec: 17 | accessModes: 18 | - ReadWriteOnce 19 | resources: 20 | requests: 21 | storage: 2Gi 22 | storageClassName: cust1-mypool-lim` 23 | 24 | func Test_dft_Process(t *testing.T) { 25 | 26 | t.Run("skip namespace", func(t *testing.T) { 27 | testMeta := metadata.New(config.Config{ChartName: "chart-name"}) 28 | testMeta.Load(internal.TestNs) 29 | testProcessor := Default() 30 | processed, templ, err := testProcessor.Process(testMeta, internal.TestNs) 31 | assert.NoError(t, err) 32 | assert.True(t, processed) 33 | assert.Nil(t, templ) 34 | }) 35 | t.Run("process", func(t *testing.T) { 36 | obj := internal.GenerateObj(pvcYaml) 37 | testMeta := metadata.New(config.Config{ChartName: "chart-name"}) 38 | testMeta.Load(obj) 39 | testProcessor := Default() 40 | processed, templ, err := testProcessor.Process(testMeta, obj) 41 | assert.NoError(t, err) 42 | assert.True(t, processed) 43 | assert.NotNil(t, templ) 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /pkg/processor/doc.go: -------------------------------------------------------------------------------- 1 | // Package processor contains processors converting k8s objects to Helm template 2 | package processor 3 | -------------------------------------------------------------------------------- /pkg/processor/job/cron.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import ( 4 | "fmt" 5 | "github.com/arttor/helmify/pkg/helmify" 6 | "github.com/arttor/helmify/pkg/processor" 7 | "github.com/arttor/helmify/pkg/processor/pod" 8 | yamlformat "github.com/arttor/helmify/pkg/yaml" 9 | "github.com/iancoleman/strcase" 10 | "io" 11 | batchv1 "k8s.io/api/batch/v1" 12 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 13 | "k8s.io/apimachinery/pkg/runtime" 14 | "k8s.io/apimachinery/pkg/runtime/schema" 15 | "strings" 16 | "text/template" 17 | ) 18 | 19 | var cronTempl, _ = template.New("cron").Parse( 20 | `{{ .Meta }} 21 | {{ .Spec }}`) 22 | 23 | var cronGVC = schema.GroupVersionKind{ 24 | Group: "batch", 25 | Version: "v1", 26 | Kind: "CronJob", 27 | } 28 | 29 | // NewCron creates processor for k8s CronJob resource. 30 | func NewCron() helmify.Processor { 31 | return &cron{} 32 | } 33 | 34 | type cron struct{} 35 | 36 | // Process k8s CronJob object into template. Returns false if not capable of processing given resource type. 37 | func (p cron) Process(appMeta helmify.AppMetadata, obj *unstructured.Unstructured) (bool, helmify.Template, error) { 38 | if obj.GroupVersionKind() != cronGVC { 39 | return false, nil, nil 40 | } 41 | meta, err := processor.ProcessObjMeta(appMeta, obj) 42 | if err != nil { 43 | return true, nil, err 44 | } 45 | name := appMeta.TrimName(obj.GetName()) 46 | nameCamelCase := strcase.ToLowerCamel(name) 47 | 48 | jobObj := batchv1.CronJob{} 49 | err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &jobObj) 50 | if err != nil { 51 | return true, nil, fmt.Errorf("%w: unable to cast to Job", err) 52 | } 53 | spec := jobObj.Spec 54 | specMap, exists, err := unstructured.NestedMap(obj.Object, "spec") 55 | if err != nil { 56 | return true, nil, fmt.Errorf("%w: unable to get job spec", err) 57 | } 58 | if !exists { 59 | return true, nil, fmt.Errorf("no job spec presented") 60 | } 61 | 62 | values := helmify.Values{} 63 | 64 | // process job spec params: 65 | if spec.Schedule != "" { 66 | err := templateSpecVal(spec.Schedule, &values, specMap, nameCamelCase, "schedule") 67 | if err != nil { 68 | return true, nil, err 69 | } 70 | } 71 | 72 | if spec.Suspend != nil { 73 | err := templateSpecVal(*spec.Suspend, &values, specMap, nameCamelCase, "suspend") 74 | if err != nil { 75 | return true, nil, err 76 | } 77 | } 78 | 79 | if spec.FailedJobsHistoryLimit != nil { 80 | err := templateSpecVal(*spec.FailedJobsHistoryLimit, &values, specMap, nameCamelCase, "failedJobsHistoryLimit") 81 | if err != nil { 82 | return true, nil, err 83 | } 84 | } 85 | 86 | if spec.StartingDeadlineSeconds != nil { 87 | err := templateSpecVal(*spec.StartingDeadlineSeconds, &values, specMap, nameCamelCase, "startingDeadlineSeconds") 88 | if err != nil { 89 | return true, nil, err 90 | } 91 | } 92 | 93 | if spec.TimeZone != nil { 94 | err := templateSpecVal(*spec.TimeZone, &values, specMap, nameCamelCase, "timeZone") 95 | if err != nil { 96 | return true, nil, err 97 | } 98 | } 99 | 100 | if spec.SuccessfulJobsHistoryLimit != nil { 101 | err := templateSpecVal(*spec.SuccessfulJobsHistoryLimit, &values, specMap, nameCamelCase, "successfulJobsHistoryLimit") 102 | if err != nil { 103 | return true, nil, err 104 | } 105 | } 106 | 107 | // process job pod template: 108 | podSpecMap, podValues, err := pod.ProcessSpec(nameCamelCase, appMeta, jobObj.Spec.JobTemplate.Spec.Template.Spec) 109 | if err != nil { 110 | return true, nil, err 111 | } 112 | err = values.Merge(podValues) 113 | if err != nil { 114 | return true, nil, err 115 | } 116 | 117 | err = unstructured.SetNestedMap(specMap, podSpecMap, "jobTemplate", "spec", "template", "spec") 118 | if err != nil { 119 | return true, nil, fmt.Errorf("%w: unable to template job spec", err) 120 | } 121 | 122 | specStr, err := yamlformat.Marshal(map[string]interface{}{"spec": specMap}, 0) 123 | if err != nil { 124 | return true, nil, err 125 | } 126 | specStr = strings.ReplaceAll(specStr, "'", "") 127 | 128 | return true, &resultCron{ 129 | name: name + ".yaml", 130 | data: struct { 131 | Meta string 132 | Spec string 133 | }{Meta: meta, Spec: specStr}, 134 | values: values, 135 | }, nil 136 | } 137 | 138 | type resultCron struct { 139 | name string 140 | data struct { 141 | Meta string 142 | Spec string 143 | } 144 | values helmify.Values 145 | } 146 | 147 | func (r *resultCron) Filename() string { 148 | return r.name 149 | } 150 | 151 | func (r *resultCron) Values() helmify.Values { 152 | return r.values 153 | } 154 | 155 | func (r *resultCron) Write(writer io.Writer) error { 156 | return cronTempl.Execute(writer, r.data) 157 | } 158 | -------------------------------------------------------------------------------- /pkg/processor/job/cron_test.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import ( 4 | "github.com/arttor/helmify/internal" 5 | "github.com/arttor/helmify/pkg/metadata" 6 | "github.com/stretchr/testify/assert" 7 | "testing" 8 | ) 9 | 10 | const ( 11 | strCron = `apiVersion: batch/v1 12 | kind: CronJob 13 | metadata: 14 | name: cron-job 15 | spec: 16 | schedule: "* * * * *" 17 | jobTemplate: 18 | spec: 19 | template: 20 | spec: 21 | containers: 22 | - name: hello 23 | image: busybox:1.28 24 | imagePullPolicy: IfNotPresent 25 | command: 26 | - /bin/sh 27 | - -c 28 | - date; echo Hello from the Kubernetes cluster 29 | restartPolicy: OnFailure` 30 | ) 31 | 32 | func Test_Cron_Process(t *testing.T) { 33 | var testInstance cron 34 | 35 | t.Run("processed", func(t *testing.T) { 36 | obj := internal.GenerateObj(strCron) 37 | processed, _, err := testInstance.Process(&metadata.Service{}, obj) 38 | assert.NoError(t, err) 39 | assert.Equal(t, true, processed) 40 | }) 41 | t.Run("skipped", func(t *testing.T) { 42 | obj := internal.TestNs 43 | processed, _, err := testInstance.Process(&metadata.Service{}, obj) 44 | assert.NoError(t, err) 45 | assert.Equal(t, false, processed) 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /pkg/processor/job/job_test.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import ( 4 | "github.com/arttor/helmify/internal" 5 | "github.com/arttor/helmify/pkg/metadata" 6 | "github.com/stretchr/testify/assert" 7 | "testing" 8 | ) 9 | 10 | const ( 11 | strJob = `apiVersion: batch/v1 12 | kind: Job 13 | metadata: 14 | name: batch-job 15 | spec: 16 | template: 17 | spec: 18 | containers: 19 | - name: pi 20 | image: perl:5.34.0 21 | command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"] 22 | restartPolicy: Never 23 | backoffLimit: 4` 24 | ) 25 | 26 | func Test_configMap_Process(t *testing.T) { 27 | var testInstance job 28 | 29 | t.Run("processed", func(t *testing.T) { 30 | obj := internal.GenerateObj(strJob) 31 | processed, _, err := testInstance.Process(&metadata.Service{}, obj) 32 | assert.NoError(t, err) 33 | assert.Equal(t, true, processed) 34 | }) 35 | t.Run("skipped", func(t *testing.T) { 36 | obj := internal.TestNs 37 | processed, _, err := testInstance.Process(&metadata.Service{}, obj) 38 | assert.NoError(t, err) 39 | assert.Equal(t, false, processed) 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /pkg/processor/meta.go: -------------------------------------------------------------------------------- 1 | package processor 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/iancoleman/strcase" 8 | 9 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 10 | 11 | "github.com/arttor/helmify/pkg/helmify" 12 | yamlformat "github.com/arttor/helmify/pkg/yaml" 13 | ) 14 | 15 | const metaTemplate = `apiVersion: %[1]s 16 | kind: %[2]s 17 | metadata: 18 | name: %[3]s 19 | %[7]s 20 | labels: 21 | %[5]s 22 | {{- include "%[4]s.labels" . | nindent 4 }} 23 | %[6]s` 24 | 25 | const annotationsTemplate = ` annotations: 26 | {{- toYaml .Values.%[1]s.%[2]s.annotations | nindent 4 }}` 27 | 28 | type MetaOpt interface { 29 | apply(*options) 30 | } 31 | 32 | type options struct { 33 | values helmify.Values 34 | annotations bool 35 | } 36 | 37 | type annotationsOption struct { 38 | values helmify.Values 39 | } 40 | 41 | func (a annotationsOption) apply(opts *options) { 42 | opts.annotations = true 43 | opts.values = a.values 44 | } 45 | 46 | func WithAnnotations(values helmify.Values) MetaOpt { 47 | return annotationsOption{ 48 | values: values, 49 | } 50 | } 51 | 52 | // ProcessObjMeta - returns object apiVersion, kind and metadata as helm template. 53 | func ProcessObjMeta(appMeta helmify.AppMetadata, obj *unstructured.Unstructured, opts ...MetaOpt) (string, error) { 54 | options := &options{} 55 | for _, opt := range opts { 56 | opt.apply(options) 57 | } 58 | 59 | var err error 60 | var labels, annotations, namespace string 61 | if len(obj.GetLabels()) != 0 { 62 | l := obj.GetLabels() 63 | // provided by Helm 64 | delete(l, "app.kubernetes.io/name") 65 | delete(l, "app.kubernetes.io/instance") 66 | delete(l, "app.kubernetes.io/version") 67 | delete(l, "app.kubernetes.io/managed-by") 68 | delete(l, "helm.sh/chart") 69 | 70 | // Since we delete labels above, it is possible that at this point there are no more labels. 71 | if len(l) > 0 { 72 | labels, err = yamlformat.Marshal(l, 4) 73 | if err != nil { 74 | return "", err 75 | } 76 | } 77 | } 78 | if len(obj.GetAnnotations()) != 0 { 79 | annotations, err = yamlformat.Marshal(map[string]interface{}{"annotations": obj.GetAnnotations()}, 2) 80 | if err != nil { 81 | return "", err 82 | } 83 | } 84 | 85 | if (obj.GetNamespace() != "") && (appMeta.Config().PreserveNs) { 86 | namespace, err = yamlformat.Marshal(map[string]interface{}{"namespace": obj.GetNamespace()}, 2) 87 | if err != nil { 88 | return "", err 89 | } 90 | } 91 | 92 | templatedName := appMeta.TemplatedName(obj.GetName()) 93 | apiVersion, kind := obj.GetObjectKind().GroupVersionKind().ToAPIVersionAndKind() 94 | 95 | var metaStr string 96 | if options.values != nil && options.annotations { 97 | name := strcase.ToLowerCamel(appMeta.TrimName(obj.GetName())) 98 | kind := strcase.ToLowerCamel(kind) 99 | valuesAnnotations := make(map[string]interface{}) 100 | for k, v := range obj.GetAnnotations() { 101 | valuesAnnotations[k] = v 102 | } 103 | err = unstructured.SetNestedField(options.values, valuesAnnotations, name, kind, "annotations") 104 | if err != nil { 105 | return "", err 106 | } 107 | 108 | annotations = fmt.Sprintf(annotationsTemplate, name, kind) 109 | } 110 | 111 | metaStr = fmt.Sprintf(metaTemplate, apiVersion, kind, templatedName, appMeta.ChartName(), labels, annotations, namespace) 112 | metaStr = strings.Trim(metaStr, " \n") 113 | metaStr = strings.ReplaceAll(metaStr, "\n\n", "\n") 114 | return metaStr, nil 115 | } 116 | -------------------------------------------------------------------------------- /pkg/processor/meta_test.go: -------------------------------------------------------------------------------- 1 | package processor 2 | 3 | import ( 4 | "github.com/arttor/helmify/pkg/config" 5 | "testing" 6 | 7 | "github.com/arttor/helmify/internal" 8 | "github.com/arttor/helmify/pkg/metadata" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestProcessObjMeta(t *testing.T) { 13 | testMeta := metadata.New(config.Config{ChartName: "chart-name"}) 14 | testMeta.Load(internal.TestNs) 15 | res, err := ProcessObjMeta(testMeta, internal.TestNs) 16 | assert.NoError(t, err) 17 | assert.Contains(t, res, "chart-name.labels") 18 | assert.Contains(t, res, "chart-name.fullname") 19 | } 20 | -------------------------------------------------------------------------------- /pkg/processor/poddisruptionbudget/pdb.go: -------------------------------------------------------------------------------- 1 | package poddisruptionbudget 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | 8 | "github.com/arttor/helmify/pkg/processor" 9 | 10 | "github.com/arttor/helmify/pkg/helmify" 11 | yamlformat "github.com/arttor/helmify/pkg/yaml" 12 | "github.com/iancoleman/strcase" 13 | policyv1 "k8s.io/api/policy/v1" 14 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 15 | "k8s.io/apimachinery/pkg/runtime" 16 | "k8s.io/apimachinery/pkg/runtime/schema" 17 | "sigs.k8s.io/yaml" 18 | ) 19 | 20 | const ( 21 | pdbTempSpec = ` 22 | spec: 23 | minAvailable: {{ .Values.%[1]s.minAvailable }} 24 | maxUnavailable: {{ .Values.%[1]s.maxUnavailable }} 25 | selector: 26 | %[2]s 27 | {{- include "%[3]s.selectorLabels" . | nindent 6 }}` 28 | ) 29 | 30 | var pdbGVC = schema.GroupVersionKind{ 31 | Group: "policy", 32 | Version: "v1", 33 | Kind: "PodDisruptionBudget", 34 | } 35 | 36 | // New creates processor for k8s Service resource. 37 | func New() helmify.Processor { 38 | return &pdb{} 39 | } 40 | 41 | type pdb struct{} 42 | 43 | // Process k8s Service object into template. Returns false if not capable of processing given resource type. 44 | func (r pdb) Process(appMeta helmify.AppMetadata, obj *unstructured.Unstructured) (bool, helmify.Template, error) { 45 | if obj.GroupVersionKind() != pdbGVC { 46 | return false, nil, nil 47 | } 48 | pdb := policyv1.PodDisruptionBudget{} 49 | err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &pdb) 50 | if err != nil { 51 | return true, nil, fmt.Errorf("%w: unable to cast to pdb", err) 52 | } 53 | spec := pdb.Spec 54 | values := helmify.Values{} 55 | 56 | meta, err := processor.ProcessObjMeta(appMeta, obj) 57 | if err != nil { 58 | return true, nil, err 59 | } 60 | 61 | name := appMeta.TrimName(obj.GetName()) 62 | nameCamel := strcase.ToLowerCamel(name) 63 | 64 | selector, _ := yaml.Marshal(pdb.Spec.Selector) 65 | selector = yamlformat.Indent(selector, 4) 66 | selector = bytes.TrimRight(selector, "\n ") 67 | 68 | if spec.MaxUnavailable != nil { 69 | _, err := values.Add(spec.MaxUnavailable.IntValue(), nameCamel, "maxUnavailable") 70 | if err != nil { 71 | return true, nil, err 72 | } 73 | } 74 | 75 | if spec.MinAvailable != nil { 76 | _, err := values.Add(spec.MinAvailable.IntValue(), nameCamel, "minAvailable") 77 | if err != nil { 78 | return true, nil, err 79 | } 80 | } 81 | 82 | res := meta + fmt.Sprintf(pdbTempSpec, nameCamel, selector, appMeta.ChartName()) 83 | return true, &result{ 84 | name: name, 85 | data: res, 86 | values: values, 87 | }, nil 88 | } 89 | 90 | type result struct { 91 | name string 92 | data string 93 | values helmify.Values 94 | } 95 | 96 | func (r *result) Filename() string { 97 | return r.name + ".yaml" 98 | } 99 | 100 | func (r *result) Values() helmify.Values { 101 | return r.values 102 | } 103 | 104 | func (r *result) Write(writer io.Writer) error { 105 | _, err := writer.Write([]byte(r.data)) 106 | return err 107 | } 108 | -------------------------------------------------------------------------------- /pkg/processor/poddisruptionbudget/pdb_test.go: -------------------------------------------------------------------------------- 1 | package poddisruptionbudget 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/arttor/helmify/pkg/metadata" 8 | 9 | "github.com/arttor/helmify/internal" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | const pdbYaml = `apiVersion: policy/v1 14 | kind: PodDisruptionBudget 15 | metadata: 16 | labels: 17 | control-plane: controller-manager 18 | name: my-operator-controller-manager-pdb 19 | namespace: my-operator-system 20 | spec: 21 | minAvailable: 2 22 | selector: 23 | matchLabels: 24 | control-plane: controller-manager` 25 | 26 | func Test_pdb_Process(t *testing.T) { 27 | var testInstance pdb 28 | 29 | t.Run("processed", func(t *testing.T) { 30 | obj := internal.GenerateObj(pdbYaml) 31 | processed, tt, err := testInstance.Process(&metadata.Service{}, obj) 32 | _ = tt.Write(os.Stdout) 33 | assert.NoError(t, err) 34 | assert.Equal(t, true, processed) 35 | }) 36 | t.Run("skipped", func(t *testing.T) { 37 | obj := internal.TestNs 38 | processed, _, err := testInstance.Process(&metadata.Service{}, obj) 39 | assert.NoError(t, err) 40 | assert.Equal(t, false, processed) 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /pkg/processor/rbac/clusterrolebinding.go: -------------------------------------------------------------------------------- 1 | package rbac 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strings" 7 | "text/template" 8 | 9 | "github.com/arttor/helmify/pkg/processor" 10 | 11 | "github.com/arttor/helmify/pkg/helmify" 12 | yamlformat "github.com/arttor/helmify/pkg/yaml" 13 | rbacv1 "k8s.io/api/rbac/v1" 14 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 15 | "k8s.io/apimachinery/pkg/runtime" 16 | "k8s.io/apimachinery/pkg/runtime/schema" 17 | ) 18 | 19 | var clusterRoleBindingTempl, _ = template.New("clusterRoleBinding").Parse( 20 | `{{ .Meta }} 21 | {{ .RoleRef }} 22 | {{ .Subjects }}`) 23 | 24 | var clusterRoleBindingGVC = schema.GroupVersionKind{ 25 | Group: "rbac.authorization.k8s.io", 26 | Version: "v1", 27 | Kind: "ClusterRoleBinding", 28 | } 29 | 30 | // ClusterRoleBinding creates processor for k8s ClusterRoleBinding resource. 31 | func ClusterRoleBinding() helmify.Processor { 32 | return &clusterRoleBinding{} 33 | } 34 | 35 | type clusterRoleBinding struct{} 36 | 37 | // Process k8s ClusterRoleBinding object into template. Returns false if not capable of processing given resource type. 38 | func (r clusterRoleBinding) Process(appMeta helmify.AppMetadata, obj *unstructured.Unstructured) (bool, helmify.Template, error) { 39 | if obj.GroupVersionKind() != clusterRoleBindingGVC { 40 | return false, nil, nil 41 | } 42 | 43 | rb := rbacv1.ClusterRoleBinding{} 44 | err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &rb) 45 | if err != nil { 46 | return true, nil, fmt.Errorf("%w: unable to cast to RoleBinding", err) 47 | } 48 | 49 | meta, err := processor.ProcessObjMeta(appMeta, obj) 50 | if err != nil { 51 | return true, nil, err 52 | } 53 | 54 | rb.RoleRef.Name = appMeta.TemplatedName(rb.RoleRef.Name) 55 | 56 | roleRef, err := yamlformat.Marshal(map[string]interface{}{"roleRef": &rb.RoleRef}, 0) 57 | if err != nil { 58 | return true, nil, err 59 | } 60 | 61 | for i, s := range rb.Subjects { 62 | s.Namespace = "{{ .Release.Namespace }}" 63 | s.Name = appMeta.TemplatedName(s.Name) 64 | rb.Subjects[i] = s 65 | } 66 | subjects, err := yamlformat.Marshal(map[string]interface{}{"subjects": &rb.Subjects}, 0) 67 | if err != nil { 68 | return true, nil, err 69 | } 70 | 71 | return true, &crbResult{ 72 | name: appMeta.TrimName(obj.GetName()), 73 | data: struct { 74 | Meta string 75 | RoleRef string 76 | Subjects string 77 | }{ 78 | Meta: meta, 79 | RoleRef: roleRef, 80 | Subjects: subjects, 81 | }, 82 | }, nil 83 | } 84 | 85 | type crbResult struct { 86 | name string 87 | data struct { 88 | Meta string 89 | RoleRef string 90 | Subjects string 91 | } 92 | } 93 | 94 | func (r *crbResult) Filename() string { 95 | return strings.TrimSuffix(r.name, "-rolebinding") + "-rbac.yaml" 96 | } 97 | 98 | func (r *crbResult) Values() helmify.Values { 99 | return helmify.Values{} 100 | } 101 | 102 | func (r *crbResult) Write(writer io.Writer) error { 103 | return clusterRoleBindingTempl.Execute(writer, r.data) 104 | } 105 | -------------------------------------------------------------------------------- /pkg/processor/rbac/clusterrolebinding_test.go: -------------------------------------------------------------------------------- 1 | package rbac 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/arttor/helmify/pkg/metadata" 7 | 8 | "github.com/arttor/helmify/internal" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | const clusterRoleBindingYaml = `apiVersion: rbac.authorization.k8s.io/v1 13 | kind: ClusterRoleBinding 14 | metadata: 15 | name: my-operator-manager-rolebinding 16 | roleRef: 17 | apiGroup: rbac.authorization.k8s.io 18 | kind: ClusterRole 19 | name: my-operator-manager-role 20 | subjects: 21 | - kind: ServiceAccount 22 | name: my-operator-controller-manager 23 | namespace: my-operator-system` 24 | 25 | func Test_clusterRoleBinding_Process(t *testing.T) { 26 | var testInstance clusterRoleBinding 27 | 28 | t.Run("processed", func(t *testing.T) { 29 | obj := internal.GenerateObj(clusterRoleBindingYaml) 30 | processed, _, err := testInstance.Process(&metadata.Service{}, obj) 31 | assert.NoError(t, err) 32 | assert.Equal(t, true, processed) 33 | }) 34 | t.Run("skipped", func(t *testing.T) { 35 | obj := internal.TestNs 36 | processed, _, err := testInstance.Process(&metadata.Service{}, obj) 37 | assert.NoError(t, err) 38 | assert.Equal(t, false, processed) 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /pkg/processor/rbac/role.go: -------------------------------------------------------------------------------- 1 | package rbac 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strings" 7 | "text/template" 8 | 9 | "github.com/arttor/helmify/pkg/processor" 10 | 11 | "github.com/arttor/helmify/pkg/helmify" 12 | yamlformat "github.com/arttor/helmify/pkg/yaml" 13 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 14 | "k8s.io/apimachinery/pkg/runtime/schema" 15 | ) 16 | 17 | var roleTempl, _ = template.New("clusterRole").Parse( 18 | `{{ .Meta }} 19 | {{- if .AggregationRule }} 20 | {{ .AggregationRule }} 21 | {{- end}} 22 | {{ .Rules }}`) 23 | 24 | var clusterRoleGVC = schema.GroupVersionKind{ 25 | Group: "rbac.authorization.k8s.io", 26 | Version: "v1", 27 | Kind: "ClusterRole", 28 | } 29 | var roleGVC = schema.GroupVersionKind{ 30 | Group: "rbac.authorization.k8s.io", 31 | Version: "v1", 32 | Kind: "Role", 33 | } 34 | 35 | // Role creates processor for k8s Role and ClusterRole resources. 36 | func Role() helmify.Processor { 37 | return &role{} 38 | } 39 | 40 | type role struct{} 41 | 42 | // Process k8s ClusterRole object into template. Returns false if not capable of processing given resource type. 43 | func (r role) Process(appMeta helmify.AppMetadata, obj *unstructured.Unstructured) (bool, helmify.Template, error) { 44 | var aggregationRule string 45 | 46 | if obj.GroupVersionKind() != clusterRoleGVC && obj.GroupVersionKind() != roleGVC { 47 | return false, nil, nil 48 | } 49 | 50 | meta, err := processor.ProcessObjMeta(appMeta, obj) 51 | if err != nil { 52 | return true, nil, err 53 | } 54 | 55 | if existingAggRule := obj.Object["aggregationRule"]; existingAggRule != nil { 56 | if obj.GroupVersionKind().Kind == "Role" { 57 | return true, nil, fmt.Errorf("unable to set aggregationRule to the kind Role in %q: unsupported", obj.GetName()) 58 | } 59 | 60 | if existingAggRule.(map[string]interface{})["clusterRoleSelectors"] != nil { 61 | aggRuleMap := map[string]interface{}{"aggregationRule": existingAggRule} 62 | 63 | aggregationRule, err = yamlformat.Marshal(aggRuleMap, 0) 64 | if err != nil { 65 | return true, nil, err 66 | } 67 | } 68 | } 69 | 70 | rules, err := yamlformat.Marshal(map[string]interface{}{"rules": obj.Object["rules"]}, 0) 71 | if err != nil { 72 | return true, nil, err 73 | } 74 | 75 | return true, &crResult{ 76 | name: appMeta.TrimName(obj.GetName()), 77 | data: struct { 78 | Meta string 79 | AggregationRule string 80 | Rules string 81 | }{Meta: meta, AggregationRule: aggregationRule, Rules: rules}, 82 | }, nil 83 | } 84 | 85 | type crResult struct { 86 | name string 87 | data struct { 88 | Meta string 89 | AggregationRule string 90 | Rules string 91 | } 92 | } 93 | 94 | func (r *crResult) Filename() string { 95 | return strings.TrimSuffix(r.name, "-role") + "-rbac.yaml" 96 | } 97 | 98 | func (r *crResult) GVK() schema.GroupVersionKind { 99 | return clusterRoleGVC 100 | } 101 | 102 | func (r *crResult) Values() helmify.Values { 103 | return helmify.Values{} 104 | } 105 | 106 | func (r *crResult) Write(writer io.Writer) error { 107 | return roleTempl.Execute(writer, r.data) 108 | } 109 | -------------------------------------------------------------------------------- /pkg/processor/rbac/role_test.go: -------------------------------------------------------------------------------- 1 | package rbac 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/arttor/helmify/pkg/metadata" 7 | 8 | "github.com/arttor/helmify/internal" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | const clusterRoleYaml = `apiVersion: rbac.authorization.k8s.io/v1 13 | kind: ClusterRole 14 | metadata: 15 | creationTimestamp: null 16 | name: my-operator-manager-role 17 | aggregationRule: 18 | clusterRoleSelectors: 19 | - matchExpressions: 20 | - key: my.operator.dev/release 21 | operator: Exists 22 | rules: 23 | - apiGroups: 24 | - "" 25 | resources: 26 | - pods 27 | verbs: 28 | - get 29 | - list` 30 | 31 | func Test_clusterRole_Process(t *testing.T) { 32 | var testInstance role 33 | 34 | t.Run("processed", func(t *testing.T) { 35 | obj := internal.GenerateObj(clusterRoleYaml) 36 | processed, _, err := testInstance.Process(&metadata.Service{}, obj) 37 | assert.NoError(t, err) 38 | assert.Equal(t, true, processed) 39 | }) 40 | t.Run("skipped", func(t *testing.T) { 41 | obj := internal.TestNs 42 | processed, _, err := testInstance.Process(&metadata.Service{}, obj) 43 | assert.NoError(t, err) 44 | assert.Equal(t, false, processed) 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /pkg/processor/rbac/rolebinding.go: -------------------------------------------------------------------------------- 1 | package rbac 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strings" 7 | "text/template" 8 | 9 | "github.com/arttor/helmify/pkg/processor" 10 | 11 | "github.com/arttor/helmify/pkg/helmify" 12 | yamlformat "github.com/arttor/helmify/pkg/yaml" 13 | rbacv1 "k8s.io/api/rbac/v1" 14 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 15 | "k8s.io/apimachinery/pkg/runtime" 16 | "k8s.io/apimachinery/pkg/runtime/schema" 17 | ) 18 | 19 | var roleBindingTempl, _ = template.New("roleBinding").Parse( 20 | `{{- .Meta }} 21 | {{ .RoleRef }} 22 | {{ .Subjects }}`) 23 | 24 | var roleBindingGVC = schema.GroupVersionKind{ 25 | Group: "rbac.authorization.k8s.io", 26 | Version: "v1", 27 | Kind: "RoleBinding", 28 | } 29 | 30 | // RoleBinding creates processor for k8s RoleBinding resource. 31 | func RoleBinding() helmify.Processor { 32 | return &roleBinding{} 33 | } 34 | 35 | type roleBinding struct{} 36 | 37 | // Process k8s RoleBinding object into helm template. Returns false if not capable of processing given resource type. 38 | func (r roleBinding) Process(appMeta helmify.AppMetadata, obj *unstructured.Unstructured) (bool, helmify.Template, error) { 39 | if obj.GroupVersionKind() != roleBindingGVC { 40 | return false, nil, nil 41 | } 42 | rb := rbacv1.RoleBinding{} 43 | err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &rb) 44 | if err != nil { 45 | return true, nil, fmt.Errorf("%w: unable to cast to RoleBinding", err) 46 | } 47 | meta, err := processor.ProcessObjMeta(appMeta, obj) 48 | if err != nil { 49 | return true, nil, err 50 | } 51 | 52 | rb.RoleRef.Name = appMeta.TemplatedName(rb.RoleRef.Name) 53 | 54 | roleRef, err := yamlformat.Marshal(map[string]interface{}{"roleRef": &rb.RoleRef}, 0) 55 | if err != nil { 56 | return true, nil, err 57 | } 58 | 59 | for i, s := range rb.Subjects { 60 | s.Namespace = "{{ .Release.Namespace }}" 61 | s.Name = appMeta.TemplatedName(s.Name) 62 | rb.Subjects[i] = s 63 | } 64 | subjects, err := yamlformat.Marshal(map[string]interface{}{"subjects": &rb.Subjects}, 0) 65 | if err != nil { 66 | return true, nil, err 67 | } 68 | 69 | return true, &rbResult{ 70 | name: appMeta.TrimName(obj.GetName()), 71 | data: struct { 72 | Meta string 73 | RoleRef string 74 | Subjects string 75 | }{ 76 | Meta: meta, 77 | RoleRef: roleRef, 78 | Subjects: subjects, 79 | }, 80 | }, nil 81 | } 82 | 83 | type rbResult struct { 84 | name string 85 | data struct { 86 | Meta string 87 | RoleRef string 88 | Subjects string 89 | } 90 | } 91 | 92 | func (r *rbResult) Filename() string { 93 | return strings.TrimSuffix(r.name, "-rolebinding") + "-rbac.yaml" 94 | } 95 | 96 | func (r *rbResult) Values() helmify.Values { 97 | return helmify.Values{} 98 | } 99 | 100 | func (r *rbResult) Write(writer io.Writer) error { 101 | return roleBindingTempl.Execute(writer, r.data) 102 | } 103 | -------------------------------------------------------------------------------- /pkg/processor/rbac/rolebinding_test.go: -------------------------------------------------------------------------------- 1 | package rbac 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/arttor/helmify/pkg/metadata" 7 | 8 | "github.com/arttor/helmify/internal" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | const roleBindingYaml = `apiVersion: rbac.authorization.k8s.io/v1 13 | kind: RoleBinding 14 | metadata: 15 | name: my-operator-leader-election-rolebinding 16 | namespace: my-operator-system 17 | roleRef: 18 | apiGroup: rbac.authorization.k8s.io 19 | kind: Role 20 | name: my-operator-leader-election-role 21 | subjects: 22 | - kind: ServiceAccount 23 | name: my-operator-controller-manager 24 | namespace: my-operator-system` 25 | 26 | func Test_roleBinding_Process(t *testing.T) { 27 | var testInstance roleBinding 28 | 29 | t.Run("processed", func(t *testing.T) { 30 | obj := internal.GenerateObj(roleBindingYaml) 31 | processed, _, err := testInstance.Process(&metadata.Service{}, obj) 32 | assert.NoError(t, err) 33 | assert.Equal(t, true, processed) 34 | }) 35 | t.Run("skipped", func(t *testing.T) { 36 | obj := internal.TestNs 37 | processed, _, err := testInstance.Process(&metadata.Service{}, obj) 38 | assert.NoError(t, err) 39 | assert.Equal(t, false, processed) 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /pkg/processor/rbac/serviceaccount.go: -------------------------------------------------------------------------------- 1 | package rbac 2 | 3 | import ( 4 | "github.com/arttor/helmify/pkg/helmify" 5 | "github.com/arttor/helmify/pkg/processor" 6 | "io" 7 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 8 | "k8s.io/apimachinery/pkg/runtime/schema" 9 | ) 10 | 11 | var serviceAccountGVC = schema.GroupVersionKind{ 12 | Group: "", 13 | Version: "v1", 14 | Kind: "ServiceAccount", 15 | } 16 | 17 | // ServiceAccount creates processor for k8s ServiceAccount resource. 18 | func ServiceAccount() helmify.Processor { 19 | return &serviceAccount{} 20 | } 21 | 22 | type serviceAccount struct{} 23 | 24 | // Process k8s ServiceAccount object into helm template. Returns false if not capable of processing given resource type. 25 | func (sa serviceAccount) Process(appMeta helmify.AppMetadata, obj *unstructured.Unstructured) (bool, helmify.Template, error) { 26 | if obj.GroupVersionKind() != serviceAccountGVC { 27 | return false, nil, nil 28 | } 29 | values := helmify.Values{} 30 | meta, err := processor.ProcessObjMeta(appMeta, obj, processor.WithAnnotations(values)) 31 | if err != nil { 32 | return true, nil, err 33 | } 34 | 35 | return true, &saResult{ 36 | data: []byte(meta), 37 | values: values, 38 | }, nil 39 | } 40 | 41 | type saResult struct { 42 | data []byte 43 | values helmify.Values 44 | } 45 | 46 | func (r *saResult) Filename() string { 47 | return "serviceaccount.yaml" 48 | } 49 | 50 | func (r *saResult) Values() helmify.Values { 51 | return r.values 52 | } 53 | 54 | func (r *saResult) Write(writer io.Writer) error { 55 | _, err := writer.Write(r.data) 56 | return err 57 | } 58 | -------------------------------------------------------------------------------- /pkg/processor/rbac/serviceaccount_test.go: -------------------------------------------------------------------------------- 1 | package rbac 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/arttor/helmify/pkg/metadata" 7 | 8 | "github.com/arttor/helmify/internal" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | const serviceAccountYaml = `apiVersion: v1 13 | kind: ServiceAccount 14 | metadata: 15 | name: my-operator-controller-manager 16 | namespace: my-operator-system` 17 | 18 | func Test_serviceAccount_Process(t *testing.T) { 19 | var testInstance serviceAccount 20 | 21 | t.Run("processed", func(t *testing.T) { 22 | obj := internal.GenerateObj(serviceAccountYaml) 23 | processed, _, err := testInstance.Process(&metadata.Service{}, obj) 24 | assert.NoError(t, err) 25 | assert.Equal(t, true, processed) 26 | }) 27 | t.Run("skipped", func(t *testing.T) { 28 | obj := internal.TestNs 29 | processed, _, err := testInstance.Process(&metadata.Service{}, obj) 30 | assert.NoError(t, err) 31 | assert.Equal(t, false, processed) 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /pkg/processor/secret/secret.go: -------------------------------------------------------------------------------- 1 | package secret 2 | 3 | import ( 4 | "fmt" 5 | "github.com/arttor/helmify/pkg/format" 6 | "io" 7 | "strings" 8 | "text/template" 9 | 10 | "github.com/arttor/helmify/pkg/processor" 11 | 12 | "github.com/arttor/helmify/pkg/helmify" 13 | yamlformat "github.com/arttor/helmify/pkg/yaml" 14 | "github.com/iancoleman/strcase" 15 | corev1 "k8s.io/api/core/v1" 16 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 17 | "k8s.io/apimachinery/pkg/runtime" 18 | "k8s.io/apimachinery/pkg/runtime/schema" 19 | ) 20 | 21 | var secretTempl, _ = template.New("secret").Parse( 22 | `{{ .Meta }} 23 | {{- if .Data }} 24 | {{ .Data }} 25 | {{- end }} 26 | {{- if .StringData }} 27 | {{ .StringData }} 28 | {{- end }} 29 | {{- if .Type }} 30 | {{ .Type }} 31 | {{- end }}`) 32 | 33 | var configMapGVC = schema.GroupVersionKind{ 34 | Group: "", 35 | Version: "v1", 36 | Kind: "Secret", 37 | } 38 | 39 | // New creates processor for k8s Secret resource. 40 | func New() helmify.Processor { 41 | return &secret{} 42 | } 43 | 44 | type secret struct{} 45 | 46 | // Process k8s Secret object into template. Returns false if not capable of processing given resource type. 47 | func (d secret) Process(appMeta helmify.AppMetadata, obj *unstructured.Unstructured) (bool, helmify.Template, error) { 48 | if obj.GroupVersionKind() != configMapGVC { 49 | return false, nil, nil 50 | } 51 | sec := corev1.Secret{} 52 | err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &sec) 53 | if err != nil { 54 | return true, nil, fmt.Errorf("%w: unable to cast to secret", err) 55 | } 56 | meta, err := processor.ProcessObjMeta(appMeta, obj) 57 | if err != nil { 58 | return true, nil, err 59 | } 60 | 61 | name := appMeta.TrimName(obj.GetName()) 62 | nameCamelCase := strcase.ToLowerCamel(name) 63 | 64 | secretType := string(sec.Type) 65 | if secretType != "" { 66 | secretType, err = yamlformat.Marshal(map[string]interface{}{"type": secretType}, 0) 67 | if err != nil { 68 | return true, nil, err 69 | } 70 | } 71 | 72 | values := helmify.Values{} 73 | var data, stringData string 74 | templatedData := map[string]string{} 75 | for key := range sec.Data { 76 | keyCamelCase := strcase.ToLowerCamel(key) 77 | if key == strings.ToUpper(key) { 78 | keyCamelCase = strcase.ToLowerCamel(strings.ToLower(key)) 79 | } 80 | templatedName, err := values.AddSecret(true, nameCamelCase, keyCamelCase) 81 | if err != nil { 82 | return true, nil, fmt.Errorf("%w: unable add secret to values", err) 83 | } 84 | templatedData[key] = templatedName 85 | } 86 | if len(templatedData) != 0 { 87 | data, err = yamlformat.Marshal(map[string]interface{}{"data": templatedData}, 0) 88 | if err != nil { 89 | return true, nil, err 90 | } 91 | data = strings.ReplaceAll(data, "'", "") 92 | data = format.FixUnterminatedQuotes(data) 93 | } 94 | 95 | templatedData = map[string]string{} 96 | for key := range sec.StringData { 97 | keyCamelCase := strcase.ToLowerCamel(key) 98 | if key == strings.ToUpper(key) { 99 | keyCamelCase = strcase.ToLowerCamel(strings.ToLower(key)) 100 | } 101 | templatedName, err := values.AddSecret(false, nameCamelCase, keyCamelCase) 102 | if err != nil { 103 | return true, nil, fmt.Errorf("%w: unable add secret to values", err) 104 | } 105 | templatedData[key] = templatedName 106 | } 107 | if len(templatedData) != 0 { 108 | stringData, err = yamlformat.Marshal(map[string]interface{}{"stringData": templatedData}, 0) 109 | if err != nil { 110 | return true, nil, err 111 | } 112 | stringData = strings.ReplaceAll(stringData, "'", "") 113 | stringData = format.FixUnterminatedQuotes(stringData) 114 | } 115 | 116 | return true, &result{ 117 | name: name + ".yaml", 118 | data: struct { 119 | Type string 120 | Meta string 121 | Data string 122 | StringData string 123 | }{Type: secretType, Meta: meta, Data: data, StringData: stringData}, 124 | values: values, 125 | }, nil 126 | } 127 | 128 | type result struct { 129 | name string 130 | data struct { 131 | Type string 132 | Meta string 133 | Data string 134 | StringData string 135 | } 136 | values helmify.Values 137 | } 138 | 139 | func (r *result) Filename() string { 140 | return r.name 141 | } 142 | 143 | func (r *result) Values() helmify.Values { 144 | return r.values 145 | } 146 | 147 | func (r *result) Write(writer io.Writer) error { 148 | return secretTempl.Execute(writer, r.data) 149 | } 150 | -------------------------------------------------------------------------------- /pkg/processor/secret/secret_test.go: -------------------------------------------------------------------------------- 1 | package secret 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/arttor/helmify/pkg/metadata" 7 | 8 | "github.com/arttor/helmify/internal" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | const secretYaml = `apiVersion: v1 13 | data: 14 | VAR1: bXlfc2VjcmV0X3Zhcl8x 15 | VAR2: bXlfc2VjcmV0X3Zhcl8y 16 | stringData: 17 | VAR3: string secret 18 | kind: Secret 19 | metadata: 20 | name: my-operator-secret-vars 21 | namespace: my-operator-system 22 | type: opaque` 23 | 24 | func Test_secret_Process(t *testing.T) { 25 | var testInstance secret 26 | 27 | t.Run("processed", func(t *testing.T) { 28 | obj := internal.GenerateObj(secretYaml) 29 | processed, _, err := testInstance.Process(&metadata.Service{}, obj) 30 | assert.NoError(t, err) 31 | assert.Equal(t, true, processed) 32 | }) 33 | t.Run("skipped", func(t *testing.T) { 34 | obj := internal.TestNs 35 | processed, _, err := testInstance.Process(&metadata.Service{}, obj) 36 | assert.NoError(t, err) 37 | assert.Equal(t, false, processed) 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /pkg/processor/security-context/container_security_context.go: -------------------------------------------------------------------------------- 1 | package security_context 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/arttor/helmify/pkg/helmify" 7 | "github.com/iancoleman/strcase" 8 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 9 | ) 10 | 11 | const ( 12 | sc = "securityContext" 13 | cscValueName = "containerSecurityContext" 14 | helmTemplate = "{{- toYaml .Values.%[1]s.%[2]s.containerSecurityContext | nindent 10 }}" 15 | ) 16 | 17 | // ProcessContainerSecurityContext adds 'securityContext' to the podSpec in specMap, if it doesn't have one already defined. 18 | func ProcessContainerSecurityContext(nameCamel string, specMap map[string]interface{}, values *helmify.Values) error { 19 | err := processSecurityContext(nameCamel, "containers", specMap, values) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | err = processSecurityContext(nameCamel, "initContainers", specMap, values) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | return nil 30 | } 31 | 32 | func processSecurityContext(nameCamel string, containerType string, specMap map[string]interface{}, values *helmify.Values) error { 33 | if containers, defined := specMap[containerType]; defined { 34 | for _, container := range containers.([]interface{}) { 35 | castedContainer := container.(map[string]interface{}) 36 | containerName := strcase.ToLowerCamel(castedContainer["name"].(string)) 37 | if _, defined2 := castedContainer["securityContext"]; defined2 { 38 | err := setSecContextValue(nameCamel, containerName, castedContainer, values) 39 | if err != nil { 40 | return err 41 | } 42 | } 43 | } 44 | err := unstructured.SetNestedField(specMap, containers, containerType) 45 | if err != nil { 46 | return err 47 | } 48 | } 49 | return nil 50 | } 51 | 52 | func setSecContextValue(resourceName string, containerName string, castedContainer map[string]interface{}, values *helmify.Values) error { 53 | if castedContainer["securityContext"] != nil { 54 | err := unstructured.SetNestedField(*values, castedContainer["securityContext"], resourceName, containerName, cscValueName) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | valueString := fmt.Sprintf(helmTemplate, resourceName, containerName) 60 | 61 | err = unstructured.SetNestedField(castedContainer, valueString, sc) 62 | if err != nil { 63 | return err 64 | } 65 | } 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /pkg/processor/security-context/container_security_context_test.go: -------------------------------------------------------------------------------- 1 | package security_context 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/arttor/helmify/pkg/helmify" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestProcessContainerSecurityContext(t *testing.T) { 11 | type args struct { 12 | nameCamel string 13 | specMap map[string]interface{} 14 | values *helmify.Values 15 | } 16 | tests := []struct { 17 | name string 18 | args args 19 | want *helmify.Values 20 | }{ 21 | { 22 | name: "test with empty specMap", 23 | args: args{ 24 | nameCamel: "someResourceName", 25 | specMap: map[string]interface{}{}, 26 | values: &helmify.Values{}, 27 | }, 28 | want: &helmify.Values{}, 29 | }, 30 | { 31 | name: "test with single container", 32 | args: args{ 33 | nameCamel: "someResourceName", 34 | specMap: map[string]interface{}{ 35 | "containers": []interface{}{ 36 | map[string]interface{}{ 37 | "name": "SomeContainerName", 38 | "securityContext": map[string]interface{}{ 39 | "privileged": true, 40 | }, 41 | }, 42 | }, 43 | }, 44 | values: &helmify.Values{}, 45 | }, 46 | want: &helmify.Values{ 47 | "someResourceName": map[string]interface{}{ 48 | "someContainerName": map[string]interface{}{ 49 | "containerSecurityContext": map[string]interface{}{ 50 | "privileged": true, 51 | }, 52 | }, 53 | }, 54 | }, 55 | }, 56 | { 57 | name: "test with multiple containers", 58 | args: args{ 59 | nameCamel: "someResourceName", 60 | specMap: map[string]interface{}{ 61 | "containers": []interface{}{ 62 | map[string]interface{}{ 63 | "name": "FirstContainer", 64 | "securityContext": map[string]interface{}{ 65 | "privileged": true, 66 | }, 67 | }, 68 | map[string]interface{}{ 69 | "name": "SecondContainer", 70 | "securityContext": map[string]interface{}{ 71 | "allowPrivilegeEscalation": true, 72 | }, 73 | }, 74 | }, 75 | }, 76 | values: &helmify.Values{}, 77 | }, 78 | want: &helmify.Values{ 79 | "someResourceName": map[string]interface{}{ 80 | "firstContainer": map[string]interface{}{ 81 | "containerSecurityContext": map[string]interface{}{ 82 | "privileged": true, 83 | }, 84 | }, 85 | "secondContainer": map[string]interface{}{ 86 | "containerSecurityContext": map[string]interface{}{ 87 | "allowPrivilegeEscalation": true, 88 | }, 89 | }, 90 | }, 91 | }, 92 | }, 93 | } 94 | for _, tt := range tests { 95 | t.Run(tt.name, func(t *testing.T) { 96 | ProcessContainerSecurityContext(tt.args.nameCamel, tt.args.specMap, tt.args.values) 97 | assert.Equal(t, tt.want, tt.args.values) 98 | }) 99 | } 100 | } 101 | 102 | func Test_setSecContextValue(t *testing.T) { 103 | type args struct { 104 | resourceName string 105 | containerName string 106 | castedContainer map[string]interface{} 107 | values *helmify.Values 108 | fieldName string 109 | useRenderedHelmTemplate bool 110 | } 111 | tests := []struct { 112 | name string 113 | args args 114 | want *helmify.Values 115 | }{ 116 | { 117 | name: "simple test with single container and single value", 118 | args: args{ 119 | resourceName: "someResource", 120 | containerName: "someContainer", 121 | castedContainer: map[string]interface{}{ 122 | "securityContext": map[string]interface{}{ 123 | "someField": "someValue", 124 | }, 125 | }, 126 | values: &helmify.Values{}, 127 | fieldName: "someField", 128 | useRenderedHelmTemplate: false, 129 | }, 130 | want: &helmify.Values{ 131 | "someResource": map[string]interface{}{ 132 | "someContainer": map[string]interface{}{ 133 | "containerSecurityContext": map[string]interface{}{ 134 | "someField": "someValue", 135 | }, 136 | }, 137 | }, 138 | }, 139 | }, 140 | } 141 | for _, tt := range tests { 142 | t.Run(tt.name, func(t *testing.T) { 143 | setSecContextValue(tt.args.resourceName, tt.args.containerName, tt.args.castedContainer, tt.args.values) 144 | assert.Equal(t, tt.want, tt.args.values) 145 | }) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /pkg/processor/service/ingress.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "fmt" 5 | "github.com/arttor/helmify/pkg/helmify" 6 | "github.com/arttor/helmify/pkg/processor" 7 | yamlformat "github.com/arttor/helmify/pkg/yaml" 8 | "io" 9 | networkingv1 "k8s.io/api/networking/v1" 10 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 11 | "k8s.io/apimachinery/pkg/runtime" 12 | "k8s.io/apimachinery/pkg/runtime/schema" 13 | "text/template" 14 | ) 15 | 16 | var ingressTempl, _ = template.New("ingress").Parse( 17 | `{{ .Meta }} 18 | {{ .Spec }}`) 19 | 20 | var ingressGVC = schema.GroupVersionKind{ 21 | Group: "networking.k8s.io", 22 | Version: "v1", 23 | Kind: "Ingress", 24 | } 25 | 26 | // NewIngress creates processor for k8s Ingress resource. 27 | func NewIngress() helmify.Processor { 28 | return &ingress{} 29 | } 30 | 31 | type ingress struct{} 32 | 33 | // Process k8s Service object into template. Returns false if not capable of processing given resource type. 34 | func (r ingress) Process(appMeta helmify.AppMetadata, obj *unstructured.Unstructured) (bool, helmify.Template, error) { 35 | if obj.GroupVersionKind() != ingressGVC { 36 | return false, nil, nil 37 | } 38 | ing := networkingv1.Ingress{} 39 | err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &ing) 40 | if err != nil { 41 | return true, nil, fmt.Errorf("%w: unable to cast to ingress", err) 42 | } 43 | meta, err := processor.ProcessObjMeta(appMeta, obj) 44 | if err != nil { 45 | return true, nil, err 46 | } 47 | name := appMeta.TrimName(obj.GetName()) 48 | processIngressSpec(appMeta, &ing.Spec) 49 | spec, err := yamlformat.Marshal(map[string]interface{}{"spec": &ing.Spec}, 0) 50 | if err != nil { 51 | return true, nil, err 52 | } 53 | 54 | return true, &ingressResult{ 55 | name: name + ".yaml", 56 | data: struct { 57 | Meta string 58 | Spec string 59 | }{Meta: meta, Spec: spec}, 60 | }, nil 61 | } 62 | 63 | func processIngressSpec(appMeta helmify.AppMetadata, ing *networkingv1.IngressSpec) { 64 | if ing.DefaultBackend != nil && ing.DefaultBackend.Service != nil { 65 | ing.DefaultBackend.Service.Name = appMeta.TemplatedName(ing.DefaultBackend.Service.Name) 66 | } 67 | for i := range ing.Rules { 68 | if ing.Rules[i].IngressRuleValue.HTTP != nil { 69 | for j := range ing.Rules[i].IngressRuleValue.HTTP.Paths { 70 | if ing.Rules[i].IngressRuleValue.HTTP.Paths[j].Backend.Service != nil { 71 | ing.Rules[i].IngressRuleValue.HTTP.Paths[j].Backend.Service.Name = appMeta.TemplatedName(ing.Rules[i].IngressRuleValue.HTTP.Paths[j].Backend.Service.Name) 72 | } 73 | } 74 | } 75 | } 76 | } 77 | 78 | type ingressResult struct { 79 | name string 80 | data struct { 81 | Meta string 82 | Spec string 83 | } 84 | } 85 | 86 | func (r *ingressResult) Filename() string { 87 | return r.name 88 | } 89 | 90 | func (r *ingressResult) Values() helmify.Values { 91 | return helmify.Values{} 92 | } 93 | 94 | func (r *ingressResult) Write(writer io.Writer) error { 95 | return ingressTempl.Execute(writer, r.data) 96 | } 97 | -------------------------------------------------------------------------------- /pkg/processor/service/ingress_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/arttor/helmify/pkg/metadata" 7 | 8 | "github.com/arttor/helmify/internal" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | const ingressYaml = `apiVersion: networking.k8s.io/v1 13 | kind: Ingress 14 | metadata: 15 | name: myapp-ingress 16 | annotations: 17 | nginx.ingress.kubernetes.io/rewrite-target: / 18 | spec: 19 | rules: 20 | - http: 21 | paths: 22 | - path: /testpath 23 | pathType: Prefix 24 | backend: 25 | service: 26 | name: myapp-service 27 | port: 28 | number: 8443` 29 | 30 | func Test_ingress_Process(t *testing.T) { 31 | var testInstance ingress 32 | 33 | t.Run("processed", func(t *testing.T) { 34 | obj := internal.GenerateObj(ingressYaml) 35 | processed, _, err := testInstance.Process(&metadata.Service{}, obj) 36 | assert.NoError(t, err) 37 | assert.Equal(t, true, processed) 38 | }) 39 | t.Run("skipped", func(t *testing.T) { 40 | obj := internal.TestNs 41 | processed, _, err := testInstance.Process(&metadata.Service{}, obj) 42 | assert.NoError(t, err) 43 | assert.Equal(t, false, processed) 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /pkg/processor/service/service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "strings" 8 | 9 | "github.com/arttor/helmify/pkg/processor" 10 | 11 | "github.com/arttor/helmify/pkg/helmify" 12 | yamlformat "github.com/arttor/helmify/pkg/yaml" 13 | "github.com/iancoleman/strcase" 14 | corev1 "k8s.io/api/core/v1" 15 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 16 | "k8s.io/apimachinery/pkg/runtime" 17 | "k8s.io/apimachinery/pkg/runtime/schema" 18 | "k8s.io/apimachinery/pkg/util/intstr" 19 | "sigs.k8s.io/yaml" 20 | ) 21 | 22 | const ( 23 | svcTempSpec = ` 24 | spec: 25 | type: {{ .Values.%[1]s.type }} 26 | selector: 27 | %[2]s 28 | {{- include "%[3]s.selectorLabels" . | nindent 4 }} 29 | ports: 30 | {{- .Values.%[1]s.ports | toYaml | nindent 2 }}` 31 | ) 32 | 33 | const ( 34 | lbSourceRangesTempSpec = ` 35 | loadBalancerSourceRanges: 36 | {{- .Values.%[1]s.loadBalancerSourceRanges | toYaml | nindent 2 }}` 37 | ) 38 | 39 | var svcGVC = schema.GroupVersionKind{ 40 | Group: "", 41 | Version: "v1", 42 | Kind: "Service", 43 | } 44 | 45 | // New creates processor for k8s Service resource. 46 | func New() helmify.Processor { 47 | return &svc{} 48 | } 49 | 50 | type svc struct{} 51 | 52 | // Process k8s Service object into template. Returns false if not capable of processing given resource type. 53 | func (r svc) Process(appMeta helmify.AppMetadata, obj *unstructured.Unstructured) (bool, helmify.Template, error) { 54 | if obj.GroupVersionKind() != svcGVC { 55 | return false, nil, nil 56 | } 57 | service := corev1.Service{} 58 | err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &service) 59 | if err != nil { 60 | return true, nil, fmt.Errorf("%w: unable to cast to service", err) 61 | } 62 | 63 | meta, err := processor.ProcessObjMeta(appMeta, obj) 64 | if err != nil { 65 | return true, nil, err 66 | } 67 | 68 | name := appMeta.TrimName(obj.GetName()) 69 | shortName := strings.TrimPrefix(name, "controller-manager-") 70 | shortNameCamel := strcase.ToLowerCamel(shortName) 71 | 72 | selector, _ := yaml.Marshal(service.Spec.Selector) 73 | selector = yamlformat.Indent(selector, 4) 74 | selector = bytes.TrimRight(selector, "\n ") 75 | 76 | values := helmify.Values{} 77 | svcType := service.Spec.Type 78 | if svcType == "" { 79 | svcType = corev1.ServiceTypeClusterIP 80 | } 81 | _ = unstructured.SetNestedField(values, string(svcType), shortNameCamel, "type") 82 | ports := make([]interface{}, len(service.Spec.Ports)) 83 | for i, p := range service.Spec.Ports { 84 | pMap := map[string]interface{}{ 85 | "port": int64(p.Port), 86 | } 87 | if p.Name != "" { 88 | pMap["name"] = p.Name 89 | } 90 | if p.NodePort != 0 { 91 | pMap["nodePort"] = int64(p.NodePort) 92 | } 93 | if p.Protocol != "" { 94 | pMap["protocol"] = string(p.Protocol) 95 | } 96 | if p.TargetPort.Type == intstr.Int { 97 | pMap["targetPort"] = int64(p.TargetPort.IntVal) 98 | } else { 99 | pMap["targetPort"] = p.TargetPort.StrVal 100 | } 101 | ports[i] = pMap 102 | } 103 | 104 | _ = unstructured.SetNestedSlice(values, ports, shortNameCamel, "ports") 105 | res := meta + fmt.Sprintf(svcTempSpec, shortNameCamel, selector, appMeta.ChartName()) 106 | 107 | res += parseLoadBalancerSourceRanges(values, service, shortNameCamel) 108 | 109 | if shortNameCamel == "webhookService" && appMeta.Config().AddWebhookOption { 110 | res = fmt.Sprintf("{{- if .Values.webhook.enabled }}\n%s\n{{- end }}", res) 111 | } 112 | return true, &result{ 113 | name: shortName, 114 | data: res, 115 | values: values, 116 | }, nil 117 | } 118 | 119 | func parseLoadBalancerSourceRanges(values helmify.Values, service corev1.Service, shortNameCamel string) string { 120 | if len(service.Spec.LoadBalancerSourceRanges) < 1 { 121 | return "" 122 | } 123 | lbSourceRanges := make([]interface{}, len(service.Spec.LoadBalancerSourceRanges)) 124 | for i, ip := range service.Spec.LoadBalancerSourceRanges { 125 | lbSourceRanges[i] = ip 126 | } 127 | _ = unstructured.SetNestedSlice(values, lbSourceRanges, shortNameCamel, "loadBalancerSourceRanges") 128 | return fmt.Sprintf(lbSourceRangesTempSpec, shortNameCamel) 129 | } 130 | 131 | type result struct { 132 | name string 133 | data string 134 | values helmify.Values 135 | } 136 | 137 | func (r *result) Filename() string { 138 | return r.name + ".yaml" 139 | } 140 | 141 | func (r *result) Values() helmify.Values { 142 | return r.values 143 | } 144 | 145 | func (r *result) Write(writer io.Writer) error { 146 | _, err := writer.Write([]byte(r.data)) 147 | return err 148 | } 149 | -------------------------------------------------------------------------------- /pkg/processor/service/service_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/arttor/helmify/pkg/metadata" 7 | 8 | "github.com/arttor/helmify/internal" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | const svcYaml = `apiVersion: v1 13 | kind: Service 14 | metadata: 15 | labels: 16 | control-plane: controller-manager 17 | name: my-operator-controller-manager-metrics-service 18 | namespace: my-operator-system 19 | spec: 20 | ports: 21 | - name: https 22 | port: 8443 23 | targetPort: https 24 | selector: 25 | control-plane: controller-manager` 26 | 27 | func Test_svc_Process(t *testing.T) { 28 | var testInstance svc 29 | 30 | t.Run("processed", func(t *testing.T) { 31 | obj := internal.GenerateObj(svcYaml) 32 | processed, _, err := testInstance.Process(&metadata.Service{}, obj) 33 | assert.NoError(t, err) 34 | assert.Equal(t, true, processed) 35 | }) 36 | t.Run("skipped", func(t *testing.T) { 37 | obj := internal.TestNs 38 | processed, _, err := testInstance.Process(&metadata.Service{}, obj) 39 | assert.NoError(t, err) 40 | assert.Equal(t, false, processed) 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /pkg/processor/statefulset/statefulset.go: -------------------------------------------------------------------------------- 1 | package statefulset 2 | 3 | import ( 4 | "fmt" 5 | "github.com/arttor/helmify/pkg/processor/pod" 6 | "io" 7 | "strings" 8 | "text/template" 9 | 10 | "github.com/arttor/helmify/pkg/helmify" 11 | "github.com/arttor/helmify/pkg/processor" 12 | yamlformat "github.com/arttor/helmify/pkg/yaml" 13 | "github.com/iancoleman/strcase" 14 | appsv1 "k8s.io/api/apps/v1" 15 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 16 | "k8s.io/apimachinery/pkg/runtime" 17 | "k8s.io/apimachinery/pkg/runtime/schema" 18 | ) 19 | 20 | var statefulsetGVC = schema.GroupVersionKind{ 21 | Group: "apps", 22 | Version: "v1", 23 | Kind: "StatefulSet", 24 | } 25 | 26 | var statefulsetTempl, _ = template.New("statefulset").Parse( 27 | `{{- .Meta }} 28 | spec: 29 | {{ .Spec }}`) 30 | 31 | // New creates processor for k8s StatefulSet resource. 32 | func New() helmify.Processor { 33 | return &statefulset{} 34 | } 35 | 36 | type statefulset struct{} 37 | 38 | // Process k8s StatefulSet object into template. Returns false if not capable of processing given resource type. 39 | func (d statefulset) Process(appMeta helmify.AppMetadata, obj *unstructured.Unstructured) (bool, helmify.Template, error) { 40 | if obj.GroupVersionKind() != statefulsetGVC { 41 | return false, nil, nil 42 | } 43 | ss := appsv1.StatefulSet{} 44 | err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &ss) 45 | if err != nil { 46 | return true, nil, fmt.Errorf("%w: unable to cast to StatefulSet", err) 47 | } 48 | meta, err := processor.ProcessObjMeta(appMeta, obj) 49 | if err != nil { 50 | return true, nil, err 51 | } 52 | 53 | ssSpec := ss.Spec 54 | ssSpecMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&ssSpec) 55 | if err != nil { 56 | return true, nil, err 57 | } 58 | delete((ssSpecMap["template"].(map[string]interface{}))["metadata"].(map[string]interface{}), "creationTimestamp") 59 | 60 | values := helmify.Values{} 61 | 62 | name := appMeta.TrimName(obj.GetName()) 63 | nameCamel := strcase.ToLowerCamel(name) 64 | 65 | if ssSpec.ServiceName != "" { 66 | servName := appMeta.TemplatedName(ssSpec.ServiceName) 67 | ssSpecMap["serviceName"] = servName 68 | } 69 | 70 | if ssSpec.Replicas != nil { 71 | repl, err := values.Add(*ssSpec.Replicas, nameCamel, "replicas") 72 | if err != nil { 73 | return true, nil, err 74 | } 75 | ssSpecMap["replicas"] = repl 76 | } 77 | 78 | for i, claim := range ssSpec.VolumeClaimTemplates { 79 | volName := claim.ObjectMeta.Name 80 | delete(((ssSpecMap["volumeClaimTemplates"].([]interface{}))[i]).(map[string]interface{}), "status") 81 | if claim.Spec.StorageClassName != nil { 82 | scName := appMeta.TemplatedName(*claim.Spec.StorageClassName) 83 | err = unstructured.SetNestedField(((ssSpecMap["volumeClaimTemplates"].([]interface{}))[i]).(map[string]interface{}), scName, "spec", "storageClassName") 84 | if err != nil { 85 | return true, nil, err 86 | } 87 | } 88 | if claim.Spec.VolumeName != "" { 89 | vName := appMeta.TemplatedName(claim.Spec.VolumeName) 90 | err = unstructured.SetNestedField(((ssSpecMap["volumeClaimTemplates"].([]interface{}))[i]).(map[string]interface{}), vName, "spec", "volumeName") 91 | if err != nil { 92 | return true, nil, err 93 | } 94 | } 95 | 96 | resMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&claim.Spec.Resources) 97 | if err != nil { 98 | return true, nil, err 99 | } 100 | resName, err := values.AddYaml(resMap, 8, true, nameCamel, "volumeClaims", volName) 101 | if err != nil { 102 | return true, nil, err 103 | } 104 | err = unstructured.SetNestedField(((ssSpecMap["volumeClaimTemplates"].([]interface{}))[i]).(map[string]interface{}), resName, "spec", "resources") 105 | if err != nil { 106 | return true, nil, err 107 | } 108 | } 109 | 110 | // process pod spec: 111 | podSpecMap, podValues, err := pod.ProcessSpec(nameCamel, appMeta, ssSpec.Template.Spec) 112 | if err != nil { 113 | return true, nil, err 114 | } 115 | err = values.Merge(podValues) 116 | if err != nil { 117 | return true, nil, err 118 | } 119 | err = unstructured.SetNestedMap(ssSpecMap, podSpecMap, "template", "spec") 120 | if err != nil { 121 | return true, nil, err 122 | } 123 | 124 | spec, err := yamlformat.Marshal(ssSpecMap, 2) 125 | if err != nil { 126 | return true, nil, err 127 | } 128 | spec = strings.ReplaceAll(spec, "'", "") 129 | 130 | return true, &result{ 131 | values: values, 132 | data: struct { 133 | Meta string 134 | Spec string 135 | }{ 136 | Meta: meta, 137 | Spec: spec, 138 | }, 139 | }, nil 140 | } 141 | 142 | type result struct { 143 | data struct { 144 | Meta string 145 | Spec string 146 | } 147 | values helmify.Values 148 | } 149 | 150 | func (r *result) Filename() string { 151 | return "statefulset.yaml" 152 | } 153 | 154 | func (r *result) Values() helmify.Values { 155 | return r.values 156 | } 157 | 158 | func (r *result) Write(writer io.Writer) error { 159 | return statefulsetTempl.Execute(writer, r.data) 160 | } 161 | -------------------------------------------------------------------------------- /pkg/processor/storage/pvc.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "fmt" 5 | "github.com/arttor/helmify/pkg/helmify" 6 | "github.com/arttor/helmify/pkg/processor" 7 | yamlformat "github.com/arttor/helmify/pkg/yaml" 8 | "github.com/iancoleman/strcase" 9 | "io" 10 | corev1 "k8s.io/api/core/v1" 11 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 12 | "k8s.io/apimachinery/pkg/runtime" 13 | "k8s.io/apimachinery/pkg/runtime/schema" 14 | "strings" 15 | "text/template" 16 | ) 17 | 18 | var pvcTempl, _ = template.New("pvc").Parse( 19 | `{{ .Meta }} 20 | {{ .Spec }}`) 21 | 22 | var pvcGVC = schema.GroupVersionKind{ 23 | Group: "", 24 | Version: "v1", 25 | Kind: "PersistentVolumeClaim", 26 | } 27 | 28 | // New creates processor for k8s PVC resource. 29 | func New() helmify.Processor { 30 | return &pvc{} 31 | } 32 | 33 | type pvc struct{} 34 | 35 | // Process k8s PVC object into template. Returns false if not capable of processing given resource type. 36 | func (p pvc) Process(appMeta helmify.AppMetadata, obj *unstructured.Unstructured) (bool, helmify.Template, error) { 37 | if obj.GroupVersionKind() != pvcGVC { 38 | return false, nil, nil 39 | } 40 | meta, err := processor.ProcessObjMeta(appMeta, obj) 41 | if err != nil { 42 | return true, nil, err 43 | } 44 | 45 | name := appMeta.TrimName(obj.GetName()) 46 | nameCamelCase := strcase.ToLowerCamel(name) 47 | values := helmify.Values{} 48 | 49 | claim := corev1.PersistentVolumeClaim{} 50 | err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &claim) 51 | if err != nil { 52 | return true, nil, fmt.Errorf("%w: unable to cast to PVC", err) 53 | } 54 | 55 | // template storage class name 56 | if claim.Spec.StorageClassName != nil { 57 | templatedSC, err := values.Add(*claim.Spec.StorageClassName, "pvc", nameCamelCase, "storageClass") 58 | if err != nil { 59 | return true, nil, err 60 | } 61 | claim.Spec.StorageClassName = &templatedSC 62 | } 63 | 64 | // template resources 65 | specMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&claim.Spec) 66 | if err != nil { 67 | return true, nil, err 68 | } 69 | 70 | storageReq, ok, _ := unstructured.NestedString(specMap, "resources", "requests", "storage") 71 | if ok { 72 | templatedStorageReq, err := values.Add(storageReq, "pvc", nameCamelCase, "storageRequest") 73 | if err != nil { 74 | return true, nil, err 75 | } 76 | err = unstructured.SetNestedField(specMap, templatedStorageReq, "resources", "requests", "storage") 77 | if err != nil { 78 | return true, nil, err 79 | } 80 | } 81 | 82 | storageLim, ok, _ := unstructured.NestedString(specMap, "resources", "limits", "storage") 83 | if ok { 84 | templatedStorageLim, err := values.Add(storageLim, "pvc", nameCamelCase, "storageLimit") 85 | if err != nil { 86 | return true, nil, err 87 | } 88 | err = unstructured.SetNestedField(specMap, templatedStorageLim, "resources", "limits", "storage") 89 | if err != nil { 90 | return true, nil, err 91 | } 92 | } 93 | 94 | spec, err := yamlformat.Marshal(map[string]interface{}{"spec": specMap}, 0) 95 | if err != nil { 96 | return true, nil, err 97 | } 98 | spec = strings.ReplaceAll(spec, "'", "") 99 | 100 | return true, &result{ 101 | name: name + ".yaml", 102 | data: struct { 103 | Meta string 104 | Spec string 105 | }{Meta: meta, Spec: spec}, 106 | values: values, 107 | }, nil 108 | } 109 | 110 | type result struct { 111 | name string 112 | data struct { 113 | Meta string 114 | Spec string 115 | } 116 | values helmify.Values 117 | } 118 | 119 | func (r *result) Filename() string { 120 | return r.name 121 | } 122 | 123 | func (r *result) Values() helmify.Values { 124 | return r.values 125 | } 126 | 127 | func (r *result) Write(writer io.Writer) error { 128 | return pvcTempl.Execute(writer, r.data) 129 | } 130 | -------------------------------------------------------------------------------- /pkg/processor/storage/pvc_test.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/arttor/helmify/pkg/metadata" 7 | 8 | "github.com/arttor/helmify/internal" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | const pvcYaml = `apiVersion: v1 13 | kind: PersistentVolumeClaim 14 | metadata: 15 | name: task-pv-claim 16 | spec: 17 | storageClassName: manual 18 | accessModes: 19 | - ReadWriteOnce 20 | resources: 21 | requests: 22 | storage: 3Gi 23 | limits: 24 | storage: 5Gi` 25 | 26 | func Test_PVC_Process(t *testing.T) { 27 | var testInstance pvc 28 | 29 | t.Run("processed", func(t *testing.T) { 30 | obj := internal.GenerateObj(pvcYaml) 31 | processed, _, err := testInstance.Process(&metadata.Service{}, obj) 32 | assert.NoError(t, err) 33 | assert.Equal(t, true, processed) 34 | }) 35 | t.Run("skipped", func(t *testing.T) { 36 | obj := internal.TestNs 37 | processed, _, err := testInstance.Process(&metadata.Service{}, obj) 38 | assert.NoError(t, err) 39 | assert.Equal(t, false, processed) 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /pkg/processor/webhook/cert.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "strings" 8 | 9 | "github.com/arttor/helmify/pkg/cluster" 10 | "github.com/arttor/helmify/pkg/helmify" 11 | yamlformat "github.com/arttor/helmify/pkg/yaml" 12 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 13 | "k8s.io/apimachinery/pkg/runtime/schema" 14 | "sigs.k8s.io/yaml" 15 | ) 16 | 17 | const ( 18 | WebhookHeader = `{{- if .Values.webhook.enabled }}` 19 | WebhookFooter = `{{- end }}` 20 | certTempl = `apiVersion: cert-manager.io/v1 21 | kind: Certificate 22 | metadata: 23 | name: {{ include "%[1]s.fullname" . }}-%[2]s 24 | labels: 25 | {{- include "%[1]s.labels" . | nindent 4 }} 26 | spec: 27 | %[3]s` 28 | certTemplWithAnno = `apiVersion: cert-manager.io/v1 29 | kind: Certificate 30 | metadata: 31 | name: {{ include "%[1]s.fullname" . }}-%[2]s 32 | annotations: 33 | "helm.sh/hook": post-install,post-upgrade 34 | "helm.sh/hook-weight": "2" 35 | labels: 36 | {{- include "%[1]s.labels" . | nindent 4 }} 37 | spec: 38 | %[3]s` 39 | ) 40 | 41 | var certGVC = schema.GroupVersionKind{ 42 | Group: "cert-manager.io", 43 | Version: "v1", 44 | Kind: "Certificate", 45 | } 46 | 47 | // Certificate creates processor for k8s Certificate resource. 48 | func Certificate() helmify.Processor { 49 | return &cert{} 50 | } 51 | 52 | type cert struct{} 53 | 54 | // Process k8s Certificate object into template. Returns false if not capable of processing given resource type. 55 | func (c cert) Process(appMeta helmify.AppMetadata, obj *unstructured.Unstructured) (bool, helmify.Template, error) { 56 | if obj.GroupVersionKind() != certGVC { 57 | return false, nil, nil 58 | } 59 | name := appMeta.TrimName(obj.GetName()) 60 | 61 | dnsNames, _, err := unstructured.NestedSlice(obj.Object, "spec", "dnsNames") 62 | if err != nil { 63 | return true, nil, fmt.Errorf("%w: unable get cert dnsNames", err) 64 | } 65 | 66 | processedDnsNames := []interface{}{} 67 | for _, dnsName := range dnsNames { 68 | dns := dnsName.(string) 69 | templatedDns := appMeta.TemplatedString(dns) 70 | processedDns := strings.ReplaceAll(templatedDns, appMeta.Namespace(), "{{ .Release.Namespace }}") 71 | processedDns = strings.ReplaceAll(processedDns, cluster.DefaultDomain, fmt.Sprintf("{{ .Values.%s }}", cluster.DomainKey)) 72 | processedDnsNames = append(processedDnsNames, processedDns) 73 | } 74 | err = unstructured.SetNestedSlice(obj.Object, processedDnsNames, "spec", "dnsNames") 75 | if err != nil { 76 | return true, nil, fmt.Errorf("%w: unable set cert dnsNames", err) 77 | } 78 | 79 | issName, _, err := unstructured.NestedString(obj.Object, "spec", "issuerRef", "name") 80 | if err != nil { 81 | return true, nil, fmt.Errorf("%w: unable get cert issuerRef", err) 82 | } 83 | issName = appMeta.TemplatedName(issName) 84 | err = unstructured.SetNestedField(obj.Object, issName, "spec", "issuerRef", "name") 85 | if err != nil { 86 | return true, nil, fmt.Errorf("%w: unable set cert issuerRef", err) 87 | } 88 | spec, _ := yaml.Marshal(obj.Object["spec"]) 89 | spec = yamlformat.Indent(spec, 2) 90 | spec = bytes.TrimRight(spec, "\n ") 91 | tmpl := "" 92 | if appMeta.Config().CertManagerAsSubchart { 93 | tmpl = certTemplWithAnno 94 | } else { 95 | tmpl = certTempl 96 | } 97 | values := helmify.Values{} 98 | if appMeta.Config().AddWebhookOption { 99 | // Add webhook.enabled value to values.yaml 100 | _, _ = values.Add(true, "webhook", "enabled") 101 | 102 | tmpl = fmt.Sprintf("%s\n%s\n%s", WebhookHeader, tmpl, WebhookFooter) 103 | } 104 | res := fmt.Sprintf(tmpl, appMeta.ChartName(), name, string(spec)) 105 | return true, &certResult{ 106 | name: name, 107 | data: []byte(res), 108 | values: values, 109 | }, nil 110 | } 111 | 112 | type certResult struct { 113 | name string 114 | data []byte 115 | values helmify.Values 116 | } 117 | 118 | func (r *certResult) Filename() string { 119 | return r.name + ".yaml" 120 | } 121 | 122 | func (r *certResult) Values() helmify.Values { 123 | return r.values 124 | } 125 | 126 | func (r *certResult) Write(writer io.Writer) error { 127 | _, err := writer.Write(r.data) 128 | return err 129 | } 130 | -------------------------------------------------------------------------------- /pkg/processor/webhook/cert_test.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/arttor/helmify/pkg/metadata" 7 | 8 | "github.com/arttor/helmify/internal" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | const certYaml = `apiVersion: cert-manager.io/v1 13 | kind: Certificate 14 | metadata: 15 | name: my-operator-serving-cert 16 | namespace: my-operator-system 17 | spec: 18 | dnsNames: 19 | - my-operator-webhook-service.my-operator-system.svc 20 | - my-operator-webhook-service.my-operator-system.svc.cluster.local 21 | issuerRef: 22 | kind: Issuer 23 | name: my-operator-selfsigned-issuer 24 | secretName: webhook-server-cert` 25 | 26 | func Test_cert_Process(t *testing.T) { 27 | var testInstance cert 28 | 29 | t.Run("processed", func(t *testing.T) { 30 | obj := internal.GenerateObj(certYaml) 31 | processed, _, err := testInstance.Process(&metadata.Service{}, obj) 32 | assert.NoError(t, err) 33 | assert.Equal(t, true, processed) 34 | }) 35 | t.Run("skipped", func(t *testing.T) { 36 | obj := internal.TestNs 37 | processed, _, err := testInstance.Process(&metadata.Service{}, obj) 38 | assert.NoError(t, err) 39 | assert.Equal(t, false, processed) 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /pkg/processor/webhook/issuer.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | 8 | "github.com/arttor/helmify/pkg/helmify" 9 | yamlformat "github.com/arttor/helmify/pkg/yaml" 10 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 11 | "k8s.io/apimachinery/pkg/runtime/schema" 12 | "sigs.k8s.io/yaml" 13 | ) 14 | 15 | const ( 16 | issuerTempl = `apiVersion: cert-manager.io/v1 17 | kind: Issuer 18 | metadata: 19 | name: {{ include "%[1]s.fullname" . }}-%[2]s 20 | labels: 21 | {{- include "%[1]s.labels" . | nindent 4 }} 22 | spec: 23 | %[3]s` 24 | issuerTemplWithAnno = `apiVersion: cert-manager.io/v1 25 | kind: Issuer 26 | metadata: 27 | name: {{ include "%[1]s.fullname" . }}-%[2]s 28 | annotations: 29 | "helm.sh/hook": post-install,post-upgrade 30 | "helm.sh/hook-weight": "1" 31 | labels: 32 | {{- include "%[1]s.labels" . | nindent 4 }} 33 | spec: 34 | %[3]s` 35 | ) 36 | 37 | var issuerGVC = schema.GroupVersionKind{ 38 | Group: "cert-manager.io", 39 | Version: "v1", 40 | Kind: "Issuer", 41 | } 42 | 43 | // Issuer creates processor for k8s Issuer resource. 44 | func Issuer() helmify.Processor { 45 | return &issuer{} 46 | } 47 | 48 | type issuer struct{} 49 | 50 | // Process k8s Issuer object into template. Returns false if not capable of processing given resource type. 51 | func (i issuer) Process(appMeta helmify.AppMetadata, obj *unstructured.Unstructured) (bool, helmify.Template, error) { 52 | if obj.GroupVersionKind() != issuerGVC { 53 | return false, nil, nil 54 | } 55 | name := appMeta.TrimName(obj.GetName()) 56 | 57 | spec, _ := yaml.Marshal(obj.Object["spec"]) 58 | spec = yamlformat.Indent(spec, 2) 59 | spec = bytes.TrimRight(spec, "\n ") 60 | tmpl := "" 61 | if appMeta.Config().CertManagerAsSubchart { 62 | tmpl = issuerTemplWithAnno 63 | } else { 64 | tmpl = issuerTempl 65 | } 66 | values := helmify.Values{} 67 | if appMeta.Config().AddWebhookOption { 68 | // Add webhook.enabled value to values.yaml 69 | _, _ = values.Add(true, "webhook", "enabled") 70 | 71 | tmpl = fmt.Sprintf("%s\n%s\n%s", WebhookHeader, tmpl, WebhookFooter) 72 | } 73 | res := fmt.Sprintf(tmpl, appMeta.ChartName(), name, string(spec)) 74 | return true, &issResult{ 75 | name: name, 76 | data: []byte(res), 77 | }, nil 78 | } 79 | 80 | type issResult struct { 81 | name string 82 | data []byte 83 | values helmify.Values 84 | } 85 | 86 | func (r *issResult) Filename() string { 87 | return r.name + ".yaml" 88 | } 89 | 90 | func (r *issResult) Values() helmify.Values { 91 | return r.values 92 | } 93 | 94 | func (r *issResult) Write(writer io.Writer) error { 95 | _, err := writer.Write(r.data) 96 | return err 97 | } 98 | -------------------------------------------------------------------------------- /pkg/processor/webhook/issuer_test.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/arttor/helmify/pkg/metadata" 7 | 8 | "github.com/arttor/helmify/internal" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | const issuerYaml = `apiVersion: cert-manager.io/v1 13 | kind: Issuer 14 | metadata: 15 | name: my-operator-selfsigned-issuer 16 | namespace: my-operator-system 17 | spec: 18 | selfSigned: {}` 19 | 20 | func Test_issuer_Process(t *testing.T) { 21 | var testInstance issuer 22 | 23 | t.Run("processed", func(t *testing.T) { 24 | obj := internal.GenerateObj(issuerYaml) 25 | processed, _, err := testInstance.Process(&metadata.Service{}, obj) 26 | assert.NoError(t, err) 27 | assert.Equal(t, true, processed) 28 | }) 29 | t.Run("skipped", func(t *testing.T) { 30 | obj := internal.TestNs 31 | processed, _, err := testInstance.Process(&metadata.Service{}, obj) 32 | assert.NoError(t, err) 33 | assert.Equal(t, false, processed) 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /pkg/processor/webhook/mutating.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "strings" 8 | 9 | "github.com/arttor/helmify/pkg/helmify" 10 | v1 "k8s.io/api/admissionregistration/v1" 11 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 12 | "k8s.io/apimachinery/pkg/runtime" 13 | "k8s.io/apimachinery/pkg/runtime/schema" 14 | "sigs.k8s.io/yaml" 15 | ) 16 | 17 | const ( 18 | mwhTempl = `apiVersion: admissionregistration.k8s.io/v1 19 | kind: MutatingWebhookConfiguration 20 | metadata: 21 | name: {{ include "%[1]s.fullname" . }}-%[2]s 22 | annotations: 23 | cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ include "%[1]s.fullname" . }}-%[3]s 24 | labels: 25 | {{- include "%[1]s.labels" . | nindent 4 }} 26 | webhooks: 27 | %[4]s` 28 | ) 29 | 30 | var mwhGVK = schema.GroupVersionKind{ 31 | Group: "admissionregistration.k8s.io", 32 | Version: "v1", 33 | Kind: "MutatingWebhookConfiguration", 34 | } 35 | 36 | // MutatingWebhook creates processor for k8s MutatingWebhookConfiguration resource. 37 | func MutatingWebhook() helmify.Processor { 38 | return &mwh{} 39 | } 40 | 41 | type mwh struct{} 42 | 43 | // Process k8s MutatingWebhookConfiguration object into template. Returns false if not capable of processing given resource type. 44 | func (w mwh) Process(appMeta helmify.AppMetadata, obj *unstructured.Unstructured) (bool, helmify.Template, error) { 45 | if obj.GroupVersionKind() != mwhGVK { 46 | return false, nil, nil 47 | } 48 | name := appMeta.TrimName(obj.GetName()) 49 | 50 | whConf := v1.MutatingWebhookConfiguration{} 51 | err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &whConf) 52 | if err != nil { 53 | return true, nil, fmt.Errorf("%w: unable to cast to MutatingWebhookConfiguration", err) 54 | } 55 | for i, whc := range whConf.Webhooks { 56 | whc.ClientConfig.Service.Name = appMeta.TemplatedName(whc.ClientConfig.Service.Name) 57 | whc.ClientConfig.Service.Namespace = strings.ReplaceAll(whc.ClientConfig.Service.Namespace, appMeta.Namespace(), `{{ .Release.Namespace }}`) 58 | whConf.Webhooks[i] = whc 59 | } 60 | webhooks, _ := yaml.Marshal(whConf.Webhooks) 61 | webhooks = bytes.TrimRight(webhooks, "\n ") 62 | certName, _, err := unstructured.NestedString(obj.Object, "metadata", "annotations", "cert-manager.io/inject-ca-from") 63 | if err != nil { 64 | return true, nil, fmt.Errorf("%w: unable get webhook certName", err) 65 | } 66 | certName = strings.TrimPrefix(certName, appMeta.Namespace()+"/") 67 | certName = appMeta.TrimName(certName) 68 | tmpl := mwhTempl 69 | values := helmify.Values{} 70 | if appMeta.Config().AddWebhookOption { 71 | // Add webhook.enabled value to values.yaml 72 | _, _ = values.Add(true, "webhook", "enabled") 73 | 74 | tmpl = fmt.Sprintf("%s\n%s\n%s", WebhookHeader, mwhTempl, WebhookFooter) 75 | } 76 | res := fmt.Sprintf(tmpl, appMeta.ChartName(), name, certName, string(webhooks)) 77 | return true, &mwhResult{ 78 | name: name, 79 | data: []byte(res), 80 | }, nil 81 | } 82 | 83 | type mwhResult struct { 84 | name string 85 | data []byte 86 | values helmify.Values 87 | } 88 | 89 | func (r *mwhResult) Filename() string { 90 | return r.name + ".yaml" 91 | } 92 | 93 | func (r *mwhResult) Values() helmify.Values { 94 | return r.values 95 | } 96 | 97 | func (r *mwhResult) Write(writer io.Writer) error { 98 | _, err := writer.Write(r.data) 99 | return err 100 | } 101 | -------------------------------------------------------------------------------- /pkg/processor/webhook/mutating_test.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/arttor/helmify/pkg/metadata" 7 | 8 | "github.com/arttor/helmify/internal" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | const mwhYaml = `apiVersion: admissionregistration.k8s.io/v1 13 | kind: MutatingWebhookConfiguration 14 | metadata: 15 | annotations: 16 | cert-manager.io/inject-ca-from: my-operator-system/my-operator-serving-cert 17 | name: my-operator-mutating-webhook-configuration 18 | webhooks: 19 | - admissionReviewVersions: 20 | - v1 21 | - v1beta1 22 | clientConfig: 23 | service: 24 | name: my-operator-webhook-service 25 | namespace: my-operator-system 26 | path: /mutate-ceph-example-com-v1alpha1-volume 27 | failurePolicy: Fail 28 | name: vvolume.kb.io 29 | rules: 30 | - apiGroups: 31 | - test.example.com 32 | apiVersions: 33 | - v1alpha1 34 | operations: 35 | - CREATE 36 | - UPDATE 37 | resources: 38 | - volumes 39 | sideEffects: None` 40 | 41 | func Test_mwh_Process(t *testing.T) { 42 | var testInstance mwh 43 | 44 | t.Run("processed", func(t *testing.T) { 45 | obj := internal.GenerateObj(mwhYaml) 46 | processed, _, err := testInstance.Process(&metadata.Service{}, obj) 47 | assert.NoError(t, err) 48 | assert.Equal(t, true, processed) 49 | }) 50 | t.Run("skipped", func(t *testing.T) { 51 | obj := internal.TestNs 52 | processed, _, err := testInstance.Process(&metadata.Service{}, obj) 53 | assert.NoError(t, err) 54 | assert.Equal(t, false, processed) 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /pkg/processor/webhook/validating.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "strings" 8 | 9 | "github.com/arttor/helmify/pkg/helmify" 10 | v1 "k8s.io/api/admissionregistration/v1" 11 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 12 | "k8s.io/apimachinery/pkg/runtime" 13 | "k8s.io/apimachinery/pkg/runtime/schema" 14 | "sigs.k8s.io/yaml" 15 | ) 16 | 17 | const ( 18 | vwhTempl = `apiVersion: admissionregistration.k8s.io/v1 19 | kind: ValidatingWebhookConfiguration 20 | metadata: 21 | name: {{ include "%[1]s.fullname" . }}-%[2]s 22 | annotations: 23 | cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ include "%[1]s.fullname" . }}-%[3]s 24 | labels: 25 | {{- include "%[1]s.labels" . | nindent 4 }} 26 | webhooks: 27 | %[4]s` 28 | ) 29 | 30 | var vwhGVK = schema.GroupVersionKind{ 31 | Group: "admissionregistration.k8s.io", 32 | Version: "v1", 33 | Kind: "ValidatingWebhookConfiguration", 34 | } 35 | 36 | // ValidatingWebhook creates processor for k8s ValidatingWebhookConfiguration resource. 37 | func ValidatingWebhook() helmify.Processor { 38 | return &vwh{} 39 | } 40 | 41 | type vwh struct{} 42 | 43 | // Process k8s ValidatingWebhookConfiguration object into template. Returns false if not capable of processing given resource type. 44 | func (w vwh) Process(appMeta helmify.AppMetadata, obj *unstructured.Unstructured) (bool, helmify.Template, error) { 45 | if obj.GroupVersionKind() != vwhGVK { 46 | return false, nil, nil 47 | } 48 | name := appMeta.TrimName(obj.GetName()) 49 | 50 | whConf := v1.ValidatingWebhookConfiguration{} 51 | err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &whConf) 52 | if err != nil { 53 | return true, nil, fmt.Errorf("%w: unable to cast to ValidatingWebhookConfiguration", err) 54 | } 55 | for i, whc := range whConf.Webhooks { 56 | whc.ClientConfig.Service.Name = appMeta.TemplatedName(whc.ClientConfig.Service.Name) 57 | whc.ClientConfig.Service.Namespace = strings.ReplaceAll(whc.ClientConfig.Service.Namespace, appMeta.Namespace(), `{{ .Release.Namespace }}`) 58 | whConf.Webhooks[i] = whc 59 | } 60 | webhooks, _ := yaml.Marshal(whConf.Webhooks) 61 | webhooks = bytes.TrimRight(webhooks, "\n ") 62 | certName, _, err := unstructured.NestedString(obj.Object, "metadata", "annotations", "cert-manager.io/inject-ca-from") 63 | if err != nil { 64 | return true, nil, fmt.Errorf("%w: unable get webhook certName", err) 65 | } 66 | certName = strings.TrimPrefix(certName, appMeta.Namespace()+"/") 67 | certName = appMeta.TrimName(certName) 68 | tmpl := vwhTempl 69 | values := helmify.Values{} 70 | if appMeta.Config().AddWebhookOption { 71 | // Add webhook.enabled value to values.yaml 72 | _, _ = values.Add(true, "webhook", "enabled") 73 | 74 | tmpl = fmt.Sprintf("%s\n%s\n%s", WebhookHeader, mwhTempl, WebhookFooter) 75 | } 76 | res := fmt.Sprintf(tmpl, appMeta.ChartName(), name, certName, string(webhooks)) 77 | return true, &vwhResult{ 78 | name: name, 79 | data: []byte(res), 80 | }, nil 81 | } 82 | 83 | type vwhResult struct { 84 | name string 85 | data []byte 86 | values helmify.Values 87 | } 88 | 89 | func (r *vwhResult) Filename() string { 90 | return r.name + ".yaml" 91 | } 92 | 93 | func (r *vwhResult) Values() helmify.Values { 94 | return r.values 95 | } 96 | 97 | func (r *vwhResult) Write(writer io.Writer) error { 98 | _, err := writer.Write(r.data) 99 | return err 100 | } 101 | -------------------------------------------------------------------------------- /pkg/processor/webhook/validating_test.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/arttor/helmify/pkg/metadata" 7 | 8 | "github.com/arttor/helmify/internal" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | const vwhYaml = `apiVersion: admissionregistration.k8s.io/v1 13 | kind: ValidatingWebhookConfiguration 14 | metadata: 15 | annotations: 16 | cert-manager.io/inject-ca-from: my-operator-system/my-operator-serving-cert 17 | name: my-operator-validating-webhook-configuration 18 | webhooks: 19 | - admissionReviewVersions: 20 | - v1 21 | - v1beta1 22 | clientConfig: 23 | service: 24 | name: my-operator-webhook-service 25 | namespace: my-operator-system 26 | path: /validate-ceph-example-com-v1alpha1-volume 27 | failurePolicy: Fail 28 | name: vvolume.kb.io 29 | rules: 30 | - apiGroups: 31 | - test.example.com 32 | apiVersions: 33 | - v1alpha1 34 | operations: 35 | - CREATE 36 | - UPDATE 37 | resources: 38 | - volumes 39 | sideEffects: None` 40 | 41 | func Test_vwh_Process(t *testing.T) { 42 | var testInstance vwh 43 | 44 | t.Run("processed", func(t *testing.T) { 45 | obj := internal.GenerateObj(vwhYaml) 46 | processed, _, err := testInstance.Process(&metadata.Service{}, obj) 47 | assert.NoError(t, err) 48 | assert.Equal(t, true, processed) 49 | }) 50 | t.Run("skipped", func(t *testing.T) { 51 | obj := internal.TestNs 52 | processed, _, err := testInstance.Process(&metadata.Service{}, obj) 53 | assert.NoError(t, err) 54 | assert.Equal(t, false, processed) 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /pkg/yaml/yaml.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | import ( 4 | "bytes" 5 | 6 | "sigs.k8s.io/yaml" 7 | ) 8 | 9 | // Indent - adds indentation to given content. 10 | func Indent(content []byte, n int) []byte { 11 | if n < 0 { 12 | return content 13 | } 14 | prefix := append([]byte("\n"), bytes.Repeat([]byte(" "), n)...) 15 | content = append(prefix[1:], content...) 16 | return bytes.ReplaceAll(content, []byte("\n"), prefix) 17 | } 18 | 19 | // Marshal object to yaml string with indentation. 20 | func Marshal(object interface{}, indent int) (string, error) { 21 | objectBytes, err := yaml.Marshal(object) 22 | if err != nil { 23 | return "", err 24 | } 25 | objectBytes = Indent(objectBytes, indent) 26 | objectBytes = bytes.TrimRight(objectBytes, "\n ") 27 | return string(objectBytes), nil 28 | } 29 | -------------------------------------------------------------------------------- /pkg/yaml/yaml_test.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestIndent(t *testing.T) { 9 | type args struct { 10 | content []byte 11 | n int 12 | } 13 | tests := []struct { 14 | name string 15 | args args 16 | want []byte 17 | }{ 18 | { 19 | name: "negative", 20 | args: args{[]byte("a"), -1}, 21 | want: []byte("a"), 22 | }, 23 | { 24 | name: "none", 25 | args: args{[]byte("a"), 0}, 26 | want: []byte("a"), 27 | }, 28 | { 29 | name: "one", 30 | args: args{[]byte("a"), 1}, 31 | want: []byte(" a"), 32 | }, 33 | { 34 | name: "two", 35 | args: args{[]byte("a"), 2}, 36 | want: []byte(" a"), 37 | }, 38 | } 39 | for _, tt := range tests { 40 | t.Run(tt.name, func(t *testing.T) { 41 | if got := Indent(tt.args.content, tt.args.n); !reflect.DeepEqual(got, tt.want) { 42 | t.Errorf("Indent() = %v, want %v", got, tt.want) 43 | } 44 | }) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test_data/dir/another_dir/stateful_set.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: web 5 | spec: 6 | serviceName: "nginx" 7 | replicas: 2 8 | selector: 9 | matchLabels: 10 | app: nginx 11 | template: 12 | metadata: 13 | labels: 14 | app: nginx 15 | spec: 16 | containers: 17 | - name: nginx 18 | image: registry.k8s.io/nginx-slim:0.8 19 | ports: 20 | - containerPort: 80 21 | name: web 22 | volumeMounts: 23 | - name: www 24 | mountPath: /usr/share/nginx/html 25 | volumeClaimTemplates: 26 | - metadata: 27 | name: www 28 | spec: 29 | accessModes: [ "ReadWriteOnce" ] 30 | resources: 31 | requests: 32 | storage: 1Gi -------------------------------------------------------------------------------- /test_data/dir/config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: my-config 5 | namespace: my-ns 6 | immutable: true 7 | data: 8 | dummyconfigmapkey: dummyconfigmapvalue 9 | my_config.properties: | 10 | health.healthProbeBindAddress=8081 11 | metrics.bindAddress=127.0.0.1:8080 12 | --- 13 | apiVersion: v1 14 | kind: ConfigMap 15 | metadata: 16 | name: my-config-props 17 | namespace: my-ns 18 | data: 19 | my.prop1: "1" 20 | my.prop2: "val 1" 21 | my.prop3: "true" 22 | myval.yaml: | 23 | apiVersion: clickhouse.altinity.com/v1 24 | kind: ClickHouseInstallationTemplate 25 | metadata: 26 | name: default-oneperhost-pod-template 27 | spec: 28 | templates: 29 | podTemplates: 30 | - name: default-oneperhost-pod-template 31 | distribution: "OnePerHost" -------------------------------------------------------------------------------- /test_data/dir/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: myapp 6 | name: myapp 7 | namespace: my-ns 8 | spec: 9 | replicas: 3 10 | selector: 11 | matchLabels: 12 | app: myapp 13 | template: 14 | metadata: 15 | labels: 16 | app: myapp 17 | spec: 18 | containers: 19 | - name: app 20 | args: 21 | - --health-probe-bind-address=:8081 22 | - --metrics-bind-address=127.0.0.1:8080 23 | - --leader-elect 24 | command: 25 | - /manager 26 | volumeMounts: 27 | - mountPath: /my_config.properties 28 | name: manager-config 29 | subPath: my_config.properties 30 | - name: secret-volume 31 | mountPath: /my.ca 32 | - name: props 33 | mountPath: /etc/props 34 | - name: sample-pv-storage 35 | mountPath: "/usr/share/nginx/html" 36 | env: 37 | - name: VAR1 38 | valueFrom: 39 | secretKeyRef: 40 | name: my-secret-vars 41 | key: VAR1 42 | - name: VAR2 43 | valueFrom: 44 | secretKeyRef: 45 | name: my-secret-vars 46 | key: VAR2 47 | image: controller:latest 48 | livenessProbe: 49 | httpGet: 50 | path: /healthz 51 | port: 8081 52 | initialDelaySeconds: 15 53 | periodSeconds: 20 54 | readinessProbe: 55 | httpGet: 56 | path: /readyz 57 | port: 8081 58 | initialDelaySeconds: 5 59 | periodSeconds: 10 60 | resources: 61 | limits: 62 | cpu: 100m 63 | memory: 30Mi 64 | requests: 65 | cpu: 100m 66 | memory: 20Mi 67 | securityContext: 68 | allowPrivilegeEscalation: false 69 | - name: proxy-sidecar 70 | args: 71 | - --secure-listen-address=0.0.0.0:8443 72 | - --v=10 73 | image: gcr.io/kubebuilder/kube-rbac-proxy:v0.8.0 74 | ports: 75 | - containerPort: 8443 76 | name: https 77 | securityContext: 78 | runAsNonRoot: true 79 | nodeSelector: 80 | region: east 81 | type: user-node 82 | terminationGracePeriodSeconds: 10 83 | volumes: 84 | - configMap: 85 | name: my-config 86 | name: manager-config 87 | - configMap: 88 | name: my-config-props 89 | name: props 90 | - name: secret-volume 91 | secret: 92 | secretName: my-secret-ca 93 | - name: sample-pv-storage 94 | persistentVolumeClaim: 95 | claimName: my-sample-pv-claim -------------------------------------------------------------------------------- /test_data/dir/secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: my-secret-ca 5 | namespace: my-ns 6 | type: opaque 7 | data: 8 | ca.crt: | 9 | c3VwZXJsb25ndGVzdGNydC1zdXBlcmxvbmd0ZXN0Y3J0LXN1cGVybG9uZ3Rlc3RjcnQtc3 10 | VwZXJsb25ndGVzdGNydC1zdXBlcmxvbmd0ZXN0Y3J0LXN1cGVybG9uZ3Rlc3RjcnQtc3Vw 11 | ZXJsb25ndGVzdGNydC0Kc3VwZXJsb25ndGVzdGNydC1zdXBlcmxvbmd0ZXN0Y3J0LXN1cG 12 | VybG9uZ3Rlc3RjcnQtc3VwZXJsb25ndGVzdGNydC1zdXBlcmxvbmd0ZXN0Y3J0LXN1cGVy 13 | bG9uZ3Rlc3RjcnQKc3VwZXJsb25ndGVzdGNydC1zdXBlcmxvbmd0ZXN0Y3J0LXN1cGVybG 14 | 9uZ3Rlc3RjcnQtc3VwZXJsb25ndGVzdGNydC1zdXBlcmxvbmd0ZXN0Y3J0LXN1cGVybG9u 15 | Z3Rlc3RjcnQ= 16 | --- 17 | apiVersion: v1 18 | kind: Secret 19 | metadata: 20 | name: my-secret-vars 21 | namespace: my-ns 22 | type: opaque 23 | data: 24 | VAR1: bXlfc2VjcmV0X3Zhcl8x 25 | VAR2: bXlfc2VjcmV0X3Zhcl8y 26 | ELASTIC_FOOBAR_HUNTER123_MEOWTOWN_VERIFY: bXlfc2VjcmV0X3Zhcl8y 27 | stringData: 28 | str: | 29 | some big not so secret string with 30 | multiple lines -------------------------------------------------------------------------------- /test_data/dir/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: myapp 6 | name: myapp-service 7 | namespace: my-ns 8 | spec: 9 | ports: 10 | - name: https 11 | port: 8443 12 | targetPort: https 13 | selector: 14 | app: myapp 15 | --- 16 | apiVersion: networking.k8s.io/v1 17 | kind: Ingress 18 | metadata: 19 | name: myapp-ingress 20 | annotations: 21 | nginx.ingress.kubernetes.io/rewrite-target: / 22 | spec: 23 | rules: 24 | - http: 25 | paths: 26 | - path: /testpath 27 | pathType: Prefix 28 | backend: 29 | service: 30 | name: myapp-service 31 | port: 32 | number: 8443 -------------------------------------------------------------------------------- /test_data/dir/storage.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolumeClaim 3 | metadata: 4 | name: my-sample-pv-claim 5 | spec: 6 | storageClassName: manual 7 | accessModes: 8 | - ReadWriteOnce 9 | resources: 10 | requests: 11 | storage: 3Gi 12 | limits: 13 | storage: 5Gi --------------------------------------------------------------------------------