app: added helm tgz handle
This commit is contained in:
Vendored
+202
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2016 The Kubernetes Authors All Rights Reserved
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
Copyright (c) for portions of walk.go are held by The Go Authors, 2009 and are
|
||||
provided under the BSD license.
|
||||
|
||||
https://github.com/golang/go/blob/master/LICENSE
|
||||
|
||||
Copyright The Helm Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package sympath
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Walk walks the file tree rooted at root, calling walkFn for each file or directory
|
||||
// in the tree, including root. All errors that arise visiting files and directories
|
||||
// are filtered by walkFn. The files are walked in lexical order, which makes the
|
||||
// output deterministic but means that for very large directories Walk can be
|
||||
// inefficient. Walk follows symbolic links.
|
||||
func Walk(root string, walkFn filepath.WalkFunc) error {
|
||||
info, err := os.Lstat(root)
|
||||
if err != nil {
|
||||
err = walkFn(root, nil, err)
|
||||
} else {
|
||||
err = symwalk(root, info, walkFn)
|
||||
}
|
||||
if err == filepath.SkipDir {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// readDirNames reads the directory named by dirname and returns
|
||||
// a sorted list of directory entries.
|
||||
func readDirNames(dirname string) ([]string, error) {
|
||||
f, err := os.Open(dirname)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
names, err := f.Readdirnames(-1)
|
||||
f.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.Strings(names)
|
||||
return names, nil
|
||||
}
|
||||
|
||||
// symwalk recursively descends path, calling walkFn.
|
||||
func symwalk(path string, info os.FileInfo, walkFn filepath.WalkFunc) error {
|
||||
// Recursively walk symlinked directories.
|
||||
if IsSymlink(info) {
|
||||
resolved, err := filepath.EvalSymlinks(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error evaluating symlink %s: %w", path, err)
|
||||
}
|
||||
// This log message is to highlight a symlink that is being used within a chart, symlinks can be used for nefarious reasons.
|
||||
slog.Info("found symbolic link in path. Contents of linked file included and used", "path", path, "resolved", resolved)
|
||||
if info, err = os.Lstat(resolved); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := symwalk(path, info, walkFn); err != nil && err != filepath.SkipDir {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := walkFn(path, info, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
names, err := readDirNames(path)
|
||||
if err != nil {
|
||||
return walkFn(path, info, err)
|
||||
}
|
||||
|
||||
for _, name := range names {
|
||||
filename := filepath.Join(path, name)
|
||||
fileInfo, err := os.Lstat(filename)
|
||||
if err != nil {
|
||||
if err := walkFn(filename, fileInfo, err); err != nil && err != filepath.SkipDir {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
err = symwalk(filename, fileInfo, walkFn)
|
||||
if err != nil {
|
||||
if (!fileInfo.IsDir() && !IsSymlink(fileInfo)) || err != filepath.SkipDir {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsSymlink is used to determine if the fileinfo is a symbolic link.
|
||||
func IsSymlink(fi os.FileInfo) bool {
|
||||
return fi.Mode()&os.ModeSymlink != 0
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package version
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
"slices"
|
||||
|
||||
_ "k8s.io/client-go/kubernetes" // Force k8s.io/client-go to be included in the build
|
||||
)
|
||||
|
||||
func K8sIOClientGoModVersion() (string, error) {
|
||||
info, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
return "", fmt.Errorf("failed to read build info")
|
||||
}
|
||||
|
||||
idx := slices.IndexFunc(info.Deps, func(m *debug.Module) bool {
|
||||
return m.Path == "k8s.io/client-go"
|
||||
})
|
||||
|
||||
if idx == -1 {
|
||||
return "", fmt.Errorf("k8s.io/client-go not found in build info")
|
||||
}
|
||||
|
||||
m := info.Deps[idx]
|
||||
|
||||
return m.Version, nil
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package version
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
)
|
||||
|
||||
var (
|
||||
// version is the current version of Helm.
|
||||
// Update this whenever making a new release.
|
||||
// The version is of the format Major.Minor.Patch[-Prerelease][+BuildMetadata]
|
||||
//
|
||||
// Increment major number for new feature additions and behavioral changes.
|
||||
// Increment minor number for bug fixes and performance enhancements.
|
||||
version = "v4.1"
|
||||
|
||||
// metadata is extra build time data
|
||||
metadata = ""
|
||||
// gitCommit is the git sha1
|
||||
gitCommit = ""
|
||||
// gitTreeState is the state of the git tree
|
||||
gitTreeState = ""
|
||||
)
|
||||
|
||||
const (
|
||||
kubeClientGoVersionTesting = "v1.20"
|
||||
)
|
||||
|
||||
// BuildInfo describes the compile time information.
|
||||
type BuildInfo struct {
|
||||
// Version is the current semver.
|
||||
Version string `json:"version,omitempty"`
|
||||
// GitCommit is the git sha1.
|
||||
GitCommit string `json:"git_commit,omitempty"`
|
||||
// GitTreeState is the state of the git tree.
|
||||
GitTreeState string `json:"git_tree_state,omitempty"`
|
||||
// GoVersion is the version of the Go compiler used.
|
||||
GoVersion string `json:"go_version,omitempty"`
|
||||
// KubeClientVersion is the version of client-go Helm was build with
|
||||
KubeClientVersion string `json:"kube_client_version"`
|
||||
}
|
||||
|
||||
// GetVersion returns the semver string of the version
|
||||
func GetVersion() string {
|
||||
if metadata == "" {
|
||||
return version
|
||||
}
|
||||
return version + "+" + metadata
|
||||
}
|
||||
|
||||
// GetUserAgent returns a user agent for user with an HTTP client
|
||||
func GetUserAgent() string {
|
||||
return "Helm/" + strings.TrimPrefix(GetVersion(), "v")
|
||||
}
|
||||
|
||||
// Get returns build info
|
||||
func Get() BuildInfo {
|
||||
|
||||
makeKubeClientVersionString := func() string {
|
||||
// Test builds don't include debug info / module info
|
||||
// (And even if they did, we probably want a stable version during tests anyway)
|
||||
// Return a default value for test builds
|
||||
if testing.Testing() {
|
||||
return kubeClientGoVersionTesting
|
||||
}
|
||||
|
||||
vstr, err := K8sIOClientGoModVersion()
|
||||
if err != nil {
|
||||
slog.Error("failed to retrieve k8s.io/client-go version", slog.Any("error", err))
|
||||
return ""
|
||||
}
|
||||
|
||||
v, err := semver.NewVersion(vstr)
|
||||
if err != nil {
|
||||
slog.Error("unable to parse k8s.io/client-go version", slog.String("version", vstr), slog.Any("error", err))
|
||||
return ""
|
||||
}
|
||||
|
||||
kubeClientVersionMajor := v.Major() + 1
|
||||
kubeClientVersionMinor := v.Minor()
|
||||
|
||||
return fmt.Sprintf("v%d.%d", kubeClientVersionMajor, kubeClientVersionMinor)
|
||||
}
|
||||
|
||||
v := BuildInfo{
|
||||
Version: GetVersion(),
|
||||
GitCommit: gitCommit,
|
||||
GitTreeState: gitTreeState,
|
||||
GoVersion: runtime.Version(),
|
||||
KubeClientVersion: makeKubeClientVersionString(),
|
||||
}
|
||||
|
||||
// HACK(bacongobbler): strip out GoVersion during a test run for consistent test output
|
||||
if flag.Lookup("test.v") != nil {
|
||||
v.GoVersion = ""
|
||||
}
|
||||
return v
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||
k8sversion "k8s.io/apimachinery/pkg/util/version"
|
||||
|
||||
helmversion "helm.sh/helm/v4/internal/version"
|
||||
)
|
||||
|
||||
const (
|
||||
kubeVersionMajorTesting = 1
|
||||
kubeVersionMinorTesting = 20
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultVersionSet is the default version set, which includes only Core V1 ("v1").
|
||||
DefaultVersionSet = allKnownVersions()
|
||||
|
||||
DefaultCapabilities = func() *Capabilities {
|
||||
caps, err := makeDefaultCapabilities()
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to create default capabilities: %v", err))
|
||||
}
|
||||
return caps
|
||||
|
||||
}()
|
||||
)
|
||||
|
||||
// Capabilities describes the capabilities of the Kubernetes cluster.
|
||||
type Capabilities struct {
|
||||
// KubeVersion is the Kubernetes version.
|
||||
KubeVersion KubeVersion
|
||||
// APIVersions are supported Kubernetes API versions.
|
||||
APIVersions VersionSet
|
||||
// HelmVersion is the build information for this helm version
|
||||
HelmVersion helmversion.BuildInfo
|
||||
}
|
||||
|
||||
func (capabilities *Capabilities) Copy() *Capabilities {
|
||||
return &Capabilities{
|
||||
KubeVersion: capabilities.KubeVersion,
|
||||
APIVersions: capabilities.APIVersions,
|
||||
HelmVersion: capabilities.HelmVersion,
|
||||
}
|
||||
}
|
||||
|
||||
// KubeVersion is the Kubernetes version.
|
||||
type KubeVersion struct {
|
||||
Version string // Full version (e.g., v1.33.4-gke.1245000)
|
||||
normalizedVersion string // Normalized for constraint checking (e.g., v1.33.4)
|
||||
Major string // Kubernetes major version
|
||||
Minor string // Kubernetes minor version
|
||||
}
|
||||
|
||||
// String implements fmt.Stringer.
|
||||
// Returns the normalized version used for constraint checking.
|
||||
func (kv *KubeVersion) String() string {
|
||||
if kv.normalizedVersion != "" {
|
||||
return kv.normalizedVersion
|
||||
}
|
||||
return kv.Version
|
||||
}
|
||||
|
||||
// GitVersion returns the full Kubernetes version string.
|
||||
//
|
||||
// Deprecated: use KubeVersion.Version.
|
||||
func (kv *KubeVersion) GitVersion() string { return kv.Version }
|
||||
|
||||
// ParseKubeVersion parses kubernetes version from string
|
||||
func ParseKubeVersion(version string) (*KubeVersion, error) {
|
||||
// Based on the original k8s version parser.
|
||||
// https://github.com/kubernetes/kubernetes/blob/b266ac2c3e42c2c4843f81e20213d2b2f43e450a/staging/src/k8s.io/apimachinery/pkg/util/version/version.go#L137
|
||||
sv, err := k8sversion.ParseGeneric(version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Preserve original input (e.g., v1.33.4-gke.1245000)
|
||||
gitVersion := version
|
||||
if !strings.HasPrefix(version, "v") {
|
||||
gitVersion = "v" + version
|
||||
}
|
||||
|
||||
// Normalize for constraint checking (strips all suffixes)
|
||||
normalizedVer := "v" + sv.String()
|
||||
|
||||
return &KubeVersion{
|
||||
Version: gitVersion,
|
||||
normalizedVersion: normalizedVer,
|
||||
Major: strconv.FormatUint(uint64(sv.Major()), 10),
|
||||
Minor: strconv.FormatUint(uint64(sv.Minor()), 10),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// VersionSet is a set of Kubernetes API versions.
|
||||
type VersionSet []string
|
||||
|
||||
// Has returns true if the version string is in the set.
|
||||
//
|
||||
// vs.Has("apps/v1")
|
||||
func (v VersionSet) Has(apiVersion string) bool {
|
||||
return slices.Contains(v, apiVersion)
|
||||
}
|
||||
|
||||
func allKnownVersions() VersionSet {
|
||||
// We should register the built in extension APIs as well so CRDs are
|
||||
// supported in the default version set. This has caused problems with `helm
|
||||
// template` in the past, so let's be safe
|
||||
apiextensionsv1beta1.AddToScheme(scheme.Scheme)
|
||||
apiextensionsv1.AddToScheme(scheme.Scheme)
|
||||
|
||||
groups := scheme.Scheme.PrioritizedVersionsAllGroups()
|
||||
vs := make(VersionSet, 0, len(groups))
|
||||
for _, gv := range groups {
|
||||
vs = append(vs, gv.String())
|
||||
}
|
||||
return vs
|
||||
}
|
||||
|
||||
func makeDefaultCapabilities() (*Capabilities, error) {
|
||||
// Test builds don't include debug info / module info
|
||||
// (And even if they did, we probably want stable capabilities for tests anyway)
|
||||
// Return a default value for test builds
|
||||
if testing.Testing() {
|
||||
return newCapabilities(kubeVersionMajorTesting, kubeVersionMinorTesting)
|
||||
}
|
||||
|
||||
vstr, err := helmversion.K8sIOClientGoModVersion()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve k8s.io/client-go version: %w", err)
|
||||
}
|
||||
|
||||
v, err := semver.NewVersion(vstr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse k8s.io/client-go version %q: %v", vstr, err)
|
||||
}
|
||||
|
||||
kubeVersionMajor := v.Major() + 1
|
||||
kubeVersionMinor := v.Minor()
|
||||
|
||||
return newCapabilities(kubeVersionMajor, kubeVersionMinor)
|
||||
}
|
||||
|
||||
func newCapabilities(kubeVersionMajor, kubeVersionMinor uint64) (*Capabilities, error) {
|
||||
|
||||
version := fmt.Sprintf("v%d.%d.0", kubeVersionMajor, kubeVersionMinor)
|
||||
return &Capabilities{
|
||||
KubeVersion: KubeVersion{
|
||||
Version: version,
|
||||
normalizedVersion: version,
|
||||
Major: fmt.Sprintf("%d", kubeVersionMajor),
|
||||
Minor: fmt.Sprintf("%d", kubeVersionMinor),
|
||||
},
|
||||
APIVersions: DefaultVersionSet,
|
||||
HelmVersion: helmversion.Get(),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ErrNoTable indicates that a chart does not have a matching table.
|
||||
type ErrNoTable struct {
|
||||
Key string
|
||||
}
|
||||
|
||||
func (e ErrNoTable) Error() string { return fmt.Sprintf("%q is not a table", e.Key) }
|
||||
|
||||
// ErrNoValue indicates that Values does not contain a key with a value
|
||||
type ErrNoValue struct {
|
||||
Key string
|
||||
}
|
||||
|
||||
func (e ErrNoValue) Error() string { return fmt.Sprintf("%q is not a value", e.Key) }
|
||||
|
||||
type ErrInvalidChartName struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (e ErrInvalidChartName) Error() string {
|
||||
return fmt.Sprintf("%q is not a valid chart name", e.Name)
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package common
|
||||
|
||||
import "time"
|
||||
|
||||
// File represents a file as a name/value pair.
|
||||
//
|
||||
// By convention, name is a relative path within the scope of the chart's
|
||||
// base directory.
|
||||
type File struct {
|
||||
// Name is the path-like name of the template.
|
||||
Name string `json:"name"`
|
||||
// Data is the template as byte data.
|
||||
Data []byte `json:"data"`
|
||||
// ModTime is the file's mod-time
|
||||
ModTime time.Time `json:"modtime,omitzero"`
|
||||
}
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// GlobalKey is the name of the Values key that is used for storing global vars.
|
||||
const GlobalKey = "global"
|
||||
|
||||
// Values represents a collection of chart values.
|
||||
type Values map[string]interface{}
|
||||
|
||||
// YAML encodes the Values into a YAML string.
|
||||
func (v Values) YAML() (string, error) {
|
||||
b, err := yaml.Marshal(v)
|
||||
return string(b), err
|
||||
}
|
||||
|
||||
// Table gets a table (YAML subsection) from a Values object.
|
||||
//
|
||||
// The table is returned as a Values.
|
||||
//
|
||||
// Compound table names may be specified with dots:
|
||||
//
|
||||
// foo.bar
|
||||
//
|
||||
// The above will be evaluated as "The table bar inside the table
|
||||
// foo".
|
||||
//
|
||||
// An ErrNoTable is returned if the table does not exist.
|
||||
func (v Values) Table(name string) (Values, error) {
|
||||
table := v
|
||||
var err error
|
||||
|
||||
for _, n := range parsePath(name) {
|
||||
if table, err = tableLookup(table, n); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return table, err
|
||||
}
|
||||
|
||||
// AsMap is a utility function for converting Values to a map[string]interface{}.
|
||||
//
|
||||
// It protects against nil map panics.
|
||||
func (v Values) AsMap() map[string]interface{} {
|
||||
if len(v) == 0 {
|
||||
return map[string]interface{}{}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// Encode writes serialized Values information to the given io.Writer.
|
||||
func (v Values) Encode(w io.Writer) error {
|
||||
out, err := yaml.Marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write(out)
|
||||
return err
|
||||
}
|
||||
|
||||
func tableLookup(v Values, simple string) (Values, error) {
|
||||
v2, ok := v[simple]
|
||||
if !ok {
|
||||
return v, ErrNoTable{simple}
|
||||
}
|
||||
if vv, ok := v2.(map[string]interface{}); ok {
|
||||
return vv, nil
|
||||
}
|
||||
|
||||
// This catches a case where a value is of type Values, but doesn't (for some
|
||||
// reason) match the map[string]interface{}. This has been observed in the
|
||||
// wild, and might be a result of a nil map of type Values.
|
||||
if vv, ok := v2.(Values); ok {
|
||||
return vv, nil
|
||||
}
|
||||
|
||||
return Values{}, ErrNoTable{simple}
|
||||
}
|
||||
|
||||
// ReadValues will parse YAML byte data into a Values.
|
||||
func ReadValues(data []byte) (vals Values, err error) {
|
||||
err = yaml.Unmarshal(data, &vals)
|
||||
if len(vals) == 0 {
|
||||
vals = Values{}
|
||||
}
|
||||
return vals, err
|
||||
}
|
||||
|
||||
// ReadValuesFile will parse a YAML file into a map of values.
|
||||
func ReadValuesFile(filename string) (Values, error) {
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return map[string]interface{}{}, err
|
||||
}
|
||||
return ReadValues(data)
|
||||
}
|
||||
|
||||
// ReleaseOptions represents the additional release options needed
|
||||
// for the composition of the final values struct
|
||||
type ReleaseOptions struct {
|
||||
Name string
|
||||
Namespace string
|
||||
Revision int
|
||||
IsUpgrade bool
|
||||
IsInstall bool
|
||||
}
|
||||
|
||||
// istable is a special-purpose function to see if the present thing matches the definition of a YAML table.
|
||||
func istable(v interface{}) bool {
|
||||
_, ok := v.(map[string]interface{})
|
||||
return ok
|
||||
}
|
||||
|
||||
// PathValue takes a path that traverses a YAML structure and returns the value at the end of that path.
|
||||
// The path starts at the root of the YAML structure and is comprised of YAML keys separated by periods.
|
||||
// Given the following YAML data the value at path "chapter.one.title" is "Loomings".
|
||||
//
|
||||
// chapter:
|
||||
// one:
|
||||
// title: "Loomings"
|
||||
func (v Values) PathValue(path string) (interface{}, error) {
|
||||
if path == "" {
|
||||
return nil, errors.New("YAML path cannot be empty")
|
||||
}
|
||||
return v.pathValue(parsePath(path))
|
||||
}
|
||||
|
||||
func (v Values) pathValue(path []string) (interface{}, error) {
|
||||
if len(path) == 1 {
|
||||
// if exists must be root key not table
|
||||
if _, ok := v[path[0]]; ok && !istable(v[path[0]]) {
|
||||
return v[path[0]], nil
|
||||
}
|
||||
return nil, ErrNoValue{path[0]}
|
||||
}
|
||||
|
||||
key, path := path[len(path)-1], path[:len(path)-1]
|
||||
// get our table for table path
|
||||
t, err := v.Table(joinPath(path...))
|
||||
if err != nil {
|
||||
return nil, ErrNoValue{key}
|
||||
}
|
||||
// check table for key and ensure value is not a table
|
||||
if k, ok := t[key]; ok && !istable(k) {
|
||||
return k, nil
|
||||
}
|
||||
return nil, ErrNoValue{key}
|
||||
}
|
||||
|
||||
func parsePath(key string) []string { return strings.Split(key, ".") }
|
||||
|
||||
func joinPath(path ...string) string { return strings.Join(path, ".") }
|
||||
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// archive provides utility functions for working with Helm chart archive files
|
||||
package archive
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MaxDecompressedChartSize is the maximum size of a chart archive that will be
|
||||
// decompressed. This is the decompressed size of all the files.
|
||||
// The default value is 100 MiB.
|
||||
var MaxDecompressedChartSize int64 = 100 * 1024 * 1024 // Default 100 MiB
|
||||
|
||||
// MaxDecompressedFileSize is the size of the largest file that Helm will attempt to load.
|
||||
// The size of the file is the decompressed version of it when it is stored in an archive.
|
||||
var MaxDecompressedFileSize int64 = 5 * 1024 * 1024 // Default 5 MiB
|
||||
|
||||
var drivePathPattern = regexp.MustCompile(`^[a-zA-Z]:/`)
|
||||
|
||||
var utf8bom = []byte{0xEF, 0xBB, 0xBF}
|
||||
|
||||
// BufferedFile represents an archive file buffered for later processing.
|
||||
type BufferedFile struct {
|
||||
Name string
|
||||
ModTime time.Time
|
||||
Data []byte
|
||||
}
|
||||
|
||||
// LoadArchiveFiles reads in files out of an archive into memory. This function
|
||||
// performs important path security checks and should always be used before
|
||||
// expanding a tarball
|
||||
func LoadArchiveFiles(in io.Reader) ([]*BufferedFile, error) {
|
||||
unzipped, err := gzip.NewReader(in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer unzipped.Close()
|
||||
|
||||
files := []*BufferedFile{}
|
||||
tr := tar.NewReader(unzipped)
|
||||
remainingSize := MaxDecompressedChartSize
|
||||
for {
|
||||
b := bytes.NewBuffer(nil)
|
||||
hd, err := tr.Next()
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if hd.FileInfo().IsDir() {
|
||||
// Use this instead of hd.Typeflag because we don't have to do any
|
||||
// inference chasing.
|
||||
continue
|
||||
}
|
||||
|
||||
switch hd.Typeflag {
|
||||
// We don't want to process these extension header files.
|
||||
case tar.TypeXGlobalHeader, tar.TypeXHeader:
|
||||
continue
|
||||
}
|
||||
|
||||
// Archive could contain \ if generated on Windows
|
||||
delimiter := "/"
|
||||
if strings.ContainsRune(hd.Name, '\\') {
|
||||
delimiter = "\\"
|
||||
}
|
||||
|
||||
parts := strings.Split(hd.Name, delimiter)
|
||||
n := strings.Join(parts[1:], delimiter)
|
||||
|
||||
// Normalize the path to the / delimiter
|
||||
n = strings.ReplaceAll(n, delimiter, "/")
|
||||
|
||||
if path.IsAbs(n) {
|
||||
return nil, errors.New("chart illegally contains absolute paths")
|
||||
}
|
||||
|
||||
n = path.Clean(n)
|
||||
if n == "." {
|
||||
// In this case, the original path was relative when it should have been absolute.
|
||||
return nil, fmt.Errorf("chart illegally contains content outside the base directory: %q", hd.Name)
|
||||
}
|
||||
if strings.HasPrefix(n, "..") {
|
||||
return nil, errors.New("chart illegally references parent directory")
|
||||
}
|
||||
|
||||
// In some particularly arcane acts of path creativity, it is possible to intermix
|
||||
// UNIX and Windows style paths in such a way that you produce a result of the form
|
||||
// c:/foo even after all the built-in absolute path checks. So we explicitly check
|
||||
// for this condition.
|
||||
if drivePathPattern.MatchString(n) {
|
||||
return nil, errors.New("chart contains illegally named files")
|
||||
}
|
||||
|
||||
if parts[0] == "Chart.yaml" {
|
||||
return nil, errors.New("chart yaml not in base directory")
|
||||
}
|
||||
|
||||
if hd.Size > remainingSize {
|
||||
return nil, fmt.Errorf("decompressed chart is larger than the maximum size %d", MaxDecompressedChartSize)
|
||||
}
|
||||
|
||||
if hd.Size > MaxDecompressedFileSize {
|
||||
return nil, fmt.Errorf("decompressed chart file %q is larger than the maximum file size %d", hd.Name, MaxDecompressedFileSize)
|
||||
}
|
||||
|
||||
limitedReader := io.LimitReader(tr, remainingSize)
|
||||
|
||||
bytesWritten, err := io.Copy(b, limitedReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
remainingSize -= bytesWritten
|
||||
// When the bytesWritten are less than the file size it means the limit reader ended
|
||||
// copying early. Here we report that error. This is important if the last file extracted
|
||||
// is the one that goes over the limit. It assumes the Size stored in the tar header
|
||||
// is correct, something many applications do.
|
||||
if bytesWritten < hd.Size || remainingSize <= 0 {
|
||||
return nil, fmt.Errorf("decompressed chart is larger than the maximum size %d", MaxDecompressedChartSize)
|
||||
}
|
||||
|
||||
data := bytes.TrimPrefix(b.Bytes(), utf8bom)
|
||||
|
||||
files = append(files, &BufferedFile{Name: n, ModTime: hd.ModTime, Data: data})
|
||||
b.Reset()
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
return nil, errors.New("no files in chart archive")
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// ensureArchive's job is to return an informative error if the file does not appear to be a gzipped archive.
|
||||
//
|
||||
// Sometimes users will provide a values.yaml for an argument where a chart is expected. One common occurrence
|
||||
// of this is invoking `helm template values.yaml mychart` which would otherwise produce a confusing error
|
||||
// if we didn't check for this.
|
||||
func EnsureArchive(name string, raw *os.File) error {
|
||||
defer raw.Seek(0, 0) // reset read offset to allow archive loading to proceed.
|
||||
|
||||
// Check the file format to give us a chance to provide the user with more actionable feedback.
|
||||
buffer := make([]byte, 512)
|
||||
_, err := raw.Read(buffer)
|
||||
if err != nil && err != io.EOF {
|
||||
return fmt.Errorf("file '%s' cannot be read: %s", name, err)
|
||||
}
|
||||
|
||||
// Helm may identify achieve of the application/x-gzip as application/vnd.ms-fontobject.
|
||||
// Fix for: https://github.com/helm/helm/issues/12261
|
||||
if contentType := http.DetectContentType(buffer); contentType != "application/x-gzip" && !isGZipApplication(buffer) {
|
||||
// TODO: Is there a way to reliably test if a file content is YAML? ghodss/yaml accepts a wide
|
||||
// variety of content (Makefile, .zshrc) as valid YAML without errors.
|
||||
|
||||
// Wrong content type. Let's check if it's yaml and give an extra hint?
|
||||
if strings.HasSuffix(name, ".yml") || strings.HasSuffix(name, ".yaml") {
|
||||
return fmt.Errorf("file '%s' seems to be a YAML file, but expected a gzipped archive", name)
|
||||
}
|
||||
return fmt.Errorf("file '%s' does not appear to be a gzipped archive; got '%s'", name, contentType)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isGZipApplication checks whether the archive is of the application/x-gzip type.
|
||||
func isGZipApplication(data []byte) bool {
|
||||
sig := []byte("\x1F\x8B\x08")
|
||||
return bytes.HasPrefix(data, sig)
|
||||
}
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package v2
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"helm.sh/helm/v4/pkg/chart/common"
|
||||
)
|
||||
|
||||
// APIVersionV1 is the API version number for version 1.
|
||||
const APIVersionV1 = "v1"
|
||||
|
||||
// APIVersionV2 is the API version number for version 2.
|
||||
const APIVersionV2 = "v2"
|
||||
|
||||
// aliasNameFormat defines the characters that are legal in an alias name.
|
||||
var aliasNameFormat = regexp.MustCompile("^[a-zA-Z0-9_-]+$")
|
||||
|
||||
// Chart is a helm package that contains metadata, a default config, zero or more
|
||||
// optionally parameterizable templates, and zero or more charts (dependencies).
|
||||
type Chart struct {
|
||||
// Raw contains the raw contents of the files originally contained in the chart archive.
|
||||
//
|
||||
// This should not be used except in special cases like `helm show values`,
|
||||
// where we want to display the raw values, comments and all.
|
||||
Raw []*common.File `json:"-"`
|
||||
// Metadata is the contents of the Chartfile.
|
||||
Metadata *Metadata `json:"metadata"`
|
||||
// Lock is the contents of Chart.lock.
|
||||
Lock *Lock `json:"lock"`
|
||||
// Templates for this chart.
|
||||
Templates []*common.File `json:"templates"`
|
||||
// Values are default config for this chart.
|
||||
Values map[string]interface{} `json:"values"`
|
||||
// Schema is an optional JSON schema for imposing structure on Values
|
||||
Schema []byte `json:"schema"`
|
||||
// SchemaModTime the schema was last modified
|
||||
SchemaModTime time.Time `json:"schemamodtime,omitempty"`
|
||||
// Files are miscellaneous files in a chart archive,
|
||||
// e.g. README, LICENSE, etc.
|
||||
Files []*common.File `json:"files"`
|
||||
// ModTime the chart metadata was last modified
|
||||
ModTime time.Time `json:"modtime,omitzero"`
|
||||
|
||||
parent *Chart
|
||||
dependencies []*Chart
|
||||
}
|
||||
|
||||
type CRD struct {
|
||||
// Name is the File.Name for the crd file
|
||||
Name string
|
||||
// Filename is the File obj Name including (sub-)chart.ChartFullPath
|
||||
Filename string
|
||||
// File is the File obj for the crd
|
||||
File *common.File
|
||||
}
|
||||
|
||||
// SetDependencies replaces the chart dependencies.
|
||||
func (ch *Chart) SetDependencies(charts ...*Chart) {
|
||||
ch.dependencies = nil
|
||||
ch.AddDependency(charts...)
|
||||
}
|
||||
|
||||
// Name returns the name of the chart.
|
||||
func (ch *Chart) Name() string {
|
||||
if ch.Metadata == nil {
|
||||
return ""
|
||||
}
|
||||
return ch.Metadata.Name
|
||||
}
|
||||
|
||||
// AddDependency determines if the chart is a subchart.
|
||||
func (ch *Chart) AddDependency(charts ...*Chart) {
|
||||
for i, x := range charts {
|
||||
charts[i].parent = ch
|
||||
ch.dependencies = append(ch.dependencies, x)
|
||||
}
|
||||
}
|
||||
|
||||
// Root finds the root chart.
|
||||
func (ch *Chart) Root() *Chart {
|
||||
if ch.IsRoot() {
|
||||
return ch
|
||||
}
|
||||
return ch.Parent().Root()
|
||||
}
|
||||
|
||||
// Dependencies are the charts that this chart depends on.
|
||||
func (ch *Chart) Dependencies() []*Chart { return ch.dependencies }
|
||||
|
||||
// IsRoot determines if the chart is the root chart.
|
||||
func (ch *Chart) IsRoot() bool { return ch.parent == nil }
|
||||
|
||||
// Parent returns a subchart's parent chart.
|
||||
func (ch *Chart) Parent() *Chart { return ch.parent }
|
||||
|
||||
// ChartPath returns the full path to this chart in dot notation.
|
||||
func (ch *Chart) ChartPath() string {
|
||||
if !ch.IsRoot() {
|
||||
return ch.Parent().ChartPath() + "." + ch.Name()
|
||||
}
|
||||
return ch.Name()
|
||||
}
|
||||
|
||||
// ChartFullPath returns the full path to this chart.
|
||||
// Note that the path may not correspond to the path where the file can be found on the file system if the path
|
||||
// points to an aliased subchart.
|
||||
func (ch *Chart) ChartFullPath() string {
|
||||
if !ch.IsRoot() {
|
||||
return ch.Parent().ChartFullPath() + "/charts/" + ch.Name()
|
||||
}
|
||||
return ch.Name()
|
||||
}
|
||||
|
||||
// Validate validates the metadata.
|
||||
func (ch *Chart) Validate() error {
|
||||
return ch.Metadata.Validate()
|
||||
}
|
||||
|
||||
// AppVersion returns the appversion of the chart.
|
||||
func (ch *Chart) AppVersion() string {
|
||||
if ch.Metadata == nil {
|
||||
return ""
|
||||
}
|
||||
return ch.Metadata.AppVersion
|
||||
}
|
||||
|
||||
// CRDs returns a list of File objects in the 'crds/' directory of a Helm chart.
|
||||
// Deprecated: use CRDObjects()
|
||||
func (ch *Chart) CRDs() []*common.File {
|
||||
files := []*common.File{}
|
||||
// Find all resources in the crds/ directory
|
||||
for _, f := range ch.Files {
|
||||
if strings.HasPrefix(f.Name, "crds/") && hasManifestExtension(f.Name) {
|
||||
files = append(files, f)
|
||||
}
|
||||
}
|
||||
// Get CRDs from dependencies, too.
|
||||
for _, dep := range ch.Dependencies() {
|
||||
files = append(files, dep.CRDs()...)
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
// CRDObjects returns a list of CRD objects in the 'crds/' directory of a Helm chart & subcharts
|
||||
func (ch *Chart) CRDObjects() []CRD {
|
||||
crds := []CRD{}
|
||||
// Find all resources in the crds/ directory
|
||||
for _, f := range ch.Files {
|
||||
if strings.HasPrefix(f.Name, "crds/") && hasManifestExtension(f.Name) {
|
||||
mycrd := CRD{Name: f.Name, Filename: filepath.Join(ch.ChartFullPath(), f.Name), File: f}
|
||||
crds = append(crds, mycrd)
|
||||
}
|
||||
}
|
||||
// Get CRDs from dependencies, too.
|
||||
for _, dep := range ch.Dependencies() {
|
||||
crds = append(crds, dep.CRDObjects()...)
|
||||
}
|
||||
return crds
|
||||
}
|
||||
|
||||
func hasManifestExtension(fname string) bool {
|
||||
ext := filepath.Ext(fname)
|
||||
return strings.EqualFold(ext, ".yaml") || strings.EqualFold(ext, ".yml") || strings.EqualFold(ext, ".json")
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package v2
|
||||
|
||||
import "time"
|
||||
|
||||
// Dependency describes a chart upon which another chart depends.
|
||||
//
|
||||
// Dependencies can be used to express developer intent, or to capture the state
|
||||
// of a chart.
|
||||
type Dependency struct {
|
||||
// Name is the name of the dependency.
|
||||
//
|
||||
// This must mach the name in the dependency's Chart.yaml.
|
||||
Name string `json:"name" yaml:"name"`
|
||||
// Version is the version (range) of this chart.
|
||||
//
|
||||
// A lock file will always produce a single version, while a dependency
|
||||
// may contain a semantic version range.
|
||||
Version string `json:"version,omitempty" yaml:"version,omitempty"`
|
||||
// The URL to the repository.
|
||||
//
|
||||
// Appending `index.yaml` to this string should result in a URL that can be
|
||||
// used to fetch the repository index.
|
||||
Repository string `json:"repository" yaml:"repository"`
|
||||
// A yaml path that resolves to a boolean, used for enabling/disabling charts (e.g. subchart1.enabled )
|
||||
Condition string `json:"condition,omitempty" yaml:"condition,omitempty"`
|
||||
// Tags can be used to group charts for enabling/disabling together
|
||||
Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"`
|
||||
// Enabled bool determines if chart should be loaded
|
||||
Enabled bool `json:"enabled,omitempty" yaml:"enabled,omitempty"`
|
||||
// ImportValues holds the mapping of source values to parent key to be imported. Each item can be a
|
||||
// string or pair of child/parent sublist items.
|
||||
ImportValues []interface{} `json:"import-values,omitempty" yaml:"import-values,omitempty"`
|
||||
// Alias usable alias to be used for the chart
|
||||
Alias string `json:"alias,omitempty" yaml:"alias,omitempty"`
|
||||
}
|
||||
|
||||
// Validate checks for common problems with the dependency datastructure in
|
||||
// the chart. This check must be done at load time before the dependency's charts are
|
||||
// loaded.
|
||||
func (d *Dependency) Validate() error {
|
||||
if d == nil {
|
||||
return ValidationError("dependencies must not contain empty or null nodes")
|
||||
}
|
||||
d.Name = sanitizeString(d.Name)
|
||||
d.Version = sanitizeString(d.Version)
|
||||
d.Repository = sanitizeString(d.Repository)
|
||||
d.Condition = sanitizeString(d.Condition)
|
||||
for i := range d.Tags {
|
||||
d.Tags[i] = sanitizeString(d.Tags[i])
|
||||
}
|
||||
if d.Alias != "" && !aliasNameFormat.MatchString(d.Alias) {
|
||||
return ValidationErrorf("dependency %q has disallowed characters in the alias", d.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Lock is a lock file for dependencies.
|
||||
//
|
||||
// It represents the state that the dependencies should be in.
|
||||
type Lock struct {
|
||||
// Generated is the date the lock file was last generated.
|
||||
Generated time.Time `json:"generated"`
|
||||
// Digest is a hash of the dependencies in Chart.yaml.
|
||||
Digest string `json:"digest"`
|
||||
// Dependencies is the list of dependencies that this lock file has locked.
|
||||
Dependencies []*Dependency `json:"dependencies"`
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
Package v2 provides chart handling for apiVersion v1 and v2 charts
|
||||
|
||||
This package and its sub-packages provide handling for apiVersion v1 and v2 charts.
|
||||
The changes from v1 to v2 charts are minor and were able to be handled with minor
|
||||
switches based on characteristics.
|
||||
*/
|
||||
package v2
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package v2
|
||||
|
||||
import "fmt"
|
||||
|
||||
// ValidationError represents a data validation error.
|
||||
type ValidationError string
|
||||
|
||||
func (v ValidationError) Error() string {
|
||||
return "validation: " + string(v)
|
||||
}
|
||||
|
||||
// ValidationErrorf takes a message and formatting options and creates a ValidationError
|
||||
func ValidationErrorf(msg string, args ...interface{}) ValidationError {
|
||||
return ValidationError(fmt.Sprintf(msg, args...))
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package loader
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"helm.sh/helm/v4/pkg/chart/loader/archive"
|
||||
chart "helm.sh/helm/v4/pkg/chart/v2"
|
||||
)
|
||||
|
||||
// FileLoader loads a chart from a file
|
||||
type FileLoader string
|
||||
|
||||
// Load loads a chart
|
||||
func (l FileLoader) Load() (*chart.Chart, error) {
|
||||
return LoadFile(string(l))
|
||||
}
|
||||
|
||||
// LoadFile loads from an archive file.
|
||||
func LoadFile(name string) (*chart.Chart, error) {
|
||||
if fi, err := os.Stat(name); err != nil {
|
||||
return nil, err
|
||||
} else if fi.IsDir() {
|
||||
return nil, errors.New("cannot load a directory")
|
||||
}
|
||||
|
||||
raw, err := os.Open(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer raw.Close()
|
||||
|
||||
err = archive.EnsureArchive(name, raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c, err := LoadArchive(raw)
|
||||
if err != nil {
|
||||
if errors.Is(err, gzip.ErrHeader) {
|
||||
return nil, fmt.Errorf("file '%s' does not appear to be a valid chart file (details: %w)", name, err)
|
||||
}
|
||||
}
|
||||
return c, err
|
||||
}
|
||||
|
||||
// LoadArchive loads from a reader containing a compressed tar archive.
|
||||
func LoadArchive(in io.Reader) (*chart.Chart, error) {
|
||||
files, err := archive.LoadArchiveFiles(in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return LoadFiles(files)
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package loader
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"helm.sh/helm/v4/internal/sympath"
|
||||
"helm.sh/helm/v4/pkg/chart/loader/archive"
|
||||
chart "helm.sh/helm/v4/pkg/chart/v2"
|
||||
"helm.sh/helm/v4/pkg/ignore"
|
||||
)
|
||||
|
||||
var utf8bom = []byte{0xEF, 0xBB, 0xBF}
|
||||
|
||||
// DirLoader loads a chart from a directory
|
||||
type DirLoader string
|
||||
|
||||
// Load loads the chart
|
||||
func (l DirLoader) Load() (*chart.Chart, error) {
|
||||
return LoadDir(string(l))
|
||||
}
|
||||
|
||||
// LoadDir loads from a directory.
|
||||
//
|
||||
// This loads charts only from directories.
|
||||
func LoadDir(dir string) (*chart.Chart, error) {
|
||||
topdir, err := filepath.Abs(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Just used for errors.
|
||||
c := &chart.Chart{}
|
||||
|
||||
rules := ignore.Empty()
|
||||
ifile := filepath.Join(topdir, ignore.HelmIgnore)
|
||||
if _, err := os.Stat(ifile); err == nil {
|
||||
r, err := ignore.ParseFile(ifile)
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
rules = r
|
||||
}
|
||||
rules.AddDefaults()
|
||||
|
||||
files := []*archive.BufferedFile{}
|
||||
topdir += string(filepath.Separator)
|
||||
|
||||
walk := func(name string, fi os.FileInfo, err error) error {
|
||||
n := strings.TrimPrefix(name, topdir)
|
||||
if n == "" {
|
||||
// No need to process top level. Avoid bug with helmignore .* matching
|
||||
// empty names. See issue 1779.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Normalize to / since it will also work on Windows
|
||||
n = filepath.ToSlash(n)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if fi.IsDir() {
|
||||
// Directory-based ignore rules should involve skipping the entire
|
||||
// contents of that directory.
|
||||
if rules.Ignore(n, fi) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// If a .helmignore file matches, skip this file.
|
||||
if rules.Ignore(n, fi) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Irregular files include devices, sockets, and other uses of files that
|
||||
// are not regular files. In Go they have a file mode type bit set.
|
||||
// See https://golang.org/pkg/os/#FileMode for examples.
|
||||
if !fi.Mode().IsRegular() {
|
||||
return fmt.Errorf("cannot load irregular file %s as it has file mode type bits set", name)
|
||||
}
|
||||
|
||||
if fi.Size() > archive.MaxDecompressedFileSize {
|
||||
return fmt.Errorf("chart file %q is larger than the maximum file size %d", fi.Name(), archive.MaxDecompressedFileSize)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading %s: %w", n, err)
|
||||
}
|
||||
|
||||
data = bytes.TrimPrefix(data, utf8bom)
|
||||
|
||||
files = append(files, &archive.BufferedFile{Name: n, ModTime: fi.ModTime(), Data: data})
|
||||
return nil
|
||||
}
|
||||
if err = sympath.Walk(topdir, walk); err != nil {
|
||||
return c, err
|
||||
}
|
||||
|
||||
return LoadFiles(files)
|
||||
}
|
||||
+249
@@ -0,0 +1,249 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package loader
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"maps"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
utilyaml "k8s.io/apimachinery/pkg/util/yaml"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"helm.sh/helm/v4/pkg/chart/common"
|
||||
"helm.sh/helm/v4/pkg/chart/loader/archive"
|
||||
chart "helm.sh/helm/v4/pkg/chart/v2"
|
||||
)
|
||||
|
||||
// ChartLoader loads a chart.
|
||||
type ChartLoader interface {
|
||||
Load() (*chart.Chart, error)
|
||||
}
|
||||
|
||||
// Loader returns a new ChartLoader appropriate for the given chart name
|
||||
func Loader(name string) (ChartLoader, error) {
|
||||
fi, err := os.Stat(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if fi.IsDir() {
|
||||
return DirLoader(name), nil
|
||||
}
|
||||
return FileLoader(name), nil
|
||||
}
|
||||
|
||||
// Load takes a string name, tries to resolve it to a file or directory, and then loads it.
|
||||
//
|
||||
// This is the preferred way to load a chart. It will discover the chart encoding
|
||||
// and hand off to the appropriate chart reader.
|
||||
//
|
||||
// If a .helmignore file is present, the directory loader will skip loading any files
|
||||
// matching it. But .helmignore is not evaluated when reading out of an archive.
|
||||
func Load(name string) (*chart.Chart, error) {
|
||||
l, err := Loader(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return l.Load()
|
||||
}
|
||||
|
||||
// LoadFiles loads from in-memory files.
|
||||
func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) {
|
||||
c := new(chart.Chart)
|
||||
subcharts := make(map[string][]*archive.BufferedFile)
|
||||
|
||||
// do not rely on assumed ordering of files in the chart and crash
|
||||
// if Chart.yaml was not coming early enough to initialize metadata
|
||||
for _, f := range files {
|
||||
c.Raw = append(c.Raw, &common.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data})
|
||||
if f.Name == "Chart.yaml" {
|
||||
if c.Metadata == nil {
|
||||
c.Metadata = new(chart.Metadata)
|
||||
}
|
||||
if err := yaml.Unmarshal(f.Data, c.Metadata); err != nil {
|
||||
return c, fmt.Errorf("cannot load Chart.yaml: %w", err)
|
||||
}
|
||||
// NOTE(bacongobbler): while the chart specification says that APIVersion must be set,
|
||||
// Helm 2 accepted charts that did not provide an APIVersion in their chart metadata.
|
||||
// Because of that, if APIVersion is unset, we should assume we're loading a v1 chart.
|
||||
if c.Metadata.APIVersion == "" {
|
||||
c.Metadata.APIVersion = chart.APIVersionV1
|
||||
}
|
||||
c.ModTime = f.ModTime
|
||||
}
|
||||
}
|
||||
for _, f := range files {
|
||||
switch {
|
||||
case f.Name == "Chart.yaml":
|
||||
// already processed
|
||||
continue
|
||||
case f.Name == "Chart.lock":
|
||||
c.Lock = new(chart.Lock)
|
||||
if err := yaml.Unmarshal(f.Data, &c.Lock); err != nil {
|
||||
return c, fmt.Errorf("cannot load Chart.lock: %w", err)
|
||||
}
|
||||
case f.Name == "values.yaml":
|
||||
values, err := LoadValues(bytes.NewReader(f.Data))
|
||||
if err != nil {
|
||||
return c, fmt.Errorf("cannot load values.yaml: %w", err)
|
||||
}
|
||||
c.Values = values
|
||||
case f.Name == "values.schema.json":
|
||||
c.Schema = f.Data
|
||||
c.SchemaModTime = f.ModTime
|
||||
|
||||
// Deprecated: requirements.yaml is deprecated use Chart.yaml.
|
||||
// We will handle it for you because we are nice people
|
||||
case f.Name == "requirements.yaml":
|
||||
if c.Metadata == nil {
|
||||
c.Metadata = new(chart.Metadata)
|
||||
}
|
||||
if c.Metadata.APIVersion != chart.APIVersionV1 {
|
||||
log.Printf("Warning: Dependencies are handled in Chart.yaml since apiVersion \"v2\". We recommend migrating dependencies to Chart.yaml.")
|
||||
}
|
||||
if err := yaml.Unmarshal(f.Data, c.Metadata); err != nil {
|
||||
return c, fmt.Errorf("cannot load requirements.yaml: %w", err)
|
||||
}
|
||||
if c.Metadata.APIVersion == chart.APIVersionV1 {
|
||||
c.Files = append(c.Files, &common.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data})
|
||||
}
|
||||
// Deprecated: requirements.lock is deprecated use Chart.lock.
|
||||
case f.Name == "requirements.lock":
|
||||
c.Lock = new(chart.Lock)
|
||||
if err := yaml.Unmarshal(f.Data, &c.Lock); err != nil {
|
||||
return c, fmt.Errorf("cannot load requirements.lock: %w", err)
|
||||
}
|
||||
if c.Metadata == nil {
|
||||
c.Metadata = new(chart.Metadata)
|
||||
}
|
||||
if c.Metadata.APIVersion != chart.APIVersionV1 {
|
||||
log.Printf("Warning: Dependency locking is handled in Chart.lock since apiVersion \"v2\". We recommend migrating to Chart.lock.")
|
||||
}
|
||||
if c.Metadata.APIVersion == chart.APIVersionV1 {
|
||||
c.Files = append(c.Files, &common.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data})
|
||||
}
|
||||
|
||||
case strings.HasPrefix(f.Name, "templates/"):
|
||||
c.Templates = append(c.Templates, &common.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data})
|
||||
case strings.HasPrefix(f.Name, "charts/"):
|
||||
if filepath.Ext(f.Name) == ".prov" {
|
||||
c.Files = append(c.Files, &common.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data})
|
||||
continue
|
||||
}
|
||||
|
||||
fname := strings.TrimPrefix(f.Name, "charts/")
|
||||
cname := strings.SplitN(fname, "/", 2)[0]
|
||||
subcharts[cname] = append(subcharts[cname], &archive.BufferedFile{Name: fname, ModTime: f.ModTime, Data: f.Data})
|
||||
default:
|
||||
c.Files = append(c.Files, &common.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data})
|
||||
}
|
||||
}
|
||||
|
||||
if c.Metadata == nil {
|
||||
return c, errors.New("Chart.yaml file is missing") //nolint:staticcheck
|
||||
}
|
||||
|
||||
if err := c.Validate(); err != nil {
|
||||
return c, err
|
||||
}
|
||||
|
||||
for n, files := range subcharts {
|
||||
var sc *chart.Chart
|
||||
var err error
|
||||
switch {
|
||||
case strings.IndexAny(n, "_.") == 0:
|
||||
continue
|
||||
case filepath.Ext(n) == ".tgz":
|
||||
file := files[0]
|
||||
if file.Name != n {
|
||||
return c, fmt.Errorf("error unpacking subchart tar in %s: expected %s, got %s", c.Name(), n, file.Name)
|
||||
}
|
||||
// Untar the chart and add to c.Dependencies
|
||||
sc, err = LoadArchive(bytes.NewBuffer(file.Data))
|
||||
default:
|
||||
// We have to trim the prefix off of every file, and ignore any file
|
||||
// that is in charts/, but isn't actually a chart.
|
||||
buff := make([]*archive.BufferedFile, 0, len(files))
|
||||
for _, f := range files {
|
||||
parts := strings.SplitN(f.Name, "/", 2)
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
f.Name = parts[1]
|
||||
buff = append(buff, f)
|
||||
}
|
||||
sc, err = LoadFiles(buff)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return c, fmt.Errorf("error unpacking subchart %s in %s: %w", n, c.Name(), err)
|
||||
}
|
||||
c.AddDependency(sc)
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// LoadValues loads values from a reader.
|
||||
//
|
||||
// The reader is expected to contain one or more YAML documents, the values of which are merged.
|
||||
// And the values can be either a chart's default values or user-supplied values.
|
||||
func LoadValues(data io.Reader) (map[string]interface{}, error) {
|
||||
values := map[string]interface{}{}
|
||||
reader := utilyaml.NewYAMLReader(bufio.NewReader(data))
|
||||
for {
|
||||
currentMap := map[string]interface{}{}
|
||||
raw, err := reader.Read()
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
return nil, fmt.Errorf("error reading yaml document: %w", err)
|
||||
}
|
||||
if err := yaml.Unmarshal(raw, ¤tMap); err != nil {
|
||||
return nil, fmt.Errorf("cannot unmarshal yaml document: %w", err)
|
||||
}
|
||||
values = MergeMaps(values, currentMap)
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
// MergeMaps merges two maps. If a key exists in both maps, the value from b will be used.
|
||||
// If the value is a map, the maps will be merged recursively.
|
||||
func MergeMaps(a, b map[string]interface{}) map[string]interface{} {
|
||||
out := make(map[string]interface{}, len(a))
|
||||
maps.Copy(out, a)
|
||||
for k, v := range b {
|
||||
if v, ok := v.(map[string]interface{}); ok {
|
||||
if bv, ok := out[k]; ok {
|
||||
if bv, ok := bv.(map[string]interface{}); ok {
|
||||
out[k] = MergeMaps(bv, v)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
out[k] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package v2
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
)
|
||||
|
||||
// Maintainer describes a Chart maintainer.
|
||||
type Maintainer struct {
|
||||
// Name is a user name or organization name
|
||||
Name string `json:"name,omitempty"`
|
||||
// Email is an optional email address to contact the named maintainer
|
||||
Email string `json:"email,omitempty"`
|
||||
// URL is an optional URL to an address for the named maintainer
|
||||
URL string `json:"url,omitempty"`
|
||||
}
|
||||
|
||||
// Validate checks valid data and sanitizes string characters.
|
||||
func (m *Maintainer) Validate() error {
|
||||
if m == nil {
|
||||
return ValidationError("maintainers must not contain empty or null nodes")
|
||||
}
|
||||
m.Name = sanitizeString(m.Name)
|
||||
m.Email = sanitizeString(m.Email)
|
||||
m.URL = sanitizeString(m.URL)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Metadata for a Chart file. This models the structure of a Chart.yaml file.
|
||||
type Metadata struct {
|
||||
// The name of the chart. Required.
|
||||
Name string `json:"name,omitempty"`
|
||||
// The URL to a relevant project page, git repo, or contact person
|
||||
Home string `json:"home,omitempty"`
|
||||
// Source is the URL to the source code of this chart
|
||||
Sources []string `json:"sources,omitempty"`
|
||||
// A version string of the chart. Required.
|
||||
Version string `json:"version,omitempty"`
|
||||
// A one-sentence description of the chart
|
||||
Description string `json:"description,omitempty"`
|
||||
// A list of string keywords
|
||||
Keywords []string `json:"keywords,omitempty"`
|
||||
// A list of name and URL/email address combinations for the maintainer(s)
|
||||
Maintainers []*Maintainer `json:"maintainers,omitempty"`
|
||||
// The URL to an icon file.
|
||||
Icon string `json:"icon,omitempty"`
|
||||
// The API Version of this chart. Required.
|
||||
APIVersion string `json:"apiVersion,omitempty"`
|
||||
// The condition to check to enable chart
|
||||
Condition string `json:"condition,omitempty"`
|
||||
// The tags to check to enable chart
|
||||
Tags string `json:"tags,omitempty"`
|
||||
// The version of the application enclosed inside of this chart.
|
||||
AppVersion string `json:"appVersion,omitempty"`
|
||||
// Whether or not this chart is deprecated
|
||||
Deprecated bool `json:"deprecated,omitempty"`
|
||||
// Annotations are additional mappings uninterpreted by Helm,
|
||||
// made available for inspection by other applications.
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
// KubeVersion is a SemVer constraint specifying the version of Kubernetes required.
|
||||
KubeVersion string `json:"kubeVersion,omitempty"`
|
||||
// Dependencies are a list of dependencies for a chart.
|
||||
Dependencies []*Dependency `json:"dependencies,omitempty"`
|
||||
// Specifies the chart type: application or library
|
||||
Type string `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
// Validate checks the metadata for known issues and sanitizes string
|
||||
// characters.
|
||||
func (md *Metadata) Validate() error {
|
||||
if md == nil {
|
||||
return ValidationError("chart.metadata is required")
|
||||
}
|
||||
|
||||
md.Name = sanitizeString(md.Name)
|
||||
md.Description = sanitizeString(md.Description)
|
||||
md.Home = sanitizeString(md.Home)
|
||||
md.Icon = sanitizeString(md.Icon)
|
||||
md.Condition = sanitizeString(md.Condition)
|
||||
md.Tags = sanitizeString(md.Tags)
|
||||
md.AppVersion = sanitizeString(md.AppVersion)
|
||||
md.KubeVersion = sanitizeString(md.KubeVersion)
|
||||
for i := range md.Sources {
|
||||
md.Sources[i] = sanitizeString(md.Sources[i])
|
||||
}
|
||||
for i := range md.Keywords {
|
||||
md.Keywords[i] = sanitizeString(md.Keywords[i])
|
||||
}
|
||||
|
||||
if md.APIVersion == "" {
|
||||
return ValidationError("chart.metadata.apiVersion is required")
|
||||
}
|
||||
if md.Name == "" {
|
||||
return ValidationError("chart.metadata.name is required")
|
||||
}
|
||||
|
||||
if md.Name != filepath.Base(md.Name) {
|
||||
return ValidationErrorf("chart.metadata.name %q is invalid", md.Name)
|
||||
}
|
||||
|
||||
if md.Version == "" {
|
||||
return ValidationError("chart.metadata.version is required")
|
||||
}
|
||||
if !isValidSemver(md.Version) {
|
||||
return ValidationErrorf("chart.metadata.version %q is invalid", md.Version)
|
||||
}
|
||||
if !isValidChartType(md.Type) {
|
||||
return ValidationError("chart.metadata.type must be application or library")
|
||||
}
|
||||
|
||||
for _, m := range md.Maintainers {
|
||||
if err := m.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Aliases need to be validated here to make sure that the alias name does
|
||||
// not contain any illegal characters.
|
||||
dependencies := map[string]*Dependency{}
|
||||
for _, dependency := range md.Dependencies {
|
||||
if err := dependency.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
key := dependency.Name
|
||||
if dependency.Alias != "" {
|
||||
key = dependency.Alias
|
||||
}
|
||||
if dependencies[key] != nil {
|
||||
return ValidationErrorf("more than one dependency with name or alias %q", key)
|
||||
}
|
||||
dependencies[key] = dependency
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isValidChartType(in string) bool {
|
||||
switch in {
|
||||
case "", "application", "library":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isValidSemver(v string) bool {
|
||||
_, err := semver.NewVersion(v)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// sanitizeString normalize spaces and removes non-printable characters.
|
||||
func sanitizeString(str string) string {
|
||||
return strings.Map(func(r rune) rune {
|
||||
if unicode.IsSpace(r) {
|
||||
return ' '
|
||||
}
|
||||
if unicode.IsPrint(r) {
|
||||
return r
|
||||
}
|
||||
return -1
|
||||
}, str)
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
Package ignore provides tools for writing ignore files (a la .gitignore).
|
||||
|
||||
This provides both an ignore parser and a file-aware processor.
|
||||
|
||||
The format of ignore files closely follows, but does not exactly match, the
|
||||
format for .gitignore files (https://git-scm.com/docs/gitignore).
|
||||
|
||||
The formatting rules are as follows:
|
||||
|
||||
- Parsing is line-by-line
|
||||
- Empty lines are ignored
|
||||
- Lines that begin with # (comments) will be ignored
|
||||
- Leading and trailing spaces are always ignored
|
||||
- Inline comments are NOT supported ('foo* # Any foo' does not contain a comment)
|
||||
- There is no support for multi-line patterns
|
||||
- Shell glob patterns are supported. See Go's "path/filepath".Match
|
||||
- If a pattern begins with a leading !, the match will be negated.
|
||||
- If a pattern begins with a leading /, only paths relatively rooted will match.
|
||||
- If the pattern ends with a trailing /, only directories will match
|
||||
- If a pattern contains no slashes, file basenames are tested (not paths)
|
||||
- The pattern sequence "**", while legal in a glob, will cause an error here
|
||||
(to indicate incompatibility with .gitignore).
|
||||
|
||||
Example:
|
||||
|
||||
# Match any file named foo.txt
|
||||
foo.txt
|
||||
|
||||
# Match any text file
|
||||
*.txt
|
||||
|
||||
# Match only directories named mydir
|
||||
mydir/
|
||||
|
||||
# Match only text files in the top-level directory
|
||||
/*.txt
|
||||
|
||||
# Match only the file foo.txt in the top-level directory
|
||||
/foo.txt
|
||||
|
||||
# Match any file named ab.txt, ac.txt, or ad.txt
|
||||
a[b-d].txt
|
||||
|
||||
Notable differences from .gitignore:
|
||||
- The '**' syntax is not supported.
|
||||
- The globbing library is Go's 'filepath.Match', not fnmatch(3)
|
||||
- Trailing spaces are always ignored (there is no supported escape sequence)
|
||||
- The evaluation of escape sequences has not been tested for compatibility
|
||||
- There is no support for '\!' as a special leading sequence.
|
||||
*/
|
||||
package ignore // import "helm.sh/helm/v4/pkg/ignore"
|
||||
+231
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package ignore
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// HelmIgnore default name of an ignorefile.
|
||||
const HelmIgnore = ".helmignore"
|
||||
|
||||
// Rules is a collection of path matching rules.
|
||||
//
|
||||
// Parse() and ParseFile() will construct and populate new Rules.
|
||||
// Empty() will create an immutable empty ruleset.
|
||||
type Rules struct {
|
||||
patterns []*pattern
|
||||
}
|
||||
|
||||
// Empty builds an empty ruleset.
|
||||
func Empty() *Rules {
|
||||
return &Rules{patterns: []*pattern{}}
|
||||
}
|
||||
|
||||
// AddDefaults adds default ignore patterns.
|
||||
//
|
||||
// Ignore all dotfiles in "templates/"
|
||||
func (r *Rules) AddDefaults() {
|
||||
r.parseRule(`templates/.?*`)
|
||||
}
|
||||
|
||||
// ParseFile parses a helmignore file and returns the *Rules.
|
||||
func ParseFile(file string) (*Rules, error) {
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
return Parse(f)
|
||||
}
|
||||
|
||||
// Parse parses a rules file
|
||||
func Parse(file io.Reader) (*Rules, error) {
|
||||
r := &Rules{patterns: []*pattern{}}
|
||||
|
||||
s := bufio.NewScanner(file)
|
||||
currentLine := 0
|
||||
utf8bom := []byte{0xEF, 0xBB, 0xBF}
|
||||
for s.Scan() {
|
||||
scannedBytes := s.Bytes()
|
||||
// We trim UTF8 BOM
|
||||
if currentLine == 0 {
|
||||
scannedBytes = bytes.TrimPrefix(scannedBytes, utf8bom)
|
||||
}
|
||||
line := string(scannedBytes)
|
||||
currentLine++
|
||||
|
||||
if err := r.parseRule(line); err != nil {
|
||||
return r, err
|
||||
}
|
||||
}
|
||||
return r, s.Err()
|
||||
}
|
||||
|
||||
// Ignore evaluates the file at the given path, and returns true if it should be ignored.
|
||||
//
|
||||
// Ignore evaluates path against the rules in order. Evaluation stops when a match
|
||||
// is found. Matching a negative rule will stop evaluation.
|
||||
func (r *Rules) Ignore(path string, fi os.FileInfo) bool {
|
||||
// Don't match on empty dirs.
|
||||
if path == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Disallow ignoring the current working directory.
|
||||
// See issue:
|
||||
// 1776 (New York City) Hamilton: "Pardon me, are you Aaron Burr, sir?"
|
||||
if path == "." || path == "./" {
|
||||
return false
|
||||
}
|
||||
for _, p := range r.patterns {
|
||||
if p.match == nil {
|
||||
slog.Info("this will be ignored no matcher supplied", "patterns", p.raw)
|
||||
return false
|
||||
}
|
||||
|
||||
// For negative rules, we need to capture and return non-matches,
|
||||
// and continue for matches.
|
||||
if p.negate {
|
||||
if p.mustDir && !fi.IsDir() {
|
||||
return true
|
||||
}
|
||||
if !p.match(path, fi) {
|
||||
return true
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// If the rule is looking for directories, and this is not a directory,
|
||||
// skip it.
|
||||
if p.mustDir && !fi.IsDir() {
|
||||
continue
|
||||
}
|
||||
if p.match(path, fi) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// parseRule parses a rule string and creates a pattern, which is then stored in the Rules object.
|
||||
func (r *Rules) parseRule(rule string) error {
|
||||
rule = strings.TrimSpace(rule)
|
||||
|
||||
// Ignore blank lines
|
||||
if rule == "" {
|
||||
return nil
|
||||
}
|
||||
// Comment
|
||||
if strings.HasPrefix(rule, "#") {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fail any rules that contain **
|
||||
if strings.Contains(rule, "**") {
|
||||
return errors.New("double-star (**) syntax is not supported")
|
||||
}
|
||||
|
||||
// Fail any patterns that can't compile. A non-empty string must be
|
||||
// given to Match() to avoid optimization that skips rule evaluation.
|
||||
if _, err := filepath.Match(rule, "abc"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p := &pattern{raw: rule}
|
||||
|
||||
// Negation is handled at a higher level, so strip the leading ! from the
|
||||
// string.
|
||||
if strings.HasPrefix(rule, "!") {
|
||||
p.negate = true
|
||||
rule = rule[1:]
|
||||
}
|
||||
|
||||
// Directory verification is handled by a higher level, so the trailing /
|
||||
// is removed from the rule. That way, a directory named "foo" matches,
|
||||
// even if the supplied string does not contain a literal slash character.
|
||||
if strings.HasSuffix(rule, "/") {
|
||||
p.mustDir = true
|
||||
rule = strings.TrimSuffix(rule, "/")
|
||||
}
|
||||
|
||||
if after, ok := strings.CutPrefix(rule, "/"); ok {
|
||||
// Require path matches the root path.
|
||||
p.match = func(n string, _ os.FileInfo) bool {
|
||||
rule = after
|
||||
ok, err := filepath.Match(rule, n)
|
||||
if err != nil {
|
||||
slog.Error("failed to compile", slog.String("rule", rule), slog.Any("error", err))
|
||||
return false
|
||||
}
|
||||
return ok
|
||||
}
|
||||
} else if strings.Contains(rule, "/") {
|
||||
// require structural match.
|
||||
p.match = func(n string, _ os.FileInfo) bool {
|
||||
ok, err := filepath.Match(rule, n)
|
||||
if err != nil {
|
||||
slog.Error(
|
||||
"failed to compile",
|
||||
slog.String("rule", rule),
|
||||
slog.Any("error", err),
|
||||
)
|
||||
return false
|
||||
}
|
||||
return ok
|
||||
}
|
||||
} else {
|
||||
p.match = func(n string, _ os.FileInfo) bool {
|
||||
// When there is no slash in the pattern, we evaluate ONLY the
|
||||
// filename.
|
||||
n = filepath.Base(n)
|
||||
ok, err := filepath.Match(rule, n)
|
||||
if err != nil {
|
||||
slog.Error("failed to compile", slog.String("rule", rule), slog.Any("error", err))
|
||||
return false
|
||||
}
|
||||
return ok
|
||||
}
|
||||
}
|
||||
|
||||
r.patterns = append(r.patterns, p)
|
||||
return nil
|
||||
}
|
||||
|
||||
// matcher is a function capable of computing a match.
|
||||
//
|
||||
// It returns true if the rule matches.
|
||||
type matcher func(name string, fi os.FileInfo) bool
|
||||
|
||||
// pattern describes a pattern to be matched in a rule set.
|
||||
type pattern struct {
|
||||
// raw is the unparsed string, with nothing stripped.
|
||||
raw string
|
||||
// match is the matcher function.
|
||||
match matcher
|
||||
// negate indicates that the rule's outcome should be negated.
|
||||
negate bool
|
||||
// mustDir indicates that the matched file must be a directory.
|
||||
mustDir bool
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
Package provenance provides tools for establishing the authenticity of packages.
|
||||
|
||||
In Helm, provenance is established via several factors. The primary factor is the
|
||||
cryptographic signature of a package. Package authors may sign packages, which in turn
|
||||
provide the necessary metadata to ensure the integrity of the package file, the
|
||||
metadata, and the referenced Docker images.
|
||||
|
||||
A provenance file is clear-signed. This provides cryptographic verification that
|
||||
a particular block of information (metadata, archive file, images) have not
|
||||
been tampered with or altered. To learn more, read the GnuPG documentation on
|
||||
clear signatures:
|
||||
https://www.gnupg.org/gph/en/manual/x135.html
|
||||
|
||||
The cryptography used by Helm should be compatible with OpenGPG. For example,
|
||||
you should be able to verify a signature by importing the desired public key
|
||||
and using `gpg --verify`, `keybase pgp verify`, or similar:
|
||||
|
||||
$ gpg --verify some.sig
|
||||
gpg: Signature made Mon Jul 25 17:23:44 2016 MDT using RSA key ID 1FC18762
|
||||
gpg: Good signature from "Helm Testing (This key should only be used for testing. DO NOT TRUST.) <helm-testing@helm.sh>" [ultimate]
|
||||
*/
|
||||
package provenance // import "helm.sh/helm/v4/pkg/provenance"
|
||||
+394
@@ -0,0 +1,394 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package provenance
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/go-crypto/openpgp" //nolint
|
||||
"github.com/ProtonMail/go-crypto/openpgp/clearsign" //nolint
|
||||
"github.com/ProtonMail/go-crypto/openpgp/packet" //nolint
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
var defaultPGPConfig = packet.Config{
|
||||
DefaultHash: crypto.SHA512,
|
||||
}
|
||||
|
||||
// SumCollection represents a collection of file and image checksums.
|
||||
//
|
||||
// Files are of the form:
|
||||
//
|
||||
// FILENAME: "sha256:SUM"
|
||||
//
|
||||
// Images are of the form:
|
||||
//
|
||||
// "IMAGE:TAG": "sha256:SUM"
|
||||
//
|
||||
// Docker optionally supports sha512, and if this is the case, the hash marker
|
||||
// will be 'sha512' instead of 'sha256'.
|
||||
type SumCollection struct {
|
||||
Files map[string]string `json:"files"`
|
||||
Images map[string]string `json:"images,omitempty"`
|
||||
}
|
||||
|
||||
// Verification contains information about a verification operation.
|
||||
type Verification struct {
|
||||
// SignedBy contains the entity that signed a package.
|
||||
SignedBy *openpgp.Entity
|
||||
// FileHash is the hash, prepended with the scheme, for the file that was verified.
|
||||
FileHash string
|
||||
// FileName is the name of the file that FileHash verifies.
|
||||
FileName string
|
||||
}
|
||||
|
||||
// Signatory signs things.
|
||||
//
|
||||
// Signatories can be constructed from a PGP private key file using NewFromFiles,
|
||||
// or they can be constructed manually by setting the Entity to a valid
|
||||
// PGP entity.
|
||||
//
|
||||
// The same Signatory can be used to sign or validate multiple packages.
|
||||
type Signatory struct {
|
||||
// The signatory for this instance of Helm. This is used for signing.
|
||||
Entity *openpgp.Entity
|
||||
// The keyring for this instance of Helm. This is used for verification.
|
||||
KeyRing openpgp.EntityList
|
||||
}
|
||||
|
||||
// NewFromFiles constructs a new Signatory from the PGP key in the given filename.
|
||||
//
|
||||
// This will emit an error if it cannot find a valid GPG keyfile (entity) at the
|
||||
// given location.
|
||||
//
|
||||
// Note that the keyfile may have just a public key, just a private key, or
|
||||
// both. The Signatory methods may have different requirements of the keys. For
|
||||
// example, ClearSign must have a valid `openpgp.Entity.PrivateKey` before it
|
||||
// can sign something.
|
||||
func NewFromFiles(keyfile, keyringfile string) (*Signatory, error) {
|
||||
e, err := loadKey(keyfile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ring, err := loadKeyRing(keyringfile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Signatory{
|
||||
Entity: e,
|
||||
KeyRing: ring,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewFromKeyring reads a keyring file and creates a Signatory.
|
||||
//
|
||||
// If id is not the empty string, this will also try to find an Entity in the
|
||||
// keyring whose name matches, and set that as the signing entity. It will return
|
||||
// an error if the id is not empty and also not found.
|
||||
func NewFromKeyring(keyringfile, id string) (*Signatory, error) {
|
||||
ring, err := loadKeyRing(keyringfile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s := &Signatory{KeyRing: ring}
|
||||
|
||||
// If the ID is empty, we can return now.
|
||||
if id == "" {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// We're gonna go all GnuPG on this and look for a string that _contains_. If
|
||||
// two or more keys contain the string and none are a direct match, we error
|
||||
// out.
|
||||
var candidate *openpgp.Entity
|
||||
vague := false
|
||||
for _, e := range ring {
|
||||
for n := range e.Identities {
|
||||
if n == id {
|
||||
s.Entity = e
|
||||
return s, nil
|
||||
}
|
||||
if strings.Contains(n, id) {
|
||||
if candidate != nil {
|
||||
vague = true
|
||||
}
|
||||
candidate = e
|
||||
}
|
||||
}
|
||||
}
|
||||
if vague {
|
||||
return s, fmt.Errorf("more than one key contain the id %q", id)
|
||||
}
|
||||
|
||||
s.Entity = candidate
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// PassphraseFetcher returns a passphrase for decrypting keys.
|
||||
//
|
||||
// This is used as a callback to read a passphrase from some other location. The
|
||||
// given name is the Name field on the key, typically of the form:
|
||||
//
|
||||
// USER_NAME (COMMENT) <EMAIL>
|
||||
type PassphraseFetcher func(name string) ([]byte, error)
|
||||
|
||||
// DecryptKey decrypts a private key in the Signatory.
|
||||
//
|
||||
// If the key is not encrypted, this will return without error.
|
||||
//
|
||||
// If the key does not exist, this will return an error.
|
||||
//
|
||||
// If the key exists, but cannot be unlocked with the passphrase returned by
|
||||
// the PassphraseFetcher, this will return an error.
|
||||
//
|
||||
// If the key is successfully unlocked, it will return nil.
|
||||
func (s *Signatory) DecryptKey(fn PassphraseFetcher) error {
|
||||
if s.Entity == nil {
|
||||
return errors.New("private key not found")
|
||||
} else if s.Entity.PrivateKey == nil {
|
||||
return errors.New("provided key is not a private key. Try providing a keyring with secret keys")
|
||||
}
|
||||
|
||||
// Nothing else to do if key is not encrypted.
|
||||
if !s.Entity.PrivateKey.Encrypted {
|
||||
return nil
|
||||
}
|
||||
|
||||
fname := "Unknown"
|
||||
for i := range s.Entity.Identities {
|
||||
if i != "" {
|
||||
fname = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
p, err := fn(fname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.Entity.PrivateKey.Decrypt(p)
|
||||
}
|
||||
|
||||
// ClearSign signs package data with the given key and pre-marshalled metadata.
|
||||
//
|
||||
// This is the core signing method that works with data in memory.
|
||||
// The Signatory must have a valid Entity.PrivateKey for this to work.
|
||||
func (s *Signatory) ClearSign(archiveData []byte, filename string, metadataBytes []byte) (string, error) {
|
||||
if s.Entity == nil {
|
||||
return "", errors.New("private key not found")
|
||||
} else if s.Entity.PrivateKey == nil {
|
||||
return "", errors.New("provided key is not a private key. Try providing a keyring with secret keys")
|
||||
}
|
||||
|
||||
out := bytes.NewBuffer(nil)
|
||||
|
||||
b, err := messageBlock(archiveData, filename, metadataBytes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Sign the buffer
|
||||
w, err := clearsign.Encode(out, s.Entity.PrivateKey, &defaultPGPConfig)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
_, err = io.Copy(w, b)
|
||||
|
||||
if err != nil {
|
||||
// NB: We intentionally don't call `w.Close()` here! `w.Close()` is the method which
|
||||
// actually does the PGP signing, and therefore is the part which uses the private key.
|
||||
// In other words, if we call Close here, there's a risk that there's an attempt to use the
|
||||
// private key to sign garbage data (since we know that io.Copy failed, `w` won't contain
|
||||
// anything useful).
|
||||
return "", fmt.Errorf("failed to write to clearsign encoder: %w", err)
|
||||
}
|
||||
|
||||
err = w.Close()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to either sign or armor message block: %w", err)
|
||||
}
|
||||
|
||||
return out.String(), nil
|
||||
}
|
||||
|
||||
// Verify checks a signature and verifies that it is legit for package data.
|
||||
// This is the core verification method that works with data in memory.
|
||||
func (s *Signatory) Verify(archiveData, provData []byte, filename string) (*Verification, error) {
|
||||
ver := &Verification{}
|
||||
|
||||
// First verify the signature
|
||||
block, _ := clearsign.Decode(provData)
|
||||
if block == nil {
|
||||
return ver, errors.New("signature block not found")
|
||||
}
|
||||
|
||||
by, err := s.verifySignature(block)
|
||||
if err != nil {
|
||||
return ver, err
|
||||
}
|
||||
ver.SignedBy = by
|
||||
|
||||
// Second, verify the hash of the data.
|
||||
sum, err := Digest(bytes.NewBuffer(archiveData))
|
||||
if err != nil {
|
||||
return ver, err
|
||||
}
|
||||
sums, err := parseMessageBlock(block.Plaintext)
|
||||
if err != nil {
|
||||
return ver, err
|
||||
}
|
||||
|
||||
sum = "sha256:" + sum
|
||||
if sha, ok := sums.Files[filename]; !ok {
|
||||
return ver, fmt.Errorf("provenance does not contain a SHA for a file named %q", filename)
|
||||
} else if sha != sum {
|
||||
return ver, fmt.Errorf("sha256 sum does not match for %s: %q != %q", filename, sha, sum)
|
||||
}
|
||||
ver.FileHash = sum
|
||||
ver.FileName = filename
|
||||
|
||||
// TODO: when image signing is added, verify that here.
|
||||
|
||||
return ver, nil
|
||||
}
|
||||
|
||||
// verifySignature verifies that the given block is validly signed, and returns the signer.
|
||||
func (s *Signatory) verifySignature(block *clearsign.Block) (*openpgp.Entity, error) {
|
||||
return openpgp.CheckDetachedSignature(
|
||||
s.KeyRing,
|
||||
bytes.NewReader(block.Bytes),
|
||||
block.ArmoredSignature.Body,
|
||||
&defaultPGPConfig,
|
||||
)
|
||||
}
|
||||
|
||||
// messageBlock creates a message block from archive data and pre-marshalled metadata
|
||||
func messageBlock(archiveData []byte, filename string, metadataBytes []byte) (*bytes.Buffer, error) {
|
||||
// Checksum the archive data
|
||||
chash, err := Digest(bytes.NewBuffer(archiveData))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sums := &SumCollection{
|
||||
Files: map[string]string{
|
||||
filename: "sha256:" + chash,
|
||||
},
|
||||
}
|
||||
|
||||
// Buffer the metadata + checksums YAML file
|
||||
// FIXME: YAML uses ---\n as a file start indicator, but this is not legal in a PGP
|
||||
// clearsign block. So we use ...\n, which is the YAML document end marker.
|
||||
// http://yaml.org/spec/1.2/spec.html#id2800168
|
||||
b := bytes.NewBuffer(metadataBytes)
|
||||
b.WriteString("\n...\n")
|
||||
|
||||
data, err := yaml.Marshal(sums)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b.Write(data)
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// parseMessageBlock parses a message block and returns only checksums (metadata ignored like upstream)
|
||||
func parseMessageBlock(data []byte) (*SumCollection, error) {
|
||||
sc := &SumCollection{}
|
||||
|
||||
// We ignore metadata, just like upstream - only need checksums for verification
|
||||
if err := ParseMessageBlock(data, nil, sc); err != nil {
|
||||
return sc, err
|
||||
}
|
||||
return sc, nil
|
||||
}
|
||||
|
||||
// ParseMessageBlock parses a message block containing metadata and checksums.
|
||||
//
|
||||
// This is the generic version that can work with any metadata type.
|
||||
// The metadata parameter should be a pointer to a struct that can be unmarshaled from YAML.
|
||||
func ParseMessageBlock(data []byte, metadata interface{}, sums *SumCollection) error {
|
||||
parts := bytes.Split(data, []byte("\n...\n"))
|
||||
if len(parts) < 2 {
|
||||
return errors.New("message block must have at least two parts")
|
||||
}
|
||||
|
||||
if metadata != nil {
|
||||
if err := yaml.Unmarshal(parts[0], metadata); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return yaml.Unmarshal(parts[1], sums)
|
||||
}
|
||||
|
||||
// loadKey loads a GPG key found at a particular path.
|
||||
func loadKey(keypath string) (*openpgp.Entity, error) {
|
||||
f, err := os.Open(keypath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
pr := packet.NewReader(f)
|
||||
return openpgp.ReadEntity(pr)
|
||||
}
|
||||
|
||||
func loadKeyRing(ringpath string) (openpgp.EntityList, error) {
|
||||
f, err := os.Open(ringpath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
return openpgp.ReadKeyRing(f)
|
||||
}
|
||||
|
||||
// DigestFile calculates a SHA256 hash (like Docker) for a given file.
|
||||
//
|
||||
// It takes the path to the archive file, and returns a string representation of
|
||||
// the SHA256 sum.
|
||||
//
|
||||
// This function can be used to generate a sum of any package archive file.
|
||||
func DigestFile(filename string) (string, error) {
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
return Digest(f)
|
||||
}
|
||||
|
||||
// Digest hashes a reader and returns a SHA256 digest.
|
||||
//
|
||||
// Helm uses SHA256 as its default hash for all non-cryptographic applications.
|
||||
func Digest(in io.Reader) (string, error) {
|
||||
hash := crypto.SHA256.New()
|
||||
if _, err := io.Copy(hash, in); err != nil {
|
||||
return "", nil
|
||||
}
|
||||
return hex.EncodeToString(hash.Sum(nil)), nil
|
||||
}
|
||||
Reference in New Issue
Block a user