├── .prettierrc.yaml ├── .prettierignore ├── resources └── images │ ├── graph.png │ ├── nelm-release-install.gif │ ├── nelm-release-install.png │ └── nelm-release-plan-install.png ├── OWNERS ├── .gitignore ├── internal ├── util │ ├── string.go │ ├── int.go │ ├── multierror.go │ ├── diff.go │ ├── json.go │ └── properties.go ├── kube │ ├── client_dynamic.go │ ├── client_static.go │ ├── fake │ │ ├── client_static.go │ │ ├── client_discovery.go │ │ ├── factory.go │ │ └── client_dynamic.go │ ├── error.go │ ├── common.go │ ├── client_mapper.go │ ├── client_discovery.go │ ├── legacy_client_getter.go │ ├── factory.go │ └── config.go ├── resource │ ├── sort.go │ ├── spec │ │ ├── sort.go │ │ ├── resource_match.go │ │ ├── patch.go │ │ ├── resource_meta.go │ │ ├── unstruct.go │ │ ├── transform.go │ │ └── util.go │ └── validate.go ├── plan │ ├── sort.go │ ├── validate.go │ ├── release_info.go │ └── operation.go ├── legacy │ └── deploy │ │ └── stages_splitter.go ├── test │ └── comparer.go ├── release │ └── release_storage.go └── chart │ └── chart_download.go ├── .dockerignore ├── pkg ├── action │ ├── action_suite_test.go │ ├── error.go │ ├── secret_key_create.go │ ├── secret_file_edit.go │ ├── secret_file_decrypt.go │ ├── secret_file_encrypt.go │ ├── secret_values_file_edit.go │ ├── secret_values_file_decrypt.go │ ├── secret_values_file_encrypt.go │ ├── secret_key_rotate.go │ └── version.go ├── log │ └── logger.go ├── legacy │ └── secret │ │ ├── encrypt.go │ │ ├── decrypt.go │ │ ├── common.go │ │ ├── rotate.go │ │ └── edit.go └── featgate │ └── feat.go ├── .github ├── actions │ ├── set-up-git-config │ │ └── action.yml │ └── upload-coverage-artifact │ │ └── action.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.yml │ └── bug_report.yml └── workflows │ ├── issues_new-add-triage-label.yml │ ├── _coverage_report.yml │ ├── test_daily.yml │ ├── _lint.yml │ ├── release_release-please.yml │ ├── tag_auto-create.yml │ ├── issues_delayed-auto-close.yml │ ├── _test_unit.yml │ ├── test_pr.yml │ └── release_trdl-release.yml ├── trdl_channels.yaml ├── trdl.yaml ├── .editorconfig ├── cmd └── nelm │ ├── common.go │ ├── release_plan.go │ ├── chart_secret_key.go │ ├── chart_dependency.go │ ├── chart_secret.go │ ├── chart_secret_file.go │ ├── chart_secret_values_file.go │ ├── repo.go │ ├── chart.go │ ├── root.go │ ├── release_history.go │ ├── release_list_legacy.go │ ├── chart_upload.go │ ├── chart_download.go │ ├── groups.go │ ├── repo_add.go │ ├── repo_remove.go │ ├── chart_pack.go │ ├── repo_login.go │ ├── repo_logout.go │ ├── repo_update.go │ ├── chart_dependency_update.go │ ├── release.go │ ├── chart_dependency_download.go │ ├── main.go │ ├── chart_secret_key_create.go │ ├── version.go │ ├── chart_secret_file_edit.go │ ├── chart_secret_values_file_edit.go │ ├── chart_secret_file_decrypt.go │ ├── chart_secret_file_encrypt.go │ ├── chart_secret_values_file_decrypt.go │ ├── chart_secret_values_file_encrypt.go │ ├── chart_secret_key_rotate.go │ └── release_list.go ├── scripts ├── verify-dist-binaries.sh └── builder │ └── Dockerfile ├── ARCHITECTURE.md ├── .golangci.yaml └── nelm.asc /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | printWidth: 9999 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/.git 2 | **/.svn 3 | **/.hg 4 | werf*.yaml 5 | werf*.yml 6 | **/templates 7 | -------------------------------------------------------------------------------- /resources/images/graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/werf/nelm/HEAD/resources/images/graph.png -------------------------------------------------------------------------------- /resources/images/nelm-release-install.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/werf/nelm/HEAD/resources/images/nelm-release-install.gif -------------------------------------------------------------------------------- /resources/images/nelm-release-install.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/werf/nelm/HEAD/resources/images/nelm-release-install.png -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | approvers: 2 | - ilya-lesikov 3 | - alexey-igrychev 4 | 5 | reviewers: 6 | - ilya-lesikov 7 | - alexey-igrychev 8 | 9 | -------------------------------------------------------------------------------- /resources/images/nelm-release-plan-install.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/werf/nelm/HEAD/resources/images/nelm-release-plan-install.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.test 2 | *.swp 3 | 4 | node_modules/ 5 | 6 | /.vscode/ 7 | /.idea/ 8 | /bin/ 9 | /build/ 10 | /dist/ 11 | /Taskfile.yaml 12 | /go.work 13 | /go.work.sum 14 | -------------------------------------------------------------------------------- /internal/util/string.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "strings" 4 | 5 | func Capitalize(s string) string { 6 | if len(s) == 0 { 7 | return s 8 | } 9 | 10 | return strings.ToUpper(s[:1]) + s[1:] 11 | } 12 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Hidden files and directories 2 | .* 3 | 4 | # Dockerfiles 5 | Dockerfile* 6 | 7 | # Documentation 8 | *.md 9 | 10 | # Configuration (except Taskfile.dist.yaml) 11 | *.yaml 12 | !Taskfile.dist.yaml 13 | -------------------------------------------------------------------------------- /internal/util/int.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "fmt" 4 | 5 | func Uint64ToInt(v uint64) int { 6 | maxInt := uint64(int(^uint(0) >> 1)) 7 | if v > maxInt { 8 | panic(fmt.Sprintf("uint64 value %d overflows int", v)) 9 | } 10 | 11 | return int(v) 12 | } 13 | -------------------------------------------------------------------------------- /internal/kube/client_dynamic.go: -------------------------------------------------------------------------------- 1 | package kube 2 | 3 | import ( 4 | "k8s.io/client-go/dynamic" 5 | ) 6 | 7 | func NewDynamicKubeClientFromKubeConfig(kubeConfig *KubeConfig) (*dynamic.DynamicClient, error) { 8 | return dynamic.NewForConfig(kubeConfig.RestConfig) 9 | } 10 | -------------------------------------------------------------------------------- /internal/kube/client_static.go: -------------------------------------------------------------------------------- 1 | package kube 2 | 3 | import ( 4 | "k8s.io/client-go/kubernetes" 5 | ) 6 | 7 | func NewStaticKubeClientFromKubeConfig(kubeConfig *KubeConfig) (*kubernetes.Clientset, error) { 8 | return kubernetes.NewForConfig(kubeConfig.RestConfig) 9 | } 10 | -------------------------------------------------------------------------------- /pkg/action/action_suite_test.go: -------------------------------------------------------------------------------- 1 | package action_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestAction(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Action Suite") 13 | } 14 | -------------------------------------------------------------------------------- /.github/actions/set-up-git-config/action.yml: -------------------------------------------------------------------------------- 1 | name: Set up git config 2 | runs: 3 | using: composite 4 | steps: 5 | - name: Set up git config 6 | run: | 7 | git config --global user.name "borya" 8 | git config --global user.email "borya@flant.com" 9 | shell: bash 10 | -------------------------------------------------------------------------------- /trdl_channels.yaml: -------------------------------------------------------------------------------- 1 | groups: 2 | - name: "1" 3 | channels: 4 | - name: alpha 5 | version: 1.20.0 6 | - name: beta 7 | version: 1.19.1 8 | - name: ea 9 | version: 1.18.1 10 | - name: stable 11 | version: 1.18.0 12 | - name: rock-solid 13 | version: 1.18.0 14 | -------------------------------------------------------------------------------- /trdl.yaml: -------------------------------------------------------------------------------- 1 | docker_image: registry.werf.io/nelm/builder:01ca717686fa750f252b4fc3ab57359d5510f4a0@sha256:dfeb683fc8186c94f0e52f1b84dffa267af290811d0fdf5629fe93af18b5e8b6 2 | commands: 3 | - export VERSION="$(echo {{ .Tag }} | cut -c2-)" 4 | - task -o group -p build:dist:all version=$VERSION 5 | - task -p verify:binaries:dist:all version=$VERSION 6 | - cp -a ./dist/$VERSION/* /result 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 💬 Discussions 4 | url: https://github.com/werf/nelm/discussions/categories/general 5 | about: Ask a question 6 | - name: 💬 Telegram channel [EN] 7 | url: https://t.me/werf_io 8 | about: Ask a question 9 | - name: 💬 Telegram channel [RU] 10 | url: https://t.me/werf_ru 11 | about: Ask a question 12 | -------------------------------------------------------------------------------- /internal/kube/fake/client_static.go: -------------------------------------------------------------------------------- 1 | package fake 2 | 3 | import ( 4 | "k8s.io/apimachinery/pkg/api/meta" 5 | staticfake "k8s.io/client-go/kubernetes/fake" 6 | ) 7 | 8 | func NewStaticClient(mapper meta.ResettableRESTMapper) *staticfake.Clientset { 9 | staticClient := staticfake.NewSimpleClientset() 10 | staticClient.PrependReactor("*", "*", prepareReaction(staticClient.Tracker(), mapper)) 11 | 12 | return staticClient 13 | } 14 | -------------------------------------------------------------------------------- /internal/util/multierror.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/hashicorp/go-multierror" 7 | ) 8 | 9 | func Multierrorf(format string, errs []error, a ...any) error { 10 | if len(errs) == 0 { 11 | return nil 12 | } 13 | 14 | if len(errs) == 1 { 15 | return fmt.Errorf(fmt.Sprintf(format, a...)+": %w", errs[0]) 16 | } 17 | 18 | return fmt.Errorf(fmt.Sprintf(format, a...)+": %w", multierror.Append(nil, errs...)) 19 | } 20 | -------------------------------------------------------------------------------- /internal/resource/sort.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/werf/nelm/internal/resource/spec" 5 | ) 6 | 7 | func InstallableResourceSortByWeightHandler(r1, r2 *InstallableResource) bool { 8 | if r1.Weight == nil { 9 | return true 10 | } else if r2.Weight == nil { 11 | return false 12 | } else if r1.Weight != r2.Weight { 13 | return *r1.Weight < *r2.Weight 14 | } 15 | 16 | return spec.ResourceSpecSortHandler(r1.ResourceSpec, r2.ResourceSpec) 17 | } 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | ; golang 15 | [*.go] 16 | indent_style = tab 17 | indent_size = 4 18 | 19 | ; yaml 20 | [*.{yml,yaml}] 21 | indent_size = 2 22 | indent_style = space 23 | 24 | ;python pep8 indentation 25 | [*.py] 26 | indent_style = space 27 | indent_size = 4 28 | -------------------------------------------------------------------------------- /cmd/nelm/common.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/samber/lo" 7 | "github.com/spf13/cobra" 8 | 9 | "github.com/werf/nelm/pkg/log" 10 | ) 11 | 12 | var helmRootCmd *cobra.Command 13 | 14 | func allowedLogColorModesHelp() string { 15 | return "Allowed: " + strings.Join(log.LogColorModes, ", ") 16 | } 17 | 18 | func allowedLogLevelsHelp() string { 19 | return "Allowed: " + strings.Join(lo.Map(log.Levels, func(lvl log.Level, _ int) string { 20 | return string(lvl) 21 | }), ", ") 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/issues_new-add-triage-label.yml: -------------------------------------------------------------------------------- 1 | name: issues:new-add-triage-label 2 | on: 3 | issues: 4 | types: 5 | - reopened 6 | - opened 7 | 8 | jobs: 9 | label_issues: 10 | runs-on: ubuntu-22.04 11 | permissions: 12 | issues: write 13 | steps: 14 | - run: gh issue edit "$NUMBER" --add-label "$LABELS" 15 | env: 16 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | GH_REPO: ${{ github.repository }} 18 | NUMBER: ${{ github.event.issue.number }} 19 | LABELS: triage 20 | -------------------------------------------------------------------------------- /internal/kube/error.go: -------------------------------------------------------------------------------- 1 | package kube 2 | 3 | import ( 4 | "strings" 5 | 6 | "k8s.io/apimachinery/pkg/api/errors" 7 | "k8s.io/apimachinery/pkg/api/meta" 8 | "k8s.io/apimachinery/pkg/api/validation" 9 | ) 10 | 11 | func IsImmutableErr(err error) bool { 12 | return err != nil && errors.IsInvalid(err) && strings.Contains(err.Error(), validation.FieldImmutableErrorMsg) 13 | } 14 | 15 | func IsNoSuchKindErr(err error) bool { 16 | return err != nil && meta.IsNoMatchError(err) 17 | } 18 | 19 | func IsNotFoundErr(err error) bool { 20 | return err != nil && errors.IsNotFound(err) 21 | } 22 | -------------------------------------------------------------------------------- /internal/kube/common.go: -------------------------------------------------------------------------------- 1 | package kube 2 | 3 | import ( 4 | "path/filepath" 5 | 6 | "k8s.io/cli-runtime/pkg/genericclioptions" 7 | "k8s.io/client-go/tools/clientcmd" 8 | "k8s.io/client-go/util/homedir" 9 | ) 10 | 11 | var ( 12 | DefaultKubectlCacheDir = filepath.Join(homedir.HomeDir(), ".kube", "cache") 13 | KubectlCacheDirEnv = "KUBECACHEDIR" 14 | KubectlHTTPCacheSubdir = "http" 15 | KubectlDiscoveryCacheSubdir = "discovery" 16 | ) 17 | 18 | func init() { 19 | genericclioptions.ErrEmptyConfig = clientcmd.NewEmptyConfigError("missing or incomplete kubeconfig") 20 | } 21 | -------------------------------------------------------------------------------- /.github/actions/upload-coverage-artifact/action.yml: -------------------------------------------------------------------------------- 1 | name: Upload coverage artifact 2 | inputs: 3 | coverage: 4 | default: false 5 | type: string 6 | runs: 7 | using: composite 8 | steps: 9 | - if: inputs.coverage == 'true' 10 | name: Set timestamp 11 | shell: bash 12 | run: echo "TIMESTAMP=$(date +%H%M%S%N)" >> $GITHUB_ENV 13 | 14 | - if: inputs.coverage == 'true' 15 | name: Upload coverage artifact 16 | uses: actions/upload-artifact@v4 17 | with: 18 | name: coverage-${{ env.TIMESTAMP }} 19 | path: ${{ github.workspace }}/coverage 20 | -------------------------------------------------------------------------------- /internal/kube/client_mapper.go: -------------------------------------------------------------------------------- 1 | package kube 2 | 3 | import ( 4 | "context" 5 | 6 | "k8s.io/apimachinery/pkg/api/meta" 7 | "k8s.io/client-go/discovery" 8 | "k8s.io/client-go/restmapper" 9 | 10 | "github.com/werf/nelm/pkg/log" 11 | ) 12 | 13 | func NewKubeMapper(ctx context.Context, discoveryClient discovery.CachedDiscoveryInterface) meta.RESTMapper { 14 | mapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient) 15 | 16 | expander := restmapper.NewShortcutExpander(mapper, discoveryClient, func(msg string) { 17 | log.Default.Warn(ctx, msg) 18 | }) 19 | 20 | return expander 21 | } 22 | -------------------------------------------------------------------------------- /cmd/nelm/release_plan.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/werf/common-go/pkg/cli" 9 | ) 10 | 11 | func newPlanCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 12 | cmd := cli.NewGroupCommand( 13 | ctx, 14 | "plan", 15 | "Show planned changes.", 16 | "Show planned changes.", 17 | releaseCmdGroup, 18 | cli.GroupCommandOptions{}, 19 | ) 20 | 21 | cmd.AddCommand(newReleasePlanInstallCommand(ctx, afterAllCommandsBuiltFuncs)) 22 | 23 | return cmd 24 | } 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: 💡 Feature 2 | description: Submit a feature request or suggestion 3 | body: 4 | - type: checkboxes 5 | attributes: 6 | label: "Before proceeding" 7 | options: 8 | - label: "I didn't find a similar [issue](https://github.com/werf/nelm/issues)" 9 | required: true 10 | - type: textarea 11 | attributes: 12 | label: Problem 13 | validations: 14 | required: true 15 | - type: textarea 16 | attributes: 17 | label: Solution (if you have one) 18 | - type: textarea 19 | attributes: 20 | label: Additional information 21 | -------------------------------------------------------------------------------- /pkg/action/error.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import "fmt" 4 | 5 | type ReleaseNotFoundError struct { 6 | ReleaseName string 7 | ReleaseNamespace string 8 | } 9 | 10 | func (e *ReleaseNotFoundError) Error() string { 11 | return fmt.Sprintf("release %q (namespace %q) not found", e.ReleaseName, e.ReleaseNamespace) 12 | } 13 | 14 | type ReleaseRevisionNotFoundError struct { 15 | ReleaseName string 16 | ReleaseNamespace string 17 | Revision int 18 | } 19 | 20 | func (e *ReleaseRevisionNotFoundError) Error() string { 21 | return fmt.Sprintf("revision %d of release %q (namespace %q) not found", e.Revision, e.ReleaseName, e.ReleaseNamespace) 22 | } 23 | -------------------------------------------------------------------------------- /cmd/nelm/chart_secret_key.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/werf/common-go/pkg/cli" 9 | ) 10 | 11 | func newChartSecretKeyCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 12 | cmd := cli.NewGroupCommand( 13 | ctx, 14 | "key", 15 | "Manage chart secret keys.", 16 | "Manage chart secret keys.", 17 | secretCmdGroup, 18 | cli.GroupCommandOptions{}, 19 | ) 20 | 21 | cmd.AddCommand(newChartSecretKeyCreateCommand(ctx, afterAllCommandsBuiltFuncs)) 22 | cmd.AddCommand(newChartSecretKeyRotateCommand(ctx, afterAllCommandsBuiltFuncs)) 23 | 24 | return cmd 25 | } 26 | -------------------------------------------------------------------------------- /cmd/nelm/chart_dependency.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/werf/common-go/pkg/cli" 9 | ) 10 | 11 | func newChartDependencyCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 12 | cmd := cli.NewGroupCommand( 13 | ctx, 14 | "dependency", 15 | "Manage chart dependencies.", 16 | "Manage chart dependencies.", 17 | dependencyCmdGroup, 18 | cli.GroupCommandOptions{}, 19 | ) 20 | 21 | cmd.AddCommand(newChartDependencyUpdateCommand(ctx, afterAllCommandsBuiltFuncs)) 22 | cmd.AddCommand(newChartDependencyDownloadCommand(ctx, afterAllCommandsBuiltFuncs)) 23 | 24 | return cmd 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/_coverage_report.yml: -------------------------------------------------------------------------------- 1 | name: xxxxx(internal) 2 | 3 | on: 4 | workflow_call: 5 | 6 | defaults: 7 | run: 8 | shell: bash 9 | 10 | env: 11 | DEBIAN_FRONTEND: "noninteractive" 12 | 13 | jobs: 14 | _: 15 | runs-on: ubuntu-22.04 16 | timeout-minutes: 30 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v4 20 | 21 | - name: Download coverage artifact 22 | uses: actions/download-artifact@v4 23 | with: 24 | path: coverage 25 | 26 | - name: Format and upload coverage report 27 | uses: qltysh/qlty-action/coverage@v1 28 | with: 29 | token: ${{secrets.QLTY_COVERAGE_TOKEN}} 30 | files: coverage/coverage-*/*.out 31 | skip-errors: false 32 | -------------------------------------------------------------------------------- /cmd/nelm/chart_secret.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/werf/common-go/pkg/cli" 9 | ) 10 | 11 | func newChartSecretCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 12 | cmd := cli.NewGroupCommand( 13 | ctx, 14 | "secret", 15 | "Manage chart secrets.", 16 | "Manage chart secrets.", 17 | secretCmdGroup, 18 | cli.GroupCommandOptions{}, 19 | ) 20 | 21 | cmd.AddCommand(newChartSecretKeyCommand(ctx, afterAllCommandsBuiltFuncs)) 22 | cmd.AddCommand(newChartSecretFileCommand(ctx, afterAllCommandsBuiltFuncs)) 23 | cmd.AddCommand(newChartSecretValuesFileCommand(ctx, afterAllCommandsBuiltFuncs)) 24 | 25 | return cmd 26 | } 27 | -------------------------------------------------------------------------------- /cmd/nelm/chart_secret_file.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/werf/common-go/pkg/cli" 9 | ) 10 | 11 | func newChartSecretFileCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 12 | cmd := cli.NewGroupCommand( 13 | ctx, 14 | "file", 15 | "Manage chart secret files.", 16 | "Manage chart secret files.", 17 | secretCmdGroup, 18 | cli.GroupCommandOptions{}, 19 | ) 20 | 21 | cmd.AddCommand(newChartSecretFileEncryptCommand(ctx, afterAllCommandsBuiltFuncs)) 22 | cmd.AddCommand(newChartSecretFileDecryptCommand(ctx, afterAllCommandsBuiltFuncs)) 23 | cmd.AddCommand(newChartSecretFileEditCommand(ctx, afterAllCommandsBuiltFuncs)) 24 | 25 | return cmd 26 | } 27 | -------------------------------------------------------------------------------- /cmd/nelm/chart_secret_values_file.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/werf/common-go/pkg/cli" 9 | ) 10 | 11 | func newChartSecretValuesFileCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 12 | cmd := cli.NewGroupCommand( 13 | ctx, 14 | "values-file", 15 | "Manage chart secret values files.", 16 | "Manage chart secret values files.", 17 | secretCmdGroup, 18 | cli.GroupCommandOptions{}, 19 | ) 20 | 21 | cmd.AddCommand(newChartSecretValuesFileEncryptCommand(ctx, afterAllCommandsBuiltFuncs)) 22 | cmd.AddCommand(newChartSecretValuesFileDecryptCommand(ctx, afterAllCommandsBuiltFuncs)) 23 | cmd.AddCommand(newChartSecretValuesFileEditCommand(ctx, afterAllCommandsBuiltFuncs)) 24 | 25 | return cmd 26 | } 27 | -------------------------------------------------------------------------------- /internal/plan/sort.go: -------------------------------------------------------------------------------- 1 | package plan 2 | 3 | import ( 4 | "github.com/werf/nelm/internal/resource" 5 | "github.com/werf/nelm/pkg/common" 6 | ) 7 | 8 | func InstallableResourceInfoSortByMustInstallHandler(r1, r2 *InstallableResourceInfo) bool { 9 | if r1.MustInstall != r2.MustInstall { 10 | return ResourceInstallTypeSortHandler(r1.MustInstall, r2.MustInstall) 11 | } 12 | 13 | if r1.Stage != r2.Stage { 14 | return common.StagesSortHandler(r1.Stage, r2.Stage) 15 | } 16 | 17 | return resource.InstallableResourceSortByWeightHandler(r1.LocalResource, r2.LocalResource) 18 | } 19 | 20 | func InstallableResourceInfoSortByStageHandler(r1, r2 *InstallableResourceInfo) bool { 21 | if r1.Stage != r2.Stage { 22 | return common.StagesSortHandler(r1.Stage, r2.Stage) 23 | } 24 | 25 | return resource.InstallableResourceSortByWeightHandler(r1.LocalResource, r2.LocalResource) 26 | } 27 | -------------------------------------------------------------------------------- /cmd/nelm/repo.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/werf/common-go/pkg/cli" 9 | ) 10 | 11 | func newRepoCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 12 | cmd := cli.NewGroupCommand( 13 | ctx, 14 | "repo", 15 | "Manage chart repositories.", 16 | "Manage chart repositories.", 17 | repoCmdGroup, 18 | cli.GroupCommandOptions{}, 19 | ) 20 | 21 | cmd.AddCommand(newRepoAddCommand(ctx, afterAllCommandsBuiltFuncs)) 22 | cmd.AddCommand(newRepoRemoveCommand(ctx, afterAllCommandsBuiltFuncs)) 23 | cmd.AddCommand(newRepoUpdateCommand(ctx, afterAllCommandsBuiltFuncs)) 24 | cmd.AddCommand(newRepoLoginCommand(ctx, afterAllCommandsBuiltFuncs)) 25 | cmd.AddCommand(newRepoLogoutCommand(ctx, afterAllCommandsBuiltFuncs)) 26 | 27 | return cmd 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/test_daily.yml: -------------------------------------------------------------------------------- 1 | name: test:daily 2 | 3 | on: 4 | schedule: 5 | - cron: "0 8 * * *" 6 | repository_dispatch: 7 | types: ["test:daily"] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | lint: 12 | uses: ./.github/workflows/_lint.yml 13 | 14 | unit: 15 | uses: ./.github/workflows/_test_unit.yml 16 | with: 17 | coverage: true 18 | 19 | coverage_report: 20 | uses: ./.github/workflows/_coverage_report.yml 21 | needs: 22 | - unit 23 | secrets: inherit 24 | 25 | notify: 26 | if: always() 27 | needs: 28 | - lint 29 | - coverage_report 30 | uses: werf/common-ci/.github/workflows/notification.yml@main 31 | secrets: 32 | loopNotificationGroup: ${{ vars.LOOP_NOTIFICATION_GROUP }} 33 | webhook: ${{ secrets.LOOP_NOTIFICATION_WEBHOOK }} 34 | notificationChannel: ${{ vars.LOOP_NOTIFICATION_CHANNEL }} 35 | -------------------------------------------------------------------------------- /.github/workflows/_lint.yml: -------------------------------------------------------------------------------- 1 | name: xxxxx(internal) 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | forceSkip: 7 | default: false 8 | type: string 9 | 10 | defaults: 11 | run: 12 | shell: bash 13 | 14 | env: 15 | DEBIAN_FRONTEND: "noninteractive" 16 | 17 | jobs: 18 | _: 19 | if: inputs.forceSkip == 'false' 20 | runs-on: ubuntu-22.04 21 | timeout-minutes: 30 22 | steps: 23 | - name: Checkout code 24 | uses: actions/checkout@v4 25 | 26 | - name: Set up Go 27 | uses: actions/setup-go@v6 28 | with: 29 | cache: false 30 | go-version-file: go.mod 31 | 32 | - name: Install Task 33 | uses: arduino/setup-task@v2 34 | with: 35 | repo-token: ${{ secrets.GITHUB_TOKEN }} 36 | 37 | - name: Install golangci-lint 38 | run: task -p deps:install:golangci-lint 39 | 40 | - name: Lint 41 | run: task -p lint 42 | -------------------------------------------------------------------------------- /scripts/verify-dist-binaries.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | script_dir="$(cd "$( dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" 5 | project_dir="$script_dir/.." 6 | 7 | version="${1:?Version should be set}" 8 | 9 | declare -A regexps 10 | regexps["$project_dir/dist/$version/linux-amd64/bin/nelm"]="x86-64.*statically linked" 11 | regexps["$project_dir/dist/$version/linux-arm64/bin/nelm"]="ARM aarch64.*statically linked" 12 | regexps["$project_dir/dist/$version/darwin-amd64/bin/nelm"]="Mach-O.*x86_64" 13 | regexps["$project_dir/dist/$version/darwin-arm64/bin/nelm"]="Mach-O.*arm64" 14 | regexps["$project_dir/dist/$version/windows-amd64/bin/nelm.exe"]="x86-64.*Windows" 15 | 16 | for filename in "${!regexps[@]}"; do 17 | if ! [[ -f "$filename" ]]; then 18 | echo Binary at "$filename" does not exist. 19 | exit 1 20 | fi 21 | 22 | file "$filename" | awk -v regexp="${regexps[$filename]}" '{print $0; if ($0 ~ regexp) { exit } else { print "Unexpected binary info ^^"; exit 1 }}' 23 | done 24 | -------------------------------------------------------------------------------- /scripts/builder/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24.9-bookworm@sha256:e400aebe4e96e1d52b510fb7a82c417d9377f595f0160eb1bd979d441711d20c 2 | ENV DEBIAN_FRONTEND=noninteractive 3 | 4 | ARG TARGETPLATFORM 5 | # linux/amd64 -> linux_amd64 6 | ENV PLATFORM=${TARGETPLATFORM/\//_} 7 | 8 | RUN apt-get -y update && \ 9 | apt-get -y install apt-utils gcc-aarch64-linux-gnu file && \ 10 | curl -sSLO https://github.com/go-task/task/releases/download/v3.43.3/task_${PLATFORM}.deb && \ 11 | apt-get -y install ./task_${PLATFORM}.deb && \ 12 | rm -rf ./task_${PLATFORM}.deb /var/cache/apt/* /var/lib/apt/lists/* /var/log/* 13 | 14 | ADD cmd /.nelm-deps/cmd 15 | ADD pkg /.nelm-deps/pkg 16 | ADD internal /.nelm-deps/internal 17 | COPY go.mod go.sum Taskfile.dist.yaml /.nelm-deps/ 18 | ADD scripts /.nelm-deps/scripts 19 | 20 | RUN cd /.nelm-deps && \ 21 | task build:dist:all version=base && \ 22 | task verify:binaries:dist:all version=base && \ 23 | rm -rf /.nelm-deps 24 | 25 | RUN git config --global --add safe.directory /git 26 | -------------------------------------------------------------------------------- /cmd/nelm/chart.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/werf/common-go/pkg/cli" 9 | ) 10 | 11 | func newChartCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 12 | cmd := cli.NewGroupCommand( 13 | ctx, 14 | "chart", 15 | "Manage charts.", 16 | "Manage charts.", 17 | chartCmdGroup, 18 | cli.GroupCommandOptions{}, 19 | ) 20 | 21 | cmd.AddCommand(newChartRenderCommand(ctx, afterAllCommandsBuiltFuncs)) 22 | cmd.AddCommand(newChartDependencyCommand(ctx, afterAllCommandsBuiltFuncs)) 23 | cmd.AddCommand(newChartDownloadCommand(ctx, afterAllCommandsBuiltFuncs)) 24 | cmd.AddCommand(newChartUploadCommand(ctx, afterAllCommandsBuiltFuncs)) 25 | cmd.AddCommand(newChartPackCommand(ctx, afterAllCommandsBuiltFuncs)) 26 | cmd.AddCommand(newChartLintCommand(ctx, afterAllCommandsBuiltFuncs)) 27 | cmd.AddCommand(newChartSecretCommand(ctx, afterAllCommandsBuiltFuncs)) 28 | 29 | return cmd 30 | } 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 🪲 Bug 2 | description: Submit a bug report 3 | body: 4 | - type: checkboxes 5 | attributes: 6 | label: "Before proceeding" 7 | options: 8 | - label: "I didn't find a similar [issue](https://github.com/werf/nelm/issues)" 9 | required: true 10 | - type: input 11 | attributes: 12 | label: Version 13 | placeholder: "1.2.3" 14 | validations: 15 | required: true 16 | - type: textarea 17 | attributes: 18 | label: How to reproduce 19 | placeholder: | 20 | 1. In this environment... 21 | 2. With this configuration... 22 | 3. Run '...' 23 | validations: 24 | required: true 25 | - type: textarea 26 | attributes: 27 | label: Result 28 | validations: 29 | required: true 30 | - type: textarea 31 | attributes: 32 | label: Expected result 33 | validations: 34 | required: true 35 | - type: textarea 36 | attributes: 37 | label: Additional information 38 | -------------------------------------------------------------------------------- /.github/workflows/release_release-please.yml: -------------------------------------------------------------------------------- 1 | name: release:release-please 2 | on: 3 | push: 4 | branches: 5 | - "main" 6 | - "[0-9]+.[0-9]+.[0-9]+*" 7 | - "[0-9]+.[0-9]+" 8 | - "[0-9]+" 9 | repository_dispatch: 10 | types: ["release:release-please"] 11 | workflow_dispatch: 12 | 13 | defaults: 14 | run: 15 | shell: bash 16 | 17 | jobs: 18 | release-please: 19 | runs-on: ubuntu-22.04 20 | steps: 21 | - name: Release 22 | id: release 23 | uses: googleapis/release-please-action@v4 24 | with: 25 | target-branch: ${{github.ref_name}} 26 | release-type: go 27 | skip-github-release: true 28 | token: ${{ secrets.RELEASE_PLEASE_TOKEN }} 29 | 30 | notify: 31 | if: failure() 32 | needs: release-please 33 | uses: werf/common-ci/.github/workflows/notification.yml@main 34 | secrets: 35 | loopNotificationGroup: ${{ vars.LOOP_NOTIFICATION_GROUP }} 36 | webhook: ${{ secrets.LOOP_NOTIFICATION_WEBHOOK }} 37 | notificationChannel: ${{ vars.LOOP_NOTIFICATION_CHANNEL }} 38 | -------------------------------------------------------------------------------- /cmd/nelm/root.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/werf/common-go/pkg/cli" 11 | "github.com/werf/nelm/pkg/common" 12 | ) 13 | 14 | func NewRootCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 15 | cobra.EnableErrorOnUnknownSubcommand = true 16 | 17 | cmd := cli.NewRootCommand( 18 | ctx, 19 | strings.ToLower(common.Brand), 20 | fmt.Sprintf("%s is a Helm 3 alternative. %s manages and deploys Helm Charts to Kubernetes just like Helm, but provides a lot of features, improvements and bug fixes on top of what Helm 3 offers.", common.Brand, common.Brand), 21 | ) 22 | 23 | cmd.SetUsageFunc(usageFunc) 24 | cmd.SetUsageTemplate(usageTemplate) 25 | cmd.SetHelpTemplate(helpTemplate) 26 | 27 | cmd.AddCommand(newReleaseCommand(ctx, afterAllCommandsBuiltFuncs)) 28 | cmd.AddCommand(newChartCommand(ctx, afterAllCommandsBuiltFuncs)) 29 | cmd.AddCommand(newRepoCommand(ctx, afterAllCommandsBuiltFuncs)) 30 | cmd.AddCommand(newVersionCommand(ctx, afterAllCommandsBuiltFuncs)) 31 | 32 | return cmd 33 | } 34 | -------------------------------------------------------------------------------- /cmd/nelm/release_history.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/samber/lo" 8 | "github.com/spf13/cobra" 9 | 10 | helm_v3 "github.com/werf/3p-helm/cmd/helm" 11 | "github.com/werf/3p-helm/pkg/chart/loader" 12 | "github.com/werf/common-go/pkg/cli" 13 | "github.com/werf/nelm/pkg/log" 14 | ) 15 | 16 | func newReleaseHistoryCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 17 | cmd := lo.Must(lo.Find(helmRootCmd.Commands(), func(c *cobra.Command) bool { 18 | return strings.HasPrefix(c.Use, "history") 19 | })) 20 | 21 | cmd.LocalFlags().AddFlagSet(cmd.InheritedFlags()) 22 | cmd.Short = "Show release history." 23 | cmd.Aliases = []string{} 24 | cli.SetSubCommandAnnotations(cmd, 30, releaseCmdGroup) 25 | 26 | originalRunE := cmd.RunE 27 | cmd.RunE = func(cmd *cobra.Command, args []string) error { 28 | helmSettings := helm_v3.Settings 29 | 30 | ctx = log.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, log.DebugLevel, log.InfoLevel), log.SetupLoggingOptions{}) 31 | 32 | loader.NoChartLockWarning = "" 33 | 34 | if err := originalRunE(cmd, args); err != nil { 35 | return err 36 | } 37 | 38 | return nil 39 | } 40 | 41 | return cmd 42 | } 43 | -------------------------------------------------------------------------------- /internal/kube/client_discovery.go: -------------------------------------------------------------------------------- 1 | package kube 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "regexp" 7 | "strings" 8 | "time" 9 | 10 | "k8s.io/client-go/discovery/cached/disk" 11 | ) 12 | 13 | func NewDiscoveryKubeClientFromKubeConfig(kubeConfig *KubeConfig) (*disk.CachedDiscoveryClient, error) { 14 | var cacheDir string 15 | if dir := os.Getenv(KubectlCacheDirEnv); dir != "" { 16 | cacheDir = dir 17 | } else { 18 | cacheDir = DefaultKubectlCacheDir 19 | } 20 | 21 | httpCacheDir := filepath.Join(cacheDir, KubectlHTTPCacheSubdir) 22 | discoveryCacheDir := computeDiscoveryCacheDir(filepath.Join(cacheDir, KubectlDiscoveryCacheSubdir), kubeConfig.RestConfig.Host) 23 | 24 | return disk.NewCachedDiscoveryClientForConfig(kubeConfig.RestConfig, discoveryCacheDir, httpCacheDir, 6*time.Hour) 25 | } 26 | 27 | // Taken from: https://github.com/kubernetes/cli-runtime/blob/e447e205e17575154e7108dbd67e6965499488a0/pkg/genericclioptions/config_flags.go#L485 28 | func computeDiscoveryCacheDir(parentDir, host string) string { 29 | schemelessHost := strings.Replace(strings.Replace(host, "https://", "", 1), "http://", "", 1) 30 | 31 | safeHost := regexp.MustCompile(`[^(\w/.)]`).ReplaceAllString(schemelessHost, "_") 32 | 33 | return filepath.Join(parentDir, safeHost) 34 | } 35 | -------------------------------------------------------------------------------- /cmd/nelm/release_list_legacy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/samber/lo" 8 | "github.com/spf13/cobra" 9 | 10 | helm_v3 "github.com/werf/3p-helm/cmd/helm" 11 | "github.com/werf/3p-helm/pkg/chart/loader" 12 | "github.com/werf/common-go/pkg/cli" 13 | "github.com/werf/nelm/pkg/log" 14 | ) 15 | 16 | func newLegacyReleaseListCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 17 | cmd := lo.Must(lo.Find(helmRootCmd.Commands(), func(c *cobra.Command) bool { 18 | return strings.HasPrefix(c.Use, "list") 19 | })) 20 | 21 | cmd.LocalFlags().AddFlagSet(cmd.InheritedFlags()) 22 | cmd.Short = "List all releases in a namespace." 23 | cmd.Aliases = []string{} 24 | cli.SetSubCommandAnnotations(cmd, 40, releaseCmdGroup) 25 | 26 | originalRunE := cmd.RunE 27 | cmd.RunE = func(cmd *cobra.Command, args []string) error { 28 | helmSettings := helm_v3.Settings 29 | 30 | ctx = log.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, log.DebugLevel, log.InfoLevel), log.SetupLoggingOptions{}) 31 | 32 | loader.NoChartLockWarning = "" 33 | 34 | if err := originalRunE(cmd, args); err != nil { 35 | return err 36 | } 37 | 38 | return nil 39 | } 40 | 41 | return cmd 42 | } 43 | -------------------------------------------------------------------------------- /ARCHITECTURE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | - [Overview](#overview) 5 | - [Dependencies](#dependencies) 6 | - [Architecture](#architecture) 7 | 8 | 9 | 10 | ## Overview 11 | 12 | Nelm originally was just a werf deployment engine, but eventually got its own repo and CLI. Nelm is reusing parts of the Helm codebase, but most of its codebase is written from scratch. 13 | 14 | ## Dependencies 15 | 16 | Nelm depends on the following projects from the "werf" organization: 17 | 18 | * https://github.com/werf/3p-helm is a fork of Helm with some modifications. We use some parts of this fork in Nelm. 19 | * https://github.com/werf/kubedog is a library for tracking Kubernetes resource statuses, collecting logs and events during Nelm deployments. 20 | * https://github.com/werf/common-go is a library with some common code shared between various werf projects. 21 | 22 | Nelm is pretty straightforward: no frameworks, no CGO, no code generation, and libraries we use are very common (e.g. Cobra for CLI). For the full list of dependencies look into the `go.mod` file. 23 | 24 | ## Architecture 25 | 26 | TODO 27 | -------------------------------------------------------------------------------- /cmd/nelm/chart_upload.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/samber/lo" 8 | "github.com/spf13/cobra" 9 | 10 | helm_v3 "github.com/werf/3p-helm/cmd/helm" 11 | "github.com/werf/3p-helm/pkg/chart/loader" 12 | "github.com/werf/common-go/pkg/cli" 13 | "github.com/werf/nelm/pkg/log" 14 | ) 15 | 16 | func newChartUploadCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 17 | cmd := lo.Must(lo.Find(helmRootCmd.Commands(), func(c *cobra.Command) bool { 18 | return strings.HasPrefix(c.Use, "push") 19 | })) 20 | 21 | cmd.LocalFlags().AddFlagSet(cmd.InheritedFlags()) 22 | cmd.Use = "upload [archive] [remote]" 23 | cmd.Short = "Upload a chart archive to a repository." 24 | cmd.Aliases = []string{} 25 | cli.SetSubCommandAnnotations(cmd, 40, chartCmdGroup) 26 | 27 | originalRunE := cmd.RunE 28 | cmd.RunE = func(cmd *cobra.Command, args []string) error { 29 | helmSettings := helm_v3.Settings 30 | 31 | ctx = log.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, log.DebugLevel, log.InfoLevel), log.SetupLoggingOptions{}) 32 | 33 | loader.NoChartLockWarning = "" 34 | 35 | if err := originalRunE(cmd, args); err != nil { 36 | return err 37 | } 38 | 39 | return nil 40 | } 41 | 42 | return cmd 43 | } 44 | -------------------------------------------------------------------------------- /pkg/action/secret_key_create.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/werf/common-go/pkg/secrets_manager" 9 | "github.com/werf/nelm/pkg/log" 10 | ) 11 | 12 | const ( 13 | DefaultSecretKeyCreateLogLevel = log.ErrorLevel 14 | ) 15 | 16 | type SecretKeyCreateOptions struct { 17 | OutputNoPrint bool 18 | TempDirPath string 19 | } 20 | 21 | func SecretKeyCreate(ctx context.Context, opts SecretKeyCreateOptions) (string, error) { 22 | opts, err := applySecretKeyCreateOptionsDefaults(opts) 23 | if err != nil { 24 | return "", fmt.Errorf("build secret key create options: %w", err) 25 | } 26 | 27 | var result string 28 | if !opts.OutputNoPrint { 29 | if keyByte, err := secrets_manager.GenerateSecretKey(); err != nil { 30 | return "", fmt.Errorf("generate secret key: %w", err) 31 | } else { 32 | result = string(keyByte) 33 | } 34 | 35 | fmt.Println(result) 36 | } 37 | 38 | return result, nil 39 | } 40 | 41 | func applySecretKeyCreateOptionsDefaults(opts SecretKeyCreateOptions) (SecretKeyCreateOptions, error) { 42 | var err error 43 | if opts.TempDirPath == "" { 44 | opts.TempDirPath, err = os.MkdirTemp("", "") 45 | if err != nil { 46 | return SecretKeyCreateOptions{}, fmt.Errorf("create temp dir: %w", err) 47 | } 48 | } 49 | 50 | return opts, nil 51 | } 52 | -------------------------------------------------------------------------------- /cmd/nelm/chart_download.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/samber/lo" 8 | "github.com/spf13/cobra" 9 | 10 | helm_v3 "github.com/werf/3p-helm/cmd/helm" 11 | "github.com/werf/3p-helm/pkg/chart/loader" 12 | "github.com/werf/common-go/pkg/cli" 13 | "github.com/werf/nelm/pkg/log" 14 | ) 15 | 16 | func newChartDownloadCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 17 | cmd := lo.Must(lo.Find(helmRootCmd.Commands(), func(c *cobra.Command) bool { 18 | return strings.HasPrefix(c.Use, "pull") 19 | })) 20 | 21 | cmd.LocalFlags().AddFlagSet(cmd.InheritedFlags()) 22 | cmd.Use = "download [chart URL | repo/chartname] [...]" 23 | cmd.Short = "Download a chart from a repository." 24 | cmd.Aliases = []string{} 25 | cli.SetSubCommandAnnotations(cmd, 40, chartCmdGroup) 26 | 27 | originalRunE := cmd.RunE 28 | cmd.RunE = func(cmd *cobra.Command, args []string) error { 29 | helmSettings := helm_v3.Settings 30 | 31 | ctx = log.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, log.DebugLevel, log.InfoLevel), log.SetupLoggingOptions{}) 32 | 33 | loader.NoChartLockWarning = "" 34 | 35 | if err := originalRunE(cmd, args); err != nil { 36 | return err 37 | } 38 | 39 | return nil 40 | } 41 | 42 | return cmd 43 | } 44 | -------------------------------------------------------------------------------- /cmd/nelm/groups.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/werf/common-go/pkg/cli" 5 | ) 6 | 7 | var ( 8 | releaseCmdGroup = cli.NewCommandGroup("release", "Release commands:", 100) 9 | chartCmdGroup = cli.NewCommandGroup("chart", "Chart commands:", 90) 10 | secretCmdGroup = cli.NewCommandGroup("secret", "Secret commands:", 80) 11 | dependencyCmdGroup = cli.NewCommandGroup("dependency", "Dependency commands:", 70) 12 | repoCmdGroup = cli.NewCommandGroup("repo", "Repo commands:", 60) 13 | miscCmdGroup = cli.NewCommandGroup("misc", "Other commands:", 0) 14 | 15 | mainFlagGroup = cli.NewFlagGroup("main", "Options:", 100) 16 | valuesFlagGroup = cli.NewFlagGroup("values", "Values options:", 90) 17 | secretFlagGroup = cli.NewFlagGroup("secret", "Secret options:", 80) 18 | patchFlagGroup = cli.NewFlagGroup("patch", "Patch options:", 70) 19 | progressFlagGroup = cli.NewFlagGroup("progress", "Progress options:", 65) 20 | chartRepoFlagGroup = cli.NewFlagGroup("chart-repo", "Chart repository options:", 60) 21 | kubeConnectionFlagGroup = cli.NewFlagGroup("kube-connection", "Kubernetes connection options:", 50) 22 | performanceFlagGroup = cli.NewFlagGroup("performance", "Performance options:", 40) 23 | miscFlagGroup = cli.NewFlagGroup("misc", "Other options:", 0) 24 | ) 25 | -------------------------------------------------------------------------------- /cmd/nelm/repo_add.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/samber/lo" 8 | "github.com/spf13/cobra" 9 | 10 | helm_v3 "github.com/werf/3p-helm/cmd/helm" 11 | "github.com/werf/3p-helm/pkg/chart/loader" 12 | "github.com/werf/common-go/pkg/cli" 13 | "github.com/werf/nelm/pkg/log" 14 | ) 15 | 16 | func newRepoAddCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 17 | repoCmd := lo.Must(lo.Find(helmRootCmd.Commands(), func(c *cobra.Command) bool { 18 | return strings.HasPrefix(c.Use, "repo") 19 | })) 20 | 21 | cmd := lo.Must(lo.Find(repoCmd.Commands(), func(c *cobra.Command) bool { 22 | return strings.HasPrefix(c.Use, "add") 23 | })) 24 | 25 | cmd.LocalFlags().AddFlagSet(cmd.InheritedFlags()) 26 | cmd.Short = "Set up a new chart repository." 27 | cmd.Aliases = []string{} 28 | cli.SetSubCommandAnnotations(cmd, 60, repoCmdGroup) 29 | 30 | originalRunE := cmd.RunE 31 | cmd.RunE = func(cmd *cobra.Command, args []string) error { 32 | helmSettings := helm_v3.Settings 33 | 34 | ctx = log.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, log.DebugLevel, log.InfoLevel), log.SetupLoggingOptions{}) 35 | 36 | loader.NoChartLockWarning = "" 37 | 38 | if err := originalRunE(cmd, args); err != nil { 39 | return err 40 | } 41 | 42 | return nil 43 | } 44 | 45 | return cmd 46 | } 47 | -------------------------------------------------------------------------------- /cmd/nelm/repo_remove.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/samber/lo" 8 | "github.com/spf13/cobra" 9 | 10 | helm_v3 "github.com/werf/3p-helm/cmd/helm" 11 | "github.com/werf/3p-helm/pkg/chart/loader" 12 | "github.com/werf/common-go/pkg/cli" 13 | "github.com/werf/nelm/pkg/log" 14 | ) 15 | 16 | func newRepoRemoveCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 17 | repoCmd := lo.Must(lo.Find(helmRootCmd.Commands(), func(c *cobra.Command) bool { 18 | return strings.HasPrefix(c.Use, "repo") 19 | })) 20 | 21 | cmd := lo.Must(lo.Find(repoCmd.Commands(), func(c *cobra.Command) bool { 22 | return strings.HasPrefix(c.Use, "remove") 23 | })) 24 | 25 | cmd.LocalFlags().AddFlagSet(cmd.InheritedFlags()) 26 | cmd.Short = "Remove a chart repository." 27 | cmd.Aliases = []string{} 28 | cli.SetSubCommandAnnotations(cmd, 50, repoCmdGroup) 29 | 30 | originalRunE := cmd.RunE 31 | cmd.RunE = func(cmd *cobra.Command, args []string) error { 32 | helmSettings := helm_v3.Settings 33 | 34 | ctx = log.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, log.DebugLevel, log.InfoLevel), log.SetupLoggingOptions{}) 35 | 36 | loader.NoChartLockWarning = "" 37 | 38 | if err := originalRunE(cmd, args); err != nil { 39 | return err 40 | } 41 | 42 | return nil 43 | } 44 | 45 | return cmd 46 | } 47 | -------------------------------------------------------------------------------- /cmd/nelm/chart_pack.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/samber/lo" 8 | "github.com/spf13/cobra" 9 | 10 | helm_v3 "github.com/werf/3p-helm/cmd/helm" 11 | "github.com/werf/3p-helm/pkg/chart/loader" 12 | "github.com/werf/common-go/pkg/cli" 13 | "github.com/werf/nelm/pkg/log" 14 | ) 15 | 16 | func newChartPackCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 17 | cmd := lo.Must(lo.Find(helmRootCmd.Commands(), func(c *cobra.Command) bool { 18 | return strings.HasPrefix(c.Use, "package") 19 | })) 20 | 21 | cmd.LocalFlags().AddFlagSet(cmd.InheritedFlags()) 22 | cmd.Use = "pack [CHART_PATH] [...]" 23 | cmd.Short = "Pack a chart into an archive to distribute via a repository." 24 | cmd.Long = strings.ReplaceAll(cmd.Long, "helm package", "nelm chart pack") 25 | cmd.Aliases = []string{} 26 | cli.SetSubCommandAnnotations(cmd, 30, chartCmdGroup) 27 | 28 | originalRunE := cmd.RunE 29 | cmd.RunE = func(cmd *cobra.Command, args []string) error { 30 | helmSettings := helm_v3.Settings 31 | 32 | ctx = log.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, log.DebugLevel, log.InfoLevel), log.SetupLoggingOptions{}) 33 | 34 | loader.NoChartLockWarning = "" 35 | 36 | if err := originalRunE(cmd, args); err != nil { 37 | return err 38 | } 39 | 40 | return nil 41 | } 42 | 43 | return cmd 44 | } 45 | -------------------------------------------------------------------------------- /cmd/nelm/repo_login.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/samber/lo" 8 | "github.com/spf13/cobra" 9 | 10 | helm_v3 "github.com/werf/3p-helm/cmd/helm" 11 | "github.com/werf/3p-helm/pkg/chart/loader" 12 | "github.com/werf/common-go/pkg/cli" 13 | "github.com/werf/nelm/pkg/log" 14 | ) 15 | 16 | func newRepoLoginCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 17 | registryCmd := lo.Must(lo.Find(helmRootCmd.Commands(), func(c *cobra.Command) bool { 18 | return strings.HasPrefix(c.Use, "registry") 19 | })) 20 | 21 | cmd := lo.Must(lo.Find(registryCmd.Commands(), func(c *cobra.Command) bool { 22 | return strings.HasPrefix(c.Use, "login") 23 | })) 24 | 25 | cmd.LocalFlags().AddFlagSet(cmd.InheritedFlags()) 26 | cmd.Short = "Log in to an OCI registry with charts." 27 | cmd.Long = "" 28 | cmd.Aliases = []string{} 29 | cli.SetSubCommandAnnotations(cmd, 30, repoCmdGroup) 30 | 31 | originalRunE := cmd.RunE 32 | cmd.RunE = func(cmd *cobra.Command, args []string) error { 33 | helmSettings := helm_v3.Settings 34 | 35 | ctx = log.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, log.DebugLevel, log.InfoLevel), log.SetupLoggingOptions{}) 36 | 37 | loader.NoChartLockWarning = "" 38 | 39 | if err := originalRunE(cmd, args); err != nil { 40 | return err 41 | } 42 | 43 | return nil 44 | } 45 | 46 | return cmd 47 | } 48 | -------------------------------------------------------------------------------- /internal/resource/spec/sort.go: -------------------------------------------------------------------------------- 1 | package spec 2 | 3 | import "github.com/werf/nelm/pkg/common" 4 | 5 | func ResourceSpecSortHandler(r1, r2 *ResourceSpec) bool { 6 | sortAs1 := r1.StoreAs 7 | sortAs2 := r2.StoreAs 8 | // TODO(v2): sorted based on sortAs for compatibility. In future should just probably sort 9 | // like this: first CRDs (any type), then helm.sh/hook hooks, then the rest 10 | if sortAs1 != sortAs2 { 11 | if sortAs1 == common.StoreAsNone { 12 | return true 13 | } else if sortAs1 == common.StoreAsHook && sortAs2 != common.StoreAsNone { 14 | return true 15 | } else { 16 | return false 17 | } 18 | } 19 | 20 | return ResourceMetaSortHandler(r1.ResourceMeta, r2.ResourceMeta) 21 | } 22 | 23 | func ResourceMetaSortHandler(r1, r2 *ResourceMeta) bool { 24 | kind1 := r1.GroupVersionKind.Kind 25 | kind2 := r2.GroupVersionKind.Kind 26 | 27 | if kind1 != kind2 { 28 | return kind1 < kind2 29 | } 30 | 31 | group1 := r1.GroupVersionKind.Group 32 | group2 := r2.GroupVersionKind.Group 33 | 34 | if group1 != group2 { 35 | return group1 < group2 36 | } 37 | 38 | version1 := r1.GroupVersionKind.Version 39 | version2 := r2.GroupVersionKind.Version 40 | 41 | if version1 != version2 { 42 | return version1 < version2 43 | } 44 | 45 | namespace1 := r1.Namespace 46 | namespace2 := r2.Namespace 47 | 48 | if namespace1 != namespace2 { 49 | return namespace1 < namespace2 50 | } 51 | 52 | return r1.Name < r2.Name 53 | } 54 | -------------------------------------------------------------------------------- /cmd/nelm/repo_logout.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/samber/lo" 8 | "github.com/spf13/cobra" 9 | 10 | helm_v3 "github.com/werf/3p-helm/cmd/helm" 11 | "github.com/werf/3p-helm/pkg/chart/loader" 12 | "github.com/werf/common-go/pkg/cli" 13 | "github.com/werf/nelm/pkg/log" 14 | ) 15 | 16 | func newRepoLogoutCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 17 | registryCmd := lo.Must(lo.Find(helmRootCmd.Commands(), func(c *cobra.Command) bool { 18 | return strings.HasPrefix(c.Use, "registry") 19 | })) 20 | 21 | cmd := lo.Must(lo.Find(registryCmd.Commands(), func(c *cobra.Command) bool { 22 | return strings.HasPrefix(c.Use, "logout") 23 | })) 24 | 25 | cmd.LocalFlags().AddFlagSet(cmd.InheritedFlags()) 26 | cmd.Short = "Log out from an OCI registry with charts." 27 | cmd.Long = "" 28 | cmd.Aliases = []string{} 29 | cli.SetSubCommandAnnotations(cmd, 20, repoCmdGroup) 30 | 31 | originalRunE := cmd.RunE 32 | cmd.RunE = func(cmd *cobra.Command, args []string) error { 33 | helmSettings := helm_v3.Settings 34 | 35 | ctx = log.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, log.DebugLevel, log.InfoLevel), log.SetupLoggingOptions{}) 36 | 37 | loader.NoChartLockWarning = "" 38 | 39 | if err := originalRunE(cmd, args); err != nil { 40 | return err 41 | } 42 | 43 | return nil 44 | } 45 | 46 | return cmd 47 | } 48 | -------------------------------------------------------------------------------- /cmd/nelm/repo_update.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/samber/lo" 8 | "github.com/spf13/cobra" 9 | 10 | helm_v3 "github.com/werf/3p-helm/cmd/helm" 11 | "github.com/werf/3p-helm/pkg/chart/loader" 12 | "github.com/werf/common-go/pkg/cli" 13 | "github.com/werf/nelm/pkg/log" 14 | ) 15 | 16 | func newRepoUpdateCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 17 | repoCmd := lo.Must(lo.Find(helmRootCmd.Commands(), func(c *cobra.Command) bool { 18 | return strings.HasPrefix(c.Use, "repo") 19 | })) 20 | 21 | cmd := lo.Must(lo.Find(repoCmd.Commands(), func(c *cobra.Command) bool { 22 | return strings.HasPrefix(c.Use, "update") 23 | })) 24 | 25 | cmd.LocalFlags().AddFlagSet(cmd.InheritedFlags()) 26 | cmd.Short = "Update info about available charts for all chart repositories." 27 | cmd.Long = "" 28 | cmd.Aliases = []string{} 29 | cli.SetSubCommandAnnotations(cmd, 40, repoCmdGroup) 30 | 31 | originalRunE := cmd.RunE 32 | cmd.RunE = func(cmd *cobra.Command, args []string) error { 33 | helmSettings := helm_v3.Settings 34 | 35 | ctx = log.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, log.DebugLevel, log.InfoLevel), log.SetupLoggingOptions{}) 36 | 37 | loader.NoChartLockWarning = "" 38 | 39 | if err := originalRunE(cmd, args); err != nil { 40 | return err 41 | } 42 | 43 | return nil 44 | } 45 | 46 | return cmd 47 | } 48 | -------------------------------------------------------------------------------- /cmd/nelm/chart_dependency_update.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/samber/lo" 8 | "github.com/spf13/cobra" 9 | 10 | helm_v3 "github.com/werf/3p-helm/cmd/helm" 11 | "github.com/werf/3p-helm/pkg/chart/loader" 12 | "github.com/werf/common-go/pkg/cli" 13 | "github.com/werf/nelm/pkg/log" 14 | ) 15 | 16 | func newChartDependencyUpdateCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 17 | dependencyCmd := lo.Must(lo.Find(helmRootCmd.Commands(), func(c *cobra.Command) bool { 18 | return strings.HasPrefix(c.Use, "dependency") 19 | })) 20 | 21 | cmd := lo.Must(lo.Find(dependencyCmd.Commands(), func(c *cobra.Command) bool { 22 | return strings.HasPrefix(c.Use, "update") 23 | })) 24 | 25 | cmd.LocalFlags().AddFlagSet(cmd.InheritedFlags()) 26 | cmd.Short = "Update Chart.lock and download chart dependencies." 27 | cmd.Aliases = []string{} 28 | cli.SetSubCommandAnnotations(cmd, 40, dependencyCmdGroup) 29 | 30 | originalRunE := cmd.RunE 31 | cmd.RunE = func(cmd *cobra.Command, args []string) error { 32 | helmSettings := helm_v3.Settings 33 | 34 | ctx = log.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, log.DebugLevel, log.InfoLevel), log.SetupLoggingOptions{}) 35 | 36 | loader.NoChartLockWarning = "" 37 | 38 | if err := originalRunE(cmd, args); err != nil { 39 | return err 40 | } 41 | 42 | return nil 43 | } 44 | 45 | return cmd 46 | } 47 | -------------------------------------------------------------------------------- /internal/resource/validate.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/samber/lo" 8 | 9 | "github.com/werf/nelm/internal/resource/spec" 10 | ) 11 | 12 | func ValidateLocal(releaseNamespace string, transformedResources []*InstallableResource) error { 13 | if err := validateNoDuplicates(releaseNamespace, transformedResources); err != nil { 14 | return fmt.Errorf("validate for no duplicated resources: %w", err) 15 | } 16 | 17 | return nil 18 | } 19 | 20 | func validateNoDuplicates(releaseNamespace string, transformedResources []*InstallableResource) error { 21 | for _, res := range transformedResources { 22 | if spec.IsReleaseNamespace(res.Unstruct.GetName(), res.Unstruct.GroupVersionKind(), releaseNamespace) { 23 | return fmt.Errorf("release namespace %q cannot be deployed as part of the release", res.Unstruct.GetName()) 24 | } 25 | } 26 | 27 | duplicates := lo.FindDuplicatesBy(transformedResources, func(instRes *InstallableResource) string { 28 | return instRes.ID() 29 | }) 30 | 31 | duplicates = lo.Filter(duplicates, func(instRes *InstallableResource, _ int) bool { 32 | return !spec.IsWebhook(instRes.GroupVersionKind.GroupKind()) 33 | }) 34 | 35 | if len(duplicates) == 0 { 36 | return nil 37 | } 38 | 39 | duplicatedIDHumans := lo.Map(duplicates, func(instRes *InstallableResource, _ int) string { 40 | return instRes.IDHuman() 41 | }) 42 | 43 | return fmt.Errorf("duplicated resources found: %s", strings.Join(duplicatedIDHumans, ", ")) 44 | } 45 | -------------------------------------------------------------------------------- /cmd/nelm/release.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/werf/common-go/pkg/cli" 9 | "github.com/werf/nelm/pkg/featgate" 10 | ) 11 | 12 | func newReleaseCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 13 | cmd := cli.NewGroupCommand( 14 | ctx, 15 | "release", 16 | "Manage Helm releases.", 17 | "Manage Helm releases.", 18 | releaseCmdGroup, 19 | cli.GroupCommandOptions{}, 20 | ) 21 | 22 | cmd.AddCommand(newReleaseInstallCommand(ctx, afterAllCommandsBuiltFuncs)) 23 | cmd.AddCommand(newReleaseRollbackCommand(ctx, afterAllCommandsBuiltFuncs)) 24 | 25 | if featgate.FeatGateNativeReleaseUninstall.Enabled() || featgate.FeatGatePreviewV2.Enabled() { 26 | cmd.AddCommand(newReleaseUninstallCommand(ctx, afterAllCommandsBuiltFuncs)) 27 | } else { 28 | cmd.AddCommand(newLegacyReleaseUninstallCommand(ctx, afterAllCommandsBuiltFuncs)) 29 | } 30 | 31 | cmd.AddCommand(newReleaseHistoryCommand(ctx, afterAllCommandsBuiltFuncs)) 32 | 33 | if featgate.FeatGateNativeReleaseList.Enabled() || featgate.FeatGatePreviewV2.Enabled() { 34 | cmd.AddCommand(newReleaseListCommand(ctx, afterAllCommandsBuiltFuncs)) 35 | } else { 36 | cmd.AddCommand(newLegacyReleaseListCommand(ctx, afterAllCommandsBuiltFuncs)) 37 | } 38 | 39 | cmd.AddCommand(newReleaseGetCommand(ctx, afterAllCommandsBuiltFuncs)) 40 | cmd.AddCommand(newPlanCommand(ctx, afterAllCommandsBuiltFuncs)) 41 | 42 | return cmd 43 | } 44 | -------------------------------------------------------------------------------- /internal/kube/legacy_client_getter.go: -------------------------------------------------------------------------------- 1 | package kube 2 | 3 | import ( 4 | "k8s.io/apimachinery/pkg/api/meta" 5 | "k8s.io/cli-runtime/pkg/genericclioptions" 6 | "k8s.io/client-go/discovery" 7 | "k8s.io/client-go/rest" 8 | "k8s.io/client-go/tools/clientcmd" 9 | ) 10 | 11 | var _ genericclioptions.RESTClientGetter = (*LegacyClientGetter)(nil) 12 | 13 | type LegacyClientGetter struct { 14 | discoveryClient discovery.CachedDiscoveryInterface 15 | mapper meta.ResettableRESTMapper 16 | restConfig *rest.Config 17 | legacyClientConfig clientcmd.ClientConfig 18 | } 19 | 20 | // TODO(v2): get rid 21 | func NewLegacyClientGetter(discoveryClient discovery.CachedDiscoveryInterface, mapper meta.ResettableRESTMapper, restConfig *rest.Config, legacyClientConfig clientcmd.ClientConfig) *LegacyClientGetter { 22 | return &LegacyClientGetter{ 23 | discoveryClient: discoveryClient, 24 | mapper: mapper, 25 | restConfig: restConfig, 26 | legacyClientConfig: legacyClientConfig, 27 | } 28 | } 29 | 30 | func (g *LegacyClientGetter) ToRESTConfig() (*rest.Config, error) { 31 | return g.restConfig, nil 32 | } 33 | 34 | func (g *LegacyClientGetter) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) { 35 | return g.discoveryClient, nil 36 | } 37 | 38 | func (g *LegacyClientGetter) ToRESTMapper() (meta.RESTMapper, error) { 39 | return g.mapper, nil 40 | } 41 | 42 | func (g *LegacyClientGetter) ToRawKubeConfigLoader() clientcmd.ClientConfig { 43 | return g.legacyClientConfig 44 | } 45 | -------------------------------------------------------------------------------- /cmd/nelm/chart_dependency_download.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/samber/lo" 8 | "github.com/spf13/cobra" 9 | 10 | helm_v3 "github.com/werf/3p-helm/cmd/helm" 11 | "github.com/werf/3p-helm/pkg/chart/loader" 12 | "github.com/werf/common-go/pkg/cli" 13 | "github.com/werf/nelm/pkg/log" 14 | ) 15 | 16 | func newChartDependencyDownloadCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 17 | dependencyCmd := lo.Must(lo.Find(helmRootCmd.Commands(), func(c *cobra.Command) bool { 18 | return strings.HasPrefix(c.Use, "dependency") 19 | })) 20 | 21 | cmd := lo.Must(lo.Find(dependencyCmd.Commands(), func(c *cobra.Command) bool { 22 | return strings.HasPrefix(c.Use, "build") 23 | })) 24 | 25 | cmd.LocalFlags().AddFlagSet(cmd.InheritedFlags()) 26 | cmd.Use = "download CHART" 27 | cmd.Short = "Download chart dependencies from Chart.lock." 28 | cmd.Long = "Download chart dependencies from Chart.lock." 29 | cmd.Aliases = []string{} 30 | cli.SetSubCommandAnnotations(cmd, 50, dependencyCmdGroup) 31 | 32 | originalRunE := cmd.RunE 33 | cmd.RunE = func(cmd *cobra.Command, args []string) error { 34 | helmSettings := helm_v3.Settings 35 | 36 | ctx = log.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, log.DebugLevel, log.InfoLevel), log.SetupLoggingOptions{}) 37 | 38 | loader.NoChartLockWarning = "" 39 | 40 | if err := originalRunE(cmd, args); err != nil { 41 | return err 42 | } 43 | 44 | return nil 45 | } 46 | 47 | return cmd 48 | } 49 | -------------------------------------------------------------------------------- /internal/kube/fake/client_discovery.go: -------------------------------------------------------------------------------- 1 | package fake 2 | 3 | import ( 4 | "github.com/chanced/caps" 5 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 6 | "k8s.io/client-go/discovery" 7 | discfake "k8s.io/client-go/discovery/fake" 8 | "k8s.io/client-go/kubernetes/scheme" 9 | "k8s.io/client-go/testing" 10 | ) 11 | 12 | var _ discovery.CachedDiscoveryInterface = (*CachedDiscoveryClient)(nil) 13 | 14 | type CachedDiscoveryClient struct { 15 | *discfake.FakeDiscovery 16 | } 17 | 18 | func NewCachedDiscoveryClient() (*CachedDiscoveryClient, error) { 19 | discClient := &discfake.FakeDiscovery{ 20 | Fake: &testing.Fake{}, 21 | } 22 | 23 | for _, gv := range scheme.Scheme.PreferredVersionAllGroups() { 24 | resourceList := &metav1.APIResourceList{ 25 | GroupVersion: gv.String(), 26 | } 27 | 28 | for kind := range scheme.Scheme.KnownTypes(gv) { 29 | resource := metav1.APIResource{ 30 | Name: caps.ToLower(kind) + "s", 31 | SingularName: caps.ToLower(kind), 32 | Namespaced: true, 33 | Group: gv.Group, 34 | Version: gv.Version, 35 | Kind: kind, 36 | Verbs: []string{"create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"}, 37 | } 38 | 39 | resourceList.APIResources = append(resourceList.APIResources, resource) 40 | } 41 | 42 | discClient.Resources = append(discClient.Resources, resourceList) 43 | } 44 | 45 | return &CachedDiscoveryClient{ 46 | FakeDiscovery: discClient, 47 | }, nil 48 | } 49 | 50 | func (c *CachedDiscoveryClient) Fresh() bool { 51 | return true 52 | } 53 | 54 | func (c *CachedDiscoveryClient) Invalidate() {} 55 | -------------------------------------------------------------------------------- /internal/legacy/deploy/stages_splitter.go: -------------------------------------------------------------------------------- 1 | package deploy 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strconv" 7 | 8 | "k8s.io/apimachinery/pkg/api/meta" 9 | "k8s.io/cli-runtime/pkg/resource" 10 | 11 | "github.com/werf/3p-helm/pkg/kube" 12 | "github.com/werf/3p-helm/pkg/phases/stages" 13 | ) 14 | 15 | var metadataAccessor = meta.NewAccessor() 16 | 17 | func NewStagesSplitter() *StagesSplitter { 18 | return &StagesSplitter{} 19 | } 20 | 21 | // TODO(v2): get rid 22 | type StagesSplitter struct{} 23 | 24 | func (s *StagesSplitter) Split(resources kube.ResourceList) (stages.SortedStageList, error) { 25 | stageList := stages.SortedStageList{} 26 | 27 | if err := resources.Visit(func(resInfo *resource.Info, err error) error { 28 | if err != nil { 29 | return err 30 | } 31 | 32 | annotations, err := metadataAccessor.Annotations(resInfo.Object) 33 | if err != nil { 34 | return fmt.Errorf("error getting annotations for object: %w", err) 35 | } 36 | 37 | var weight int 38 | if w, ok := annotations[StageWeightAnnoName]; ok { 39 | weight, err = strconv.Atoi(w) 40 | if err != nil { 41 | return fmt.Errorf("error parsing annotation \"%s: %s\" — value should be an integer: %w", StageWeightAnnoName, w, err) 42 | } 43 | } 44 | 45 | stage := stageList.StageByWeight(weight) 46 | 47 | if stage == nil { 48 | stage = &stages.Stage{ 49 | Weight: weight, 50 | } 51 | stageList = append(stageList, stage) 52 | } 53 | 54 | stage.DesiredResources.Append(resInfo) 55 | 56 | return nil 57 | }); err != nil { 58 | return nil, fmt.Errorf("error visiting resources list: %w", err) 59 | } 60 | 61 | sort.Sort(stageList) 62 | 63 | return stageList, nil 64 | } 65 | -------------------------------------------------------------------------------- /internal/util/diff.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/aymanbagabas/go-udiff" 7 | "github.com/aymanbagabas/go-udiff/myers" 8 | "github.com/gookit/color" 9 | "github.com/samber/lo" 10 | ) 11 | 12 | func ColoredUnifiedDiff(from, to string, diffContextLines int) string { 13 | edits := myers.ComputeEdits(from, to) 14 | if len(edits) == 0 { 15 | return "" 16 | } 17 | 18 | uncoloredUDiff := lo.Must1(udiff.ToUnified("", "", from, edits, diffContextLines)) 19 | 20 | var ( 21 | uDiffLines []string 22 | firstHunkHeaderStripped bool 23 | ) 24 | 25 | lines := strings.Split(uncoloredUDiff, "\n") 26 | for i, line := range lines { 27 | if strings.HasPrefix(line, "--- ") || strings.HasPrefix(line, "+++ ") || (i == len(lines)-1 && strings.TrimSpace(line) == "") { 28 | continue 29 | } 30 | 31 | if strings.HasPrefix(line, "@@ ") && strings.HasSuffix(line, " @@") { 32 | if !firstHunkHeaderStripped { 33 | firstHunkHeaderStripped = true 34 | continue 35 | } 36 | 37 | uDiffLines = append(uDiffLines, color.Gray.Renderln(" ...")) 38 | } else if strings.HasPrefix(line, "+") { 39 | uDiffLines = append(uDiffLines, color.Green.Renderln(line[:1]+" "+line[1:])) 40 | } else if strings.HasPrefix(line, "-") { 41 | uDiffLines = append(uDiffLines, color.Red.Renderln(line[:1]+" "+line[1:])) 42 | } else if strings.TrimSpace(line) == "" { 43 | uDiffLines = append(uDiffLines, color.Gray.Renderln(line)) 44 | } else { 45 | uDiffLines = append(uDiffLines, color.Gray.Renderln(" "+line)) 46 | } 47 | } 48 | 49 | if len(uDiffLines) == 0 { 50 | return "" 51 | } 52 | 53 | return strings.Trim(strings.Join(uDiffLines, "\n"), "\n") 54 | } 55 | -------------------------------------------------------------------------------- /.github/workflows/tag_auto-create.yml: -------------------------------------------------------------------------------- 1 | name: tag:auto-create 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - "[0-9]+.[0-9]+.[0-9]+*" 8 | - "[0-9]+.[0-9]+" 9 | - "[0-9]+" 10 | paths: 11 | - CHANGELOG.md 12 | 13 | jobs: 14 | release: 15 | name: Create release tag 16 | runs-on: ubuntu-22.04 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | fetch-tags: true 23 | 24 | - name: Relabel closed release PR 25 | env: 26 | GH_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN }} 27 | run: | 28 | PR_NUMBER=$(gh pr list --state closed --label "autorelease: pending" --limit 1 --json number -q '.[0].number') 29 | gh pr edit $PR_NUMBER --remove-label "autorelease: pending" 30 | gh pr edit $PR_NUMBER --add-label "autorelease: tagged" 31 | 32 | - name: Get version from CHANGELOG.md 33 | id: get_version 34 | run: | 35 | VERSION=$(grep -m1 '^#\+ \[[0-9]\+\.[0-9]\+\.[0-9]\+\]' CHANGELOG.md | sed -E 's/^#+ \[([0-9]+\.[0-9]+\.[0-9]+)\].*/\1/') 36 | echo "version=$VERSION" >> $GITHUB_OUTPUT 37 | 38 | - name: Create tag via GitHub API 39 | env: 40 | GH_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN }} 41 | run: | 42 | TAG="v${{ steps.get_version.outputs.version }}" 43 | 44 | if gh api repos/${{ github.repository }}/git/ref/tags/$TAG &>/dev/null; then 45 | echo "Tag $TAG already exists. Skipping..." 46 | exit 0 47 | fi 48 | 49 | COMMIT_SHA=$(git rev-parse HEAD) 50 | 51 | gh api repos/${{ github.repository }}/git/refs \ 52 | -f ref="refs/tags/$TAG" \ 53 | -f sha="$COMMIT_SHA" 54 | -------------------------------------------------------------------------------- /pkg/action/secret_file_edit.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/samber/lo" 9 | 10 | "github.com/werf/common-go/pkg/secrets_manager" 11 | "github.com/werf/nelm/pkg/legacy/secret" 12 | "github.com/werf/nelm/pkg/log" 13 | ) 14 | 15 | const ( 16 | DefaultSecretFileEditLogLevel = log.ErrorLevel 17 | ) 18 | 19 | type SecretFileEditOptions struct { 20 | SecretKey string 21 | SecretWorkDir string 22 | TempDirPath string 23 | } 24 | 25 | func SecretFileEdit(ctx context.Context, filePath string, opts SecretFileEditOptions) error { 26 | currentDir, err := os.Getwd() 27 | if err != nil { 28 | return fmt.Errorf("get current working directory: %w", err) 29 | } 30 | 31 | opts, err = applySecretFileEditOptionsDefaults(opts, currentDir) 32 | if err != nil { 33 | return fmt.Errorf("build secret file edit options: %w", err) 34 | } 35 | 36 | if opts.SecretKey != "" { 37 | lo.Must0(os.Setenv("WERF_SECRET_KEY", opts.SecretKey)) 38 | } 39 | 40 | if err := secret.SecretEdit(ctx, secrets_manager.Manager, opts.SecretWorkDir, opts.TempDirPath, filePath, false); err != nil { 41 | return fmt.Errorf("secret edit: %w", err) 42 | } 43 | 44 | return nil 45 | } 46 | 47 | func applySecretFileEditOptionsDefaults(opts SecretFileEditOptions, currentDir string) (SecretFileEditOptions, error) { 48 | var err error 49 | if opts.TempDirPath == "" { 50 | opts.TempDirPath, err = os.MkdirTemp("", "") 51 | if err != nil { 52 | return SecretFileEditOptions{}, fmt.Errorf("create temp dir: %w", err) 53 | } 54 | } 55 | 56 | if opts.SecretWorkDir == "" { 57 | var err error 58 | 59 | opts.SecretWorkDir, err = os.Getwd() 60 | if err != nil { 61 | return SecretFileEditOptions{}, fmt.Errorf("get current working directory: %w", err) 62 | } 63 | } 64 | 65 | return opts, nil 66 | } 67 | -------------------------------------------------------------------------------- /.github/workflows/issues_delayed-auto-close.yml: -------------------------------------------------------------------------------- 1 | name: issues:delayed-auto-close 2 | 3 | on: 4 | schedule: 5 | - cron: "0 8 * * *" 6 | issue_comment: 7 | types: 8 | - created 9 | issues: 10 | types: 11 | - labeled 12 | pull_request_target: 13 | types: 14 | - labeled 15 | repository_dispatch: 16 | types: ["issues:delayed-auto-close"] 17 | workflow_dispatch: 18 | 19 | jobs: 20 | manage: 21 | runs-on: ubuntu-22.04 22 | timeout-minutes: 20 23 | steps: 24 | - uses: tiangolo/issue-manager@0.4.0 25 | with: 26 | token: ${{ secrets.GITHUB_TOKEN }} 27 | config: > 28 | { 29 | "solved": { 30 | "delay": "604800", 31 | "message": "This issue appears to be resolved, so we’re closing it for now. If you encounter further issues, please feel free to reopen or create a new issue at any time.", 32 | "remove_label_on_comment": true, 33 | "remove_label_on_close": true 34 | }, 35 | "awaiting response": { 36 | "delay": "604800", 37 | "message": "As we haven’t received additional information, we’re closing this issue for now. If there’s more to add, feel free to reopen or open a new issue whenever needed.", 38 | "remove_label_on_comment": true, 39 | "remove_label_on_close": true 40 | } 41 | } 42 | 43 | notify: 44 | if: github.event_name == 'schedule' && always() 45 | needs: manage 46 | uses: werf/common-ci/.github/workflows/notification.yml@main 47 | secrets: 48 | loopNotificationGroup: ${{ vars.LOOP_NOTIFICATION_GROUP }} 49 | webhook: ${{ secrets.LOOP_NOTIFICATION_WEBHOOK }} 50 | notificationChannel: ${{ vars.LOOP_NOTIFICATION_CHANNEL }} 51 | -------------------------------------------------------------------------------- /internal/resource/spec/resource_match.go: -------------------------------------------------------------------------------- 1 | package spec 2 | 3 | type ResourceMatcher struct { 4 | Names []string 5 | Namespaces []string 6 | Groups []string 7 | Versions []string 8 | Kinds []string 9 | } 10 | 11 | func (s *ResourceMatcher) Match(resMeta *ResourceMeta) bool { 12 | var nameMatch bool 13 | if len(s.Names) == 0 { 14 | nameMatch = true 15 | } else { 16 | for _, name := range s.Names { 17 | if resMeta.Name == name { 18 | nameMatch = true 19 | break 20 | } 21 | } 22 | } 23 | 24 | if !nameMatch { 25 | return false 26 | } 27 | 28 | var namespaceMatch bool 29 | if len(s.Namespaces) == 0 { 30 | namespaceMatch = true 31 | } else { 32 | for _, namespace := range s.Namespaces { 33 | if resMeta.Namespace == namespace { 34 | namespaceMatch = true 35 | break 36 | } 37 | } 38 | } 39 | 40 | if !namespaceMatch { 41 | return false 42 | } 43 | 44 | var groupMatch bool 45 | if len(s.Groups) == 0 { 46 | groupMatch = true 47 | } else { 48 | for _, group := range s.Groups { 49 | if resMeta.GroupVersionKind.Group == group { 50 | groupMatch = true 51 | break 52 | } 53 | } 54 | } 55 | 56 | if !groupMatch { 57 | return false 58 | } 59 | 60 | var versionMatch bool 61 | if len(s.Versions) == 0 { 62 | versionMatch = true 63 | } else { 64 | for _, version := range s.Versions { 65 | if resMeta.GroupVersionKind.Version == version { 66 | versionMatch = true 67 | break 68 | } 69 | } 70 | } 71 | 72 | if !versionMatch { 73 | return false 74 | } 75 | 76 | var kindMatch bool 77 | if len(s.Kinds) == 0 { 78 | kindMatch = true 79 | } else { 80 | for _, kind := range s.Kinds { 81 | if resMeta.GroupVersionKind.Kind == kind { 82 | kindMatch = true 83 | break 84 | } 85 | } 86 | } 87 | 88 | if !kindMatch { 89 | return false 90 | } 91 | 92 | return true 93 | } 94 | -------------------------------------------------------------------------------- /internal/test/comparer.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "regexp" 5 | 6 | "github.com/davecgh/go-spew/spew" 7 | "github.com/dominikbraun/graph" 8 | "github.com/google/go-cmp/cmp" 9 | "github.com/google/go-cmp/cmp/cmpopts" 10 | "github.com/samber/lo" 11 | 12 | "github.com/werf/nelm/internal/resource" 13 | ) 14 | 15 | func CompareResourceMetadataOption(releaseNamespace string) cmp.Option { 16 | return cmp.FilterPath(func(p cmp.Path) bool { 17 | if len(p) < 2 { 18 | return false 19 | } 20 | 21 | return p.Index(len(p)-2).String() == ".Object" && p.Index(len(p)-1).String() == `["metadata"]` 22 | }, cmp.Transformer("CleanMetadata", func(m interface{}) interface{} { 23 | metadata, ok := m.(map[string]interface{}) 24 | if !ok { 25 | return m 26 | } 27 | 28 | cleanMetadata := lo.PickByKeys(metadata, []string{ 29 | "name", "namespace", "labels", "annotations", 30 | }) 31 | 32 | if ns, ok := cleanMetadata["namespace"]; ok { 33 | if ns == releaseNamespace || ns == "" { 34 | delete(cleanMetadata, "namespace") 35 | } 36 | } 37 | 38 | return cleanMetadata 39 | })) 40 | } 41 | 42 | func CompareInternalDependencyOption() cmp.Option { 43 | sp := &spew.ConfigState{ 44 | Indent: " ", 45 | DisablePointerAddresses: true, 46 | DisableCapacities: true, 47 | SortKeys: true, 48 | SpewKeys: true, 49 | } 50 | 51 | return cmpopts.SortSlices(func(a, b *resource.InternalDependency) bool { 52 | return sp.Sdump(a) < sp.Sdump(b) 53 | }) 54 | } 55 | 56 | func CompareRegexpOption() cmp.Option { 57 | return cmp.Comparer(func(a, b *regexp.Regexp) bool { 58 | if a == nil || b == nil { 59 | return a == b 60 | } 61 | 62 | return a.String() == b.String() 63 | }) 64 | } 65 | 66 | func IgnoreEdgeOption() cmp.Option { 67 | return cmp.FilterValues(func(x, y graph.Edge[string]) bool { return true }, cmp.Ignore()) 68 | } 69 | -------------------------------------------------------------------------------- /pkg/action/secret_file_decrypt.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/samber/lo" 9 | 10 | "github.com/werf/common-go/pkg/secrets_manager" 11 | "github.com/werf/nelm/pkg/legacy/secret" 12 | "github.com/werf/nelm/pkg/log" 13 | ) 14 | 15 | const ( 16 | DefaultSecretFileDecryptLogLevel = log.ErrorLevel 17 | ) 18 | 19 | type SecretFileDecryptOptions struct { 20 | OutputFilePath string 21 | SecretKey string 22 | SecretWorkDir string 23 | TempDirPath string 24 | } 25 | 26 | func SecretFileDecrypt(ctx context.Context, filePath string, opts SecretFileDecryptOptions) error { 27 | currentDir, err := os.Getwd() 28 | if err != nil { 29 | return fmt.Errorf("get current working directory: %w", err) 30 | } 31 | 32 | opts, err = applySecretFileDecryptOptionsDefaults(opts, currentDir) 33 | if err != nil { 34 | return fmt.Errorf("build secret file decrypt options: %w", err) 35 | } 36 | 37 | if opts.SecretKey != "" { 38 | lo.Must0(os.Setenv("WERF_SECRET_KEY", opts.SecretKey)) 39 | } 40 | 41 | if err := secret.SecretFileDecrypt(ctx, secrets_manager.Manager, opts.SecretWorkDir, filePath, opts.OutputFilePath); err != nil { 42 | return fmt.Errorf("secret file decrypt: %w", err) 43 | } 44 | 45 | return nil 46 | } 47 | 48 | func applySecretFileDecryptOptionsDefaults(opts SecretFileDecryptOptions, currentDir string) (SecretFileDecryptOptions, error) { 49 | var err error 50 | if opts.TempDirPath == "" { 51 | opts.TempDirPath, err = os.MkdirTemp("", "") 52 | if err != nil { 53 | return SecretFileDecryptOptions{}, fmt.Errorf("create temp dir: %w", err) 54 | } 55 | } 56 | 57 | if opts.SecretWorkDir == "" { 58 | var err error 59 | 60 | opts.SecretWorkDir, err = os.Getwd() 61 | if err != nil { 62 | return SecretFileDecryptOptions{}, fmt.Errorf("get current working directory: %w", err) 63 | } 64 | } 65 | 66 | return opts, nil 67 | } 68 | -------------------------------------------------------------------------------- /pkg/action/secret_file_encrypt.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/samber/lo" 9 | 10 | "github.com/werf/common-go/pkg/secrets_manager" 11 | "github.com/werf/nelm/pkg/legacy/secret" 12 | "github.com/werf/nelm/pkg/log" 13 | ) 14 | 15 | const ( 16 | DefaultSecretFileEncryptLogLevel = log.ErrorLevel 17 | ) 18 | 19 | type SecretFileEncryptOptions struct { 20 | OutputFilePath string 21 | SecretKey string 22 | SecretWorkDir string 23 | TempDirPath string 24 | } 25 | 26 | func SecretFileEncrypt(ctx context.Context, filePath string, opts SecretFileEncryptOptions) error { 27 | currentDir, err := os.Getwd() 28 | if err != nil { 29 | return fmt.Errorf("get current working directory: %w", err) 30 | } 31 | 32 | opts, err = applySecretFileEncryptOptionsDefaults(opts, currentDir) 33 | if err != nil { 34 | return fmt.Errorf("build secret file encrypt options: %w", err) 35 | } 36 | 37 | if opts.SecretKey != "" { 38 | lo.Must0(os.Setenv("WERF_SECRET_KEY", opts.SecretKey)) 39 | } 40 | 41 | if err := secret.SecretFileEncrypt(ctx, secrets_manager.Manager, opts.SecretWorkDir, filePath, opts.OutputFilePath); err != nil { 42 | return fmt.Errorf("secret file encrypt: %w", err) 43 | } 44 | 45 | return nil 46 | } 47 | 48 | func applySecretFileEncryptOptionsDefaults(opts SecretFileEncryptOptions, currentDir string) (SecretFileEncryptOptions, error) { 49 | var err error 50 | if opts.TempDirPath == "" { 51 | opts.TempDirPath, err = os.MkdirTemp("", "") 52 | if err != nil { 53 | return SecretFileEncryptOptions{}, fmt.Errorf("create temp dir: %w", err) 54 | } 55 | } 56 | 57 | if opts.SecretWorkDir == "" { 58 | var err error 59 | 60 | opts.SecretWorkDir, err = os.Getwd() 61 | if err != nil { 62 | return SecretFileEncryptOptions{}, fmt.Errorf("get current working directory: %w", err) 63 | } 64 | } 65 | 66 | return opts, nil 67 | } 68 | -------------------------------------------------------------------------------- /pkg/action/secret_values_file_edit.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/samber/lo" 9 | 10 | "github.com/werf/common-go/pkg/secrets_manager" 11 | "github.com/werf/nelm/pkg/legacy/secret" 12 | "github.com/werf/nelm/pkg/log" 13 | ) 14 | 15 | const ( 16 | DefaultSecretValuesFileEditLogLevel = log.ErrorLevel 17 | ) 18 | 19 | type SecretValuesFileEditOptions struct { 20 | SecretKey string 21 | SecretWorkDir string 22 | TempDirPath string 23 | } 24 | 25 | func SecretValuesFileEdit(ctx context.Context, valuesFilePath string, opts SecretValuesFileEditOptions) error { 26 | currentDir, err := os.Getwd() 27 | if err != nil { 28 | return fmt.Errorf("get current working directory: %w", err) 29 | } 30 | 31 | opts, err = applySecretValuesFileEditOptionsDefaults(opts, currentDir) 32 | if err != nil { 33 | return fmt.Errorf("build secret values file edit options: %w", err) 34 | } 35 | 36 | if opts.SecretKey != "" { 37 | lo.Must0(os.Setenv("WERF_SECRET_KEY", opts.SecretKey)) 38 | } 39 | 40 | if err := secret.SecretEdit(ctx, secrets_manager.Manager, opts.SecretWorkDir, opts.TempDirPath, valuesFilePath, true); err != nil { 41 | return fmt.Errorf("secret edit: %w", err) 42 | } 43 | 44 | return nil 45 | } 46 | 47 | func applySecretValuesFileEditOptionsDefaults(opts SecretValuesFileEditOptions, currentDir string) (SecretValuesFileEditOptions, error) { 48 | var err error 49 | if opts.TempDirPath == "" { 50 | opts.TempDirPath, err = os.MkdirTemp("", "") 51 | if err != nil { 52 | return SecretValuesFileEditOptions{}, fmt.Errorf("create temp dir: %w", err) 53 | } 54 | } 55 | 56 | if opts.SecretWorkDir == "" { 57 | var err error 58 | 59 | opts.SecretWorkDir, err = os.Getwd() 60 | if err != nil { 61 | return SecretValuesFileEditOptions{}, fmt.Errorf("get current working directory: %w", err) 62 | } 63 | } 64 | 65 | return opts, nil 66 | } 67 | -------------------------------------------------------------------------------- /pkg/action/secret_values_file_decrypt.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/samber/lo" 9 | 10 | "github.com/werf/common-go/pkg/secrets_manager" 11 | "github.com/werf/nelm/pkg/legacy/secret" 12 | "github.com/werf/nelm/pkg/log" 13 | ) 14 | 15 | const ( 16 | DefaultSecretValuesFileDecryptLogLevel = log.ErrorLevel 17 | ) 18 | 19 | type SecretValuesFileDecryptOptions struct { 20 | OutputFilePath string 21 | SecretKey string 22 | SecretWorkDir string 23 | TempDirPath string 24 | } 25 | 26 | func SecretValuesFileDecrypt(ctx context.Context, valuesFilePath string, opts SecretValuesFileDecryptOptions) error { 27 | currentDir, err := os.Getwd() 28 | if err != nil { 29 | return fmt.Errorf("get current working directory: %w", err) 30 | } 31 | 32 | opts, err = applySecretValuesFileDecryptOptionsDefaults(opts, currentDir) 33 | if err != nil { 34 | return fmt.Errorf("build secret values file decrypt options: %w", err) 35 | } 36 | 37 | if opts.SecretKey != "" { 38 | lo.Must0(os.Setenv("WERF_SECRET_KEY", opts.SecretKey)) 39 | } 40 | 41 | if err := secret.SecretValuesDecrypt(ctx, secrets_manager.Manager, opts.SecretWorkDir, valuesFilePath, opts.OutputFilePath); err != nil { 42 | return fmt.Errorf("secret values decrypt: %w", err) 43 | } 44 | 45 | return nil 46 | } 47 | 48 | func applySecretValuesFileDecryptOptionsDefaults(opts SecretValuesFileDecryptOptions, currentDir string) (SecretValuesFileDecryptOptions, error) { 49 | var err error 50 | if opts.TempDirPath == "" { 51 | opts.TempDirPath, err = os.MkdirTemp("", "") 52 | if err != nil { 53 | return SecretValuesFileDecryptOptions{}, fmt.Errorf("create temp dir: %w", err) 54 | } 55 | } 56 | 57 | if opts.SecretWorkDir == "" { 58 | var err error 59 | 60 | opts.SecretWorkDir, err = os.Getwd() 61 | if err != nil { 62 | return SecretValuesFileDecryptOptions{}, fmt.Errorf("get current working directory: %w", err) 63 | } 64 | } 65 | 66 | return opts, nil 67 | } 68 | -------------------------------------------------------------------------------- /pkg/action/secret_values_file_encrypt.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/samber/lo" 9 | 10 | "github.com/werf/common-go/pkg/secrets_manager" 11 | "github.com/werf/nelm/pkg/legacy/secret" 12 | "github.com/werf/nelm/pkg/log" 13 | ) 14 | 15 | const ( 16 | DefaultSecretValuesFileEncryptLogLevel = log.ErrorLevel 17 | ) 18 | 19 | type SecretValuesFileEncryptOptions struct { 20 | OutputFilePath string 21 | SecretKey string 22 | SecretWorkDir string 23 | TempDirPath string 24 | } 25 | 26 | func SecretValuesFileEncrypt(ctx context.Context, valuesFilePath string, opts SecretValuesFileEncryptOptions) error { 27 | currentDir, err := os.Getwd() 28 | if err != nil { 29 | return fmt.Errorf("get current working directory: %w", err) 30 | } 31 | 32 | opts, err = applySecretValuesFileEncryptOptionsDefaults(opts, currentDir) 33 | if err != nil { 34 | return fmt.Errorf("build secret values file encrypt options: %w", err) 35 | } 36 | 37 | if opts.SecretKey != "" { 38 | lo.Must0(os.Setenv("WERF_SECRET_KEY", opts.SecretKey)) 39 | } 40 | 41 | if err := secret.SecretValuesEncrypt(ctx, secrets_manager.Manager, opts.SecretWorkDir, valuesFilePath, opts.OutputFilePath); err != nil { 42 | return fmt.Errorf("secret values encrypt: %w", err) 43 | } 44 | 45 | return nil 46 | } 47 | 48 | func applySecretValuesFileEncryptOptionsDefaults(opts SecretValuesFileEncryptOptions, currentDir string) (SecretValuesFileEncryptOptions, error) { 49 | var err error 50 | if opts.TempDirPath == "" { 51 | opts.TempDirPath, err = os.MkdirTemp("", "") 52 | if err != nil { 53 | return SecretValuesFileEncryptOptions{}, fmt.Errorf("create temp dir: %w", err) 54 | } 55 | } 56 | 57 | if opts.SecretWorkDir == "" { 58 | var err error 59 | 60 | opts.SecretWorkDir, err = os.Getwd() 61 | if err != nil { 62 | return SecretValuesFileEncryptOptions{}, fmt.Errorf("get current working directory: %w", err) 63 | } 64 | } 65 | 66 | return opts, nil 67 | } 68 | -------------------------------------------------------------------------------- /pkg/action/secret_key_rotate.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/samber/lo" 9 | 10 | "github.com/werf/nelm/pkg/legacy/secret" 11 | "github.com/werf/nelm/pkg/log" 12 | ) 13 | 14 | const ( 15 | DefaultSecretKeyRotateLogLevel = log.InfoLevel 16 | ) 17 | 18 | type SecretKeyRotateOptions struct { 19 | ChartDirPath string 20 | NewSecretKey string 21 | OldSecretKey string 22 | SecretValuesFiles []string 23 | SecretWorkDir string 24 | TempDirPath string 25 | } 26 | 27 | func SecretKeyRotate(ctx context.Context, opts SecretKeyRotateOptions) error { 28 | currentDir, err := os.Getwd() 29 | if err != nil { 30 | return fmt.Errorf("get current working directory: %w", err) 31 | } 32 | 33 | opts, err = applySecretKeyRotateOptionsDefaults(opts, currentDir) 34 | if err != nil { 35 | return fmt.Errorf("build secret key rotate options: %w", err) 36 | } 37 | 38 | if opts.OldSecretKey != "" { 39 | lo.Must0(os.Setenv("WERF_OLD_SECRET_KEY", opts.OldSecretKey)) 40 | } 41 | 42 | if opts.NewSecretKey != "" { 43 | lo.Must0(os.Setenv("WERF_SECRET_KEY", opts.NewSecretKey)) 44 | } 45 | 46 | if err := secret.RotateSecretKey(ctx, opts.ChartDirPath, opts.SecretWorkDir, opts.SecretValuesFiles...); err != nil { 47 | return fmt.Errorf("rotate secret key: %w", err) 48 | } 49 | 50 | return nil 51 | } 52 | 53 | func applySecretKeyRotateOptionsDefaults(opts SecretKeyRotateOptions, currentDir string) (SecretKeyRotateOptions, error) { 54 | var err error 55 | if opts.TempDirPath == "" { 56 | opts.TempDirPath, err = os.MkdirTemp("", "") 57 | if err != nil { 58 | return SecretKeyRotateOptions{}, fmt.Errorf("create temp dir: %w", err) 59 | } 60 | } 61 | 62 | if opts.ChartDirPath == "" { 63 | opts.ChartDirPath = currentDir 64 | } 65 | 66 | if opts.SecretWorkDir == "" { 67 | var err error 68 | 69 | opts.SecretWorkDir, err = os.Getwd() 70 | if err != nil { 71 | return SecretKeyRotateOptions{}, fmt.Errorf("get current working directory: %w", err) 72 | } 73 | } 74 | 75 | return opts, nil 76 | } 77 | -------------------------------------------------------------------------------- /pkg/log/logger.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type Logger interface { 8 | Trace(ctx context.Context, format string, a ...interface{}) 9 | TraceStruct(ctx context.Context, obj interface{}, format string, a ...interface{}) 10 | TracePush(ctx context.Context, group, format string, a ...interface{}) 11 | TracePop(ctx context.Context, group string) 12 | Debug(ctx context.Context, format string, a ...interface{}) 13 | DebugPush(ctx context.Context, group, format string, a ...interface{}) 14 | DebugPop(ctx context.Context, group string) 15 | Info(ctx context.Context, format string, a ...interface{}) 16 | InfoPush(ctx context.Context, group, format string, a ...interface{}) 17 | InfoPop(ctx context.Context, group string) 18 | Warn(ctx context.Context, format string, a ...interface{}) 19 | WarnPush(ctx context.Context, group, format string, a ...interface{}) 20 | WarnPop(ctx context.Context, group string) 21 | Error(ctx context.Context, format string, a ...interface{}) 22 | ErrorPush(ctx context.Context, group, format string, a ...interface{}) 23 | ErrorPop(ctx context.Context, group string) 24 | InfoBlock(ctx context.Context, opts BlockOptions, fn func()) 25 | InfoBlockErr(ctx context.Context, opts BlockOptions, fn func() error) error 26 | BlockContentWidth(ctx context.Context) int 27 | SetLevel(ctx context.Context, lvl Level) 28 | Level(ctx context.Context) Level 29 | AcceptLevel(ctx context.Context, lvl Level) bool 30 | } 31 | 32 | type Level string 33 | 34 | const ( 35 | SilentLevel Level = "silent" 36 | ErrorLevel Level = "error" 37 | WarningLevel Level = "warning" 38 | InfoLevel Level = "info" 39 | DebugLevel Level = "debug" 40 | TraceLevel Level = "trace" 41 | ) 42 | 43 | var Levels = []Level{SilentLevel, ErrorLevel, WarningLevel, InfoLevel, DebugLevel, TraceLevel} 44 | 45 | type BlockOptions struct { 46 | BlockTitle string 47 | } 48 | 49 | const ( 50 | LogColorModeAuto = "auto" 51 | LogColorModeOff = "off" 52 | LogColorModeOn = "on" 53 | ) 54 | 55 | var LogColorModes = []string{LogColorModeAuto, LogColorModeOff, LogColorModeOn} 56 | -------------------------------------------------------------------------------- /.github/workflows/_test_unit.yml: -------------------------------------------------------------------------------- 1 | name: xxxxx(internal) 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | packages: 7 | description: Comma-separated package paths to test 8 | type: string 9 | excludePackages: 10 | description: Comma-separated package paths to exclude from testing 11 | type: string 12 | coverage: 13 | default: false 14 | type: string 15 | forceSkip: 16 | default: false 17 | type: string 18 | 19 | defaults: 20 | run: 21 | shell: bash 22 | 23 | env: 24 | DEBIAN_FRONTEND: "noninteractive" 25 | 26 | jobs: 27 | _: 28 | if: inputs.forceSkip == 'false' 29 | runs-on: ubuntu-22.04 30 | timeout-minutes: 60 31 | steps: 32 | - name: Checkout code 33 | uses: actions/checkout@v4 34 | 35 | - name: Set up Go 36 | uses: actions/setup-go@v6 37 | with: 38 | go-version-file: go.mod 39 | 40 | - name: Install Task 41 | uses: arduino/setup-task@v2 42 | with: 43 | repo-token: ${{ secrets.GITHUB_TOKEN }} 44 | 45 | - name: Set up git config 46 | uses: ./.github/actions/set-up-git-config 47 | 48 | # TODO: don't build ginkgo everytime? We need distributable binaries 49 | - name: Install ginkgo 50 | run: task -p deps:install:ginkgo 51 | 52 | - name: Prepare coverage dir 53 | if: ${{ inputs.coverage }} 54 | run: mkdir -p "$GITHUB_WORKSPACE/coverage" 55 | 56 | - name: Test 57 | run: | 58 | if ${{ inputs.coverage }}; then 59 | task -p test:unit paths="$(echo ${{ inputs.packages }} | tr , ' ')" -- --coverprofile="$(openssl rand -hex 6)-coverage.out" --keep-going --skip-package '${{ inputs.excludePackages }}' 60 | mv *-coverage.out "$GITHUB_WORKSPACE/coverage/" 61 | else 62 | task -p test:unit paths="$(echo ${{ inputs.packages }} | tr , ' ')" -- --keep-going --skip-package '${{ inputs.excludePackages }}' 63 | fi 64 | echo loadavg: $(cat /proc/loadavg) 65 | 66 | - name: Upload coverage artifact 67 | uses: ./.github/actions/upload-coverage-artifact 68 | with: 69 | coverage: ${{ inputs.coverage }} 70 | -------------------------------------------------------------------------------- /internal/release/release_storage.go: -------------------------------------------------------------------------------- 1 | package release 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "k8s.io/client-go/kubernetes" 8 | 9 | helmaction "github.com/werf/3p-helm/pkg/action" 10 | helmrelease "github.com/werf/3p-helm/pkg/release" 11 | helmstorage "github.com/werf/3p-helm/pkg/storage" 12 | helmdriver "github.com/werf/3p-helm/pkg/storage/driver" 13 | "github.com/werf/nelm/internal/kube" 14 | "github.com/werf/nelm/pkg/log" 15 | ) 16 | 17 | var _ ReleaseStorager = (*helmstorage.Storage)(nil) 18 | 19 | type ReleaseStorager interface { 20 | Create(rls *helmrelease.Release) error 21 | Update(rls *helmrelease.Release) error 22 | Delete(name string, version int) (*helmrelease.Release, error) 23 | Query(labels map[string]string) ([]*helmrelease.Release, error) 24 | } 25 | 26 | type ReleaseStorageOptions struct { 27 | HistoryLimit int 28 | SQLConnection string 29 | } 30 | 31 | func NewReleaseStorage(ctx context.Context, namespace, storageDriver string, clientFactory kube.ClientFactorier, opts ReleaseStorageOptions) (*helmstorage.Storage, error) { 32 | var storage *helmstorage.Storage 33 | 34 | lazyClient := helmaction.NewLazyClient(namespace, func() (*kubernetes.Clientset, error) { 35 | return clientFactory.Static().(*kubernetes.Clientset), nil 36 | }) 37 | 38 | logFn := func(format string, a ...interface{}) { 39 | log.Default.Debug(ctx, format, a...) 40 | } 41 | 42 | switch storageDriver { 43 | case "secret", "secrets", "": 44 | driver := helmdriver.NewSecrets(helmaction.NewSecretClient(lazyClient)) 45 | driver.Log = logFn 46 | 47 | storage = helmstorage.Init(driver) 48 | case "configmap", "configmaps": 49 | driver := helmdriver.NewConfigMaps(helmaction.NewConfigMapClient(lazyClient)) 50 | driver.Log = logFn 51 | 52 | storage = helmstorage.Init(driver) 53 | case "memory": 54 | driver := helmdriver.NewMemory() 55 | driver.SetNamespace(namespace) 56 | 57 | storage = helmstorage.Init(driver) 58 | case "sql": 59 | driver, err := helmdriver.NewSQL(opts.SQLConnection, logFn, namespace) 60 | if err != nil { 61 | return nil, fmt.Errorf("construct sql driver: %w", err) 62 | } 63 | 64 | storage = helmstorage.Init(driver) 65 | default: 66 | panic(fmt.Sprintf("Unknown storage driver: %s", storageDriver)) 67 | } 68 | 69 | storage.MaxHistory = opts.HistoryLimit 70 | 71 | return storage, nil 72 | } 73 | -------------------------------------------------------------------------------- /cmd/nelm/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "runtime" 8 | "strings" 9 | "time" 10 | 11 | "github.com/chanced/caps" 12 | "github.com/pkg/errors" 13 | "github.com/samber/lo" 14 | "github.com/spf13/cobra" 15 | 16 | helm_v3 "github.com/werf/3p-helm/cmd/helm" 17 | "github.com/werf/common-go/pkg/cli" 18 | "github.com/werf/logboek" 19 | "github.com/werf/nelm/pkg/action" 20 | "github.com/werf/nelm/pkg/common" 21 | "github.com/werf/nelm/pkg/featgate" 22 | "github.com/werf/nelm/pkg/log" 23 | ) 24 | 25 | func main() { 26 | if featgate.FeatGatePeriodicStackTraces.Enabled() { 27 | periodicStackTraces() 28 | } 29 | 30 | ctx := logboek.NewContext(context.Background(), logboek.DefaultLogger()) 31 | 32 | cli.FlagEnvVarsPrefix = caps.ToScreamingSnake(common.Brand) + "_" 33 | afterAllCommandsBuiltFuncs := make(map[*cobra.Command]func(cmd *cobra.Command) error) 34 | 35 | // Needed for embedding original Helm 3 commands. 36 | var err error 37 | 38 | helmRootCmd, err = helm_v3.Init() 39 | if err != nil { 40 | abort(ctx, fmt.Errorf("init helm: %w", err), 1) 41 | } 42 | 43 | rootCmd := NewRootCommand(ctx, afterAllCommandsBuiltFuncs) 44 | 45 | for cmd, fn := range afterAllCommandsBuiltFuncs { 46 | if err := fn(cmd); err != nil { 47 | abort(ctx, err, 1) 48 | } 49 | } 50 | 51 | featGatesEnvVars := lo.Map(featgate.FeatGates, func(fg *featgate.FeatGate, index int) string { 52 | return fg.EnvVarName() 53 | }) 54 | 55 | if unsupportedEnvVars := lo.Without(cli.FindUndefinedFlagEnvVarsInEnviron(), featGatesEnvVars...); len(unsupportedEnvVars) > 0 { 56 | abort(ctx, fmt.Errorf("unsupported environment variable(s): %s", strings.Join(unsupportedEnvVars, ",")), 1) 57 | } 58 | 59 | if err := rootCmd.ExecuteContext(ctx); err != nil { 60 | var exitCode int 61 | if errors.Is(err, action.ErrChangesPlanned) || errors.Is(err, action.ErrResourceChangesPlanned) { 62 | exitCode = 2 63 | } else if errors.Is(err, action.ErrReleaseInstallPlanned) { 64 | exitCode = 3 65 | } else { 66 | exitCode = 1 67 | } 68 | 69 | abort(ctx, err, exitCode) 70 | } 71 | } 72 | 73 | func abort(ctx context.Context, err error, exitCode int) { 74 | log.Default.WarnPop(ctx, "final") 75 | log.Default.Error(ctx, "Error: %s", err) 76 | os.Exit(exitCode) 77 | } 78 | 79 | func periodicStackTraces() { 80 | go func() { 81 | for { 82 | buf := make([]byte, 1<<20) 83 | runtime.Stack(buf, true) 84 | fmt.Printf("%s", buf) 85 | 86 | time.Sleep(time.Second * time.Duration(10)) 87 | } 88 | }() 89 | } 90 | -------------------------------------------------------------------------------- /pkg/legacy/secret/encrypt.go: -------------------------------------------------------------------------------- 1 | package secret 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "os" 8 | 9 | "golang.org/x/crypto/ssh/terminal" 10 | 11 | "github.com/werf/common-go/pkg/secret" 12 | "github.com/werf/common-go/pkg/secrets_manager" 13 | ) 14 | 15 | func SecretFileEncrypt( 16 | ctx context.Context, 17 | m *secrets_manager.SecretsManager, 18 | workingDir, filePath, outputFilePath string, 19 | ) error { 20 | options := &GenerateOptions{ 21 | FilePath: filePath, 22 | OutputFilePath: outputFilePath, 23 | Values: false, 24 | } 25 | 26 | return secretEncrypt(ctx, m, workingDir, options) 27 | } 28 | 29 | func SecretValuesEncrypt( 30 | ctx context.Context, 31 | m *secrets_manager.SecretsManager, 32 | workingDir, filePath, outputFilePath string, 33 | ) error { 34 | options := &GenerateOptions{ 35 | FilePath: filePath, 36 | OutputFilePath: outputFilePath, 37 | Values: true, 38 | } 39 | 40 | return secretEncrypt(ctx, m, workingDir, options) 41 | } 42 | 43 | func secretEncrypt( 44 | ctx context.Context, 45 | m *secrets_manager.SecretsManager, 46 | workingDir string, 47 | options *GenerateOptions, 48 | ) error { 49 | var data []byte 50 | var encodedData []byte 51 | var err error 52 | 53 | var encoder *secret.YamlEncoder 54 | if enc, err := m.GetYamlEncoder(ctx, workingDir, false); err != nil { 55 | return err 56 | } else { 57 | encoder = enc 58 | } 59 | 60 | switch { 61 | case options.FilePath != "": 62 | data, err = readFileData(options.FilePath) 63 | if err != nil { 64 | return err 65 | } 66 | case !terminal.IsTerminal(int(os.Stdin.Fd())): 67 | data, err = InputFromStdin() 68 | if err != nil { 69 | return err 70 | } 71 | 72 | if len(data) == 0 { 73 | return nil 74 | } 75 | default: 76 | return ExpectedFilePathOrPipeError() 77 | } 78 | 79 | if options.Values { 80 | encodedData, err = encoder.EncryptYamlData(data) 81 | if err != nil { 82 | return err 83 | } 84 | } else { 85 | encodedData, err = encoder.Encrypt(data) 86 | if err != nil { 87 | return err 88 | } 89 | } 90 | 91 | if !bytes.HasSuffix(encodedData, []byte("\n")) { 92 | encodedData = append(encodedData, []byte("\n")...) 93 | } 94 | 95 | if options.OutputFilePath != "" { 96 | if err := SaveGeneratedData(options.OutputFilePath, encodedData); err != nil { 97 | return err 98 | } 99 | } else { 100 | fmt.Printf("%s", string(encodedData)) 101 | } 102 | 103 | return nil 104 | } 105 | -------------------------------------------------------------------------------- /internal/kube/fake/factory.go: -------------------------------------------------------------------------------- 1 | package fake 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | 8 | "github.com/samber/lo" 9 | apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 10 | apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 11 | "k8s.io/apimachinery/pkg/api/meta" 12 | "k8s.io/client-go/discovery" 13 | "k8s.io/client-go/dynamic" 14 | "k8s.io/client-go/kubernetes" 15 | "k8s.io/client-go/kubernetes/scheme" 16 | 17 | "github.com/werf/nelm/internal/kube" 18 | ) 19 | 20 | var _ kube.ClientFactorier = (*ClientFactory)(nil) 21 | 22 | type ClientFactory struct { 23 | discoveryClient discovery.CachedDiscoveryInterface 24 | dynamicClient dynamic.Interface 25 | kubeClient kube.KubeClienter 26 | mapper meta.ResettableRESTMapper 27 | staticClient kubernetes.Interface 28 | } 29 | 30 | func NewClientFactory(ctx context.Context) (*ClientFactory, error) { 31 | kube.AddToScheme.Do(func() { 32 | lo.Must0(apiextv1.AddToScheme(scheme.Scheme)) 33 | lo.Must0(apiextv1beta1.AddToScheme(scheme.Scheme)) 34 | }) 35 | 36 | discoveryClient, err := NewCachedDiscoveryClient() 37 | if err != nil { 38 | return nil, fmt.Errorf("construct fake cached discovery client: %w", err) 39 | } 40 | 41 | mapper := reflect.ValueOf(kube.NewKubeMapper(ctx, discoveryClient)).Interface().(meta.ResettableRESTMapper) 42 | staticClient := NewStaticClient(mapper) 43 | dynamicClient := NewDynamicClient(staticClient, mapper) 44 | kubeClient := kube.NewKubeClient(staticClient, dynamicClient, discoveryClient, mapper) 45 | 46 | clientFactory := &ClientFactory{ 47 | discoveryClient: discoveryClient, 48 | dynamicClient: dynamicClient, 49 | kubeClient: kubeClient, 50 | mapper: mapper, 51 | staticClient: staticClient, 52 | } 53 | 54 | return clientFactory, nil 55 | } 56 | 57 | func (f *ClientFactory) KubeClient() kube.KubeClienter { 58 | return f.kubeClient 59 | } 60 | 61 | func (f *ClientFactory) Static() kubernetes.Interface { 62 | return f.staticClient 63 | } 64 | 65 | func (f *ClientFactory) Dynamic() dynamic.Interface { 66 | return f.dynamicClient 67 | } 68 | 69 | func (f *ClientFactory) Discovery() discovery.CachedDiscoveryInterface { 70 | return f.discoveryClient 71 | } 72 | 73 | func (f *ClientFactory) Mapper() meta.ResettableRESTMapper { 74 | return f.mapper 75 | } 76 | 77 | func (f *ClientFactory) LegacyClientGetter() *kube.LegacyClientGetter { 78 | panic("not implemented yet") 79 | } 80 | 81 | func (f *ClientFactory) KubeConfig() *kube.KubeConfig { 82 | panic("not implemented yet") 83 | } 84 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | linters: 4 | default: none 5 | enable: 6 | # Default linters. 7 | - ineffassign 8 | - govet 9 | - errcheck 10 | - staticcheck 11 | - unused 12 | 13 | # Extra linters. 14 | - asciicheck 15 | - bidichk 16 | - bodyclose 17 | - errname 18 | - errorlint 19 | - copyloopvar 20 | - gocritic 21 | - misspell 22 | - nolintlint 23 | - embeddedstructfieldcheck 24 | - exptostd 25 | - funcorder 26 | - ginkgolinter 27 | - gocheckcompilerdirectives 28 | - gochecksumtype 29 | - gocritic 30 | - godox 31 | - gosec 32 | - gosmopolitan 33 | - govet 34 | - grouper 35 | - iface 36 | - makezero 37 | - mirror 38 | - misspell 39 | - musttag 40 | - nilnesserr 41 | - nilnil 42 | - predeclared 43 | - tagliatelle 44 | - testifylint 45 | - testpackage 46 | - unconvert 47 | - usestdlibvars 48 | - usetesting 49 | - wastedassign 50 | - wsl_v5 51 | 52 | # Enable eventually. 53 | # - err113 54 | settings: 55 | errorlint: 56 | asserts: false 57 | comparison: false 58 | gocritic: 59 | disabled-checks: 60 | - ifElseChain 61 | - unlambda 62 | misspell: 63 | locale: US 64 | staticcheck: 65 | checks: 66 | - all 67 | - -S1016 68 | - -SA1019 69 | - -QF1008 70 | govet: 71 | disable: 72 | - lostcancel 73 | godox: 74 | keywords: 75 | - FIXME 76 | wsl_v5: 77 | allow-whole-block: true 78 | exclusions: 79 | generated: lax 80 | presets: 81 | - comments 82 | - common-false-positives 83 | - legacy 84 | - std-error-handling 85 | paths: 86 | - scripts 87 | - docs 88 | - third_party$ 89 | - builtin$ 90 | - examples$ 91 | - pkg/legacy 92 | - internal/legacy 93 | - pkg/action/release_uninstall_legacy.go$ 94 | 95 | formatters: 96 | enable: 97 | - gci 98 | - gofumpt 99 | settings: 100 | gci: 101 | sections: 102 | - standard 103 | - default 104 | - prefix(github.com/werf/) 105 | gofumpt: 106 | extra-rules: true 107 | exclusions: 108 | generated: lax 109 | paths: 110 | - scripts 111 | - docs 112 | - third_party$ 113 | - builtin$ 114 | - examples$ 115 | 116 | issues: 117 | max-issues-per-linter: 0 118 | max-same-issues: 0 119 | -------------------------------------------------------------------------------- /cmd/nelm/chart_secret_key_create.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "cmp" 5 | "context" 6 | "fmt" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/werf/common-go/pkg/cli" 11 | "github.com/werf/nelm/pkg/action" 12 | "github.com/werf/nelm/pkg/common" 13 | "github.com/werf/nelm/pkg/log" 14 | ) 15 | 16 | type chartSecretKeyCreateOptions struct { 17 | action.SecretKeyCreateOptions 18 | 19 | LogColorMode string 20 | LogLevel string 21 | } 22 | 23 | func newChartSecretKeyCreateCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 24 | cfg := &chartSecretKeyCreateOptions{} 25 | 26 | cmd := cli.NewSubCommand( 27 | ctx, 28 | "create [options...]", 29 | "Create a new chart secret key.", 30 | "Create a new chart secret key.", 31 | 80, 32 | secretCmdGroup, 33 | cli.SubCommandOptions{}, 34 | func(cmd *cobra.Command, args []string) error { 35 | ctx = log.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultSecretKeyCreateLogLevel), log.SetupLoggingOptions{ 36 | ColorMode: cfg.LogColorMode, 37 | LogIsParseable: true, 38 | }) 39 | 40 | if _, err := action.SecretKeyCreate(ctx, cfg.SecretKeyCreateOptions); err != nil { 41 | return fmt.Errorf("secret key create: %w", err) 42 | } 43 | 44 | return nil 45 | }, 46 | ) 47 | 48 | afterAllCommandsBuiltFuncs[cmd] = func(cmd *cobra.Command) error { 49 | if err := cli.AddFlag(cmd, &cfg.LogColorMode, "color-mode", common.DefaultLogColorMode, "Color mode for logs. "+allowedLogColorModesHelp(), cli.AddFlagOptions{ 50 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 51 | Group: miscFlagGroup, 52 | }); err != nil { 53 | return fmt.Errorf("add flag: %w", err) 54 | } 55 | 56 | if err := cli.AddFlag(cmd, &cfg.LogLevel, "log-level", string(action.DefaultSecretKeyCreateLogLevel), "Set log level. "+allowedLogLevelsHelp(), cli.AddFlagOptions{ 57 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 58 | Group: miscFlagGroup, 59 | }); err != nil { 60 | return fmt.Errorf("add flag: %w", err) 61 | } 62 | 63 | if err := cli.AddFlag(cmd, &cfg.TempDirPath, "temp-dir", "", "The directory for temporary files. By default, create a new directory in the default system directory for temporary files", cli.AddFlagOptions{ 64 | GetEnvVarRegexesFunc: cli.GetFlagGlobalEnvVarRegexes, 65 | Group: miscFlagGroup, 66 | Type: cli.FlagTypeDir, 67 | }); err != nil { 68 | return fmt.Errorf("add flag: %w", err) 69 | } 70 | 71 | return nil 72 | } 73 | 74 | return cmd 75 | } 76 | -------------------------------------------------------------------------------- /internal/plan/validate.go: -------------------------------------------------------------------------------- 1 | package plan 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/werf/nelm/internal/resource/spec" 8 | "github.com/werf/nelm/internal/util" 9 | "github.com/werf/nelm/pkg/common" 10 | ) 11 | 12 | func ValidateRemote(releaseName, releaseNamespace string, installableResourceInfos []*InstallableResourceInfo, forceAdoption bool) error { 13 | if !forceAdoption { 14 | if err := validateAdoptableResources(releaseName, releaseNamespace, installableResourceInfos); err != nil { 15 | return fmt.Errorf("validate adoptable resources: %w", err) 16 | } 17 | } 18 | 19 | return nil 20 | } 21 | 22 | func validateAdoptableResources(releaseName, releaseNamespace string, resourceInfos []*InstallableResourceInfo) error { 23 | var errs []error 24 | for _, info := range resourceInfos { 25 | if info.GetResult == nil { 26 | continue 27 | } 28 | 29 | if info.LocalResource.Ownership == common.OwnershipAnyone { 30 | continue 31 | } 32 | 33 | if adoptable, nonAdoptableReason := adoptableBy(info.LocalResource.ResourceMeta, releaseName, releaseNamespace); !adoptable { 34 | errs = append(errs, fmt.Errorf("resource %q is not adoptable: %s", info.IDHuman(), nonAdoptableReason)) 35 | } 36 | } 37 | 38 | return util.Multierrorf("adoption validation failed", errs) 39 | } 40 | 41 | func adoptableBy(meta *spec.ResourceMeta, releaseName, releaseNamespace string) (adoptable bool, nonAdoptableReason string) { 42 | nonAdoptableReasons := []string{} 43 | 44 | if key, value, found := spec.FindAnnotationOrLabelByKeyPattern(meta.Annotations, common.AnnotationKeyPatternReleaseName); found { 45 | if value != releaseName { 46 | nonAdoptableReasons = append(nonAdoptableReasons, fmt.Sprintf(`annotation "%s=%s" must have value %q`, key, value, releaseName)) 47 | } 48 | } else { 49 | nonAdoptableReasons = append(nonAdoptableReasons, fmt.Sprintf(`annotation %q not found, must be set to %q`, common.AnnotationKeyHumanReleaseName, releaseName)) 50 | } 51 | 52 | if key, value, found := spec.FindAnnotationOrLabelByKeyPattern(meta.Annotations, common.AnnotationKeyPatternReleaseNamespace); found { 53 | if value != releaseNamespace { 54 | nonAdoptableReasons = append(nonAdoptableReasons, fmt.Sprintf(`annotation "%s=%s" must have value %q`, key, value, releaseNamespace)) 55 | } 56 | } else { 57 | nonAdoptableReasons = append(nonAdoptableReasons, fmt.Sprintf(`annotation %q not found, must be set to %q`, common.AnnotationKeyHumanReleaseNamespace, releaseNamespace)) 58 | } 59 | 60 | nonAdoptableReason = strings.Join(nonAdoptableReasons, ", ") 61 | 62 | return len(nonAdoptableReasons) == 0, nonAdoptableReason 63 | } 64 | -------------------------------------------------------------------------------- /pkg/featgate/feat.go: -------------------------------------------------------------------------------- 1 | package featgate 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/chanced/caps" 7 | "github.com/samber/lo" 8 | 9 | "github.com/werf/nelm/pkg/common" 10 | ) 11 | 12 | var ( 13 | FeatGateEnvVarsPrefix = caps.ToScreamingSnake(common.Brand) + "_FEAT_" 14 | FeatGates = []*FeatGate{} 15 | 16 | FeatGateRemoteCharts = NewFeatGate( 17 | "remote-charts", 18 | `Allow not only local, but also remote charts as an argument to cli commands. Also adds the "--chart-version" option`, 19 | ) 20 | 21 | FeatGateNativeReleaseList = NewFeatGate( 22 | "native-release-list", 23 | `Use the native "release list" command instead of "helm list" exposed as "release list"`, 24 | ) 25 | 26 | FeatGatePeriodicStackTraces = NewFeatGate( 27 | "periodic-stack-traces", 28 | `Print stack traces periodically to help with debugging deadlocks and other issues`, 29 | ) 30 | 31 | FeatGateNativeReleaseUninstall = NewFeatGate( 32 | "native-release-uninstall", 33 | `Use the new "release uninstall" command implementation (not fully backwards compatible)`, 34 | ) 35 | 36 | FeatGateFieldSensitive = NewFeatGate( 37 | "field-sensitive", 38 | `Enable JSONPath-based selective sensitive field redaction`, 39 | ) 40 | 41 | FeatGatePreviewV2 = NewFeatGate( 42 | "preview-v2", 43 | `Activate all feature gates that will be enabled by default in Nelm v2`, 44 | ) 45 | 46 | FeatGateCleanNullFields = NewFeatGate( 47 | "clean-null-fields", 48 | `Enable cleaning of null fields from resource manifests for better Helm chart compatibility`, 49 | ) 50 | 51 | FeatGateMoreDetailedExitCodeForPlan = NewFeatGate( 52 | "more-detailed-exit-code-for-plan", 53 | `Make the "plan" command with the flag "--exit-code" return an exit code 3 instead of 2 when no resource changes, but still must install the release`, 54 | ) 55 | ) 56 | 57 | type FeatGate struct { 58 | Name string 59 | Help string 60 | 61 | forceEnabled *bool 62 | } 63 | 64 | func NewFeatGate(name, help string) *FeatGate { 65 | fg := &FeatGate{ 66 | Name: name, 67 | Help: help, 68 | } 69 | 70 | FeatGates = append(FeatGates, fg) 71 | 72 | return fg 73 | } 74 | 75 | func (g *FeatGate) EnvVarName() string { 76 | return FeatGateEnvVarsPrefix + caps.ToScreamingSnake(g.Name) 77 | } 78 | 79 | func (g *FeatGate) Default() bool { 80 | return false 81 | } 82 | 83 | func (g *FeatGate) Enabled() bool { 84 | if g.forceEnabled != nil { 85 | return *g.forceEnabled 86 | } 87 | 88 | return os.Getenv(g.EnvVarName()) == "true" 89 | } 90 | 91 | func (g *FeatGate) Enable() { 92 | g.forceEnabled = lo.ToPtr(true) 93 | } 94 | 95 | func (g *FeatGate) Disable() { 96 | g.forceEnabled = lo.ToPtr(false) 97 | } 98 | -------------------------------------------------------------------------------- /pkg/legacy/secret/decrypt.go: -------------------------------------------------------------------------------- 1 | package secret 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "os" 8 | 9 | "golang.org/x/crypto/ssh/terminal" 10 | 11 | "github.com/werf/common-go/pkg/secret" 12 | "github.com/werf/common-go/pkg/secrets_manager" 13 | ) 14 | 15 | func SecretFileDecrypt( 16 | ctx context.Context, 17 | m *secrets_manager.SecretsManager, 18 | workingDir, filePath, outputFilePath string, 19 | ) error { 20 | options := &GenerateOptions{ 21 | FilePath: filePath, 22 | OutputFilePath: outputFilePath, 23 | Values: false, 24 | } 25 | 26 | return secretDecrypt(ctx, m, workingDir, options) 27 | } 28 | 29 | func SecretValuesDecrypt( 30 | ctx context.Context, 31 | m *secrets_manager.SecretsManager, 32 | workingDir, filePath, outputFilePath string, 33 | ) error { 34 | options := &GenerateOptions{ 35 | FilePath: filePath, 36 | OutputFilePath: outputFilePath, 37 | Values: true, 38 | } 39 | 40 | return secretDecrypt(ctx, m, workingDir, options) 41 | } 42 | 43 | func secretDecrypt( 44 | ctx context.Context, 45 | m *secrets_manager.SecretsManager, 46 | workingDir string, 47 | options *GenerateOptions, 48 | ) error { 49 | var encodedData []byte 50 | var data []byte 51 | var err error 52 | 53 | var encoder *secret.YamlEncoder 54 | if enc, err := m.GetYamlEncoder(ctx, workingDir, false); err != nil { 55 | return err 56 | } else { 57 | encoder = enc 58 | } 59 | 60 | if options.FilePath != "" { 61 | encodedData, err = readFileData(options.FilePath) 62 | if err != nil { 63 | return err 64 | } 65 | } else { 66 | if !terminal.IsTerminal(int(os.Stdin.Fd())) { 67 | encodedData, err = InputFromStdin() 68 | if err != nil { 69 | return err 70 | } 71 | } else { 72 | return ExpectedFilePathOrPipeError() 73 | } 74 | 75 | if len(encodedData) == 0 { 76 | return nil 77 | } 78 | } 79 | 80 | encodedData = bytes.TrimSpace(encodedData) 81 | 82 | if options.Values { 83 | data, err = encoder.DecryptYamlData(encodedData) 84 | if err != nil { 85 | return err 86 | } 87 | } else { 88 | data, err = encoder.Decrypt(encodedData) 89 | if err != nil { 90 | return err 91 | } 92 | } 93 | 94 | if options.OutputFilePath != "" { 95 | if err := SaveGeneratedData(options.OutputFilePath, data); err != nil { 96 | return err 97 | } 98 | } else { 99 | if terminal.IsTerminal(int(os.Stdout.Fd())) { 100 | if !bytes.HasSuffix(data, []byte("\n")) { 101 | data = append(data, []byte("\n")...) 102 | } 103 | } 104 | 105 | fmt.Printf("%s", string(data)) 106 | } 107 | 108 | return nil 109 | } 110 | -------------------------------------------------------------------------------- /internal/util/json.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | jsonpatch "github.com/evanphx/json-patch" 8 | "github.com/tidwall/sjson" 9 | "github.com/wI2L/jsondiff" 10 | ) 11 | 12 | func MergeJSON(mergeA, toB []byte) (result []byte, changed bool, err error) { 13 | ops, err := jsondiff.CompareJSON(toB, mergeA) 14 | if err != nil { 15 | return nil, false, fmt.Errorf("error comparing json: %w", err) 16 | } 17 | 18 | var addOps []jsondiff.Operation 19 | for _, op := range ops { 20 | switch t := op.Type; t { 21 | case jsondiff.OperationAdd: 22 | addOps = append(addOps, op) 23 | case jsondiff.OperationRemove, jsondiff.OperationReplace: 24 | continue 25 | default: 26 | panic(fmt.Sprintf("unexpected operation type: %s", t)) 27 | } 28 | } 29 | 30 | if len(addOps) == 0 { 31 | return toB, false, nil 32 | } 33 | 34 | var opStrings []string 35 | for _, op := range addOps { 36 | opStrings = append(opStrings, op.String()) 37 | } 38 | 39 | patchString := "[" + strings.Join(opStrings, ",") + "]" 40 | 41 | jpatch, err := jsonpatch.DecodePatch([]byte(patchString)) 42 | if err != nil { 43 | return nil, false, fmt.Errorf("error decoding patch: %w", err) 44 | } 45 | 46 | result, err = jpatch.Apply(toB) 47 | if err != nil { 48 | return nil, false, fmt.Errorf("error applying patch: %w", err) 49 | } 50 | 51 | return result, true, nil 52 | } 53 | 54 | func SubtractJSON(fromA, subtractB []byte) (result []byte, changed bool, err error) { 55 | ops, err := jsondiff.CompareJSON(subtractB, fromA) 56 | if err != nil { 57 | return nil, false, fmt.Errorf("error comparing json: %w", err) 58 | } 59 | 60 | var addOps []jsondiff.Operation 61 | for _, op := range ops { 62 | switch t := op.Type; t { 63 | case jsondiff.OperationAdd, jsondiff.OperationReplace: 64 | addOps = append(addOps, op) 65 | case jsondiff.OperationRemove: 66 | continue 67 | default: 68 | panic(fmt.Sprintf("unexpected operation type: %s", t)) 69 | } 70 | } 71 | 72 | res := "{}" 73 | for _, op := range addOps { 74 | jsonPath := JSONPatchPathToJSONPath(op.Path) 75 | 76 | var err error 77 | 78 | res, err = sjson.Set(res, jsonPath, op.Value) 79 | if err != nil { 80 | return nil, false, fmt.Errorf("error setting value by jsonpath: %w", err) 81 | } 82 | } 83 | 84 | return []byte(res), string(fromA) != res, nil 85 | } 86 | 87 | func JSONPatchPathToJSONPath(path string) string { 88 | path = strings.TrimPrefix(path, "/") 89 | path = strings.ReplaceAll(path, ".", `\.`) 90 | path = strings.ReplaceAll(path, ":", `\:`) 91 | path = strings.ReplaceAll(path, "/", ".") 92 | path = strings.ReplaceAll(path, "~1", "/") 93 | 94 | return strings.ReplaceAll(path, "~0", "~") 95 | } 96 | -------------------------------------------------------------------------------- /cmd/nelm/version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "cmp" 5 | "context" 6 | "fmt" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/werf/common-go/pkg/cli" 11 | "github.com/werf/nelm/pkg/action" 12 | "github.com/werf/nelm/pkg/common" 13 | "github.com/werf/nelm/pkg/log" 14 | ) 15 | 16 | type versionConfig struct { 17 | action.VersionOptions 18 | 19 | LogColorMode string 20 | LogLevel string 21 | } 22 | 23 | func newVersionCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 24 | cfg := &versionConfig{} 25 | 26 | cmd := cli.NewSubCommand( 27 | ctx, 28 | "version [options...]", 29 | "Show version.", 30 | "Show version.", 31 | 0, 32 | miscCmdGroup, 33 | cli.SubCommandOptions{}, 34 | func(cmd *cobra.Command, args []string) error { 35 | ctx = log.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultVersionLogLevel), log.SetupLoggingOptions{ 36 | ColorMode: cfg.LogColorMode, 37 | LogIsParseable: true, 38 | }) 39 | 40 | if _, err := action.Version(ctx, cfg.VersionOptions); err != nil { 41 | return fmt.Errorf("version: %w", err) 42 | } 43 | 44 | return nil 45 | }, 46 | ) 47 | 48 | afterAllCommandsBuiltFuncs[cmd] = func(cmd *cobra.Command) error { 49 | if err := cli.AddFlag(cmd, &cfg.LogColorMode, "color-mode", common.DefaultLogColorMode, "Color mode for logs. "+allowedLogColorModesHelp(), cli.AddFlagOptions{ 50 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 51 | Group: miscFlagGroup, 52 | }); err != nil { 53 | return fmt.Errorf("add flag: %w", err) 54 | } 55 | 56 | if err := cli.AddFlag(cmd, &cfg.LogLevel, "log-level", string(action.DefaultVersionLogLevel), "Set log level. "+allowedLogLevelsHelp(), cli.AddFlagOptions{ 57 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 58 | Group: miscFlagGroup, 59 | }); err != nil { 60 | return fmt.Errorf("add flag: %w", err) 61 | } 62 | 63 | if err := cli.AddFlag(cmd, &cfg.OutputFormat, "output-format", action.DefaultVersionOutputFormat, "Result output format", cli.AddFlagOptions{ 64 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 65 | Group: miscFlagGroup, 66 | }); err != nil { 67 | return fmt.Errorf("add flag: %w", err) 68 | } 69 | 70 | if err := cli.AddFlag(cmd, &cfg.TempDirPath, "temp-dir", "", "The directory for temporary files. By default, create a new directory in the default system directory for temporary files", cli.AddFlagOptions{ 71 | GetEnvVarRegexesFunc: cli.GetFlagGlobalEnvVarRegexes, 72 | Group: miscFlagGroup, 73 | Type: cli.FlagTypeDir, 74 | }); err != nil { 75 | return fmt.Errorf("add flag: %w", err) 76 | } 77 | 78 | return nil 79 | } 80 | 81 | return cmd 82 | } 83 | -------------------------------------------------------------------------------- /pkg/legacy/secret/common.go: -------------------------------------------------------------------------------- 1 | package secret 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/gookit/color" 11 | "github.com/moby/term" 12 | "golang.org/x/crypto/ssh/terminal" 13 | 14 | "github.com/werf/common-go/pkg/util" 15 | ) 16 | 17 | type GenerateOptions struct { 18 | FilePath string 19 | OutputFilePath string 20 | Values bool 21 | } 22 | 23 | func ExpectedFilePathOrPipeError() error { 24 | return errors.New("expected FILE_PATH or pipe") 25 | } 26 | 27 | func InputFromInteractiveStdin(prompt string) ([]byte, error) { 28 | var data []byte 29 | var err error 30 | 31 | isStdoutTerminal := terminal.IsTerminal(int(os.Stdout.Fd())) 32 | if isStdoutTerminal { 33 | fmt.Printf(color.New(color.Bold).Sprintf(prompt)) 34 | } 35 | 36 | prepareTerminal := func() (func() error, error) { 37 | state, err := term.SetRawTerminal(os.Stdin.Fd()) 38 | if err != nil { 39 | return nil, fmt.Errorf("unable to put terminal into raw mode: %w", err) 40 | } 41 | 42 | restored := false 43 | 44 | return func() error { 45 | if restored { 46 | return nil 47 | } 48 | if err := term.RestoreTerminal(os.Stdin.Fd(), state); err != nil { 49 | return err 50 | } 51 | restored = true 52 | return nil 53 | }, nil 54 | } 55 | 56 | restoreTerminal, err := prepareTerminal() 57 | if err != nil { 58 | return nil, err 59 | } 60 | defer restoreTerminal() 61 | 62 | data, err = terminal.ReadPassword(int(os.Stdin.Fd())) 63 | 64 | if err := restoreTerminal(); err != nil { 65 | return nil, fmt.Errorf("unable to restore terminal: %w", err) 66 | } 67 | 68 | if isStdoutTerminal { 69 | fmt.Println() 70 | } 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | return data, nil 76 | } 77 | 78 | func InputFromStdin() ([]byte, error) { 79 | var data []byte 80 | var err error 81 | 82 | data, err = ioutil.ReadAll(os.Stdin) 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | return data, nil 88 | } 89 | 90 | func SaveGeneratedData(filePath string, data []byte) error { 91 | if err := os.MkdirAll(filepath.Dir(filePath), 0o777); err != nil { 92 | return err 93 | } 94 | 95 | if err := ioutil.WriteFile(filePath, data, 0o644); err != nil { 96 | return err 97 | } 98 | 99 | return nil 100 | } 101 | 102 | func readFileData(filePath string) ([]byte, error) { 103 | if exist, err := util.FileExists(filePath); err != nil { 104 | return nil, err 105 | } else if !exist { 106 | absFilePath, err := filepath.Abs(filePath) 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | return nil, fmt.Errorf("secret file %q not found", absFilePath) 112 | } 113 | 114 | fileData, err := ioutil.ReadFile(filePath) 115 | if err != nil { 116 | return nil, err 117 | } 118 | 119 | return fileData, err 120 | } 121 | -------------------------------------------------------------------------------- /internal/resource/spec/patch.go: -------------------------------------------------------------------------------- 1 | package spec 2 | 3 | import ( 4 | "context" 5 | 6 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 7 | 8 | "github.com/werf/nelm/pkg/common" 9 | ) 10 | 11 | var ( 12 | _ ResourcePatcher = (*ExtraMetadataPatcher)(nil) 13 | _ ResourcePatcher = (*ReleaseMetadataPatcher)(nil) 14 | ) 15 | 16 | type ResourcePatcher interface { 17 | Match(ctx context.Context, resourceInfo *ResourcePatcherResourceInfo) (matched bool, err error) 18 | Patch(ctx context.Context, matchedResourceInfo *ResourcePatcherResourceInfo) (output *unstructured.Unstructured, err error) 19 | Type() ResourcePatcherType 20 | } 21 | 22 | type ResourcePatcherResourceInfo struct { 23 | Obj *unstructured.Unstructured 24 | Ownership common.Ownership 25 | } 26 | 27 | type ResourcePatcherType string 28 | 29 | const ( 30 | TypeExtraMetadataPatcher ResourcePatcherType = "extra-metadata-patcher" 31 | TypeReleaseMetadataPatcher ResourcePatcherType = "release-metadata-patcher" 32 | ) 33 | 34 | type ExtraMetadataPatcher struct { 35 | annotations map[string]string 36 | labels map[string]string 37 | } 38 | 39 | func NewExtraMetadataPatcher(annotations, labels map[string]string) *ExtraMetadataPatcher { 40 | return &ExtraMetadataPatcher{ 41 | annotations: annotations, 42 | labels: labels, 43 | } 44 | } 45 | 46 | func (p *ExtraMetadataPatcher) Match(ctx context.Context, info *ResourcePatcherResourceInfo) (bool, error) { 47 | return true, nil 48 | } 49 | 50 | func (p *ExtraMetadataPatcher) Patch(ctx context.Context, info *ResourcePatcherResourceInfo) (*unstructured.Unstructured, error) { 51 | setAnnotationsAndLabels(info.Obj, p.annotations, p.labels) 52 | return info.Obj, nil 53 | } 54 | 55 | func (p *ExtraMetadataPatcher) Type() ResourcePatcherType { 56 | return TypeExtraMetadataPatcher 57 | } 58 | 59 | type ReleaseMetadataPatcher struct { 60 | releaseName string 61 | releaseNamespace string 62 | } 63 | 64 | func NewReleaseMetadataPatcher(releaseName, releaseNamespace string) *ReleaseMetadataPatcher { 65 | return &ReleaseMetadataPatcher{ 66 | releaseName: releaseName, 67 | releaseNamespace: releaseNamespace, 68 | } 69 | } 70 | 71 | func (p *ReleaseMetadataPatcher) Match(ctx context.Context, info *ResourcePatcherResourceInfo) (bool, error) { 72 | return info.Ownership == common.OwnershipRelease, nil 73 | } 74 | 75 | func (p *ReleaseMetadataPatcher) Patch(ctx context.Context, info *ResourcePatcherResourceInfo) (*unstructured.Unstructured, error) { 76 | annos := map[string]string{} 77 | annos["meta.helm.sh/release-name"] = p.releaseName 78 | annos["meta.helm.sh/release-namespace"] = p.releaseNamespace 79 | 80 | labels := map[string]string{} 81 | labels["app.kubernetes.io/managed-by"] = "Helm" 82 | 83 | setAnnotationsAndLabels(info.Obj, annos, labels) 84 | 85 | return info.Obj, nil 86 | } 87 | 88 | func (p *ReleaseMetadataPatcher) Type() ResourcePatcherType { 89 | return TypeReleaseMetadataPatcher 90 | } 91 | -------------------------------------------------------------------------------- /internal/plan/release_info.go: -------------------------------------------------------------------------------- 1 | package plan 2 | 3 | import ( 4 | "context" 5 | 6 | helmrelease "github.com/werf/3p-helm/pkg/release" 7 | "github.com/werf/nelm/pkg/common" 8 | ) 9 | 10 | type ReleaseType string 11 | 12 | const ( 13 | ReleaseTypeNone ReleaseType = "none" 14 | ReleaseTypeInstall ReleaseType = "install" 15 | ReleaseTypeUpgrade ReleaseType = "upgrade" 16 | ReleaseTypeRollback ReleaseType = "rollback" 17 | ReleaseTypeSupersede ReleaseType = "supersede" 18 | ReleaseTypeUninstall ReleaseType = "uninstall" 19 | ReleaseTypeDelete ReleaseType = "delete" 20 | ) 21 | 22 | type ReleaseInfo struct { 23 | Release *helmrelease.Release 24 | 25 | Must ReleaseType 26 | MustFailOnFailedDeploy bool 27 | } 28 | 29 | func BuildReleaseInfos(ctx context.Context, deployType common.DeployType, prevReleases []*helmrelease.Release, newRel *helmrelease.Release) ([]*ReleaseInfo, error) { 30 | var infos []*ReleaseInfo 31 | switch deployType { 32 | case common.DeployTypeInitial, common.DeployTypeInstall: 33 | infos = append(infos, &ReleaseInfo{ 34 | Release: newRel, 35 | Must: ReleaseTypeInstall, 36 | MustFailOnFailedDeploy: true, 37 | }) 38 | 39 | for _, rel := range prevReleases { 40 | if rel.Info.Status == helmrelease.StatusDeployed { 41 | infos = append(infos, &ReleaseInfo{ 42 | Release: rel, 43 | Must: ReleaseTypeSupersede, 44 | }) 45 | } 46 | } 47 | case common.DeployTypeUpgrade: 48 | infos = append(infos, &ReleaseInfo{ 49 | Release: newRel, 50 | Must: ReleaseTypeUpgrade, 51 | MustFailOnFailedDeploy: true, 52 | }) 53 | 54 | for _, rel := range prevReleases { 55 | if rel.Info.Status == helmrelease.StatusDeployed { 56 | infos = append(infos, &ReleaseInfo{ 57 | Release: rel, 58 | Must: ReleaseTypeSupersede, 59 | }) 60 | } 61 | } 62 | case common.DeployTypeRollback: 63 | infos = append(infos, &ReleaseInfo{ 64 | Release: newRel, 65 | Must: ReleaseTypeRollback, 66 | MustFailOnFailedDeploy: true, 67 | }) 68 | 69 | for _, rel := range prevReleases { 70 | if rel.Info.Status == helmrelease.StatusDeployed { 71 | infos = append(infos, &ReleaseInfo{ 72 | Release: rel, 73 | Must: ReleaseTypeSupersede, 74 | }) 75 | } 76 | } 77 | case common.DeployTypeUninstall: 78 | for i := 0; i < len(prevReleases); i++ { 79 | var ( 80 | releaseType ReleaseType 81 | failOnFailedDeploy bool 82 | ) 83 | 84 | if i == len(prevReleases)-1 { 85 | releaseType = ReleaseTypeUninstall 86 | failOnFailedDeploy = true 87 | } else { 88 | releaseType = ReleaseTypeDelete 89 | } 90 | 91 | infos = append(infos, &ReleaseInfo{ 92 | Release: prevReleases[i], 93 | Must: releaseType, 94 | MustFailOnFailedDeploy: failOnFailedDeploy, 95 | }) 96 | } 97 | default: 98 | panic("unexpected deploy type") 99 | } 100 | 101 | return infos, nil 102 | } 103 | -------------------------------------------------------------------------------- /nelm.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | xsFNBGfbI00BEADSeeYyWaeRZ5nmnhwHime19f8P6liuNqwAk8DTRYp+Zei08wod 4 | 6UIXZJFmHDefPqF8heqfG15p2ydV/U+ve4K+zRhaKP1sO7ByFB+N9KpfJkKcE55Y 5 | u4B2B8rEMoit9DNlf7kb3UmnrqL3P1nYnkgjW/uKpJsqzNoLtKd26dw3G5cWDprz 6 | yGM2MW1Ged4zCcghYCPaWTdumwd3aK23PbteiJKh1gtbQ3zTyKSLt8jbdGUPQ1iC 7 | IvgiBl0057wJTaey4zQSYxCZRGp9DCYbbMGLxt5VsCD438tR+qPjFVKPySOmr8iZ 8 | ECZqR7f9bw37dqIk81R/lMHVJ6ySp9yhTEglsiuE6E3b/tU3edRnkEK2GuMjoBhm 9 | Zs06Ki6S0g1n7p+64HeAzaPGoOzcOk8sPndjyiBYXQF1iUKDG0lsjSOWZxcxr2ng 10 | W03hrqfnkEJikLi6aNHMf65uosq/4Qxz7qw1JyZqclsf/0CLCym5lX/7I0faqi2W 11 | wIU0lBdEGHz7EibrZSwK1XwL1ARgocCiaC+CfP5tXYEBDiCeRwXKmevYQse1jIpv 12 | yp+WxIABcCdGobTNr7qEn15DtechvNtJQpZcrIb8OyDECWzCEjXOUbmHKzccQ6qn 13 | zKNp4em9FfRAVIfr/j2GTbWYSlBQtdcMgkzpTD91wNPTdxIYx+VlBuWm8QARAQAB 14 | zR50cmRsICh0cmRsIHNlcnZlciBhdXRvIHNpZ25lcinCwWgEEwEIABwFAmfbI00J 15 | ECAtJdHqfOtHAhsDAhkBAgsHAhUIAABEfBAAq3xyKEv2Z2I0H/1IJ9Hn/Y89p2AO 16 | m4+HDhMQpS5qtecrOjt84UIA79n+O5a4q/dyXHL/v3RKDMSAa6rA/9WwQZD7NzLB 17 | sT2DGs451LOqPSFkkCaui3lxEWDqwtluvFKBCRuym9iPl6d67QeZpaZybBr4Q6fO 18 | xCCwyUP8lS8FrN5GUBWpQL3NKhl3obsJE0ycWV8qDGKw/FprX6lnb2OJy1+LPMy8 19 | VJ90cz1M0p/tFXIeFsMvSgHP4QAuSSxYmhACdBM6Iz2D0zdGfuS5IQHHeCv5DR20 20 | WkJBNLyVQBorO3+/fq2VzOxC3clmJMZ2ejFnHfrWCC1IH+NzKb4PGpdi19S7NOpF 21 | JrrVxOC2XBCqyepAXCUWB67mSHuQt75FnHz5wnMpUdNCMp69X4V0XZ9ANuxcvBfo 22 | lLA/3gIo/yCL+NPe+9gD9scGsgAsKW1dhhFsvTfr5NG5q9wStWfMnRGGLrbWNoUB 23 | Sq9m8dWdeNk3ZQEI7H1E6jCxG7ejIN0qMk7rbzUDQApK9hBIyUPtMGp2vHr0w4do 24 | Wi2UEfNeBzhQWaog2STm/kUAB7axjmWpqdcE9JnhiXgMvIDC1uVRRT2kIbxrDj7g 25 | OAqyzlpX4swvkp58YbMSJ1/myamWgNV7irq+dMrCV/SEDs6Nko4YVJrzmfOeg0xX 26 | YRdhi/+sZH3gfujOwU0EZ9sjTQEQANs8CHRTKlGQ/pctdfC0QqUHUDYewigAO19B 27 | ng/E2pxQ7LckfRLG/AwzUizKk9KueTpJ0VDkZtWY/ysbL0NaJu7WY8df6HILC2Vp 28 | WmFowDMocX4PJQGu0/V5GVZEhpq569iut0s2HJ9p9xKDobDPYbXC02+sJYCJlOPf 29 | ISVDkhTLqoG2+2P4HJ8UuVbqpSDw4oUlee9E/gkwT0v32LKKN8N7Yk8nHWwhYA0Q 30 | eBFVa3XMhLbLqh65FyEus/Tx3UT044X1X8Lt+SptADYxEMecNqHeAOQDiHZyAY/A 31 | YbCNYqXgB3BcrvTZy+Iuh7y2geOSF1lC5VKrKlMrZg3gcmtQd9wYgcdQblzl4zks 32 | uFkyCHjupq1aL9MraVTN+CHvC2DBr+xFK0LB2RZocUYeYHUjfdsDPKaBKzqh+kk0 33 | b7eZXrzIbvO9DzVXnr+B59uJ3tIQL4e/I8L5B/gfSheOjUsswS7DyKvkiY1bNzZK 34 | YREjq5C+KRb2wgUwCIR20KpTWC85r/M/vltsWiJBc1f1KLKoKIFWy/5sdEKIVN12 35 | dI6Khio+iHJTNzWV9zVx8Xp4tjiDjO8tJNX/yPROGHW30GrVosA9JBVwC4f0adDX 36 | NzRHpoei+ivJHR5mRJ3efEfA72InjuBdAFGfmEqUJHIjufsMwU2Ls/8YTTI1RsWz 37 | dIRBbnbbABEBAAHCwV8EGAEIABMFAmfbI00JECAtJdHqfOtHAhsMAACFNxAAMXWq 38 | a5DDc0ASLorEiVM265HjmmRUOTrnSqvdrlAfoKEmBGurJHA5ldJOi3iC90VhYhZE 39 | p5rNhFVqBGeujTISpMn7cQl3g3W2CiXgXis7DuccTRbuzCddZf7MVOuqD4Z8vWw6 40 | 8NSIw6em0cwnhsmDZ2naCZuAo1ktyBVBxpD2N0TlVK+jQ1bqsG3YCWpMDntJBGvz 41 | W05JIO/3Ebd3i4UoahFJQRCbXxdqbyee2iwe8dQp3q/zMQ0HRiYF+uo0MpAIqDOs 42 | wH3jAsvd+b5pcZby1aDFAGGdoC8KWSkB0A6pdpY4ZIcgcAntlpCUTVkY2jgDd2ow 43 | g4ipnbehEoGg2giSJSTxQwd7mMjCR2wIIZmrPd7Kq1xS2FmA/vLPrG8koeiLskLj 44 | zUvMTlS/N5SBj+BRDGRf5KIzs8mi7LFgqInF1hFYvBoOL8L+W5ylPsdySSD7iix7 45 | nFjjN5rw2nasuwTteaN70DX9YFDeIcT5nK4lF1JyCoJPEKZLAc8Rug3uN6Mb/2VZ 46 | LGmZTostkmaPRQ7sqRIRTmuX0Dd/tI03FnzrsLPzamDGR+YvS9VUyVr3AUJWhGVP 47 | Ycuqzn+dJk/6Pcm+y7JghFzqHjbk8e9NEZc+UzCah3zixnovedCFcpDMpH4iXEfX 48 | HZfIDWjHk3sYyolMoxdJL4/Z8Yq6bhKJpCPRbEU= 49 | =5aFS 50 | -----END PGP PUBLIC KEY BLOCK----- 51 | -------------------------------------------------------------------------------- /cmd/nelm/chart_secret_file_edit.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "cmp" 5 | "context" 6 | "fmt" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/werf/common-go/pkg/cli" 11 | "github.com/werf/nelm/pkg/action" 12 | "github.com/werf/nelm/pkg/common" 13 | "github.com/werf/nelm/pkg/log" 14 | ) 15 | 16 | type chartSecretFileEditOptions struct { 17 | action.SecretFileEditOptions 18 | 19 | File string 20 | LogColorMode string 21 | LogLevel string 22 | } 23 | 24 | func newChartSecretFileEditCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 25 | cfg := &chartSecretFileEditOptions{} 26 | 27 | cmd := cli.NewSubCommand( 28 | ctx, 29 | "edit [options...] --secret-key secret-key file", 30 | "Interactively edit encrypted file.", 31 | "Interactively edit encrypted file.", 32 | 30, 33 | secretCmdGroup, 34 | cli.SubCommandOptions{ 35 | Args: cobra.ExactArgs(1), 36 | ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 37 | return nil, cobra.ShellCompDirectiveDefault 38 | }, 39 | }, 40 | func(cmd *cobra.Command, args []string) error { 41 | ctx = log.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultSecretFileEditLogLevel), log.SetupLoggingOptions{ 42 | ColorMode: cfg.LogColorMode, 43 | }) 44 | 45 | cfg.File = args[0] 46 | 47 | if err := action.SecretFileEdit(ctx, cfg.File, cfg.SecretFileEditOptions); err != nil { 48 | return fmt.Errorf("secret file edit: %w", err) 49 | } 50 | 51 | return nil 52 | }, 53 | ) 54 | 55 | afterAllCommandsBuiltFuncs[cmd] = func(cmd *cobra.Command) error { 56 | if err := cli.AddFlag(cmd, &cfg.LogColorMode, "color-mode", common.DefaultLogColorMode, "Color mode for logs", cli.AddFlagOptions{ 57 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 58 | Group: miscFlagGroup, 59 | }); err != nil { 60 | return fmt.Errorf("add flag: %w", err) 61 | } 62 | 63 | if err := cli.AddFlag(cmd, &cfg.LogLevel, "log-level", string(action.DefaultSecretFileEditLogLevel), "Set log level. "+allowedLogLevelsHelp(), cli.AddFlagOptions{ 64 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 65 | Group: miscFlagGroup, 66 | }); err != nil { 67 | return fmt.Errorf("add flag: %w", err) 68 | } 69 | 70 | if err := cli.AddFlag(cmd, &cfg.SecretKey, "secret-key", "", "Secret key", cli.AddFlagOptions{ 71 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 72 | Group: mainFlagGroup, 73 | Required: true, 74 | }); err != nil { 75 | return fmt.Errorf("add flag: %w", err) 76 | } 77 | 78 | if err := cli.AddFlag(cmd, &cfg.TempDirPath, "temp-dir", "", "The directory for temporary files. By default, create a new directory in the default system directory for temporary files", cli.AddFlagOptions{ 79 | GetEnvVarRegexesFunc: cli.GetFlagGlobalEnvVarRegexes, 80 | Group: miscFlagGroup, 81 | Type: cli.FlagTypeDir, 82 | }); err != nil { 83 | return fmt.Errorf("add flag: %w", err) 84 | } 85 | 86 | return nil 87 | } 88 | 89 | return cmd 90 | } 91 | -------------------------------------------------------------------------------- /internal/plan/operation.go: -------------------------------------------------------------------------------- 1 | package plan 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type OperationCategory string 9 | 10 | const ( 11 | OperationCategoryMeta OperationCategory = "meta" 12 | OperationCategoryResource OperationCategory = "resource" 13 | OperationCategoryTrack OperationCategory = "track" 14 | OperationCategoryRelease OperationCategory = "release" 15 | ) 16 | 17 | type OperationType string 18 | 19 | const ( 20 | OperationTypeApply OperationType = "apply" 21 | OperationTypeCreate OperationType = "create" 22 | OperationTypeCreateRelease OperationType = "create-release" 23 | OperationTypeDelete OperationType = "delete" 24 | OperationTypeDeleteRelease OperationType = "delete-release" 25 | OperationTypeNoop OperationType = "noop" 26 | OperationTypeRecreate OperationType = "recreate" 27 | OperationTypeTrackAbsence OperationType = "track-absence" 28 | OperationTypeTrackPresence OperationType = "track-presence" 29 | OperationTypeTrackReadiness OperationType = "track-readiness" 30 | OperationTypeUpdate OperationType = "update" 31 | OperationTypeUpdateRelease OperationType = "update-release" 32 | ) 33 | 34 | type OperationVersion int 35 | 36 | const ( 37 | OperationVersionApply OperationVersion = 1 38 | OperationVersionCreate OperationVersion = 1 39 | OperationVersionCreateRelease OperationVersion = 1 40 | OperationVersionDelete OperationVersion = 1 41 | OperationVersionDeleteRelease OperationVersion = 1 42 | OperationVersionNoop OperationVersion = 1 43 | OperationVersionRecreate OperationVersion = 1 44 | OperationVersionTrackAbsence OperationVersion = 1 45 | OperationVersionTrackPresence OperationVersion = 1 46 | OperationVersionTrackReadiness OperationVersion = 1 47 | OperationVersionUpdate OperationVersion = 1 48 | OperationVersionUpdateRelease OperationVersion = 1 49 | ) 50 | 51 | type OperationStatus string 52 | 53 | const ( 54 | OperationStatusUnknown OperationStatus = "" 55 | OperationStatusPending OperationStatus = "pending" 56 | OperationStatusCompleted OperationStatus = "completed" 57 | OperationStatusFailed OperationStatus = "failed" 58 | ) 59 | 60 | type OperationIteration int 61 | 62 | type Operation struct { 63 | Type OperationType 64 | Version OperationVersion 65 | Category OperationCategory 66 | Iteration OperationIteration 67 | Status OperationStatus 68 | Config OperationConfig 69 | } 70 | 71 | func (o *Operation) ID() string { 72 | return OperationID(o.Type, o.Version, o.Iteration, o.Config.ID()) 73 | } 74 | 75 | func (o *Operation) IDHuman() string { 76 | return OperationIDHuman(o.Type, o.Iteration, o.Config.IDHuman()) 77 | } 78 | 79 | func OperationID(t OperationType, version OperationVersion, iteration OperationIteration, configID string) string { 80 | return fmt.Sprintf("%s/%d/%d/%s", t, version, iteration, configID) 81 | } 82 | 83 | func OperationIDHuman(t OperationType, iteration OperationIteration, configIDHuman string) string { 84 | id := fmt.Sprintf("%s: ", strings.ReplaceAll(string(t), "-", " ")) 85 | 86 | id += configIDHuman 87 | if iteration > 0 { 88 | id += fmt.Sprintf(" (iteration=%d)", iteration) 89 | } 90 | 91 | return id 92 | } 93 | -------------------------------------------------------------------------------- /cmd/nelm/chart_secret_values_file_edit.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "cmp" 5 | "context" 6 | "fmt" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/werf/common-go/pkg/cli" 11 | "github.com/werf/nelm/pkg/action" 12 | "github.com/werf/nelm/pkg/common" 13 | "github.com/werf/nelm/pkg/log" 14 | ) 15 | 16 | type chartSecretValuesFileEditOptions struct { 17 | action.SecretValuesFileEditOptions 18 | 19 | LogColorMode string 20 | LogLevel string 21 | ValuesFile string 22 | } 23 | 24 | func newChartSecretValuesFileEditCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 25 | cfg := &chartSecretValuesFileEditOptions{} 26 | 27 | cmd := cli.NewSubCommand( 28 | ctx, 29 | "edit [options...] --secret-key secret-key values-file", 30 | "Interactively edit encrypted values file.", 31 | "Interactively edit encrypted values file.", 32 | 60, 33 | secretCmdGroup, 34 | cli.SubCommandOptions{ 35 | Args: cobra.ExactArgs(1), 36 | ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 37 | return nil, cobra.ShellCompDirectiveDefault 38 | }, 39 | }, 40 | func(cmd *cobra.Command, args []string) error { 41 | ctx = log.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultSecretValuesFileEditLogLevel), log.SetupLoggingOptions{ 42 | ColorMode: cfg.LogColorMode, 43 | }) 44 | 45 | cfg.ValuesFile = args[0] 46 | 47 | if err := action.SecretValuesFileEdit(ctx, cfg.ValuesFile, cfg.SecretValuesFileEditOptions); err != nil { 48 | return fmt.Errorf("secret values file edit: %w", err) 49 | } 50 | 51 | return nil 52 | }, 53 | ) 54 | 55 | afterAllCommandsBuiltFuncs[cmd] = func(cmd *cobra.Command) error { 56 | if err := cli.AddFlag(cmd, &cfg.LogColorMode, "color-mode", common.DefaultLogColorMode, "Color mode for logs. "+allowedLogColorModesHelp(), cli.AddFlagOptions{ 57 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 58 | Group: miscFlagGroup, 59 | }); err != nil { 60 | return fmt.Errorf("add flag: %w", err) 61 | } 62 | 63 | if err := cli.AddFlag(cmd, &cfg.LogLevel, "log-level", string(action.DefaultSecretValuesFileEditLogLevel), "Set log level. "+allowedLogLevelsHelp(), cli.AddFlagOptions{ 64 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 65 | Group: miscFlagGroup, 66 | }); err != nil { 67 | return fmt.Errorf("add flag: %w", err) 68 | } 69 | 70 | if err := cli.AddFlag(cmd, &cfg.SecretKey, "secret-key", "", "Secret key", cli.AddFlagOptions{ 71 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 72 | Group: mainFlagGroup, 73 | Required: true, 74 | }); err != nil { 75 | return fmt.Errorf("add flag: %w", err) 76 | } 77 | 78 | if err := cli.AddFlag(cmd, &cfg.TempDirPath, "temp-dir", "", "The directory for temporary files. By default, create a new directory in the default system directory for temporary files", cli.AddFlagOptions{ 79 | GetEnvVarRegexesFunc: cli.GetFlagGlobalEnvVarRegexes, 80 | Group: miscFlagGroup, 81 | Type: cli.FlagTypeDir, 82 | }); err != nil { 83 | return fmt.Errorf("add flag: %w", err) 84 | } 85 | 86 | return nil 87 | } 88 | 89 | return cmd 90 | } 91 | -------------------------------------------------------------------------------- /internal/resource/spec/resource_meta.go: -------------------------------------------------------------------------------- 1 | package spec 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 9 | "k8s.io/apimachinery/pkg/runtime/schema" 10 | "k8s.io/client-go/kubernetes/scheme" 11 | ) 12 | 13 | type ResourceMeta struct { 14 | Name string 15 | Namespace string 16 | GroupVersionKind schema.GroupVersionKind 17 | FilePath string 18 | Annotations map[string]string 19 | Labels map[string]string 20 | } 21 | 22 | func NewResourceMeta(name, namespace, releaseNamespace, filePath string, gvk schema.GroupVersionKind, annotations, labels map[string]string) *ResourceMeta { 23 | if releaseNamespace == namespace { 24 | namespace = "" 25 | } 26 | 27 | if annotations == nil { 28 | annotations = map[string]string{} 29 | } 30 | 31 | if labels == nil { 32 | labels = map[string]string{} 33 | } 34 | 35 | return &ResourceMeta{ 36 | Name: name, 37 | Namespace: namespace, 38 | GroupVersionKind: gvk, 39 | FilePath: filePath, 40 | Annotations: annotations, 41 | Labels: labels, 42 | } 43 | } 44 | 45 | func NewResourceMetaFromUnstructured(unstruct *unstructured.Unstructured, releaseNamespace, filePath string) *ResourceMeta { 46 | return NewResourceMeta(unstruct.GetName(), unstruct.GetNamespace(), releaseNamespace, filePath, unstruct.GroupVersionKind(), unstruct.GetAnnotations(), unstruct.GetLabels()) 47 | } 48 | 49 | func NewResourceMetaFromPartialMetadata(meta *v1.PartialObjectMetadata, releaseNamespace, filePath string) *ResourceMeta { 50 | return NewResourceMeta(meta.GetName(), meta.GetNamespace(), releaseNamespace, filePath, meta.GroupVersionKind(), meta.GetAnnotations(), meta.GetLabels()) 51 | } 52 | 53 | func NewResourceMetaFromManifest(manifest, releaseNamespace string) (*ResourceMeta, error) { 54 | var filePath string 55 | if strings.HasPrefix(manifest, "# Source: ") { 56 | firstLine := strings.TrimSpace(strings.Split(manifest, "\n")[0]) 57 | filePath = strings.TrimPrefix(firstLine, "# Source: ") 58 | } 59 | 60 | obj, _, err := scheme.Codecs.UniversalDecoder().Decode([]byte(manifest), nil, &v1.PartialObjectMetadata{}) 61 | if err != nil { 62 | return nil, fmt.Errorf("decode resource (file: %q): %w", filePath, err) 63 | } 64 | 65 | return NewResourceMetaFromPartialMetadata(obj.(*v1.PartialObjectMetadata), releaseNamespace, filePath), nil 66 | } 67 | 68 | func (m *ResourceMeta) ID() string { 69 | return ID(m.Name, m.Namespace, m.GroupVersionKind.Group, m.GroupVersionKind.Kind) 70 | } 71 | 72 | func (m *ResourceMeta) IDWithVersion() string { 73 | return IDWithVersion(m.Name, m.Namespace, m.GroupVersionKind.Group, m.GroupVersionKind.Version, m.GroupVersionKind.Kind) 74 | } 75 | 76 | func (m *ResourceMeta) IDHuman() string { 77 | return IDHuman(m.Name, m.Namespace, m.GroupVersionKind.Group, m.GroupVersionKind.Kind) 78 | } 79 | 80 | func ID(name, namespace, group, kind string) string { 81 | return fmt.Sprintf("%s:%s:%s:%s", namespace, group, kind, name) 82 | } 83 | 84 | func IDWithVersion(name, namespace, group, version, kind string) string { 85 | return fmt.Sprintf("%s:%s:%s:%s:%s", namespace, group, version, kind, name) 86 | } 87 | 88 | func IDHuman(name, namespace, group, kind string) string { 89 | id := fmt.Sprintf("%s/%s", kind, name) 90 | 91 | if namespace != "" { 92 | id = fmt.Sprintf("%s (namespace=%s)", id, namespace) 93 | } 94 | 95 | return id 96 | } 97 | -------------------------------------------------------------------------------- /.github/workflows/test_pr.yml: -------------------------------------------------------------------------------- 1 | name: test:pr 2 | 3 | on: 4 | pull_request: 5 | repository_dispatch: 6 | types: ["test:pr"] 7 | workflow_dispatch: 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | detect-changes: 15 | name: Detect changed file groups 16 | runs-on: ubuntu-22.04 17 | timeout-minutes: 10 18 | permissions: 19 | pull-requests: read 20 | outputs: 21 | src_changed: ${{ steps.changed-files.outputs.src_any_changed }} 22 | lint_changed: ${{ steps.changed-files.outputs.lint_any_changed }} 23 | steps: 24 | - name: Checkout code 25 | uses: actions/checkout@v4 26 | 27 | - name: Detect file groups 28 | id: changed-files 29 | uses: tj-actions/changed-files@v47 30 | with: 31 | files_yaml: | 32 | src: 33 | - '**/*.go' 34 | - 'go.mod' 35 | - 'go.sum' 36 | - 'cmd/nelm/**' 37 | - 'internal/**' 38 | - 'pkg/**' 39 | - 'scripts/**' 40 | lint: 41 | - '**/*.go' 42 | - '**/*.yaml' 43 | - '**/*.yml' 44 | 45 | lint: 46 | needs: detect-changes 47 | uses: ./.github/workflows/_lint.yml 48 | with: 49 | forceSkip: ${{ github.event_name == 'pull_request' && needs.detect-changes.outputs.lint_changed == 'false' }} 50 | 51 | unit: 52 | needs: detect-changes 53 | uses: ./.github/workflows/_test_unit.yml 54 | with: 55 | forceSkip: ${{ github.event_name == 'pull_request' && needs.detect-changes.outputs.src_changed == 'false' }} 56 | 57 | build: 58 | needs: detect-changes 59 | if: needs.detect-changes.outputs.src_changed == 'true' 60 | runs-on: ubuntu-22.04 61 | timeout-minutes: 60 62 | steps: 63 | - name: Install build dependencies 64 | run: | 65 | sudo apt update 66 | sudo apt install -y gcc-aarch64-linux-gnu file 67 | 68 | - name: Checkout code 69 | uses: actions/checkout@v4 70 | 71 | - name: Set up Go 72 | id: setup-go 73 | uses: actions/setup-go@v6 74 | with: 75 | cache: false 76 | go-version-file: go.mod 77 | 78 | - name: Cache Go modules and build cache 79 | uses: actions/cache@v4 80 | with: 81 | path: | 82 | ~/.cache/go-build 83 | ~/go/pkg/mod 84 | key: ${{ runner.os }}-go-${{ steps.setup-go.outputs.go-version }}-${{ hashFiles('**/go.sum') }} 85 | restore-keys: | 86 | ${{ runner.os }}-go-${{ steps.setup-go.outputs.go-version }}- 87 | 88 | - name: Install Task 89 | uses: arduino/setup-task@v2 90 | with: 91 | repo-token: ${{ secrets.GITHUB_TOKEN }} 92 | 93 | - name: Build 94 | run: task -p build:dev:all 95 | 96 | notify: 97 | if: | 98 | (github.event_name == 'pull_request' && github.event.pull_request.draft == false && failure()) || 99 | (github.event_name != 'pull_request' && failure()) 100 | needs: 101 | - lint 102 | - unit 103 | uses: werf/common-ci/.github/workflows/notification.yml@main 104 | secrets: 105 | loopNotificationGroup: ${{ vars.LOOP_NOTIFICATION_GROUP }} 106 | webhook: ${{ secrets.LOOP_NOTIFICATION_WEBHOOK }} 107 | notificationChannel: ${{ vars.LOOP_NOTIFICATION_CHANNEL }} 108 | -------------------------------------------------------------------------------- /cmd/nelm/chart_secret_file_decrypt.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "cmp" 5 | "context" 6 | "fmt" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/werf/common-go/pkg/cli" 11 | "github.com/werf/nelm/pkg/action" 12 | "github.com/werf/nelm/pkg/common" 13 | "github.com/werf/nelm/pkg/log" 14 | ) 15 | 16 | type chartSecretFileDecryptOptions struct { 17 | action.SecretFileDecryptOptions 18 | 19 | File string 20 | LogColorMode string 21 | LogLevel string 22 | } 23 | 24 | func newChartSecretFileDecryptCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 25 | cfg := &chartSecretFileDecryptOptions{} 26 | 27 | cmd := cli.NewSubCommand( 28 | ctx, 29 | "decrypt [options...] --secret-key secret-key file", 30 | "Decrypt file and print result to stdout.", 31 | "Decrypt file and print result to stdout.", 32 | 10, 33 | secretCmdGroup, 34 | cli.SubCommandOptions{ 35 | Args: cobra.ExactArgs(1), 36 | ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 37 | return nil, cobra.ShellCompDirectiveDefault 38 | }, 39 | }, 40 | func(cmd *cobra.Command, args []string) error { 41 | ctx = log.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultSecretFileDecryptLogLevel), log.SetupLoggingOptions{ 42 | ColorMode: cfg.LogColorMode, 43 | LogIsParseable: true, 44 | }) 45 | 46 | cfg.File = args[0] 47 | 48 | if err := action.SecretFileDecrypt(ctx, cfg.File, cfg.SecretFileDecryptOptions); err != nil { 49 | return fmt.Errorf("secret file decrypt: %w", err) 50 | } 51 | 52 | return nil 53 | }, 54 | ) 55 | 56 | afterAllCommandsBuiltFuncs[cmd] = func(cmd *cobra.Command) error { 57 | if err := cli.AddFlag(cmd, &cfg.LogColorMode, "color-mode", common.DefaultLogColorMode, "Color mode for logs. "+allowedLogColorModesHelp(), cli.AddFlagOptions{ 58 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 59 | Group: miscFlagGroup, 60 | }); err != nil { 61 | return fmt.Errorf("add flag: %w", err) 62 | } 63 | 64 | if err := cli.AddFlag(cmd, &cfg.LogLevel, "log-level", string(action.DefaultSecretFileDecryptLogLevel), "Set log level. "+allowedLogLevelsHelp(), cli.AddFlagOptions{ 65 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 66 | Group: miscFlagGroup, 67 | }); err != nil { 68 | return fmt.Errorf("add flag: %w", err) 69 | } 70 | 71 | if err := cli.AddFlag(cmd, &cfg.OutputFilePath, "save-output-to", "", "Save decrypted output to a file", cli.AddFlagOptions{ 72 | Type: cli.FlagTypeFile, 73 | Group: mainFlagGroup, 74 | }); err != nil { 75 | return fmt.Errorf("add flag: %w", err) 76 | } 77 | 78 | if err := cli.AddFlag(cmd, &cfg.SecretKey, "secret-key", "", "Secret key", cli.AddFlagOptions{ 79 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 80 | Group: mainFlagGroup, 81 | Required: true, 82 | }); err != nil { 83 | return fmt.Errorf("add flag: %w", err) 84 | } 85 | 86 | if err := cli.AddFlag(cmd, &cfg.TempDirPath, "temp-dir", "", "The directory for temporary files. By default, create a new directory in the default system directory for temporary files", cli.AddFlagOptions{ 87 | GetEnvVarRegexesFunc: cli.GetFlagGlobalEnvVarRegexes, 88 | Group: miscFlagGroup, 89 | Type: cli.FlagTypeDir, 90 | }); err != nil { 91 | return fmt.Errorf("add flag: %w", err) 92 | } 93 | 94 | return nil 95 | } 96 | 97 | return cmd 98 | } 99 | -------------------------------------------------------------------------------- /cmd/nelm/chart_secret_file_encrypt.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "cmp" 5 | "context" 6 | "fmt" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/werf/common-go/pkg/cli" 11 | "github.com/werf/nelm/pkg/action" 12 | "github.com/werf/nelm/pkg/common" 13 | "github.com/werf/nelm/pkg/log" 14 | ) 15 | 16 | type chartSecretFileEncryptOptions struct { 17 | action.SecretFileEncryptOptions 18 | 19 | File string 20 | LogColorMode string 21 | LogLevel string 22 | } 23 | 24 | func newChartSecretFileEncryptCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 25 | cfg := &chartSecretFileEncryptOptions{} 26 | 27 | cmd := cli.NewSubCommand( 28 | ctx, 29 | "encrypt [options...] --secret-key secret-key file", 30 | "Encrypt file and print result to stdout.", 31 | "Encrypt file and print result to stdout.", 32 | 20, 33 | secretCmdGroup, 34 | cli.SubCommandOptions{ 35 | Args: cobra.ExactArgs(1), 36 | ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 37 | return nil, cobra.ShellCompDirectiveDefault 38 | }, 39 | }, 40 | func(cmd *cobra.Command, args []string) error { 41 | ctx = log.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultSecretFileEncryptLogLevel), log.SetupLoggingOptions{ 42 | ColorMode: cfg.LogColorMode, 43 | LogIsParseable: true, 44 | }) 45 | 46 | cfg.File = args[0] 47 | 48 | if err := action.SecretFileEncrypt(ctx, cfg.File, cfg.SecretFileEncryptOptions); err != nil { 49 | return fmt.Errorf("secret file encrypt: %w", err) 50 | } 51 | 52 | return nil 53 | }, 54 | ) 55 | 56 | afterAllCommandsBuiltFuncs[cmd] = func(cmd *cobra.Command) error { 57 | if err := cli.AddFlag(cmd, &cfg.LogColorMode, "color-mode", common.DefaultLogColorMode, "Color mode for logs. "+allowedLogColorModesHelp(), cli.AddFlagOptions{ 58 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 59 | Group: miscFlagGroup, 60 | }); err != nil { 61 | return fmt.Errorf("add flag: %w", err) 62 | } 63 | 64 | if err := cli.AddFlag(cmd, &cfg.LogLevel, "log-level", string(action.DefaultSecretFileEncryptLogLevel), "Set log level. "+allowedLogLevelsHelp(), cli.AddFlagOptions{ 65 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 66 | Group: miscFlagGroup, 67 | }); err != nil { 68 | return fmt.Errorf("add flag: %w", err) 69 | } 70 | 71 | if err := cli.AddFlag(cmd, &cfg.OutputFilePath, "save-output-to", "", "Save encrypted output to a file", cli.AddFlagOptions{ 72 | Type: cli.FlagTypeFile, 73 | Group: mainFlagGroup, 74 | }); err != nil { 75 | return fmt.Errorf("add flag: %w", err) 76 | } 77 | 78 | if err := cli.AddFlag(cmd, &cfg.SecretKey, "secret-key", "", "Secret key", cli.AddFlagOptions{ 79 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 80 | Group: mainFlagGroup, 81 | Required: true, 82 | }); err != nil { 83 | return fmt.Errorf("add flag: %w", err) 84 | } 85 | 86 | if err := cli.AddFlag(cmd, &cfg.TempDirPath, "temp-dir", "", "The directory for temporary files. By default, create a new directory in the default system directory for temporary files", cli.AddFlagOptions{ 87 | GetEnvVarRegexesFunc: cli.GetFlagGlobalEnvVarRegexes, 88 | Group: miscFlagGroup, 89 | Type: cli.FlagTypeDir, 90 | }); err != nil { 91 | return fmt.Errorf("add flag: %w", err) 92 | } 93 | 94 | return nil 95 | } 96 | 97 | return cmd 98 | } 99 | -------------------------------------------------------------------------------- /pkg/action/version.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | "strings" 9 | 10 | "github.com/Masterminds/semver/v3" 11 | "github.com/goccy/go-yaml" 12 | "github.com/gookit/color" 13 | 14 | "github.com/werf/3p-helm/pkg/chart/loader" 15 | "github.com/werf/nelm/internal/util" 16 | "github.com/werf/nelm/pkg/common" 17 | "github.com/werf/nelm/pkg/log" 18 | ) 19 | 20 | const ( 21 | DefaultVersionOutputFormat = common.OutputFormatYAML 22 | DefaultVersionLogLevel = log.ErrorLevel 23 | ) 24 | 25 | type VersionOptions struct { 26 | // OutputFormat specifies the output format for version information. 27 | // Valid values: "yaml" (default), "json". 28 | // Defaults to DefaultVersionOutputFormat (yaml) if not specified. 29 | OutputFormat string 30 | // OutputNoPrint, when true, suppresses printing the output and only returns the result data structure. 31 | // Useful when calling this programmatically. 32 | OutputNoPrint bool 33 | // TempDirPath is the directory for temporary files during the operation. 34 | // A temporary directory is created automatically if not specified. 35 | TempDirPath string 36 | } 37 | 38 | func Version(ctx context.Context, opts VersionOptions) (*VersionResult, error) { 39 | opts, err := applyVersionOptionsDefaults(opts) 40 | if err != nil { 41 | return nil, fmt.Errorf("build version options: %w", err) 42 | } 43 | 44 | loader.NoChartLockWarning = "" 45 | 46 | result := &VersionResult{ 47 | FullVersion: common.Version, 48 | } 49 | 50 | if semVer, err := semver.StrictNewVersion(common.Version); err == nil { 51 | result.MajorVersion = util.Uint64ToInt(semVer.Major()) 52 | result.MinorVersion = util.Uint64ToInt(semVer.Minor()) 53 | result.PatchVersion = util.Uint64ToInt(semVer.Patch()) 54 | } 55 | 56 | if !opts.OutputNoPrint { 57 | var resultMessage string 58 | 59 | switch opts.OutputFormat { 60 | case common.OutputFormatJSON: 61 | b, err := json.MarshalIndent(result, "", strings.Repeat(" ", 2)) 62 | if err != nil { 63 | return nil, fmt.Errorf("marshal result to json: %w", err) 64 | } 65 | 66 | resultMessage = string(b) 67 | case common.OutputFormatYAML: 68 | b, err := yaml.MarshalContext(ctx, result, yaml.UseLiteralStyleIfMultiline(true)) 69 | if err != nil { 70 | return nil, fmt.Errorf("marshal result to yaml: %w", err) 71 | } 72 | 73 | resultMessage = string(b) 74 | default: 75 | return nil, fmt.Errorf("unknown output format %q", opts.OutputFormat) 76 | } 77 | 78 | var colorLevel color.Level 79 | if color.Enable { 80 | colorLevel = color.TermColorLevel() 81 | } 82 | 83 | if err := writeWithSyntaxHighlight(os.Stdout, resultMessage, opts.OutputFormat, colorLevel); err != nil { 84 | return nil, fmt.Errorf("write result to output: %w", err) 85 | } 86 | } 87 | 88 | return result, nil 89 | } 90 | 91 | func applyVersionOptionsDefaults(opts VersionOptions) (VersionOptions, error) { 92 | var err error 93 | if opts.TempDirPath == "" { 94 | opts.TempDirPath, err = os.MkdirTemp("", "") 95 | if err != nil { 96 | return VersionOptions{}, fmt.Errorf("create temp dir: %w", err) 97 | } 98 | } 99 | 100 | if opts.OutputFormat == "" { 101 | opts.OutputFormat = DefaultVersionOutputFormat 102 | } 103 | 104 | return opts, nil 105 | } 106 | 107 | type VersionResult struct { 108 | FullVersion string `json:"full"` 109 | MajorVersion int `json:"major"` 110 | MinorVersion int `json:"minor"` 111 | PatchVersion int `json:"patch"` 112 | } 113 | -------------------------------------------------------------------------------- /cmd/nelm/chart_secret_values_file_decrypt.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "cmp" 5 | "context" 6 | "fmt" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/werf/common-go/pkg/cli" 11 | "github.com/werf/nelm/pkg/action" 12 | "github.com/werf/nelm/pkg/common" 13 | "github.com/werf/nelm/pkg/log" 14 | ) 15 | 16 | type chartSecretValuesFileDecryptOptions struct { 17 | action.SecretValuesFileDecryptOptions 18 | 19 | LogColorMode string 20 | LogLevel string 21 | ValuesFile string 22 | } 23 | 24 | func newChartSecretValuesFileDecryptCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 25 | cfg := &chartSecretValuesFileDecryptOptions{} 26 | 27 | cmd := cli.NewSubCommand( 28 | ctx, 29 | "decrypt [options...] --secret-key secret-key values-file", 30 | "Decrypt values file and print result to stdout.", 31 | "Decrypt values file and print result to stdout.", 32 | 40, 33 | secretCmdGroup, 34 | cli.SubCommandOptions{ 35 | Args: cobra.ExactArgs(1), 36 | ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 37 | return nil, cobra.ShellCompDirectiveDefault 38 | }, 39 | }, 40 | func(cmd *cobra.Command, args []string) error { 41 | ctx = log.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultSecretValuesFileDecryptLogLevel), log.SetupLoggingOptions{ 42 | ColorMode: cfg.LogColorMode, 43 | LogIsParseable: true, 44 | }) 45 | 46 | cfg.ValuesFile = args[0] 47 | 48 | if err := action.SecretValuesFileDecrypt(ctx, cfg.ValuesFile, cfg.SecretValuesFileDecryptOptions); err != nil { 49 | return fmt.Errorf("secret values file decrypt: %w", err) 50 | } 51 | 52 | return nil 53 | }, 54 | ) 55 | 56 | afterAllCommandsBuiltFuncs[cmd] = func(cmd *cobra.Command) error { 57 | if err := cli.AddFlag(cmd, &cfg.LogColorMode, "color-mode", common.DefaultLogColorMode, "Color mode for logs. "+allowedLogColorModesHelp(), cli.AddFlagOptions{ 58 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 59 | Group: miscFlagGroup, 60 | }); err != nil { 61 | return fmt.Errorf("add flag: %w", err) 62 | } 63 | 64 | if err := cli.AddFlag(cmd, &cfg.LogLevel, "log-level", string(action.DefaultSecretValuesFileDecryptLogLevel), "Set log level. "+allowedLogLevelsHelp(), cli.AddFlagOptions{ 65 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 66 | Group: miscFlagGroup, 67 | }); err != nil { 68 | return fmt.Errorf("add flag: %w", err) 69 | } 70 | 71 | if err := cli.AddFlag(cmd, &cfg.OutputFilePath, "save-output-to", "", "Save decrypted output to a file", cli.AddFlagOptions{ 72 | Type: cli.FlagTypeFile, 73 | Group: mainFlagGroup, 74 | }); err != nil { 75 | return fmt.Errorf("add flag: %w", err) 76 | } 77 | 78 | if err := cli.AddFlag(cmd, &cfg.SecretKey, "secret-key", "", "Secret key", cli.AddFlagOptions{ 79 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 80 | Group: mainFlagGroup, 81 | Required: true, 82 | }); err != nil { 83 | return fmt.Errorf("add flag: %w", err) 84 | } 85 | 86 | if err := cli.AddFlag(cmd, &cfg.TempDirPath, "temp-dir", "", "The directory for temporary files. By default, create a new directory in the default system directory for temporary files", cli.AddFlagOptions{ 87 | GetEnvVarRegexesFunc: cli.GetFlagGlobalEnvVarRegexes, 88 | Group: miscFlagGroup, 89 | Type: cli.FlagTypeDir, 90 | }); err != nil { 91 | return fmt.Errorf("add flag: %w", err) 92 | } 93 | 94 | return nil 95 | } 96 | 97 | return cmd 98 | } 99 | -------------------------------------------------------------------------------- /cmd/nelm/chart_secret_values_file_encrypt.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "cmp" 5 | "context" 6 | "fmt" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/werf/common-go/pkg/cli" 11 | "github.com/werf/nelm/pkg/action" 12 | "github.com/werf/nelm/pkg/common" 13 | "github.com/werf/nelm/pkg/log" 14 | ) 15 | 16 | type chartSecretValuesFileEncryptOptions struct { 17 | action.SecretValuesFileEncryptOptions 18 | 19 | LogColorMode string 20 | LogLevel string 21 | ValuesFile string 22 | } 23 | 24 | func newChartSecretValuesFileEncryptCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 25 | cfg := &chartSecretValuesFileEncryptOptions{} 26 | 27 | cmd := cli.NewSubCommand( 28 | ctx, 29 | "encrypt [options...] --secret-key secret-key values-file", 30 | "Encrypt values file and print result to stdout.", 31 | "Encrypt values file and print result to stdout.", 32 | 50, 33 | secretCmdGroup, 34 | cli.SubCommandOptions{ 35 | Args: cobra.ExactArgs(1), 36 | ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 37 | return nil, cobra.ShellCompDirectiveDefault 38 | }, 39 | }, 40 | func(cmd *cobra.Command, args []string) error { 41 | ctx = log.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultSecretValuesFileEncryptLogLevel), log.SetupLoggingOptions{ 42 | ColorMode: cfg.LogColorMode, 43 | LogIsParseable: true, 44 | }) 45 | 46 | cfg.ValuesFile = args[0] 47 | 48 | if err := action.SecretValuesFileEncrypt(ctx, cfg.ValuesFile, cfg.SecretValuesFileEncryptOptions); err != nil { 49 | return fmt.Errorf("secret values file encrypt: %w", err) 50 | } 51 | 52 | return nil 53 | }, 54 | ) 55 | 56 | afterAllCommandsBuiltFuncs[cmd] = func(cmd *cobra.Command) error { 57 | if err := cli.AddFlag(cmd, &cfg.LogColorMode, "color-mode", common.DefaultLogColorMode, "Color mode for logs. "+allowedLogColorModesHelp(), cli.AddFlagOptions{ 58 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 59 | Group: miscFlagGroup, 60 | }); err != nil { 61 | return fmt.Errorf("add flag: %w", err) 62 | } 63 | 64 | if err := cli.AddFlag(cmd, &cfg.LogLevel, "log-level", string(action.DefaultSecretValuesFileEncryptLogLevel), "Set log level. "+allowedLogLevelsHelp(), cli.AddFlagOptions{ 65 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 66 | Group: miscFlagGroup, 67 | }); err != nil { 68 | return fmt.Errorf("add flag: %w", err) 69 | } 70 | 71 | if err := cli.AddFlag(cmd, &cfg.OutputFilePath, "save-output-to", "", "Save encrypted output to a file", cli.AddFlagOptions{ 72 | Type: cli.FlagTypeFile, 73 | Group: mainFlagGroup, 74 | }); err != nil { 75 | return fmt.Errorf("add flag: %w", err) 76 | } 77 | 78 | if err := cli.AddFlag(cmd, &cfg.SecretKey, "secret-key", "", "Secret key", cli.AddFlagOptions{ 79 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 80 | Group: mainFlagGroup, 81 | Required: true, 82 | }); err != nil { 83 | return fmt.Errorf("add flag: %w", err) 84 | } 85 | 86 | if err := cli.AddFlag(cmd, &cfg.TempDirPath, "temp-dir", "", "The directory for temporary files. By default, create a new directory in the default system directory for temporary files", cli.AddFlagOptions{ 87 | GetEnvVarRegexesFunc: cli.GetFlagGlobalEnvVarRegexes, 88 | Group: miscFlagGroup, 89 | Type: cli.FlagTypeDir, 90 | }); err != nil { 91 | return fmt.Errorf("add flag: %w", err) 92 | } 93 | 94 | return nil 95 | } 96 | 97 | return cmd 98 | } 99 | -------------------------------------------------------------------------------- /internal/kube/fake/client_dynamic.go: -------------------------------------------------------------------------------- 1 | package fake 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | 7 | jsonpatch "github.com/evanphx/json-patch" 8 | "k8s.io/apimachinery/pkg/api/meta" 9 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 10 | "k8s.io/apimachinery/pkg/runtime" 11 | "k8s.io/apimachinery/pkg/types" 12 | "k8s.io/apimachinery/pkg/util/json" 13 | dynamicfake "k8s.io/client-go/dynamic/fake" 14 | staticfake "k8s.io/client-go/kubernetes/fake" 15 | "k8s.io/client-go/kubernetes/scheme" 16 | "k8s.io/client-go/testing" 17 | 18 | "github.com/werf/nelm/internal/kube" 19 | "github.com/werf/nelm/internal/resource/spec" 20 | ) 21 | 22 | func NewDynamicClient(staticClient *staticfake.Clientset, mapper meta.ResettableRESTMapper) *dynamicfake.FakeDynamicClient { 23 | dynClient := dynamicfake.NewSimpleDynamicClient(scheme.Scheme) 24 | dynClient.PrependReactor("*", "*", prepareReaction(dynClient.Tracker(), mapper)) 25 | 26 | return dynClient 27 | } 28 | 29 | func prepareReaction(tracker testing.ObjectTracker, mapper meta.ResettableRESTMapper) testing.ReactionFunc { 30 | return func(action testing.Action) (bool, runtime.Object, error) { 31 | actionImpl, ok := action.(testing.PatchActionImpl) 32 | if !ok { 33 | return false, nil, nil 34 | } 35 | 36 | switch actionImpl.PatchType { 37 | // Default fake client doesn't support StrategicMergePatchType 38 | case types.StrategicMergePatchType: 39 | return mergePatch(actionImpl, tracker) 40 | // Default fake client doesn't support ApplyPatchType 41 | case types.ApplyPatchType: 42 | getObj, err := tracker.Get(actionImpl.Resource, actionImpl.Namespace, actionImpl.Name) 43 | if err != nil { 44 | if !kube.IsNotFoundErr(err) && !kube.IsNoSuchKindErr(err) { 45 | return true, nil, fmt.Errorf("get object for apply patch: %w", err) 46 | } 47 | } 48 | 49 | if getObj != nil { 50 | return mergePatch(actionImpl, tracker) 51 | } else { 52 | obj, gvk, err := scheme.Codecs.UniversalDecoder().Decode(actionImpl.Patch, nil, &unstructured.Unstructured{}) 53 | if err != nil { 54 | return true, nil, fmt.Errorf("decode object for apply patch: %w", err) 55 | } 56 | 57 | gvr, _, err := spec.GVKtoGVR(*gvk, mapper) 58 | if err != nil { 59 | return true, nil, fmt.Errorf("map gvk to gvr for apply patch: %w", err) 60 | } 61 | 62 | if err := tracker.Create(gvr, obj, actionImpl.Namespace); err != nil { 63 | return true, nil, fmt.Errorf("create object for apply patch: %w", err) 64 | } 65 | 66 | return true, obj, nil 67 | } 68 | } 69 | 70 | return false, nil, nil 71 | } 72 | } 73 | 74 | // From: https://github.com/kubernetes/client-go/blob/d7cf8c9b31936f927a83634cc840fa2bad7368d9/testing/fixture.go#L175 75 | func mergePatch(action testing.PatchActionImpl, tracker testing.ObjectTracker) (bool, runtime.Object, error) { 76 | ns := action.GetNamespace() 77 | gvr := action.GetResource() 78 | 79 | obj, err := tracker.Get(gvr, ns, action.GetName()) 80 | if err != nil { 81 | return true, nil, err 82 | } 83 | 84 | old, err := json.Marshal(obj) 85 | if err != nil { 86 | return true, nil, err 87 | } 88 | 89 | value := reflect.ValueOf(obj) 90 | value.Elem().Set(reflect.New(value.Type().Elem()).Elem()) 91 | 92 | modified, err := jsonpatch.MergePatch(old, action.GetPatch()) 93 | if err != nil { 94 | return true, nil, err 95 | } 96 | 97 | if err := json.Unmarshal(modified, obj); err != nil { 98 | return true, nil, err 99 | } 100 | 101 | if err = tracker.Update(gvr, obj, ns); err != nil { 102 | return true, nil, err 103 | } 104 | 105 | return true, obj, nil 106 | } 107 | -------------------------------------------------------------------------------- /internal/kube/factory.go: -------------------------------------------------------------------------------- 1 | package kube 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | "sync" 8 | 9 | "github.com/samber/lo" 10 | apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 11 | apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 12 | "k8s.io/apimachinery/pkg/api/meta" 13 | "k8s.io/client-go/discovery" 14 | "k8s.io/client-go/dynamic" 15 | "k8s.io/client-go/kubernetes" 16 | "k8s.io/client-go/kubernetes/scheme" 17 | ) 18 | 19 | var ( 20 | _ ClientFactorier = (*ClientFactory)(nil) 21 | 22 | AddToScheme sync.Once 23 | ) 24 | 25 | type ClientFactorier interface { 26 | KubeClient() KubeClienter 27 | Static() kubernetes.Interface 28 | Dynamic() dynamic.Interface 29 | Discovery() discovery.CachedDiscoveryInterface 30 | Mapper() meta.ResettableRESTMapper 31 | LegacyClientGetter() *LegacyClientGetter 32 | KubeConfig() *KubeConfig 33 | } 34 | 35 | type ClientFactory struct { 36 | discoveryClient discovery.CachedDiscoveryInterface 37 | dynamicClient dynamic.Interface 38 | kubeClient KubeClienter 39 | kubeConfig *KubeConfig 40 | legacyClientGetter *LegacyClientGetter 41 | mapper meta.ResettableRESTMapper 42 | staticClient kubernetes.Interface 43 | } 44 | 45 | func NewClientFactory(ctx context.Context, kubeConfig *KubeConfig) (*ClientFactory, error) { 46 | AddToScheme.Do(func() { 47 | lo.Must0(apiextv1.AddToScheme(scheme.Scheme)) 48 | lo.Must0(apiextv1beta1.AddToScheme(scheme.Scheme)) 49 | }) 50 | 51 | staticClient, err := NewStaticKubeClientFromKubeConfig(kubeConfig) 52 | if err != nil { 53 | return nil, fmt.Errorf("construct static kubernetes client: %w", err) 54 | } 55 | 56 | if _, err := staticClient.ServerVersion(); err != nil { 57 | return nil, fmt.Errorf("check kubernetes cluster version to check kubernetes connectivity: %w", err) 58 | } 59 | 60 | dynamicClient, err := NewDynamicKubeClientFromKubeConfig(kubeConfig) 61 | if err != nil { 62 | return nil, fmt.Errorf("construct dynamic kubernetes client: %w", err) 63 | } 64 | 65 | discoveryClient, err := NewDiscoveryKubeClientFromKubeConfig(kubeConfig) 66 | if err != nil { 67 | return nil, fmt.Errorf("construct discovery kubernetes client: %w", err) 68 | } 69 | 70 | mapper := reflect.ValueOf(NewKubeMapper(ctx, discoveryClient)).Interface().(meta.ResettableRESTMapper) 71 | kubeClient := NewKubeClient(staticClient, dynamicClient, discoveryClient, mapper) 72 | legacyClientGetter := NewLegacyClientGetter(discoveryClient, mapper, kubeConfig.RestConfig, kubeConfig.LegacyClientConfig) 73 | 74 | clientFactory := &ClientFactory{ 75 | discoveryClient: discoveryClient, 76 | dynamicClient: dynamicClient, 77 | kubeClient: kubeClient, 78 | kubeConfig: kubeConfig, 79 | legacyClientGetter: legacyClientGetter, 80 | mapper: mapper, 81 | staticClient: staticClient, 82 | } 83 | 84 | return clientFactory, nil 85 | } 86 | 87 | func (f *ClientFactory) KubeClient() KubeClienter { 88 | return f.kubeClient 89 | } 90 | 91 | func (f *ClientFactory) Static() kubernetes.Interface { 92 | return f.staticClient 93 | } 94 | 95 | func (f *ClientFactory) Dynamic() dynamic.Interface { 96 | return f.dynamicClient 97 | } 98 | 99 | func (f *ClientFactory) Discovery() discovery.CachedDiscoveryInterface { 100 | return f.discoveryClient 101 | } 102 | 103 | func (f *ClientFactory) Mapper() meta.ResettableRESTMapper { 104 | return f.mapper 105 | } 106 | 107 | func (f *ClientFactory) LegacyClientGetter() *LegacyClientGetter { 108 | return f.legacyClientGetter 109 | } 110 | 111 | func (f *ClientFactory) KubeConfig() *KubeConfig { 112 | return f.kubeConfig 113 | } 114 | -------------------------------------------------------------------------------- /cmd/nelm/chart_secret_key_rotate.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "cmp" 5 | "context" 6 | "fmt" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/werf/common-go/pkg/cli" 11 | "github.com/werf/nelm/pkg/action" 12 | "github.com/werf/nelm/pkg/common" 13 | "github.com/werf/nelm/pkg/log" 14 | ) 15 | 16 | type chartSecretKeyRotateOptions struct { 17 | action.SecretKeyRotateOptions 18 | 19 | LogColorMode string 20 | LogLevel string 21 | } 22 | 23 | func newChartSecretKeyRotateCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 24 | cfg := &chartSecretKeyRotateOptions{} 25 | 26 | cmd := cli.NewSubCommand( 27 | ctx, 28 | "rotate [options...] --old-secret-key secret-key --new-secret-key secret-key [chart-dir]", 29 | "Reencrypt secret files with a new secret key.", 30 | "Decrypt with an old secret key, then encrypt with a new secret key chart files secret-values.yaml and secret/*.", 31 | 70, 32 | secretCmdGroup, 33 | cli.SubCommandOptions{ 34 | Args: cobra.MaximumNArgs(1), 35 | ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 36 | return nil, cobra.ShellCompDirectiveFilterDirs 37 | }, 38 | }, 39 | func(cmd *cobra.Command, args []string) error { 40 | ctx = log.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultSecretKeyRotateLogLevel), log.SetupLoggingOptions{ 41 | ColorMode: cfg.LogColorMode, 42 | }) 43 | 44 | if len(args) > 0 { 45 | cfg.ChartDirPath = args[0] 46 | } 47 | 48 | if err := action.SecretKeyRotate(ctx, cfg.SecretKeyRotateOptions); err != nil { 49 | return fmt.Errorf("secret key rotate: %w", err) 50 | } 51 | 52 | return nil 53 | }, 54 | ) 55 | 56 | afterAllCommandsBuiltFuncs[cmd] = func(cmd *cobra.Command) error { 57 | if err := cli.AddFlag(cmd, &cfg.LogColorMode, "color-mode", common.DefaultLogColorMode, "Color mode for logs. "+allowedLogColorModesHelp(), cli.AddFlagOptions{ 58 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 59 | Group: miscFlagGroup, 60 | }); err != nil { 61 | return fmt.Errorf("add flag: %w", err) 62 | } 63 | 64 | if err := cli.AddFlag(cmd, &cfg.LogLevel, "log-level", string(action.DefaultSecretKeyRotateLogLevel), "Set log level. "+allowedLogLevelsHelp(), cli.AddFlagOptions{ 65 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 66 | Group: miscFlagGroup, 67 | }); err != nil { 68 | return fmt.Errorf("add flag: %w", err) 69 | } 70 | 71 | if err := cli.AddFlag(cmd, &cfg.NewSecretKey, "new-secret-key", "", "New secret key", cli.AddFlagOptions{ 72 | Group: mainFlagGroup, 73 | Required: true, 74 | }); err != nil { 75 | return fmt.Errorf("add flag: %w", err) 76 | } 77 | 78 | if err := cli.AddFlag(cmd, &cfg.OldSecretKey, "old-secret-key", "", "Old secret key", cli.AddFlagOptions{ 79 | Group: mainFlagGroup, 80 | Required: true, 81 | }); err != nil { 82 | return fmt.Errorf("add flag: %w", err) 83 | } 84 | 85 | if err := cli.AddFlag(cmd, &cfg.SecretValuesFiles, "secret-values", []string{}, "Secret values files paths", cli.AddFlagOptions{ 86 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 87 | Group: mainFlagGroup, 88 | Type: cli.FlagTypeFile, 89 | }); err != nil { 90 | return fmt.Errorf("add flag: %w", err) 91 | } 92 | 93 | if err := cli.AddFlag(cmd, &cfg.TempDirPath, "temp-dir", "", "The directory for temporary files. By default, create a new directory in the default system directory for temporary files", cli.AddFlagOptions{ 94 | GetEnvVarRegexesFunc: cli.GetFlagGlobalEnvVarRegexes, 95 | Group: miscFlagGroup, 96 | Type: cli.FlagTypeDir, 97 | }); err != nil { 98 | return fmt.Errorf("add flag: %w", err) 99 | } 100 | 101 | return nil 102 | } 103 | 104 | return cmd 105 | } 106 | -------------------------------------------------------------------------------- /internal/resource/spec/unstruct.go: -------------------------------------------------------------------------------- 1 | package spec 2 | 3 | import ( 4 | "regexp" 5 | 6 | "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 8 | 9 | "github.com/werf/nelm/pkg/common" 10 | ) 11 | 12 | type CleanUnstructOptions struct { 13 | CleanHelmShAnnos bool 14 | CleanManagedFields bool 15 | CleanNullFields bool 16 | CleanReleaseAnnosLabels bool 17 | CleanRuntimeData bool 18 | CleanWerfIoAnnos bool 19 | CleanWerfIoRuntimeAnnos bool 20 | } 21 | 22 | func CleanUnstruct(unstruct *unstructured.Unstructured, opts CleanUnstructOptions) *unstructured.Unstructured { 23 | unstructCopy := unstruct.DeepCopy() 24 | 25 | if opts.CleanRuntimeData { 26 | cleanRuntimeDataFromUnstruct(unstructCopy) 27 | } 28 | 29 | if opts.CleanManagedFields { 30 | unstructCopy.SetManagedFields(nil) 31 | } 32 | 33 | var ( 34 | cleanAnnotationsRegexes []*regexp.Regexp 35 | cleanLabelsRegexes []*regexp.Regexp 36 | ) 37 | 38 | if opts.CleanHelmShAnnos { 39 | cleanAnnotationsRegexes = append(cleanAnnotationsRegexes, regexp.MustCompile(`^helm\.sh/.+`)) 40 | } 41 | 42 | if opts.CleanWerfIoAnnos { 43 | cleanAnnotationsRegexes = append(cleanAnnotationsRegexes, regexp.MustCompile(`.*werf\.io/.+`)) 44 | } 45 | 46 | if opts.CleanWerfIoRuntimeAnnos { 47 | cleanAnnotationsRegexes = append(cleanAnnotationsRegexes, 48 | regexp.MustCompile(`.*ci\.werf\.io/.+`), 49 | regexp.MustCompile(`^project\.werf\.io/.+`), 50 | regexp.MustCompile(`^werf\.io/version$`), regexp.MustCompile(`^werf\.io/release-channel$`), 51 | ) 52 | } 53 | 54 | if opts.CleanReleaseAnnosLabels { 55 | cleanAnnotationsRegexes = append(cleanAnnotationsRegexes, common.AnnotationKeyPatternReleaseName, common.AnnotationKeyPatternReleaseNamespace) 56 | cleanLabelsRegexes = append(cleanLabelsRegexes, common.LabelKeyPatternManagedBy) 57 | } 58 | 59 | if annos := unstructCopy.GetAnnotations(); len(annos) > 0 { 60 | filteredAnnos := filterAnnosOrLabels(annos, cleanAnnotationsRegexes) 61 | unstructCopy.SetAnnotations(filteredAnnos) 62 | } 63 | 64 | if labels := unstructCopy.GetLabels(); len(labels) > 0 { 65 | filteredLabels := filterAnnosOrLabels(labels, cleanLabelsRegexes) 66 | unstructCopy.SetLabels(filteredLabels) 67 | } 68 | 69 | if opts.CleanNullFields { 70 | unstructCopy.Object = cleanNulls(unstructCopy.Object).(map[string]interface{}) 71 | } 72 | 73 | return unstructCopy 74 | } 75 | 76 | func filterAnnosOrLabels(annosOrLabels map[string]string, regexes []*regexp.Regexp) map[string]string { 77 | filtered := map[string]string{} 78 | 79 | annoOrLabelLoop: 80 | for key, val := range annosOrLabels { 81 | for _, regex := range regexes { 82 | if regex.MatchString(key) { 83 | continue annoOrLabelLoop 84 | } 85 | } 86 | 87 | filtered[key] = val 88 | } 89 | 90 | return filtered 91 | } 92 | 93 | func cleanRuntimeDataFromUnstruct(unstruct *unstructured.Unstructured) { 94 | unstruct.SetResourceVersion("") 95 | unstruct.SetGeneration(0) 96 | unstruct.SetUID("") 97 | unstruct.SetCreationTimestamp(v1.Time{}) 98 | unstruct.SetSelfLink("") 99 | unstruct.SetFinalizers(nil) 100 | delete(unstruct.Object, "status") 101 | 102 | managedFields := unstruct.GetManagedFields() 103 | for i := 0; i < len(managedFields); i++ { 104 | managedFields[i].Time = nil 105 | } 106 | 107 | unstruct.SetManagedFields(managedFields) 108 | } 109 | 110 | func cleanNulls(field interface{}) interface{} { 111 | switch f := field.(type) { 112 | case map[string]interface{}: 113 | cleanedF := map[string]interface{}{} 114 | for k, v := range f { 115 | cleanedV := cleanNulls(v) 116 | if cleanedV != nil { 117 | cleanedF[k] = cleanedV 118 | } 119 | } 120 | 121 | return cleanedF 122 | case []interface{}: 123 | cleanedF := []interface{}{} 124 | for _, v := range f { 125 | cleanedVal := cleanNulls(v) 126 | if cleanedVal != nil { 127 | cleanedF = append(cleanedF, cleanedVal) 128 | } 129 | } 130 | 131 | return cleanedF 132 | default: 133 | return f 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /internal/resource/spec/transform.go: -------------------------------------------------------------------------------- 1 | package spec 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 8 | "k8s.io/apimachinery/pkg/runtime" 9 | 10 | "github.com/werf/nelm/pkg/log" 11 | ) 12 | 13 | var ( 14 | _ ResourceTransformer = (*DropInvalidAnnotationsAndLabelsTransformer)(nil) 15 | _ ResourceTransformer = (*ResourceListsTransformer)(nil) 16 | ) 17 | 18 | type ResourceTransformer interface { 19 | Match(ctx context.Context, resourceInfo *ResourceTransformerResourceInfo) (matched bool, err error) 20 | Transform(ctx context.Context, matchedResourceInfo *ResourceTransformerResourceInfo) (output []*unstructured.Unstructured, err error) 21 | Type() ResourceTransformerType 22 | } 23 | 24 | type ResourceTransformerResourceInfo struct { 25 | Obj *unstructured.Unstructured 26 | } 27 | 28 | type ResourceTransformerType string 29 | 30 | const ( 31 | TypeDropInvalidAnnotationsAndLabelsTransformer ResourceTransformerType = "drop-invalid-annotations-and-labels-transformer" 32 | TypeResourceListsTransformer ResourceTransformerType = "resource-lists-transformer" 33 | ) 34 | 35 | // TODO(v2): remove this transformer. Replace it with proper early validation of resource Heads. 36 | type DropInvalidAnnotationsAndLabelsTransformer struct{} 37 | 38 | func NewDropInvalidAnnotationsAndLabelsTransformer() *DropInvalidAnnotationsAndLabelsTransformer { 39 | return &DropInvalidAnnotationsAndLabelsTransformer{} 40 | } 41 | 42 | func (t *DropInvalidAnnotationsAndLabelsTransformer) Match(ctx context.Context, info *ResourceTransformerResourceInfo) (matched bool, err error) { 43 | return true, nil 44 | } 45 | 46 | func (t *DropInvalidAnnotationsAndLabelsTransformer) Transform(ctx context.Context, info *ResourceTransformerResourceInfo) ([]*unstructured.Unstructured, error) { 47 | annotations, _, _ := unstructured.NestedMap(info.Obj.Object, "metadata", "annotations") 48 | 49 | resultAnnotations := make(map[string]string) 50 | for annoKey, rawAnnoValue := range annotations { 51 | annoValue, valIsString := rawAnnoValue.(string) 52 | if !valIsString { 53 | log.Default.Warn(ctx, "Dropped invalid annotation %q in resource %q (%s): key is not a string", annoKey, info.Obj.GetName(), info.Obj.GroupVersionKind().String()) 54 | continue 55 | } 56 | 57 | resultAnnotations[annoKey] = annoValue 58 | } 59 | 60 | labels, _, _ := unstructured.NestedMap(info.Obj.Object, "metadata", "labels") 61 | 62 | resultLabels := make(map[string]string) 63 | for labelKey, rawLabelValue := range labels { 64 | labelValue, valIsString := rawLabelValue.(string) 65 | if !valIsString { 66 | log.Default.Warn(ctx, "Dropped invalid label %q in resource %q (%s): key is not a string", labelKey, info.Obj.GetName(), info.Obj.GroupVersionKind().String()) 67 | continue 68 | } 69 | 70 | resultLabels[labelKey] = labelValue 71 | } 72 | 73 | info.Obj.SetAnnotations(resultAnnotations) 74 | info.Obj.SetLabels(resultLabels) 75 | 76 | return []*unstructured.Unstructured{info.Obj}, nil 77 | } 78 | 79 | func (t *DropInvalidAnnotationsAndLabelsTransformer) Type() ResourceTransformerType { 80 | return TypeDropInvalidAnnotationsAndLabelsTransformer 81 | } 82 | 83 | type ResourceListsTransformer struct{} 84 | 85 | func NewResourceListsTransformer() *ResourceListsTransformer { 86 | return &ResourceListsTransformer{} 87 | } 88 | 89 | func (t *ResourceListsTransformer) Match(ctx context.Context, info *ResourceTransformerResourceInfo) (matched bool, err error) { 90 | return info.Obj.IsList(), nil 91 | } 92 | 93 | func (t *ResourceListsTransformer) Transform(ctx context.Context, info *ResourceTransformerResourceInfo) ([]*unstructured.Unstructured, error) { 94 | var result []*unstructured.Unstructured 95 | 96 | if err := info.Obj.EachListItem( 97 | func(obj runtime.Object) error { 98 | result = append(result, obj.(*unstructured.Unstructured)) 99 | return nil 100 | }, 101 | ); err != nil { 102 | return nil, fmt.Errorf("error iterating over list items: %w", err) 103 | } 104 | 105 | return result, nil 106 | } 107 | 108 | func (t *ResourceListsTransformer) Type() ResourceTransformerType { 109 | return TypeResourceListsTransformer 110 | } 111 | -------------------------------------------------------------------------------- /internal/kube/config.go: -------------------------------------------------------------------------------- 1 | package kube 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "fmt" 7 | 8 | "k8s.io/client-go/rest" 9 | "k8s.io/client-go/tools/clientcmd" 10 | "k8s.io/client-go/tools/clientcmd/api" 11 | 12 | "github.com/werf/nelm/pkg/common" 13 | "github.com/werf/nelm/pkg/log" 14 | ) 15 | 16 | type KubeConfig struct { 17 | LegacyClientConfig clientcmd.ClientConfig 18 | Namespace string 19 | RawConfig *api.Config 20 | RestConfig *rest.Config 21 | } 22 | 23 | type KubeConfigOptions struct { 24 | common.KubeConnectionOptions 25 | 26 | KubeContextNamespace string 27 | } 28 | 29 | func NewKubeConfig(ctx context.Context, kubeConfigPaths []string, opts KubeConfigOptions) (*KubeConfig, error) { 30 | var authProviderConfig *api.AuthProviderConfig 31 | if opts.KubeAuthProviderName != "" || len(opts.KubeAuthProviderConfig) != 0 { 32 | authProviderConfig = &api.AuthProviderConfig{ 33 | Name: opts.KubeAuthProviderName, 34 | Config: opts.KubeAuthProviderConfig, 35 | } 36 | } 37 | 38 | overrides := &clientcmd.ConfigOverrides{ 39 | AuthInfo: api.AuthInfo{ 40 | AuthProvider: authProviderConfig, 41 | ClientCertificate: opts.KubeTLSClientCertPath, 42 | ClientCertificateData: []byte(opts.KubeTLSClientCertData), 43 | ClientKey: opts.KubeTLSClientKeyPath, 44 | ClientKeyData: []byte(opts.KubeTLSClientKeyData), 45 | Impersonate: opts.KubeImpersonateUser, 46 | ImpersonateGroups: opts.KubeImpersonateGroups, 47 | ImpersonateUID: opts.KubeImpersonateUID, 48 | Password: opts.KubeBasicAuthPassword, 49 | Token: opts.KubeBearerTokenData, 50 | TokenFile: opts.KubeBearerTokenPath, 51 | Username: opts.KubeBasicAuthUsername, 52 | }, 53 | ClusterDefaults: clientcmd.ClusterDefaults, 54 | ClusterInfo: api.Cluster{ 55 | CertificateAuthority: opts.KubeTLSCAPath, 56 | CertificateAuthorityData: []byte(opts.KubeTLSCAData), 57 | InsecureSkipTLSVerify: opts.KubeSkipTLSVerify, 58 | ProxyURL: opts.KubeProxyURL, 59 | Server: opts.KubeAPIServerAddress, 60 | TLSServerName: opts.KubeTLSServerName, 61 | }, 62 | Context: api.Context{ 63 | AuthInfo: opts.KubeContextUser, 64 | Cluster: opts.KubeContextCluster, 65 | Namespace: opts.KubeContextNamespace, 66 | }, 67 | CurrentContext: opts.KubeContextCurrent, 68 | Timeout: opts.KubeRequestTimeout.String(), 69 | } 70 | 71 | var clientConfig clientcmd.ClientConfig 72 | if opts.KubeConfigBase64 != "" { 73 | config, err := loadKubeConfigBase64(opts.KubeConfigBase64) 74 | if err != nil { 75 | return nil, fmt.Errorf("load kubeconfig from base64: %w", err) 76 | } 77 | 78 | clientConfig = clientcmd.NewDefaultClientConfig(*config, overrides) 79 | } else { 80 | loadingRules := &clientcmd.ClientConfigLoadingRules{ 81 | Precedence: kubeConfigPaths, 82 | MigrationRules: clientcmd.NewDefaultClientConfigLoadingRules().MigrationRules, 83 | DefaultClientConfig: &clientcmd.DefaultClientConfig, 84 | } 85 | 86 | clientConfig = clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, overrides) 87 | } 88 | 89 | namespace, _, err := clientConfig.Namespace() 90 | if err != nil { 91 | return nil, fmt.Errorf("get namespace: %w", err) 92 | } 93 | 94 | rawConfig, err := clientConfig.RawConfig() 95 | if err != nil { 96 | return nil, fmt.Errorf("get raw config: %w", err) 97 | } 98 | 99 | restConfig, err := clientConfig.ClientConfig() 100 | if err != nil { 101 | return nil, fmt.Errorf("get rest config: %w", err) 102 | } 103 | 104 | restConfig.QPS = float32(opts.KubeQPSLimit) 105 | restConfig.Burst = opts.KubeBurstLimit 106 | 107 | kubeConfig := &KubeConfig{ 108 | LegacyClientConfig: clientConfig, 109 | Namespace: namespace, 110 | RawConfig: &rawConfig, 111 | RestConfig: restConfig, 112 | } 113 | 114 | log.Default.TraceStruct(ctx, kubeConfig, "Constructed KubeConfig:") 115 | 116 | return kubeConfig, nil 117 | } 118 | 119 | func loadKubeConfigBase64(kubeConfigBase64 string) (*api.Config, error) { 120 | configData, err := base64.StdEncoding.DecodeString(kubeConfigBase64) 121 | if err != nil { 122 | return nil, fmt.Errorf("decode base64 string: %w", err) 123 | } 124 | 125 | config, err := clientcmd.Load(configData) 126 | if err != nil { 127 | return nil, fmt.Errorf("load data: %w", err) 128 | } 129 | 130 | return config, nil 131 | } 132 | -------------------------------------------------------------------------------- /cmd/nelm/release_list.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "cmp" 5 | "context" 6 | "fmt" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/werf/common-go/pkg/cli" 11 | "github.com/werf/nelm/pkg/action" 12 | "github.com/werf/nelm/pkg/common" 13 | "github.com/werf/nelm/pkg/log" 14 | ) 15 | 16 | type releaseListConfig struct { 17 | action.ReleaseListOptions 18 | 19 | LogColorMode string 20 | LogLevel string 21 | } 22 | 23 | func newReleaseListCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 24 | cfg := &releaseListConfig{} 25 | 26 | cmd := cli.NewSubCommand( 27 | ctx, 28 | "list [options...] [-n namespace]", 29 | "List all deployed releases.", 30 | "List all deployed releases.", 31 | 40, 32 | releaseCmdGroup, 33 | cli.SubCommandOptions{}, 34 | func(cmd *cobra.Command, args []string) error { 35 | ctx = log.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultReleaseListLogLevel), log.SetupLoggingOptions{ 36 | ColorMode: cfg.LogColorMode, 37 | LogIsParseable: true, 38 | }) 39 | 40 | if _, err := action.ReleaseList(ctx, cfg.ReleaseListOptions); err != nil { 41 | return fmt.Errorf("release list: %w", err) 42 | } 43 | 44 | return nil 45 | }, 46 | ) 47 | 48 | afterAllCommandsBuiltFuncs[cmd] = func(cmd *cobra.Command) error { 49 | if err := AddKubeConnectionFlags(cmd, &cfg.KubeConnectionOptions); err != nil { 50 | return fmt.Errorf("add kube connection flags: %w", err) 51 | } 52 | 53 | if err := cli.AddFlag(cmd, &cfg.NetworkParallelism, "network-parallelism", common.DefaultNetworkParallelism, "Limit of network-related tasks to run in parallel", cli.AddFlagOptions{ 54 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 55 | Group: performanceFlagGroup, 56 | }); err != nil { 57 | return fmt.Errorf("add flag: %w", err) 58 | } 59 | 60 | // TODO(ilya-lesikov): restrict values 61 | if err := cli.AddFlag(cmd, &cfg.OutputFormat, "output-format", action.DefaultReleaseListOutputFormat, "Result output format", cli.AddFlagOptions{ 62 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 63 | Group: miscFlagGroup, 64 | }); err != nil { 65 | return fmt.Errorf("add flag: %w", err) 66 | } 67 | 68 | if err := cli.AddFlag(cmd, &cfg.ReleaseNamespace, "namespace", "", "The release namespace. Query all namespaces if not specified", cli.AddFlagOptions{ 69 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 70 | Group: mainFlagGroup, 71 | ShortName: "n", 72 | }); err != nil { 73 | return fmt.Errorf("add flag: %w", err) 74 | } 75 | 76 | // TODO(ilya-lesikov): restrict allowed values 77 | if err := cli.AddFlag(cmd, &cfg.ReleaseStorageDriver, "release-storage", "", "How releases should be stored", cli.AddFlagOptions{ 78 | GetEnvVarRegexesFunc: cli.GetFlagGlobalEnvVarRegexes, 79 | Group: miscFlagGroup, 80 | }); err != nil { 81 | return fmt.Errorf("add flag: %w", err) 82 | } 83 | 84 | if err := cli.AddFlag(cmd, &cfg.ReleaseStorageSQLConnection, "release-storage-sql-connection", "", "SQL connection string for MySQL release storage driver", cli.AddFlagOptions{ 85 | GetEnvVarRegexesFunc: cli.GetFlagGlobalEnvVarRegexes, 86 | Group: miscFlagGroup, 87 | }); err != nil { 88 | return fmt.Errorf("add flag: %w", err) 89 | } 90 | 91 | if err := cli.AddFlag(cmd, &cfg.TempDirPath, "temp-dir", "", "The directory for temporary files. By default, create a new directory in the default system directory for temporary files", cli.AddFlagOptions{ 92 | GetEnvVarRegexesFunc: cli.GetFlagGlobalEnvVarRegexes, 93 | Group: miscFlagGroup, 94 | Type: cli.FlagTypeDir, 95 | }); err != nil { 96 | return fmt.Errorf("add flag: %w", err) 97 | } 98 | 99 | if err := cli.AddFlag(cmd, &cfg.LogColorMode, "color-mode", common.DefaultLogColorMode, "Color mode for logs. "+allowedLogColorModesHelp(), cli.AddFlagOptions{ 100 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 101 | Group: miscFlagGroup, 102 | }); err != nil { 103 | return fmt.Errorf("add flag: %w", err) 104 | } 105 | 106 | if err := cli.AddFlag(cmd, &cfg.LogLevel, "log-level", string(action.DefaultReleaseListLogLevel), "Set log level. "+allowedLogLevelsHelp(), cli.AddFlagOptions{ 107 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 108 | Group: miscFlagGroup, 109 | }); err != nil { 110 | return fmt.Errorf("add flag: %w", err) 111 | } 112 | 113 | return nil 114 | } 115 | 116 | return cmd 117 | } 118 | -------------------------------------------------------------------------------- /internal/resource/spec/util.go: -------------------------------------------------------------------------------- 1 | package spec 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | 8 | "github.com/samber/lo" 9 | "k8s.io/apimachinery/pkg/api/meta" 10 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 11 | "k8s.io/apimachinery/pkg/runtime/schema" 12 | 13 | "github.com/werf/nelm/pkg/common" 14 | ) 15 | 16 | func GVRtoGVK(gvr schema.GroupVersionResource, restMapper meta.RESTMapper) (schema.GroupVersionKind, error) { 17 | var gvk schema.GroupVersionKind 18 | if preferredKinds, err := restMapper.KindsFor(gvr); err != nil { 19 | return gvk, fmt.Errorf("match GroupVersionResource %q: %w", gvr.String(), err) 20 | } else if len(preferredKinds) == 0 { 21 | return gvk, fmt.Errorf("no matches for %q", gvr.String()) 22 | } else { 23 | gvk = preferredKinds[0] 24 | } 25 | 26 | return gvk, nil 27 | } 28 | 29 | func GVKtoGVR(gvk schema.GroupVersionKind, mapper meta.RESTMapper) (gvr schema.GroupVersionResource, namespaced bool, err error) { 30 | mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version) 31 | if err != nil { 32 | return schema.GroupVersionResource{}, false, fmt.Errorf("get resource mapping for %q: %w", gvk.String(), err) 33 | } 34 | 35 | return mapping.Resource, mapping.Scope == meta.RESTScopeNamespace, nil 36 | } 37 | 38 | func Namespaced(gvk schema.GroupVersionKind, mapper meta.RESTMapper) (bool, error) { 39 | mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version) 40 | if err != nil { 41 | return false, fmt.Errorf("get resource mapping for %q: %w", gvk.String(), err) 42 | } 43 | 44 | return mapping.Scope == meta.RESTScopeNamespace, nil 45 | } 46 | 47 | func ParseKubectlResourceStringtoGVK(resource string, restMapper meta.RESTMapper) (schema.GroupVersionKind, error) { 48 | var gvk schema.GroupVersionKind 49 | 50 | gvr := ParseKubectlResourceStringToGVR(resource) 51 | 52 | gvk, err := GVRtoGVK(gvr, restMapper) 53 | if err != nil { 54 | return gvk, fmt.Errorf("convert GroupVersionResource to GroupVersionKind: %w", err) 55 | } 56 | 57 | return gvk, nil 58 | } 59 | 60 | func ParseKubectlResourceStringToGVR(resource string) schema.GroupVersionResource { 61 | var result schema.GroupVersionResource 62 | if gvr, gr := schema.ParseResourceArg(resource); gvr != nil { 63 | result = *gvr 64 | } else { 65 | result = gr.WithVersion("") 66 | } 67 | 68 | return result 69 | } 70 | 71 | func IsCRD(groupKind schema.GroupKind) bool { 72 | return groupKind == schema.GroupKind{ 73 | Group: "apiextensions.k8s.io", 74 | Kind: "CustomResourceDefinition", 75 | } 76 | } 77 | 78 | func IsCRDFromGR(groupKind schema.GroupResource) bool { 79 | return groupKind == schema.GroupResource{ 80 | Group: "apiextensions.k8s.io", 81 | Resource: "customresourcedefinitions", 82 | } 83 | } 84 | 85 | func IsHook(annotations map[string]string) bool { 86 | _, _, found := FindAnnotationOrLabelByKeyPattern(annotations, common.AnnotationKeyPatternHook) 87 | return found 88 | } 89 | 90 | func IsWebhook(groupKind schema.GroupKind) bool { 91 | return groupKind == schema.GroupKind{ 92 | Group: "admissionregistration.k8s.io", 93 | Kind: "MutatingWebhookConfiguration", 94 | } || groupKind == schema.GroupKind{ 95 | Group: "admissionregistration.k8s.io", 96 | Kind: "ValidatingWebhookConfiguration", 97 | } 98 | } 99 | 100 | func IsReleaseNamespace(resourceName string, resourceGVK schema.GroupVersionKind, releaseNamespace string) bool { 101 | return resourceGVK.Group == "" && resourceGVK.Kind == "Namespace" && resourceName == releaseNamespace 102 | } 103 | 104 | func FindAnnotationOrLabelByKeyPattern(annotationsOrLabels map[string]string, pattern *regexp.Regexp) (key, value string, found bool) { 105 | key, found = lo.FindKeyBy(annotationsOrLabels, func(k, _ string) bool { 106 | return pattern.MatchString(k) 107 | }) 108 | if found { 109 | value = strings.TrimSpace(annotationsOrLabels[key]) 110 | } 111 | 112 | return key, value, found 113 | } 114 | 115 | func FindAnnotationsOrLabelsByKeyPattern(annotationsOrLabels map[string]string, pattern *regexp.Regexp) (result map[string]string, found bool) { 116 | result = map[string]string{} 117 | 118 | for key, value := range annotationsOrLabels { 119 | if pattern.MatchString(key) { 120 | result[key] = strings.TrimSpace(value) 121 | } 122 | } 123 | 124 | return result, len(result) > 0 125 | } 126 | 127 | func setAnnotationsAndLabels(res *unstructured.Unstructured, annotations, labels map[string]string) { 128 | if len(annotations) > 0 { 129 | annos := res.GetAnnotations() 130 | if annos == nil { 131 | annos = map[string]string{} 132 | } 133 | 134 | for k, v := range annotations { 135 | annos[k] = v 136 | } 137 | 138 | res.SetAnnotations(annos) 139 | } 140 | 141 | if len(labels) > 0 { 142 | lbls := res.GetLabels() 143 | if lbls == nil { 144 | lbls = map[string]string{} 145 | } 146 | 147 | for k, v := range labels { 148 | lbls[k] = v 149 | } 150 | 151 | res.SetLabels(lbls) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /internal/chart/chart_download.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "net/url" 8 | "os" 9 | 10 | "github.com/werf/3p-helm/pkg/cli" 11 | helmdownloader "github.com/werf/3p-helm/pkg/downloader" 12 | helmgetter "github.com/werf/3p-helm/pkg/getter" 13 | "github.com/werf/3p-helm/pkg/helmpath" 14 | helmregistry "github.com/werf/3p-helm/pkg/registry" 15 | helmrepo "github.com/werf/3p-helm/pkg/repo" 16 | "github.com/werf/nelm/pkg/common" 17 | "github.com/werf/nelm/pkg/featgate" 18 | "github.com/werf/nelm/pkg/log" 19 | ) 20 | 21 | type chartDownloaderOptions struct { 22 | common.ChartRepoConnectionOptions 23 | 24 | ChartProvenanceKeyring string 25 | ChartProvenanceStrategy string 26 | ChartVersion string 27 | } 28 | 29 | func newChartDownloader(ctx context.Context, chartRef string, registryClient *helmregistry.Client, opts chartDownloaderOptions) (*helmdownloader.ChartDownloader, string, error) { 30 | var out io.Writer 31 | if log.Default.AcceptLevel(ctx, log.WarningLevel) { 32 | // TODO(log): 33 | out = os.Stdout 34 | } else { 35 | out = io.Discard 36 | } 37 | 38 | downloader := &helmdownloader.ChartDownloader{ 39 | Out: out, 40 | Verify: helmdownloader.VerificationStrategyString(opts.ChartProvenanceStrategy).ToVerificationStrategy(), 41 | Keyring: opts.ChartProvenanceKeyring, 42 | Getters: helmgetter.Providers{helmgetter.HttpProvider, helmgetter.OCIProvider}, 43 | Options: []helmgetter.Option{ 44 | helmgetter.WithPassCredentialsAll(opts.ChartRepoPassCreds), 45 | helmgetter.WithTLSClientConfig(opts.ChartRepoCertPath, opts.ChartRepoKeyPath, opts.ChartRepoCAPath), 46 | helmgetter.WithInsecureSkipVerifyTLS(opts.ChartRepoSkipTLSVerify), 47 | helmgetter.WithPlainHTTP(opts.ChartRepoInsecure), 48 | helmgetter.WithRegistryClient(registryClient), 49 | helmgetter.WithTimeout(opts.ChartRepoRequestTimeout), 50 | }, 51 | RegistryClient: registryClient, 52 | // TODO(v2): get rid of HELM_ env vars support 53 | RepositoryConfig: cli.EnvOr("HELM_REPOSITORY_CONFIG", helmpath.ConfigPath("repositories.yaml")), 54 | // TODO(v2): get rid of HELM_ env vars support 55 | RepositoryCache: cli.EnvOr("HELM_REPOSITORY_CACHE", helmpath.CachePath("repository")), 56 | } 57 | 58 | if opts.ChartRepoURL != "" { 59 | chartURL, err := helmrepo.FindChartInAuthAndTLSAndPassRepoURL(opts.ChartRepoURL, opts.ChartRepoBasicAuthUsername, opts.ChartRepoBasicAuthPassword, chartRef, opts.ChartVersion, opts.ChartRepoCertPath, opts.ChartRepoKeyPath, opts.ChartRepoCAPath, opts.ChartRepoSkipTLSVerify, opts.ChartRepoPassCreds, helmgetter.Providers{helmgetter.HttpProvider, helmgetter.OCIProvider}) 60 | if err != nil { 61 | return nil, "", fmt.Errorf("get chart URL: %w", err) 62 | } 63 | 64 | rURL, err := url.Parse(opts.ChartRepoURL) 65 | if err != nil { 66 | return nil, "", fmt.Errorf("parse repo URL: %w", err) 67 | } 68 | 69 | cURL, err := url.Parse(chartURL) 70 | if err != nil { 71 | return nil, "", fmt.Errorf("parse chart URL: %w", err) 72 | } 73 | 74 | if opts.ChartRepoPassCreds || (rURL.Scheme == cURL.Scheme && rURL.Host == cURL.Host) { 75 | downloader.Options = append(downloader.Options, helmgetter.WithBasicAuth(opts.ChartRepoBasicAuthUsername, opts.ChartRepoBasicAuthPassword)) 76 | } else { 77 | downloader.Options = append(downloader.Options, helmgetter.WithBasicAuth("", "")) 78 | } 79 | 80 | chartRef = chartURL 81 | } else { 82 | downloader.Options = append(downloader.Options, helmgetter.WithBasicAuth(opts.ChartRepoBasicAuthUsername, opts.ChartRepoBasicAuthPassword)) 83 | } 84 | 85 | return downloader, chartRef, nil 86 | } 87 | 88 | func downloadChart(ctx context.Context, chartPath string, registryClient *helmregistry.Client, opts RenderChartOptions) (string, error) { 89 | if (featgate.FeatGateRemoteCharts.Enabled() || featgate.FeatGatePreviewV2.Enabled()) && !isLocalChart(chartPath) { 90 | chartDownloader, chartRef, err := newChartDownloader(ctx, chartPath, registryClient, chartDownloaderOptions{ 91 | ChartRepoConnectionOptions: opts.ChartRepoConnectionOptions, 92 | ChartProvenanceKeyring: opts.ChartProvenanceKeyring, 93 | ChartProvenanceStrategy: opts.ChartProvenanceStrategy, 94 | ChartVersion: opts.ChartVersion, 95 | }) 96 | if err != nil { 97 | return "", fmt.Errorf("construct chart downloader: %w", err) 98 | } 99 | 100 | // TODO(v2): get rid of HELM_ env vars support 101 | if err := os.MkdirAll(cli.EnvOr("HELM_REPOSITORY_CACHE", helmpath.CachePath("repository")), 0o755); err != nil { 102 | return "", fmt.Errorf("create repository cache directory: %w", err) 103 | } 104 | 105 | // TODO(v2): get rid of HELM_ env vars support 106 | chartPath, _, err = chartDownloader.DownloadTo(chartRef, opts.ChartVersion, cli.EnvOr("HELM_REPOSITORY_CACHE", helmpath.CachePath("repository"))) 107 | if err != nil { 108 | return "", fmt.Errorf("download chart %q: %w", chartRef, err) 109 | } 110 | } 111 | 112 | return chartPath, nil 113 | } 114 | -------------------------------------------------------------------------------- /pkg/legacy/secret/rotate.go: -------------------------------------------------------------------------------- 1 | package secret 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "path/filepath" 10 | 11 | "github.com/werf/common-go/pkg/secret" 12 | "github.com/werf/common-go/pkg/secrets_manager" 13 | "github.com/werf/common-go/pkg/util" 14 | "github.com/werf/nelm/pkg/log" 15 | ) 16 | 17 | func RotateSecretKey( 18 | ctx context.Context, 19 | helmChartDir string, 20 | secretWorkingDir string, 21 | secretValuesPaths ...string, 22 | ) error { 23 | secretsManager := secrets_manager.Manager 24 | 25 | newEncoder, err := secretsManager.GetYamlEncoder(ctx, secretWorkingDir, false) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | oldEncoder, err := secretsManager.GetYamlEncoderForOldKey(ctx) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | return secretsRegenerate(ctx, newEncoder, oldEncoder, helmChartDir, secretValuesPaths...) 36 | } 37 | 38 | func secretsRegenerate( 39 | ctx context.Context, 40 | newEncoder, oldEncoder *secret.YamlEncoder, 41 | helmChartDir string, 42 | secretValuesPaths ...string, 43 | ) error { 44 | var secretFilesPaths []string 45 | var secretFilesData map[string][]byte 46 | var secretValuesFilesData map[string][]byte 47 | regeneratedFilesData := map[string][]byte{} 48 | 49 | isHelmChartDirExist, err := util.FileExists(helmChartDir) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | if isHelmChartDirExist { 55 | defaultSecretValuesPath := filepath.Join(helmChartDir, "secret-values.yaml") 56 | isDefaultSecretValuesExist, err := util.FileExists(defaultSecretValuesPath) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | if isDefaultSecretValuesExist { 62 | secretValuesPaths = append(secretValuesPaths, defaultSecretValuesPath) 63 | } 64 | 65 | secretDirectory := filepath.Join(helmChartDir, "secret") 66 | isSecretDirectoryExist, err := util.FileExists(secretDirectory) 67 | if err != nil { 68 | return err 69 | } 70 | 71 | if isSecretDirectoryExist { 72 | err = filepath.Walk(secretDirectory, 73 | func(path string, info os.FileInfo, err error) error { 74 | if err != nil { 75 | return err 76 | } 77 | 78 | fileInfo, err := os.Stat(path) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | if !fileInfo.IsDir() { 84 | secretFilesPaths = append(secretFilesPaths, path) 85 | } 86 | 87 | return nil 88 | }) 89 | if err != nil { 90 | return err 91 | } 92 | } 93 | } 94 | 95 | pwd, err := os.Getwd() 96 | if err != nil { 97 | return err 98 | } 99 | 100 | secretFilesData, err = readFilesToDecode(secretFilesPaths, pwd) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | secretValuesFilesData, err = readFilesToDecode(secretValuesPaths, pwd) 106 | if err != nil { 107 | return err 108 | } 109 | 110 | if err := regenerateSecrets(ctx, secretFilesData, regeneratedFilesData, oldEncoder.Decrypt, newEncoder.Encrypt); err != nil { 111 | return err 112 | } 113 | 114 | if err := regenerateSecrets(ctx, secretValuesFilesData, regeneratedFilesData, oldEncoder.DecryptYamlData, newEncoder.EncryptYamlData); err != nil { 115 | return err 116 | } 117 | 118 | for filePath, fileData := range regeneratedFilesData { 119 | if err := log.Default.InfoBlockErr(ctx, log.BlockOptions{ 120 | BlockTitle: fmt.Sprintf("Saving file %q", filePath), 121 | }, func() error { 122 | fileData = append(bytes.TrimSpace(fileData), []byte("\n")...) 123 | return ioutil.WriteFile(filePath, fileData, 0o644) 124 | }); err != nil { 125 | return err 126 | } 127 | } 128 | 129 | return nil 130 | } 131 | 132 | func regenerateSecrets( 133 | ctx context.Context, 134 | filesData, regeneratedFilesData map[string][]byte, 135 | decodeFunc, encodeFunc func([]byte) ([]byte, error), 136 | ) error { 137 | for filePath, fileData := range filesData { 138 | if err := log.Default.InfoBlockErr(ctx, log.BlockOptions{ 139 | BlockTitle: fmt.Sprintf("Regenerating file %q", filePath), 140 | }, func() error { 141 | data, err := decodeFunc(fileData) 142 | if err != nil { 143 | return fmt.Errorf("check old encryption key and file data: %w", err) 144 | } 145 | 146 | resultData, err := encodeFunc(data) 147 | if err != nil { 148 | return err 149 | } 150 | 151 | regeneratedFilesData[filePath] = resultData 152 | 153 | return nil 154 | }); err != nil { 155 | return err 156 | } 157 | } 158 | 159 | return nil 160 | } 161 | 162 | func readFilesToDecode(filePaths []string, pwd string) (map[string][]byte, error) { 163 | filesData := map[string][]byte{} 164 | for _, filePath := range filePaths { 165 | fileData, err := ioutil.ReadFile(filePath) 166 | if err != nil { 167 | return nil, err 168 | } 169 | 170 | if filepath.IsAbs(filePath) { 171 | filePath, err = filepath.Rel(pwd, filePath) 172 | if err != nil { 173 | return nil, err 174 | } 175 | } 176 | 177 | filesData[filePath] = bytes.TrimSpace(fileData) 178 | } 179 | 180 | return filesData, nil 181 | } 182 | -------------------------------------------------------------------------------- /.github/workflows/release_trdl-release.yml: -------------------------------------------------------------------------------- 1 | name: release:trdl-release 2 | on: 3 | push: 4 | tags: 5 | - "v[0-9]+.[0-9]+.[0-9]+*" 6 | repository_dispatch: 7 | types: ["release:trdl-release"] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | release: 12 | name: Perform release using trdl server 13 | runs-on: ubuntu-22.04 14 | steps: 15 | - name: Notify 16 | uses: mattermost/action-mattermost-notify@master 17 | with: 18 | MATTERMOST_WEBHOOK_URL: ${{ secrets.LOOP_NOTIFICATION_WEBHOOK }} 19 | MATTERMOST_CHANNEL: ${{ vars.LOOP_NOTIFICATION_CHANNEL }} 20 | TEXT: | 21 | ${{ vars.LOOP_NOTIFICATION_GROUP }} [${{ github.workflow }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) nelm task sign pls 22 | 23 | - name: Release with retry 24 | uses: werf/trdl-vault-actions/release@main 25 | with: 26 | vault-addr: ${{ secrets.TRDL_VAULT_ADDR }} 27 | project-name: nelm 28 | git-tag: ${{ github.ref_name }} 29 | vault-auth-method: approle 30 | vault-role-id: ${{ secrets.TRDL_VAULT_ROLE_ID }} 31 | vault-secret-id: ${{ secrets.TRDL_VAULT_SECRET_ID }} 32 | 33 | - name: Checkout code 34 | uses: actions/checkout@v4 35 | with: 36 | fetch-depth: 0 37 | fetch-tags: true 38 | - name: Get version from CHANGELOG.md 39 | id: get_version 40 | run: | 41 | VERSION=$(grep -m1 '^#\+ \[[0-9]\+\.[0-9]\+\.[0-9]\+\]' CHANGELOG.md | sed -E 's/^#+ \[([0-9]+\.[0-9]+\.[0-9]+)\].*/\1/') 42 | echo "version=$VERSION" >> $GITHUB_OUTPUT 43 | - name: Generate notes.md 44 | id: notes 45 | run: | 46 | VERSION="${{ steps.get_version.outputs.version }}" 47 | echo "## Changelog" > notes.md 48 | awk -v version="$VERSION" ' 49 | $0 ~ "^#+ \\[" version "\\]" {capture=1; next} 50 | capture && $0 ~ "^#+ \\[" && $0 !~ "^#+ \\[" version "\\]" {exit} 51 | capture {print} 52 | ' CHANGELOG.md >> notes.md 53 | 54 | cat <> notes.md 55 | ## Install via trdl (with autoupdates, highly secure) 56 | 57 | 1. [Install trdl client binary](https://github.com/werf/trdl/releases/latest), preferably to \`~/bin\`. 58 | 2. Add Nelm TUF repository to trdl: 59 | 60 | \`\`\`shell 61 | trdl add nelm https://tuf.nelm.sh 1 2122fb476c48de4609fe6d3636759645996088ff6796857fc23ba4b8331a6e3a58fc40f1714c31bda64c709ef6f49bcc4691d091bad6cb1b9a631d8e06e1f308 62 | \`\`\` 63 | 64 | 3. Make \`nelm\` binary available in the current shell: 65 | 66 | \`\`\`shell 67 | source "\$(trdl use nelm 1 stable)" 68 | \`\`\` 69 | ## Install binaries directly (no autoupdates) 70 | 71 | 72 | Download \`nelm\` binaries from here: 73 | * [Linux amd64](https://tuf.nelm.sh/targets/releases/$VERSION/linux-amd64/bin/nelm) ([PGP signature](https://tuf.nelm.sh/targets/signatures/$VERSION/linux-amd64/bin/nelm.sig)) 74 | * [Linux arm64](https://tuf.nelm.sh/targets/releases/$VERSION/linux-arm64/bin/nelm) ([PGP signature](https://tuf.nelm.sh/targets/signatures/$VERSION/linux-arm64/bin/nelm.sig)) 75 | * [macOS amd64](https://tuf.nelm.sh/targets/releases/$VERSION/darwin-amd64/bin/nelm) ([PGP signature](https://tuf.nelm.sh/targets/signatures/$VERSION/darwin-amd64/bin/nelm.sig)) 76 | * [macOS arm64](https://tuf.nelm.sh/targets/releases/$VERSION/darwin-arm64/bin/nelm) ([PGP signature](https://tuf.nelm.sh/targets/signatures/$VERSION/darwin-arm64/bin/nelm.sig)) 77 | * [Windows amd64](https://tuf.nelm.sh/targets/releases/$VERSION/windows-amd64/bin/nelm.exe) ([PGP signature](https://tuf.nelm.sh/targets/signatures/$VERSION/windows-amd64/bin/nelm.exe.sig)) 78 | 79 | These binaries were signed with PGP and could be verified with the [Nelm PGP public key](https://raw.githubusercontent.com/werf/nelm/refs/heads/main/nelm.asc). For example, \`nelm\` binary can be downloaded and verified with \`gpg\` on Linux with these commands: 80 | 81 | \`\`\`shell 82 | curl -sSLO "https://tuf.nelm.sh/targets/releases/$VERSION/linux-amd64/bin/nelm" -O "https://tuf.nelm.sh/targets/signatures/$VERSION/linux-amd64/bin/nelm.sig" 83 | curl -sSL https://raw.githubusercontent.com/werf/nelm/refs/heads/main/nelm.asc | gpg --import 84 | gpg --verify nelm.sig nelm 85 | \`\`\` 86 | EOF 87 | - name: Create release 88 | env: 89 | GH_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN }} 90 | run: | 91 | gh release create "v${{ steps.get_version.outputs.version }}" \ 92 | --title "v${{ steps.get_version.outputs.version }}" \ 93 | --prerelease \ 94 | --notes-file notes.md 95 | 96 | notify: 97 | if: always() 98 | needs: release 99 | uses: werf/common-ci/.github/workflows/notification.yml@main 100 | secrets: 101 | loopNotificationGroup: ${{ vars.LOOP_NOTIFICATION_GROUP }} 102 | webhook: ${{ secrets.LOOP_NOTIFICATION_WEBHOOK }} 103 | notificationChannel: ${{ vars.LOOP_NOTIFICATION_CHANNEL }} 104 | -------------------------------------------------------------------------------- /internal/util/properties.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "fmt" 7 | "io" 8 | "strings" 9 | "unicode" 10 | 11 | "github.com/looplab/fsm" 12 | "github.com/samber/lo" 13 | ) 14 | 15 | const ( 16 | commaRune = ',' 17 | equalsRune = '=' 18 | singleQuoteRune = '\'' 19 | doubleQuoteRune = '"' 20 | ) 21 | 22 | func ParseProperties(ctx context.Context, input string) (map[string]any, error) { 23 | result := make(map[string]any) 24 | 25 | machine := fsm.NewFSM("start", 26 | fsm.Events{ 27 | { 28 | Name: "foundKeyChar", 29 | Src: []string{ 30 | "start", 31 | "entriesSeparator", 32 | }, 33 | Dst: "key", 34 | }, 35 | { 36 | Name: "foundValueChar", 37 | Src: []string{ 38 | "KVSeparator", 39 | }, 40 | Dst: "value", 41 | }, 42 | { 43 | Name: "foundQuotedValueChar", 44 | Src: []string{ 45 | "openingValueQuote", 46 | }, 47 | Dst: "quotedValue", 48 | }, 49 | { 50 | Name: "foundKVSeparator", 51 | Src: []string{ 52 | "key", 53 | }, 54 | Dst: "KVSeparator", 55 | }, 56 | { 57 | Name: "foundEntriesSeparator", 58 | Src: []string{ 59 | "start", 60 | "key", 61 | "value", 62 | "KVSeparator", 63 | "closingValueQuote", 64 | }, 65 | Dst: "entriesSeparator", 66 | }, 67 | { 68 | Name: "foundOpeningValueQuote", 69 | Src: []string{ 70 | "KVSeparator", 71 | }, 72 | Dst: "openingValueQuote", 73 | }, 74 | { 75 | Name: "foundClosingValueQuote", 76 | Src: []string{ 77 | "quotedValue", 78 | }, 79 | Dst: "closingValueQuote", 80 | }, 81 | }, 82 | fsm.Callbacks{}, 83 | ) 84 | 85 | var ( 86 | key []rune 87 | value []rune 88 | valQuote rune 89 | valPresent bool 90 | ) 91 | 92 | reader := bufio.NewReader(strings.NewReader(input)) 93 | for { 94 | r, _, err := reader.ReadRune() 95 | if err != nil { 96 | if err == io.EOF { 97 | if len(key) > 0 { 98 | k, v := convertKeyValue(key, value, valPresent, machine.Is("closingValueQuote")) 99 | result[k] = v 100 | } 101 | 102 | break 103 | } else { 104 | return nil, fmt.Errorf("error reading input: %w", err) 105 | } 106 | } 107 | 108 | switch r { 109 | case commaRune: 110 | if machine.Can("foundEntriesSeparator") { 111 | if len(key) == 0 { 112 | lo.Must0(machine.Event(ctx, "foundEntriesSeparator")) 113 | break 114 | } 115 | 116 | k, v := convertKeyValue(key, value, valPresent, machine.Is("closingValueQuote")) 117 | result[k] = v 118 | 119 | key = []rune{} 120 | value = []rune{} 121 | valPresent = false 122 | 123 | lo.Must0(machine.Event(ctx, "foundEntriesSeparator")) 124 | } else if machine.Is("key") { 125 | key = append(key, r) 126 | } else if machine.Is("value") || machine.Is("quotedValue") { 127 | value = append(value, r) 128 | } 129 | case equalsRune: 130 | if machine.Can("foundKVSeparator") { 131 | valPresent = true 132 | 133 | lo.Must0(machine.Event(ctx, "foundKVSeparator")) 134 | } else if machine.Is("key") { 135 | key = append(key, r) 136 | } else if machine.Is("value") || machine.Is("quotedValue") { 137 | value = append(value, r) 138 | } 139 | case doubleQuoteRune, singleQuoteRune: 140 | if machine.Can("foundOpeningValueQuote") { 141 | valQuote = r 142 | 143 | lo.Must0(machine.Event(ctx, "foundOpeningValueQuote")) 144 | } else if machine.Can("foundClosingValueQuote") && valQuote == r && (len(value) == 0 || value[len(value)-1] != '\\') { 145 | lo.Must0(machine.Event(ctx, "foundClosingValueQuote")) 146 | } else if machine.Is("key") { 147 | key = append(key, r) 148 | } else if machine.Is("value") || machine.Is("quotedValue") { 149 | value = append(value, r) 150 | } 151 | default: 152 | if unicode.IsSpace(r) && 153 | (machine.Is("KVSeparator") || machine.Is("entriesSeparator")) { 154 | break 155 | } 156 | 157 | if machine.Can("foundKeyChar") { 158 | lo.Must0(machine.Event(ctx, "foundKeyChar")) 159 | } else if machine.Can("foundValueChar") { 160 | lo.Must0(machine.Event(ctx, "foundValueChar")) 161 | } else if machine.Can("foundQuotedValueChar") { 162 | lo.Must0(machine.Event(ctx, "foundQuotedValueChar")) 163 | } 164 | 165 | if machine.Is("key") { 166 | key = append(key, r) 167 | } else if machine.Is("value") || machine.Is("quotedValue") { 168 | value = append(value, r) 169 | } 170 | } 171 | } 172 | 173 | return result, nil 174 | } 175 | 176 | func convertKeyValue(key, value []rune, valPresent, valQuoted bool) (string, any) { 177 | k := string(key) 178 | k = strings.TrimSpace(k) 179 | k = strings.ToLower(k) 180 | 181 | var v any 182 | if !valPresent { 183 | if strings.HasPrefix(strings.ToLower(k), "no") { 184 | k = trimLeftChars(k, 2) 185 | v = false 186 | } else { 187 | v = true 188 | } 189 | } else { 190 | v = string(value) 191 | if !valQuoted { 192 | v = strings.TrimSpace(v.(string)) 193 | } 194 | } 195 | 196 | return k, v 197 | } 198 | 199 | func trimLeftChars(s string, n int) string { 200 | m := 0 201 | for i := range s { 202 | if m >= n { 203 | return s[i:] 204 | } 205 | 206 | m++ 207 | } 208 | 209 | return s[:0] 210 | } 211 | -------------------------------------------------------------------------------- /pkg/legacy/secret/edit.go: -------------------------------------------------------------------------------- 1 | package secret 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "os" 10 | "os/exec" 11 | "path/filepath" 12 | "runtime" 13 | "strings" 14 | 15 | "github.com/google/uuid" 16 | "github.com/gookit/color" 17 | "golang.org/x/crypto/ssh/terminal" 18 | 19 | "github.com/werf/common-go/pkg/secret" 20 | "github.com/werf/common-go/pkg/secrets_manager" 21 | "github.com/werf/common-go/pkg/util" 22 | "github.com/werf/nelm/pkg/log" 23 | ) 24 | 25 | func SecretEdit( 26 | ctx context.Context, 27 | m *secrets_manager.SecretsManager, 28 | workingDir, tempDir, filePath string, 29 | values bool, 30 | ) error { 31 | var encoder *secret.YamlEncoder 32 | if enc, err := m.GetYamlEncoder(ctx, workingDir, false); err != nil { 33 | return err 34 | } else { 35 | encoder = enc 36 | } 37 | 38 | data, encodedData, err := readEditedFile(filePath, values, encoder) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | tmpFilePath := filepath.Join(tempDir, fmt.Sprintf("werf-edit-secret-%s.yaml", uuid.NewString())) 44 | defer os.RemoveAll(tmpFilePath) 45 | 46 | if err := createTmpEditedFile(tmpFilePath, data); err != nil { 47 | return err 48 | } 49 | 50 | bin, binArgs, err := editor() 51 | if err != nil { 52 | return err 53 | } 54 | 55 | args := binArgs 56 | args = append(args, tmpFilePath) 57 | editIteration := func() error { 58 | cmd := exec.Command(bin, args...) 59 | cmd.Stdout = os.Stdout 60 | cmd.Stdin = os.Stdin 61 | cmd.Stderr = os.Stderr 62 | err := cmd.Run() 63 | if err != nil { 64 | return err 65 | } 66 | 67 | newData, err := ioutil.ReadFile(tmpFilePath) 68 | if err != nil { 69 | return err 70 | } 71 | 72 | var newEncodedData []byte 73 | if values { 74 | newEncodedData, err = encoder.EncryptYamlData(newData) 75 | if err != nil { 76 | return err 77 | } 78 | } else { 79 | newEncodedData, err = encoder.Encrypt(newData) 80 | if err != nil { 81 | return err 82 | } 83 | 84 | newEncodedData = append(newEncodedData, []byte("\n")...) 85 | } 86 | 87 | if !bytes.Equal(data, newData) { 88 | if values { 89 | newEncodedData, err = secret.MergeEncodedYaml(data, newData, encodedData, newEncodedData) 90 | if err != nil { 91 | return fmt.Errorf("unable to merge changed values of encoded yaml: %w", err) 92 | } 93 | } 94 | 95 | if err := SaveGeneratedData(filePath, newEncodedData); err != nil { 96 | return err 97 | } 98 | } 99 | 100 | return nil 101 | } 102 | 103 | for { 104 | err := editIteration() 105 | if err != nil { 106 | if strings.HasPrefix(err.Error(), "encryption failed") { 107 | log.Default.Warn(ctx, "Error: %s\n", err) 108 | ok, err := askForConfirmation() 109 | if err != nil { 110 | return err 111 | } 112 | 113 | if ok { 114 | continue 115 | } 116 | } 117 | 118 | return err 119 | } 120 | 121 | break 122 | } 123 | 124 | return nil 125 | } 126 | 127 | func readEditedFile(filePath string, values bool, encoder *secret.YamlEncoder) ( 128 | []byte, 129 | []byte, 130 | error, 131 | ) { 132 | var data, encodedData []byte 133 | 134 | exist, err := util.FileExists(filePath) 135 | if err != nil { 136 | return nil, nil, err 137 | } 138 | 139 | if exist { 140 | encodedData, err = ioutil.ReadFile(filePath) 141 | if err != nil { 142 | return nil, nil, err 143 | } 144 | 145 | encodedData = bytes.TrimSpace(encodedData) 146 | 147 | if values { 148 | data, err = encoder.DecryptYamlData(encodedData) 149 | if err != nil { 150 | return nil, nil, err 151 | } 152 | } else { 153 | data, err = encoder.Decrypt(encodedData) 154 | if err != nil { 155 | return nil, nil, err 156 | } 157 | } 158 | } 159 | 160 | return data, encodedData, nil 161 | } 162 | 163 | func askForConfirmation() (bool, error) { 164 | r := os.Stdin 165 | 166 | fmt.Println(color.New(color.Bold).Sprintf("Do you want to continue editing the file (Y/n)?")) 167 | 168 | isTerminal := terminal.IsTerminal(int(r.Fd())) 169 | if isTerminal { 170 | if oldState, err := terminal.MakeRaw(int(r.Fd())); err != nil { 171 | return false, err 172 | } else { 173 | defer terminal.Restore(int(r.Fd()), oldState) 174 | } 175 | } 176 | 177 | var buf [1]byte 178 | n, err := r.Read(buf[:]) 179 | if n > 0 { 180 | switch buf[0] { 181 | case 'y', 'Y', 13: 182 | return true, nil 183 | default: 184 | return false, nil 185 | } 186 | } 187 | 188 | if err != nil && err != io.EOF { 189 | return false, err 190 | } 191 | 192 | return false, nil 193 | } 194 | 195 | func createTmpEditedFile(filePath string, data []byte) error { 196 | if err := SaveGeneratedData(filePath, data); err != nil { 197 | return err 198 | } 199 | return nil 200 | } 201 | 202 | func editor() (string, []string, error) { 203 | var editorArgs []string 204 | 205 | editorValue := os.Getenv("EDITOR") 206 | if editorValue != "" { 207 | editorFields := strings.Fields(editorValue) 208 | return editorFields[0], editorFields[1:], nil 209 | } 210 | 211 | var defaultEditors []string 212 | if runtime.GOOS == "windows" { 213 | defaultEditors = []string{"notepad"} 214 | } else { 215 | defaultEditors = []string{"vim", "vi", "nano"} 216 | } 217 | 218 | for _, bin := range defaultEditors { 219 | if _, err := exec.LookPath(bin); err != nil { 220 | continue 221 | } 222 | 223 | return bin, editorArgs, nil 224 | } 225 | 226 | return "", editorArgs, fmt.Errorf("editor not detected") 227 | } 228 | --------------------------------------------------------------------------------