mirror of https://github.com/helm/helm
Signed-off-by: HARPHUNA <109001160+HARPHUNA@users.noreply.github.com>pull/11491/head
parent
7ff00a3099
commit
0f3826af7c
@ -1,51 +0,0 @@
|
|||||||
/*
|
|
||||||
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 fileutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"helm.sh/helm/v3/internal/third_party/dep/fs"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AtomicWriteFile atomically (as atomic as os.Rename allows) writes a file to a
|
|
||||||
// disk.
|
|
||||||
func AtomicWriteFile(filename string, reader io.Reader, mode os.FileMode) error {
|
|
||||||
tempFile, err := ioutil.TempFile(filepath.Split(filename))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
tempName := tempFile.Name()
|
|
||||||
|
|
||||||
if _, err := io.Copy(tempFile, reader); err != nil {
|
|
||||||
tempFile.Close() // return value is ignored as we are already on error path
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tempFile.Close(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.Chmod(tempName, mode); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return fs.RenameWithFallback(tempName, filename)
|
|
||||||
}
|
|
@ -1,58 +0,0 @@
|
|||||||
/*
|
|
||||||
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 fileutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestAtomicWriteFile(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
|
|
||||||
testpath := filepath.Join(dir, "test")
|
|
||||||
stringContent := "Test content"
|
|
||||||
reader := bytes.NewReader([]byte(stringContent))
|
|
||||||
mode := os.FileMode(0644)
|
|
||||||
|
|
||||||
err := AtomicWriteFile(testpath, reader, mode)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("AtomicWriteFile error: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
got, err := ioutil.ReadFile(testpath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if stringContent != string(got) {
|
|
||||||
t.Fatalf("expected: %s, got: %s", stringContent, string(got))
|
|
||||||
}
|
|
||||||
|
|
||||||
gotinfo, err := os.Stat(testpath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if mode != gotinfo.Mode() {
|
|
||||||
t.Fatalf("expected %s: to be the same mode as %s",
|
|
||||||
mode, gotinfo.Mode())
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,67 +0,0 @@
|
|||||||
/*
|
|
||||||
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 the 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/v3/internal/ignore"
|
|
@ -1,228 +0,0 @@
|
|||||||
/*
|
|
||||||
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"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
log.Printf("ignore: no matcher supplied for %q", 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 strings.HasPrefix(rule, "/") {
|
|
||||||
// Require path matches the root path.
|
|
||||||
p.match = func(n string, fi os.FileInfo) bool {
|
|
||||||
rule = strings.TrimPrefix(rule, "/")
|
|
||||||
ok, err := filepath.Match(rule, n)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed to compile %q: %s", rule, err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
} else if strings.Contains(rule, "/") {
|
|
||||||
// require structural match.
|
|
||||||
p.match = func(n string, fi os.FileInfo) bool {
|
|
||||||
ok, err := filepath.Match(rule, n)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed to compile %q: %s", rule, err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
p.match = func(n string, fi 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 {
|
|
||||||
log.Printf("Failed to compile %q: %s", rule, 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
|
|
||||||
}
|
|
@ -1,155 +0,0 @@
|
|||||||
/*
|
|
||||||
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 (
|
|
||||||
"bytes"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
var testdata = "./testdata"
|
|
||||||
|
|
||||||
func TestParse(t *testing.T) {
|
|
||||||
rules := `#ignore
|
|
||||||
|
|
||||||
#ignore
|
|
||||||
foo
|
|
||||||
bar/*
|
|
||||||
baz/bar/foo.txt
|
|
||||||
|
|
||||||
one/more
|
|
||||||
`
|
|
||||||
r, err := parseString(rules)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error parsing rules: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(r.patterns) != 4 {
|
|
||||||
t.Errorf("Expected 4 rules, got %d", len(r.patterns))
|
|
||||||
}
|
|
||||||
|
|
||||||
expects := []string{"foo", "bar/*", "baz/bar/foo.txt", "one/more"}
|
|
||||||
for i, p := range r.patterns {
|
|
||||||
if p.raw != expects[i] {
|
|
||||||
t.Errorf("Expected %q, got %q", expects[i], p.raw)
|
|
||||||
}
|
|
||||||
if p.match == nil {
|
|
||||||
t.Errorf("Expected %s to have a matcher function.", p.raw)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseFail(t *testing.T) {
|
|
||||||
shouldFail := []string{"foo/**/bar", "[z-"}
|
|
||||||
for _, fail := range shouldFail {
|
|
||||||
_, err := parseString(fail)
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("Rule %q should have failed", fail)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseFile(t *testing.T) {
|
|
||||||
f := filepath.Join(testdata, HelmIgnore)
|
|
||||||
if _, err := os.Stat(f); err != nil {
|
|
||||||
t.Fatalf("Fixture %s missing: %s", f, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r, err := ParseFile(f)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to parse rules file: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(r.patterns) != 3 {
|
|
||||||
t.Errorf("Expected 3 patterns, got %d", len(r.patterns))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIgnore(t *testing.T) {
|
|
||||||
// Test table: Given pattern and name, Ignore should return expect.
|
|
||||||
tests := []struct {
|
|
||||||
pattern string
|
|
||||||
name string
|
|
||||||
expect bool
|
|
||||||
}{
|
|
||||||
// Glob tests
|
|
||||||
{`helm.txt`, "helm.txt", true},
|
|
||||||
{`helm.*`, "helm.txt", true},
|
|
||||||
{`helm.*`, "rudder.txt", false},
|
|
||||||
{`*.txt`, "tiller.txt", true},
|
|
||||||
{`*.txt`, "cargo/a.txt", true},
|
|
||||||
{`cargo/*.txt`, "cargo/a.txt", true},
|
|
||||||
{`cargo/*.*`, "cargo/a.txt", true},
|
|
||||||
{`cargo/*.txt`, "mast/a.txt", false},
|
|
||||||
{`ru[c-e]?er.txt`, "rudder.txt", true},
|
|
||||||
{`templates/.?*`, "templates/.dotfile", true},
|
|
||||||
// "." should never get ignored. https://github.com/helm/helm/issues/1776
|
|
||||||
{`.*`, ".", false},
|
|
||||||
{`.*`, "./", false},
|
|
||||||
{`.*`, ".joonix", true},
|
|
||||||
{`.*`, "helm.txt", false},
|
|
||||||
{`.*`, "", false},
|
|
||||||
|
|
||||||
// Directory tests
|
|
||||||
{`cargo/`, "cargo", true},
|
|
||||||
{`cargo/`, "cargo/", true},
|
|
||||||
{`cargo/`, "mast/", false},
|
|
||||||
{`helm.txt/`, "helm.txt", false},
|
|
||||||
|
|
||||||
// Negation tests
|
|
||||||
{`!helm.txt`, "helm.txt", false},
|
|
||||||
{`!helm.txt`, "tiller.txt", true},
|
|
||||||
{`!*.txt`, "cargo", true},
|
|
||||||
{`!cargo/`, "mast/", true},
|
|
||||||
|
|
||||||
// Absolute path tests
|
|
||||||
{`/a.txt`, "a.txt", true},
|
|
||||||
{`/a.txt`, "cargo/a.txt", false},
|
|
||||||
{`/cargo/a.txt`, "cargo/a.txt", true},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
r, err := parseString(test.pattern)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to parse: %s", err)
|
|
||||||
}
|
|
||||||
fi, err := os.Stat(filepath.Join(testdata, test.name))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Fixture missing: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.Ignore(test.name, fi) != test.expect {
|
|
||||||
t.Errorf("Expected %q to be %v for pattern %q", test.name, test.expect, test.pattern)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAddDefaults(t *testing.T) {
|
|
||||||
r := Rules{}
|
|
||||||
r.AddDefaults()
|
|
||||||
|
|
||||||
if len(r.patterns) != 1 {
|
|
||||||
t.Errorf("Expected 1 default patterns, got %d", len(r.patterns))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseString(str string) (*Rules, error) {
|
|
||||||
b := bytes.NewBuffer([]byte(str))
|
|
||||||
return Parse(b)
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
mast/a.txt
|
|
||||||
.DS_Store
|
|
||||||
.git
|
|
@ -1,68 +0,0 @@
|
|||||||
/*
|
|
||||||
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 monocular
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"net/url"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ErrHostnameNotProvided indicates the url is missing a hostname
|
|
||||||
var ErrHostnameNotProvided = errors.New("no hostname provided")
|
|
||||||
|
|
||||||
// Client represents a client capable of communicating with the Monocular API.
|
|
||||||
type Client struct {
|
|
||||||
|
|
||||||
// The base URL for requests
|
|
||||||
BaseURL string
|
|
||||||
|
|
||||||
// The internal logger to use
|
|
||||||
Log func(string, ...interface{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a new client
|
|
||||||
func New(u string) (*Client, error) {
|
|
||||||
|
|
||||||
// Validate we have a URL
|
|
||||||
if err := validate(u); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Client{
|
|
||||||
BaseURL: u,
|
|
||||||
Log: nopLogger,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var nopLogger = func(_ string, _ ...interface{}) {}
|
|
||||||
|
|
||||||
// Validate if the base URL for monocular is valid.
|
|
||||||
func validate(u string) error {
|
|
||||||
|
|
||||||
// Check if it is parsable
|
|
||||||
p, err := url.Parse(u)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that a host is attached
|
|
||||||
if p.Hostname() == "" {
|
|
||||||
return ErrHostnameNotProvided
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
/*
|
|
||||||
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 monocular
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNew(t *testing.T) {
|
|
||||||
c, err := New("https://hub.helm.sh")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("error creating client: %s", err)
|
|
||||||
}
|
|
||||||
if c.BaseURL != "https://hub.helm.sh" {
|
|
||||||
t.Errorf("incorrect BaseURL. Expected \"https://hub.helm.sh\" but got %q", c.BaseURL)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
/*
|
|
||||||
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 monocular contains the logic for interacting with a Monocular
|
|
||||||
// compatible search API endpoint. For example, as implemented by the Artifact
|
|
||||||
// Hub.
|
|
||||||
//
|
|
||||||
// This is a library for interacting with a monocular compatible search API
|
|
||||||
package monocular
|
|
@ -1,145 +0,0 @@
|
|||||||
/*
|
|
||||||
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 monocular
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"path"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"helm.sh/helm/v3/internal/version"
|
|
||||||
"helm.sh/helm/v3/pkg/chart"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SearchPath is the url path to the search API in monocular.
|
|
||||||
const SearchPath = "api/chartsvc/v1/charts/search"
|
|
||||||
|
|
||||||
// The structs below represent the structure of the response from the monocular
|
|
||||||
// search API. The structs were not imported from monocular because monocular
|
|
||||||
// imports from Helm v2 (avoiding circular version dependency) and the mappings
|
|
||||||
// are slightly different (monocular search results do not directly reflect
|
|
||||||
// the struct definitions).
|
|
||||||
|
|
||||||
// SearchResult represents an individual chart result
|
|
||||||
type SearchResult struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
ArtifactHub ArtifactHub `json:"artifactHub"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Attributes Chart `json:"attributes"`
|
|
||||||
Links Links `json:"links"`
|
|
||||||
Relationships Relationships `json:"relationships"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ArtifactHub represents data specific to Artifact Hub instances
|
|
||||||
type ArtifactHub struct {
|
|
||||||
PackageURL string `json:"packageUrl"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chart is the attributes for the chart
|
|
||||||
type Chart struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Repo Repo `json:"repo"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Home string `json:"home"`
|
|
||||||
Keywords []string `json:"keywords"`
|
|
||||||
Maintainers []chart.Maintainer `json:"maintainers"`
|
|
||||||
Sources []string `json:"sources"`
|
|
||||||
Icon string `json:"icon"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Repo contains the name in monocular the url for the repository
|
|
||||||
type Repo struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Links provides a set of links relative to the chartsvc base
|
|
||||||
type Links struct {
|
|
||||||
Self string `json:"self"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Relationships provides information on the latest version of the chart
|
|
||||||
type Relationships struct {
|
|
||||||
LatestChartVersion LatestChartVersion `json:"latestChartVersion"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// LatestChartVersion provides the details on the latest version of the chart
|
|
||||||
type LatestChartVersion struct {
|
|
||||||
Data ChartVersion `json:"data"`
|
|
||||||
Links Links `json:"links"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChartVersion provides the specific data on the chart version
|
|
||||||
type ChartVersion struct {
|
|
||||||
Version string `json:"version"`
|
|
||||||
AppVersion string `json:"app_version"`
|
|
||||||
Created time.Time `json:"created"`
|
|
||||||
Digest string `json:"digest"`
|
|
||||||
Urls []string `json:"urls"`
|
|
||||||
Readme string `json:"readme"`
|
|
||||||
Values string `json:"values"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search performs a search against the monocular search API
|
|
||||||
func (c *Client) Search(term string) ([]SearchResult, error) {
|
|
||||||
|
|
||||||
// Create the URL to the search endpoint
|
|
||||||
// Note, this is currently an internal API for the Hub. This should be
|
|
||||||
// formatted without showing how monocular operates.
|
|
||||||
p, err := url.Parse(c.BaseURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the path to the monocular API endpoint for search
|
|
||||||
p.Path = path.Join(p.Path, SearchPath)
|
|
||||||
|
|
||||||
p.RawQuery = "q=" + url.QueryEscape(term)
|
|
||||||
|
|
||||||
// Create request
|
|
||||||
req, err := http.NewRequest("GET", p.String(), nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the user agent so that monocular can identify where the request
|
|
||||||
// is coming from
|
|
||||||
req.Header.Set("User-Agent", version.GetUserAgent())
|
|
||||||
|
|
||||||
res, err := http.DefaultClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
|
|
||||||
if res.StatusCode != 200 {
|
|
||||||
return nil, fmt.Errorf("failed to fetch %s : %s", p.String(), res.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
result := &searchResponse{}
|
|
||||||
|
|
||||||
json.NewDecoder(res.Body).Decode(result)
|
|
||||||
|
|
||||||
return result.Data, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type searchResponse struct {
|
|
||||||
Data []SearchResult `json:"data"`
|
|
||||||
}
|
|
@ -1,49 +0,0 @@
|
|||||||
/*
|
|
||||||
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 monocular
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// A search response for phpmyadmin containing 2 results
|
|
||||||
var searchResult = `{"data":[{"id":"stable/phpmyadmin","type":"chart","attributes":{"name":"phpmyadmin","repo":{"name":"stable","url":"https://charts.helm.sh/stable"},"description":"phpMyAdmin is an mysql administration frontend","home":"https://www.phpmyadmin.net/","keywords":["mariadb","mysql","phpmyadmin"],"maintainers":[{"name":"Bitnami","email":"containers@bitnami.com"}],"sources":["https://github.com/bitnami/bitnami-docker-phpmyadmin"],"icon":""},"links":{"self":"/v1/charts/stable/phpmyadmin"},"relationships":{"latestChartVersion":{"data":{"version":"3.0.0","app_version":"4.9.0-1","created":"2019-08-08T17:57:31.38Z","digest":"119c499251bffd4b06ff0cd5ac98c2ce32231f84899fb4825be6c2d90971c742","urls":["https://charts.helm.sh/stable/phpmyadmin-3.0.0.tgz"],"readme":"/v1/assets/stable/phpmyadmin/versions/3.0.0/README.md","values":"/v1/assets/stable/phpmyadmin/versions/3.0.0/values.yaml"},"links":{"self":"/v1/charts/stable/phpmyadmin/versions/3.0.0"}}}},{"id":"bitnami/phpmyadmin","type":"chart","attributes":{"name":"phpmyadmin","repo":{"name":"bitnami","url":"https://charts.bitnami.com"},"description":"phpMyAdmin is an mysql administration frontend","home":"https://www.phpmyadmin.net/","keywords":["mariadb","mysql","phpmyadmin"],"maintainers":[{"name":"Bitnami","email":"containers@bitnami.com"}],"sources":["https://github.com/bitnami/bitnami-docker-phpmyadmin"],"icon":""},"links":{"self":"/v1/charts/bitnami/phpmyadmin"},"relationships":{"latestChartVersion":{"data":{"version":"3.0.0","app_version":"4.9.0-1","created":"2019-08-08T18:34:13.341Z","digest":"66d77cf6d8c2b52c488d0a294cd4996bd5bad8dc41d3829c394498fb401c008a","urls":["https://charts.bitnami.com/bitnami/phpmyadmin-3.0.0.tgz"],"readme":"/v1/assets/bitnami/phpmyadmin/versions/3.0.0/README.md","values":"/v1/assets/bitnami/phpmyadmin/versions/3.0.0/values.yaml"},"links":{"self":"/v1/charts/bitnami/phpmyadmin/versions/3.0.0"}}}}]}`
|
|
||||||
|
|
||||||
func TestSearch(t *testing.T) {
|
|
||||||
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
fmt.Fprintln(w, searchResult)
|
|
||||||
}))
|
|
||||||
defer ts.Close()
|
|
||||||
|
|
||||||
c, err := New(ts.URL)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("unable to create monocular client: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
results, err := c.Search("phpmyadmin")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("unable to search monocular: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(results) != 2 {
|
|
||||||
t.Error("Did not receive the expected number of results")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,263 +0,0 @@
|
|||||||
/*
|
|
||||||
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 resolver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
|
|
||||||
"helm.sh/helm/v3/pkg/chart"
|
|
||||||
"helm.sh/helm/v3/pkg/chart/loader"
|
|
||||||
"helm.sh/helm/v3/pkg/helmpath"
|
|
||||||
"helm.sh/helm/v3/pkg/provenance"
|
|
||||||
"helm.sh/helm/v3/pkg/registry"
|
|
||||||
"helm.sh/helm/v3/pkg/repo"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Resolver resolves dependencies from semantic version ranges to a particular version.
|
|
||||||
type Resolver struct {
|
|
||||||
chartpath string
|
|
||||||
cachepath string
|
|
||||||
registryClient *registry.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a new resolver for a given chart, helm home and registry client.
|
|
||||||
func New(chartpath, cachepath string, registryClient *registry.Client) *Resolver {
|
|
||||||
return &Resolver{
|
|
||||||
chartpath: chartpath,
|
|
||||||
cachepath: cachepath,
|
|
||||||
registryClient: registryClient,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve resolves dependencies and returns a lock file with the resolution.
|
|
||||||
func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string) (*chart.Lock, error) {
|
|
||||||
|
|
||||||
// Now we clone the dependencies, locking as we go.
|
|
||||||
locked := make([]*chart.Dependency, len(reqs))
|
|
||||||
missing := []string{}
|
|
||||||
for i, d := range reqs {
|
|
||||||
constraint, err := semver.NewConstraint(d.Version)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(err, "dependency %q has an invalid version/constraint format", d.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
if d.Repository == "" {
|
|
||||||
// Local chart subfolder
|
|
||||||
if _, err := GetLocalPath(filepath.Join("charts", d.Name), r.chartpath); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
locked[i] = &chart.Dependency{
|
|
||||||
Name: d.Name,
|
|
||||||
Repository: "",
|
|
||||||
Version: d.Version,
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(d.Repository, "file://") {
|
|
||||||
|
|
||||||
chartpath, err := GetLocalPath(d.Repository, r.chartpath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
ch, err := loader.LoadDir(chartpath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
v, err := semver.NewVersion(ch.Metadata.Version)
|
|
||||||
if err != nil {
|
|
||||||
// Not a legit entry.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !constraint.Check(v) {
|
|
||||||
missing = append(missing, d.Name)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
locked[i] = &chart.Dependency{
|
|
||||||
Name: d.Name,
|
|
||||||
Repository: d.Repository,
|
|
||||||
Version: ch.Metadata.Version,
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
repoName := repoNames[d.Name]
|
|
||||||
// if the repository was not defined, but the dependency defines a repository url, bypass the cache
|
|
||||||
if repoName == "" && d.Repository != "" {
|
|
||||||
locked[i] = &chart.Dependency{
|
|
||||||
Name: d.Name,
|
|
||||||
Repository: d.Repository,
|
|
||||||
Version: d.Version,
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var vs repo.ChartVersions
|
|
||||||
var version string
|
|
||||||
var ok bool
|
|
||||||
found := true
|
|
||||||
if !registry.IsOCI(d.Repository) {
|
|
||||||
repoIndex, err := repo.LoadIndexFile(filepath.Join(r.cachepath, helmpath.CacheIndexFile(repoName)))
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(err, "no cached repository for %s found. (try 'helm repo update')", repoName)
|
|
||||||
}
|
|
||||||
|
|
||||||
vs, ok = repoIndex.Entries[d.Name]
|
|
||||||
if !ok {
|
|
||||||
return nil, errors.Errorf("%s chart not found in repo %s", d.Name, d.Repository)
|
|
||||||
}
|
|
||||||
found = false
|
|
||||||
} else {
|
|
||||||
version = d.Version
|
|
||||||
|
|
||||||
// Check to see if an explicit version has been provided
|
|
||||||
_, err := semver.NewVersion(version)
|
|
||||||
|
|
||||||
// Use an explicit version, otherwise search for tags
|
|
||||||
if err == nil {
|
|
||||||
vs = []*repo.ChartVersion{{
|
|
||||||
Metadata: &chart.Metadata{
|
|
||||||
Version: version,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// Retrieve list of tags for repository
|
|
||||||
ref := fmt.Sprintf("%s/%s", strings.TrimPrefix(d.Repository, fmt.Sprintf("%s://", registry.OCIScheme)), d.Name)
|
|
||||||
tags, err := r.registryClient.Tags(ref)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(err, "could not retrieve list of tags for repository %s", d.Repository)
|
|
||||||
}
|
|
||||||
|
|
||||||
vs = make(repo.ChartVersions, len(tags))
|
|
||||||
for ti, t := range tags {
|
|
||||||
// Mock chart version objects
|
|
||||||
version := &repo.ChartVersion{
|
|
||||||
Metadata: &chart.Metadata{
|
|
||||||
Version: t,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
vs[ti] = version
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
locked[i] = &chart.Dependency{
|
|
||||||
Name: d.Name,
|
|
||||||
Repository: d.Repository,
|
|
||||||
Version: version,
|
|
||||||
}
|
|
||||||
// The version are already sorted and hence the first one to satisfy the constraint is used
|
|
||||||
for _, ver := range vs {
|
|
||||||
v, err := semver.NewVersion(ver.Version)
|
|
||||||
// OCI does not need URLs
|
|
||||||
if err != nil || (!registry.IsOCI(d.Repository) && len(ver.URLs) == 0) {
|
|
||||||
// Not a legit entry.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if constraint.Check(v) {
|
|
||||||
found = true
|
|
||||||
locked[i].Version = v.Original()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
missing = append(missing, d.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(missing) > 0 {
|
|
||||||
return nil, errors.Errorf("can't get a valid version for repositories %s. Try changing the version constraint in Chart.yaml", strings.Join(missing, ", "))
|
|
||||||
}
|
|
||||||
|
|
||||||
digest, err := HashReq(reqs, locked)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &chart.Lock{
|
|
||||||
Generated: time.Now(),
|
|
||||||
Digest: digest,
|
|
||||||
Dependencies: locked,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// HashReq generates a hash of the dependencies.
|
|
||||||
//
|
|
||||||
// This should be used only to compare against another hash generated by this
|
|
||||||
// function.
|
|
||||||
func HashReq(req, lock []*chart.Dependency) (string, error) {
|
|
||||||
data, err := json.Marshal([2][]*chart.Dependency{req, lock})
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
s, err := provenance.Digest(bytes.NewBuffer(data))
|
|
||||||
return "sha256:" + s, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// HashV2Req generates a hash of requirements generated in Helm v2.
|
|
||||||
//
|
|
||||||
// This should be used only to compare against another hash generated by the
|
|
||||||
// Helm v2 hash function. It is to handle issue:
|
|
||||||
// https://github.com/helm/helm/issues/7233
|
|
||||||
func HashV2Req(req []*chart.Dependency) (string, error) {
|
|
||||||
dep := make(map[string][]*chart.Dependency)
|
|
||||||
dep["dependencies"] = req
|
|
||||||
data, err := json.Marshal(dep)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
s, err := provenance.Digest(bytes.NewBuffer(data))
|
|
||||||
return "sha256:" + s, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLocalPath generates absolute local path when use
|
|
||||||
// "file://" in repository of dependencies
|
|
||||||
func GetLocalPath(repo, chartpath string) (string, error) {
|
|
||||||
var depPath string
|
|
||||||
var err error
|
|
||||||
p := strings.TrimPrefix(repo, "file://")
|
|
||||||
|
|
||||||
// root path is absolute
|
|
||||||
if strings.HasPrefix(p, "/") {
|
|
||||||
if depPath, err = filepath.Abs(p); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
depPath = filepath.Join(chartpath, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = os.Stat(depPath); os.IsNotExist(err) {
|
|
||||||
return "", errors.Errorf("directory %s not found", depPath)
|
|
||||||
} else if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return depPath, nil
|
|
||||||
}
|
|
@ -1,310 +0,0 @@
|
|||||||
/*
|
|
||||||
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 resolver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"runtime"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"helm.sh/helm/v3/pkg/chart"
|
|
||||||
"helm.sh/helm/v3/pkg/registry"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestResolve(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
req []*chart.Dependency
|
|
||||||
expect *chart.Lock
|
|
||||||
err bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "repo from invalid version",
|
|
||||||
req: []*chart.Dependency{
|
|
||||||
{Name: "base", Repository: "file://base", Version: "1.1.0"},
|
|
||||||
},
|
|
||||||
expect: &chart.Lock{
|
|
||||||
Dependencies: []*chart.Dependency{
|
|
||||||
{Name: "base", Repository: "file://base", Version: "0.1.0"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
err: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "version failure",
|
|
||||||
req: []*chart.Dependency{
|
|
||||||
{Name: "oedipus-rex", Repository: "http://example.com", Version: ">a1"},
|
|
||||||
},
|
|
||||||
err: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "cache index failure",
|
|
||||||
req: []*chart.Dependency{
|
|
||||||
{Name: "oedipus-rex", Repository: "http://example.com", Version: "1.0.0"},
|
|
||||||
},
|
|
||||||
expect: &chart.Lock{
|
|
||||||
Dependencies: []*chart.Dependency{
|
|
||||||
{Name: "oedipus-rex", Repository: "http://example.com", Version: "1.0.0"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "chart not found failure",
|
|
||||||
req: []*chart.Dependency{
|
|
||||||
{Name: "redis", Repository: "http://example.com", Version: "1.0.0"},
|
|
||||||
},
|
|
||||||
err: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "constraint not satisfied failure",
|
|
||||||
req: []*chart.Dependency{
|
|
||||||
{Name: "alpine", Repository: "http://example.com", Version: ">=1.0.0"},
|
|
||||||
},
|
|
||||||
err: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "valid lock",
|
|
||||||
req: []*chart.Dependency{
|
|
||||||
{Name: "alpine", Repository: "http://example.com", Version: ">=0.1.0"},
|
|
||||||
},
|
|
||||||
expect: &chart.Lock{
|
|
||||||
Dependencies: []*chart.Dependency{
|
|
||||||
{Name: "alpine", Repository: "http://example.com", Version: "0.2.0"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "repo from valid local path",
|
|
||||||
req: []*chart.Dependency{
|
|
||||||
{Name: "base", Repository: "file://base", Version: "0.1.0"},
|
|
||||||
},
|
|
||||||
expect: &chart.Lock{
|
|
||||||
Dependencies: []*chart.Dependency{
|
|
||||||
{Name: "base", Repository: "file://base", Version: "0.1.0"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "repo from valid local path with range resolution",
|
|
||||||
req: []*chart.Dependency{
|
|
||||||
{Name: "base", Repository: "file://base", Version: "^0.1.0"},
|
|
||||||
},
|
|
||||||
expect: &chart.Lock{
|
|
||||||
Dependencies: []*chart.Dependency{
|
|
||||||
{Name: "base", Repository: "file://base", Version: "0.1.0"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "repo from invalid local path",
|
|
||||||
req: []*chart.Dependency{
|
|
||||||
{Name: "nonexistent", Repository: "file://testdata/nonexistent", Version: "0.1.0"},
|
|
||||||
},
|
|
||||||
err: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "repo from valid path under charts path",
|
|
||||||
req: []*chart.Dependency{
|
|
||||||
{Name: "localdependency", Repository: "", Version: "0.1.0"},
|
|
||||||
},
|
|
||||||
expect: &chart.Lock{
|
|
||||||
Dependencies: []*chart.Dependency{
|
|
||||||
{Name: "localdependency", Repository: "", Version: "0.1.0"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "repo from invalid path under charts path",
|
|
||||||
req: []*chart.Dependency{
|
|
||||||
{Name: "nonexistentdependency", Repository: "", Version: "0.1.0"},
|
|
||||||
},
|
|
||||||
expect: &chart.Lock{
|
|
||||||
Dependencies: []*chart.Dependency{
|
|
||||||
{Name: "nonexistentlocaldependency", Repository: "", Version: "0.1.0"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
err: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
repoNames := map[string]string{"alpine": "kubernetes-charts", "redis": "kubernetes-charts"}
|
|
||||||
registryClient, _ := registry.NewClient()
|
|
||||||
r := New("testdata/chartpath", "testdata/repository", registryClient)
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
l, err := r.Resolve(tt.req, repoNames)
|
|
||||||
if err != nil {
|
|
||||||
if tt.err {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if tt.err {
|
|
||||||
t.Fatalf("Expected error in test %q", tt.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
if h, err := HashReq(tt.req, tt.expect.Dependencies); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
} else if h != l.Digest {
|
|
||||||
t.Errorf("%q: hashes don't match.", tt.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check fields.
|
|
||||||
if len(l.Dependencies) != len(tt.req) {
|
|
||||||
t.Errorf("%s: wrong number of dependencies in lock", tt.name)
|
|
||||||
}
|
|
||||||
d0 := l.Dependencies[0]
|
|
||||||
e0 := tt.expect.Dependencies[0]
|
|
||||||
if d0.Name != e0.Name {
|
|
||||||
t.Errorf("%s: expected name %s, got %s", tt.name, e0.Name, d0.Name)
|
|
||||||
}
|
|
||||||
if d0.Repository != e0.Repository {
|
|
||||||
t.Errorf("%s: expected repo %s, got %s", tt.name, e0.Repository, d0.Repository)
|
|
||||||
}
|
|
||||||
if d0.Version != e0.Version {
|
|
||||||
t.Errorf("%s: expected version %s, got %s", tt.name, e0.Version, d0.Version)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHashReq(t *testing.T) {
|
|
||||||
expect := "sha256:fb239e836325c5fa14b29d1540a13b7d3ba13151b67fe719f820e0ef6d66aaaf"
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
chartVersion string
|
|
||||||
lockVersion string
|
|
||||||
wantError bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "chart with the expected digest",
|
|
||||||
chartVersion: "0.1.0",
|
|
||||||
lockVersion: "0.1.0",
|
|
||||||
wantError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "ranged version but same resolved lock version",
|
|
||||||
chartVersion: "^0.1.0",
|
|
||||||
lockVersion: "0.1.0",
|
|
||||||
wantError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "ranged version resolved as higher version",
|
|
||||||
chartVersion: "^0.1.0",
|
|
||||||
lockVersion: "0.1.2",
|
|
||||||
wantError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "different version",
|
|
||||||
chartVersion: "0.1.2",
|
|
||||||
lockVersion: "0.1.2",
|
|
||||||
wantError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "different version with a range",
|
|
||||||
chartVersion: "^0.1.2",
|
|
||||||
lockVersion: "0.1.2",
|
|
||||||
wantError: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
req := []*chart.Dependency{
|
|
||||||
{Name: "alpine", Version: tt.chartVersion, Repository: "http://localhost:8879/charts"},
|
|
||||||
}
|
|
||||||
lock := []*chart.Dependency{
|
|
||||||
{Name: "alpine", Version: tt.lockVersion, Repository: "http://localhost:8879/charts"},
|
|
||||||
}
|
|
||||||
h, err := HashReq(req, lock)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if !tt.wantError && expect != h {
|
|
||||||
t.Errorf("Expected %q, got %q", expect, h)
|
|
||||||
} else if tt.wantError && expect == h {
|
|
||||||
t.Errorf("Expected not %q, but same", expect)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetLocalPath(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
repo string
|
|
||||||
chartpath string
|
|
||||||
expect string
|
|
||||||
winExpect string
|
|
||||||
err bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "absolute path",
|
|
||||||
repo: "file:////",
|
|
||||||
expect: "/",
|
|
||||||
winExpect: "\\",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "relative path",
|
|
||||||
repo: "file://../../testdata/chartpath/base",
|
|
||||||
chartpath: "foo/bar",
|
|
||||||
expect: "testdata/chartpath/base",
|
|
||||||
winExpect: "testdata\\chartpath\\base",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "current directory path",
|
|
||||||
repo: "../charts/localdependency",
|
|
||||||
chartpath: "testdata/chartpath/charts",
|
|
||||||
expect: "testdata/chartpath/charts/localdependency",
|
|
||||||
winExpect: "testdata\\chartpath\\charts\\localdependency",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid local path",
|
|
||||||
repo: "file://testdata/nonexistent",
|
|
||||||
chartpath: "testdata/chartpath",
|
|
||||||
err: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid path under current directory",
|
|
||||||
repo: "charts/nonexistentdependency",
|
|
||||||
chartpath: "testdata/chartpath/charts",
|
|
||||||
err: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
p, err := GetLocalPath(tt.repo, tt.chartpath)
|
|
||||||
if err != nil {
|
|
||||||
if tt.err {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if tt.err {
|
|
||||||
t.Fatalf("Expected error in test %q", tt.name)
|
|
||||||
}
|
|
||||||
expect := tt.expect
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
expect = tt.winExpect
|
|
||||||
}
|
|
||||||
if p != expect {
|
|
||||||
t.Errorf("%q: expected %q, got %q", tt.name, expect, p)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
apiVersion: v2
|
|
||||||
name: base
|
|
||||||
version: 0.1.0
|
|
@ -1,3 +0,0 @@
|
|||||||
description: A Helm chart for Kubernetes
|
|
||||||
name: localdependency
|
|
||||||
version: 0.1.0
|
|
@ -1,49 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
entries:
|
|
||||||
alpine:
|
|
||||||
- name: alpine
|
|
||||||
urls:
|
|
||||||
- https://charts.helm.sh/stable/alpine-0.1.0.tgz
|
|
||||||
checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d
|
|
||||||
home: https://helm.sh/helm
|
|
||||||
sources:
|
|
||||||
- https://github.com/helm/helm
|
|
||||||
version: 0.2.0
|
|
||||||
description: Deploy a basic Alpine Linux pod
|
|
||||||
keywords: []
|
|
||||||
maintainers: []
|
|
||||||
icon: ""
|
|
||||||
apiVersion: v2
|
|
||||||
- name: alpine
|
|
||||||
urls:
|
|
||||||
- https://charts.helm.sh/stable/alpine-0.2.0.tgz
|
|
||||||
checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d
|
|
||||||
home: https://helm.sh/helm
|
|
||||||
sources:
|
|
||||||
- https://github.com/helm/helm
|
|
||||||
version: 0.1.0
|
|
||||||
description: Deploy a basic Alpine Linux pod
|
|
||||||
keywords: []
|
|
||||||
maintainers: []
|
|
||||||
icon: ""
|
|
||||||
apiVersion: v2
|
|
||||||
mariadb:
|
|
||||||
- name: mariadb
|
|
||||||
urls:
|
|
||||||
- https://charts.helm.sh/stable/mariadb-0.3.0.tgz
|
|
||||||
checksum: 65229f6de44a2be9f215d11dbff311673fc8ba56
|
|
||||||
home: https://mariadb.org
|
|
||||||
sources:
|
|
||||||
- https://github.com/bitnami/bitnami-docker-mariadb
|
|
||||||
version: 0.3.0
|
|
||||||
description: Chart for MariaDB
|
|
||||||
keywords:
|
|
||||||
- mariadb
|
|
||||||
- mysql
|
|
||||||
- database
|
|
||||||
- sql
|
|
||||||
maintainers:
|
|
||||||
- name: Bitnami
|
|
||||||
email: containers@bitnami.com
|
|
||||||
icon: ""
|
|
||||||
apiVersion: v2
|
|
@ -1,119 +0,0 @@
|
|||||||
/*
|
|
||||||
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 (
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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 errors.Wrapf(err, "error evaluating symlink %s", path)
|
|
||||||
}
|
|
||||||
log.Printf("found symbolic link in path: %s resolves to %s. Contents of linked file included and used", path, 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
|
|
||||||
}
|
|
@ -1,151 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright (c) for portions of walk_test.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 (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Node struct {
|
|
||||||
name string
|
|
||||||
entries []*Node // nil if the entry is a file
|
|
||||||
marks int
|
|
||||||
expectedMarks int
|
|
||||||
symLinkedTo string
|
|
||||||
}
|
|
||||||
|
|
||||||
var tree = &Node{
|
|
||||||
"testdata",
|
|
||||||
[]*Node{
|
|
||||||
{"a", nil, 0, 1, ""},
|
|
||||||
{"b", []*Node{}, 0, 1, ""},
|
|
||||||
{"c", nil, 0, 2, ""},
|
|
||||||
{"d", nil, 0, 0, "c"},
|
|
||||||
{
|
|
||||||
"e",
|
|
||||||
[]*Node{
|
|
||||||
{"x", nil, 0, 1, ""},
|
|
||||||
{"y", []*Node{}, 0, 1, ""},
|
|
||||||
{
|
|
||||||
"z",
|
|
||||||
[]*Node{
|
|
||||||
{"u", nil, 0, 1, ""},
|
|
||||||
{"v", nil, 0, 1, ""},
|
|
||||||
{"w", nil, 0, 1, ""},
|
|
||||||
},
|
|
||||||
0,
|
|
||||||
1,
|
|
||||||
"",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
0,
|
|
||||||
1,
|
|
||||||
"",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
0,
|
|
||||||
1,
|
|
||||||
"",
|
|
||||||
}
|
|
||||||
|
|
||||||
func walkTree(n *Node, path string, f func(path string, n *Node)) {
|
|
||||||
f(path, n)
|
|
||||||
for _, e := range n.entries {
|
|
||||||
walkTree(e, filepath.Join(path, e.name), f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeTree(t *testing.T) {
|
|
||||||
walkTree(tree, tree.name, func(path string, n *Node) {
|
|
||||||
if n.entries == nil {
|
|
||||||
if n.symLinkedTo != "" {
|
|
||||||
if err := os.Symlink(n.symLinkedTo, path); err != nil {
|
|
||||||
t.Fatalf("makeTree: %v", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fd, err := os.Create(path)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("makeTree: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fd.Close()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := os.Mkdir(path, 0770); err != nil {
|
|
||||||
t.Fatalf("makeTree: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkMarks(t *testing.T, report bool) {
|
|
||||||
walkTree(tree, tree.name, func(path string, n *Node) {
|
|
||||||
if n.marks != n.expectedMarks && report {
|
|
||||||
t.Errorf("node %s mark = %d; expected %d", path, n.marks, n.expectedMarks)
|
|
||||||
}
|
|
||||||
n.marks = 0
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assumes that each node name is unique. Good enough for a test.
|
|
||||||
// If clear is true, any incoming error is cleared before return. The errors
|
|
||||||
// are always accumulated, though.
|
|
||||||
func mark(info os.FileInfo, err error, errors *[]error, clear bool) error {
|
|
||||||
if err != nil {
|
|
||||||
*errors = append(*errors, err)
|
|
||||||
if clear {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
name := info.Name()
|
|
||||||
walkTree(tree, tree.name, func(path string, n *Node) {
|
|
||||||
if n.name == name {
|
|
||||||
n.marks++
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWalk(t *testing.T) {
|
|
||||||
makeTree(t)
|
|
||||||
errors := make([]error, 0, 10)
|
|
||||||
clear := true
|
|
||||||
markFn := func(path string, info os.FileInfo, err error) error {
|
|
||||||
return mark(info, err, &errors, clear)
|
|
||||||
}
|
|
||||||
// Expect no errors.
|
|
||||||
err := Walk(tree.name, markFn)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("no error expected, found: %s", err)
|
|
||||||
}
|
|
||||||
if len(errors) != 0 {
|
|
||||||
t.Fatalf("unexpected errors: %s", errors)
|
|
||||||
}
|
|
||||||
checkMarks(t, true)
|
|
||||||
|
|
||||||
// cleanup
|
|
||||||
if err := os.RemoveAll(tree.name); err != nil {
|
|
||||||
t.Errorf("removeTree: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,70 +0,0 @@
|
|||||||
/*
|
|
||||||
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 ensure
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"helm.sh/helm/v3/pkg/helmpath"
|
|
||||||
"helm.sh/helm/v3/pkg/helmpath/xdg"
|
|
||||||
)
|
|
||||||
|
|
||||||
// HelmHome sets up a Helm Home in a temp dir.
|
|
||||||
func HelmHome(t *testing.T) func() {
|
|
||||||
t.Helper()
|
|
||||||
base := TempDir(t)
|
|
||||||
os.Setenv(xdg.CacheHomeEnvVar, base)
|
|
||||||
os.Setenv(xdg.ConfigHomeEnvVar, base)
|
|
||||||
os.Setenv(xdg.DataHomeEnvVar, base)
|
|
||||||
os.Setenv(helmpath.CacheHomeEnvVar, "")
|
|
||||||
os.Setenv(helmpath.ConfigHomeEnvVar, "")
|
|
||||||
os.Setenv(helmpath.DataHomeEnvVar, "")
|
|
||||||
return func() {
|
|
||||||
os.RemoveAll(base)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TempDir ensures a scratch test directory for unit testing purposes.
|
|
||||||
func TempDir(t *testing.T) string {
|
|
||||||
t.Helper()
|
|
||||||
d, err := ioutil.TempDir("", "helm")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
// TempFile ensures a temp file for unit testing purposes.
|
|
||||||
//
|
|
||||||
// It returns the path to the directory (to which you will still need to join the filename)
|
|
||||||
//
|
|
||||||
// You must clean up the directory that is returned.
|
|
||||||
//
|
|
||||||
// tempdir := TempFile(t, "foo", []byte("bar"))
|
|
||||||
// defer os.RemoveAll(tempdir)
|
|
||||||
// filename := filepath.Join(tempdir, "foo")
|
|
||||||
func TempFile(t *testing.T, name string, data []byte) string {
|
|
||||||
path := TempDir(t)
|
|
||||||
filename := filepath.Join(path, name)
|
|
||||||
if err := ioutil.WriteFile(filename, data, 0755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
return path
|
|
||||||
}
|
|
@ -1,96 +0,0 @@
|
|||||||
/*
|
|
||||||
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 test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"flag"
|
|
||||||
"io/ioutil"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// UpdateGolden writes out the golden files with the latest values, rather than failing the test.
|
|
||||||
var updateGolden = flag.Bool("update", false, "update golden files")
|
|
||||||
|
|
||||||
// TestingT describes a testing object compatible with the critical functions from the testing.T type
|
|
||||||
type TestingT interface {
|
|
||||||
Fatal(...interface{})
|
|
||||||
Fatalf(string, ...interface{})
|
|
||||||
HelperT
|
|
||||||
}
|
|
||||||
|
|
||||||
// HelperT describes a test with a helper function
|
|
||||||
type HelperT interface {
|
|
||||||
Helper()
|
|
||||||
}
|
|
||||||
|
|
||||||
// AssertGoldenString asserts that the given string matches the contents of the given file.
|
|
||||||
func AssertGoldenString(t TestingT, actual, filename string) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
if err := compare([]byte(actual), path(filename)); err != nil {
|
|
||||||
t.Fatalf("%v\n", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AssertGoldenFile asserts that the content of the actual file matches the contents of the expected file
|
|
||||||
func AssertGoldenFile(t TestingT, actualFileName string, expectedFilename string) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
actual, err := ioutil.ReadFile(actualFileName)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%v", err)
|
|
||||||
}
|
|
||||||
AssertGoldenString(t, string(actual), expectedFilename)
|
|
||||||
}
|
|
||||||
|
|
||||||
func path(filename string) string {
|
|
||||||
if filepath.IsAbs(filename) {
|
|
||||||
return filename
|
|
||||||
}
|
|
||||||
return filepath.Join("testdata", filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
func compare(actual []byte, filename string) error {
|
|
||||||
actual = normalize(actual)
|
|
||||||
if err := update(filename, actual); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
expected, err := ioutil.ReadFile(filename)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "unable to read testdata %s", filename)
|
|
||||||
}
|
|
||||||
expected = normalize(expected)
|
|
||||||
if !bytes.Equal(expected, actual) {
|
|
||||||
return errors.Errorf("does not match golden file %s\n\nWANT:\n'%s'\n\nGOT:\n'%s'", filename, expected, actual)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func update(filename string, in []byte) error {
|
|
||||||
if !*updateGolden {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return ioutil.WriteFile(filename, normalize(in), 0666)
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalize(in []byte) []byte {
|
|
||||||
return bytes.Replace(in, []byte("\r\n"), []byte("\n"), -1)
|
|
||||||
}
|
|
@ -1,372 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright (c) for portions of fs.go are held by The Go Authors, 2016 and are provided under
|
|
||||||
the BSD license.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions are
|
|
||||||
met:
|
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer.
|
|
||||||
* Redistributions in binary form must reproduce the above
|
|
||||||
copyright notice, this list of conditions and the following disclaimer
|
|
||||||
in the documentation and/or other materials provided with the
|
|
||||||
distribution.
|
|
||||||
* Neither the name of Google Inc. nor the names of its
|
|
||||||
contributors may be used to endorse or promote products derived from
|
|
||||||
this software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
||||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
||||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
||||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
||||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
||||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
||||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package fs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// fs contains a copy of a few functions from dep tool code to avoid a dependency on golang/dep.
|
|
||||||
// This code is copied from https://github.com/golang/dep/blob/37d6c560cdf407be7b6cd035b23dba89df9275cf/internal/fs/fs.go
|
|
||||||
// No changes to the code were made other than removing some unused functions
|
|
||||||
|
|
||||||
// RenameWithFallback attempts to rename a file or directory, but falls back to
|
|
||||||
// copying in the event of a cross-device link error. If the fallback copy
|
|
||||||
// succeeds, src is still removed, emulating normal rename behavior.
|
|
||||||
func RenameWithFallback(src, dst string) error {
|
|
||||||
_, err := os.Stat(src)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "cannot stat %s", src)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.Rename(src, dst)
|
|
||||||
if err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return renameFallback(err, src, dst)
|
|
||||||
}
|
|
||||||
|
|
||||||
// renameByCopy attempts to rename a file or directory by copying it to the
|
|
||||||
// destination and then removing the src thus emulating the rename behavior.
|
|
||||||
func renameByCopy(src, dst string) error {
|
|
||||||
var cerr error
|
|
||||||
if dir, _ := IsDir(src); dir {
|
|
||||||
cerr = CopyDir(src, dst)
|
|
||||||
if cerr != nil {
|
|
||||||
cerr = errors.Wrap(cerr, "copying directory failed")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cerr = copyFile(src, dst)
|
|
||||||
if cerr != nil {
|
|
||||||
cerr = errors.Wrap(cerr, "copying file failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if cerr != nil {
|
|
||||||
return errors.Wrapf(cerr, "rename fallback failed: cannot rename %s to %s", src, dst)
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.Wrapf(os.RemoveAll(src), "cannot delete %s", src)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
errSrcNotDir = errors.New("source is not a directory")
|
|
||||||
errDstExist = errors.New("destination already exists")
|
|
||||||
)
|
|
||||||
|
|
||||||
// CopyDir recursively copies a directory tree, attempting to preserve permissions.
|
|
||||||
// Source directory must exist, destination directory must *not* exist.
|
|
||||||
func CopyDir(src, dst string) error {
|
|
||||||
src = filepath.Clean(src)
|
|
||||||
dst = filepath.Clean(dst)
|
|
||||||
|
|
||||||
// We use os.Lstat() here to ensure we don't fall in a loop where a symlink
|
|
||||||
// actually links to a one of its parent directories.
|
|
||||||
fi, err := os.Lstat(src)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !fi.IsDir() {
|
|
||||||
return errSrcNotDir
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = os.Stat(dst)
|
|
||||||
if err != nil && !os.IsNotExist(err) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err == nil {
|
|
||||||
return errDstExist
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = os.MkdirAll(dst, fi.Mode()); err != nil {
|
|
||||||
return errors.Wrapf(err, "cannot mkdir %s", dst)
|
|
||||||
}
|
|
||||||
|
|
||||||
entries, err := os.ReadDir(src)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "cannot read directory %s", dst)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, entry := range entries {
|
|
||||||
srcPath := filepath.Join(src, entry.Name())
|
|
||||||
dstPath := filepath.Join(dst, entry.Name())
|
|
||||||
|
|
||||||
if entry.IsDir() {
|
|
||||||
if err = CopyDir(srcPath, dstPath); err != nil {
|
|
||||||
return errors.Wrap(err, "copying directory failed")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// This will include symlinks, which is what we want when
|
|
||||||
// copying things.
|
|
||||||
if err = copyFile(srcPath, dstPath); err != nil {
|
|
||||||
return errors.Wrap(err, "copying file failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// copyFile copies the contents of the file named src to the file named
|
|
||||||
// by dst. The file will be created if it does not already exist. If the
|
|
||||||
// destination file exists, all its contents will be replaced by the contents
|
|
||||||
// of the source file. The file mode will be copied from the source.
|
|
||||||
func copyFile(src, dst string) (err error) {
|
|
||||||
if sym, err := IsSymlink(src); err != nil {
|
|
||||||
return errors.Wrap(err, "symlink check failed")
|
|
||||||
} else if sym {
|
|
||||||
if err := cloneSymlink(src, dst); err != nil {
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
// If cloning the symlink fails on Windows because the user
|
|
||||||
// does not have the required privileges, ignore the error and
|
|
||||||
// fall back to copying the file contents.
|
|
||||||
//
|
|
||||||
// ERROR_PRIVILEGE_NOT_HELD is 1314 (0x522):
|
|
||||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms681385(v=vs.85).aspx
|
|
||||||
if lerr, ok := err.(*os.LinkError); ok && lerr.Err != syscall.Errno(1314) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
in, err := os.Open(src)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer in.Close()
|
|
||||||
|
|
||||||
out, err := os.Create(dst)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = io.Copy(out, in); err != nil {
|
|
||||||
out.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for write errors on Close
|
|
||||||
if err = out.Close(); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
si, err := os.Stat(src)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Temporary fix for Go < 1.9
|
|
||||||
//
|
|
||||||
// See: https://github.com/golang/dep/issues/774
|
|
||||||
// and https://github.com/golang/go/issues/20829
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
dst = fixLongPath(dst)
|
|
||||||
}
|
|
||||||
err = os.Chmod(dst, si.Mode())
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// cloneSymlink will create a new symlink that points to the resolved path of sl.
|
|
||||||
// If sl is a relative symlink, dst will also be a relative symlink.
|
|
||||||
func cloneSymlink(sl, dst string) error {
|
|
||||||
resolved, err := os.Readlink(sl)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return os.Symlink(resolved, dst)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsDir determines is the path given is a directory or not.
|
|
||||||
func IsDir(name string) (bool, error) {
|
|
||||||
fi, err := os.Stat(name)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if !fi.IsDir() {
|
|
||||||
return false, errors.Errorf("%q is not a directory", name)
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsSymlink determines if the given path is a symbolic link.
|
|
||||||
func IsSymlink(path string) (bool, error) {
|
|
||||||
l, err := os.Lstat(path)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return l.Mode()&os.ModeSymlink == os.ModeSymlink, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// fixLongPath returns the extended-length (\\?\-prefixed) form of
|
|
||||||
// path when needed, in order to avoid the default 260 character file
|
|
||||||
// path limit imposed by Windows. If path is not easily converted to
|
|
||||||
// the extended-length form (for example, if path is a relative path
|
|
||||||
// or contains .. elements), or is short enough, fixLongPath returns
|
|
||||||
// path unmodified.
|
|
||||||
//
|
|
||||||
// See https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#maxpath
|
|
||||||
func fixLongPath(path string) string {
|
|
||||||
// Do nothing (and don't allocate) if the path is "short".
|
|
||||||
// Empirically (at least on the Windows Server 2013 builder),
|
|
||||||
// the kernel is arbitrarily okay with < 248 bytes. That
|
|
||||||
// matches what the docs above say:
|
|
||||||
// "When using an API to create a directory, the specified
|
|
||||||
// path cannot be so long that you cannot append an 8.3 file
|
|
||||||
// name (that is, the directory name cannot exceed MAX_PATH
|
|
||||||
// minus 12)." Since MAX_PATH is 260, 260 - 12 = 248.
|
|
||||||
//
|
|
||||||
// The MSDN docs appear to say that a normal path that is 248 bytes long
|
|
||||||
// will work; empirically the path must be less then 248 bytes long.
|
|
||||||
if len(path) < 248 {
|
|
||||||
// Don't fix. (This is how Go 1.7 and earlier worked,
|
|
||||||
// not automatically generating the \\?\ form)
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
|
|
||||||
// The extended form begins with \\?\, as in
|
|
||||||
// \\?\c:\windows\foo.txt or \\?\UNC\server\share\foo.txt.
|
|
||||||
// The extended form disables evaluation of . and .. path
|
|
||||||
// elements and disables the interpretation of / as equivalent
|
|
||||||
// to \. The conversion here rewrites / to \ and elides
|
|
||||||
// . elements as well as trailing or duplicate separators. For
|
|
||||||
// simplicity it avoids the conversion entirely for relative
|
|
||||||
// paths or paths containing .. elements. For now,
|
|
||||||
// \\server\share paths are not converted to
|
|
||||||
// \\?\UNC\server\share paths because the rules for doing so
|
|
||||||
// are less well-specified.
|
|
||||||
if len(path) >= 2 && path[:2] == `\\` {
|
|
||||||
// Don't canonicalize UNC paths.
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
if !isAbs(path) {
|
|
||||||
// Relative path
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
|
|
||||||
const prefix = `\\?`
|
|
||||||
|
|
||||||
pathbuf := make([]byte, len(prefix)+len(path)+len(`\`))
|
|
||||||
copy(pathbuf, prefix)
|
|
||||||
n := len(path)
|
|
||||||
r, w := 0, len(prefix)
|
|
||||||
for r < n {
|
|
||||||
switch {
|
|
||||||
case os.IsPathSeparator(path[r]):
|
|
||||||
// empty block
|
|
||||||
r++
|
|
||||||
case path[r] == '.' && (r+1 == n || os.IsPathSeparator(path[r+1])):
|
|
||||||
// /./
|
|
||||||
r++
|
|
||||||
case r+1 < n && path[r] == '.' && path[r+1] == '.' && (r+2 == n || os.IsPathSeparator(path[r+2])):
|
|
||||||
// /../ is currently unhandled
|
|
||||||
return path
|
|
||||||
default:
|
|
||||||
pathbuf[w] = '\\'
|
|
||||||
w++
|
|
||||||
for ; r < n && !os.IsPathSeparator(path[r]); r++ {
|
|
||||||
pathbuf[w] = path[r]
|
|
||||||
w++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// A drive's root directory needs a trailing \
|
|
||||||
if w == len(`\\?\c:`) {
|
|
||||||
pathbuf[w] = '\\'
|
|
||||||
w++
|
|
||||||
}
|
|
||||||
return string(pathbuf[:w])
|
|
||||||
}
|
|
||||||
|
|
||||||
func isAbs(path string) (b bool) {
|
|
||||||
v := volumeName(path)
|
|
||||||
if v == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
path = path[len(v):]
|
|
||||||
if path == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return os.IsPathSeparator(path[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
func volumeName(path string) (v string) {
|
|
||||||
if len(path) < 2 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
// with drive letter
|
|
||||||
c := path[0]
|
|
||||||
if path[1] == ':' &&
|
|
||||||
('0' <= c && c <= '9' || 'a' <= c && c <= 'z' ||
|
|
||||||
'A' <= c && c <= 'Z') {
|
|
||||||
return path[:2]
|
|
||||||
}
|
|
||||||
// is it UNC
|
|
||||||
if l := len(path); l >= 5 && os.IsPathSeparator(path[0]) && os.IsPathSeparator(path[1]) &&
|
|
||||||
!os.IsPathSeparator(path[2]) && path[2] != '.' {
|
|
||||||
// first, leading `\\` and next shouldn't be `\`. its server name.
|
|
||||||
for n := 3; n < l-1; n++ {
|
|
||||||
// second, next '\' shouldn't be repeated.
|
|
||||||
if os.IsPathSeparator(path[n]) {
|
|
||||||
n++
|
|
||||||
// third, following something characters. its share name.
|
|
||||||
if !os.IsPathSeparator(path[n]) {
|
|
||||||
if path[n] == '.' {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
for ; n < l; n++ {
|
|
||||||
if os.IsPathSeparator(path[n]) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return path[:n]
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
@ -1,643 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright (c) for portions of fs_test.go are held by The Go Authors, 2016 and are provided under
|
|
||||||
the BSD license.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions are
|
|
||||||
met:
|
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer.
|
|
||||||
* Redistributions in binary form must reproduce the above
|
|
||||||
copyright notice, this list of conditions and the following disclaimer
|
|
||||||
in the documentation and/or other materials provided with the
|
|
||||||
distribution.
|
|
||||||
* Neither the name of Google Inc. nor the names of its
|
|
||||||
contributors may be used to endorse or promote products derived from
|
|
||||||
this software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
||||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
||||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
||||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
||||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
||||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
||||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package fs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
mu sync.Mutex
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestRenameWithFallback(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
|
|
||||||
if err := RenameWithFallback(filepath.Join(dir, "does_not_exists"), filepath.Join(dir, "dst")); err == nil {
|
|
||||||
t.Fatal("expected an error for non existing file, but got nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
srcpath := filepath.Join(dir, "src")
|
|
||||||
|
|
||||||
if srcf, err := os.Create(srcpath); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
} else {
|
|
||||||
srcf.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := RenameWithFallback(srcpath, filepath.Join(dir, "dst")); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
srcpath = filepath.Join(dir, "a")
|
|
||||||
if err := os.MkdirAll(srcpath, 0777); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
dstpath := filepath.Join(dir, "b")
|
|
||||||
if err := os.MkdirAll(dstpath, 0777); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := RenameWithFallback(srcpath, dstpath); err == nil {
|
|
||||||
t.Fatal("expected an error if dst is an existing directory, but got nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCopyDir(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
|
|
||||||
srcdir := filepath.Join(dir, "src")
|
|
||||||
if err := os.MkdirAll(srcdir, 0755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
files := []struct {
|
|
||||||
path string
|
|
||||||
contents string
|
|
||||||
fi os.FileInfo
|
|
||||||
}{
|
|
||||||
{path: "myfile", contents: "hello world"},
|
|
||||||
{path: filepath.Join("subdir", "file"), contents: "subdir file"},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create structure indicated in 'files'
|
|
||||||
for i, file := range files {
|
|
||||||
fn := filepath.Join(srcdir, file.path)
|
|
||||||
dn := filepath.Dir(fn)
|
|
||||||
if err := os.MkdirAll(dn, 0755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fh, err := os.Create(fn)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = fh.Write([]byte(file.contents)); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
fh.Close()
|
|
||||||
|
|
||||||
files[i].fi, err = os.Stat(fn)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
destdir := filepath.Join(dir, "dest")
|
|
||||||
if err := CopyDir(srcdir, destdir); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare copy against structure indicated in 'files'
|
|
||||||
for _, file := range files {
|
|
||||||
fn := filepath.Join(srcdir, file.path)
|
|
||||||
dn := filepath.Dir(fn)
|
|
||||||
dirOK, err := IsDir(dn)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if !dirOK {
|
|
||||||
t.Fatalf("expected %s to be a directory", dn)
|
|
||||||
}
|
|
||||||
|
|
||||||
got, err := ioutil.ReadFile(fn)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if file.contents != string(got) {
|
|
||||||
t.Fatalf("expected: %s, got: %s", file.contents, string(got))
|
|
||||||
}
|
|
||||||
|
|
||||||
gotinfo, err := os.Stat(fn)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if file.fi.Mode() != gotinfo.Mode() {
|
|
||||||
t.Fatalf("expected %s: %#v\n to be the same mode as %s: %#v",
|
|
||||||
file.path, file.fi.Mode(), fn, gotinfo.Mode())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCopyDirFail_SrcInaccessible(t *testing.T) {
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
// XXX: setting permissions works differently in
|
|
||||||
// Microsoft Windows. Skipping this until a
|
|
||||||
// compatible implementation is provided.
|
|
||||||
t.Skip("skipping on windows")
|
|
||||||
}
|
|
||||||
|
|
||||||
var currentUID = os.Getuid()
|
|
||||||
|
|
||||||
if currentUID == 0 {
|
|
||||||
// Skipping if root, because all files are accessible
|
|
||||||
t.Skip("Skipping for root user")
|
|
||||||
}
|
|
||||||
|
|
||||||
var srcdir, dstdir string
|
|
||||||
|
|
||||||
cleanup := setupInaccessibleDir(t, func(dir string) error {
|
|
||||||
srcdir = filepath.Join(dir, "src")
|
|
||||||
return os.MkdirAll(srcdir, 0755)
|
|
||||||
})
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
dir := t.TempDir()
|
|
||||||
|
|
||||||
dstdir = filepath.Join(dir, "dst")
|
|
||||||
if err := CopyDir(srcdir, dstdir); err == nil {
|
|
||||||
t.Fatalf("expected error for CopyDir(%s, %s), got none", srcdir, dstdir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCopyDirFail_DstInaccessible(t *testing.T) {
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
// XXX: setting permissions works differently in
|
|
||||||
// Microsoft Windows. Skipping this until a
|
|
||||||
// compatible implementation is provided.
|
|
||||||
t.Skip("skipping on windows")
|
|
||||||
}
|
|
||||||
|
|
||||||
var currentUID = os.Getuid()
|
|
||||||
|
|
||||||
if currentUID == 0 {
|
|
||||||
// Skipping if root, because all files are accessible
|
|
||||||
t.Skip("Skipping for root user")
|
|
||||||
}
|
|
||||||
|
|
||||||
var srcdir, dstdir string
|
|
||||||
|
|
||||||
dir := t.TempDir()
|
|
||||||
|
|
||||||
srcdir = filepath.Join(dir, "src")
|
|
||||||
if err := os.MkdirAll(srcdir, 0755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup := setupInaccessibleDir(t, func(dir string) error {
|
|
||||||
dstdir = filepath.Join(dir, "dst")
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
if err := CopyDir(srcdir, dstdir); err == nil {
|
|
||||||
t.Fatalf("expected error for CopyDir(%s, %s), got none", srcdir, dstdir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCopyDirFail_SrcIsNotDir(t *testing.T) {
|
|
||||||
var srcdir, dstdir string
|
|
||||||
var err error
|
|
||||||
|
|
||||||
dir := t.TempDir()
|
|
||||||
|
|
||||||
srcdir = filepath.Join(dir, "src")
|
|
||||||
if _, err = os.Create(srcdir); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
dstdir = filepath.Join(dir, "dst")
|
|
||||||
|
|
||||||
if err = CopyDir(srcdir, dstdir); err == nil {
|
|
||||||
t.Fatalf("expected error for CopyDir(%s, %s), got none", srcdir, dstdir)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != errSrcNotDir {
|
|
||||||
t.Fatalf("expected %v error for CopyDir(%s, %s), got %s", errSrcNotDir, srcdir, dstdir, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCopyDirFail_DstExists(t *testing.T) {
|
|
||||||
var srcdir, dstdir string
|
|
||||||
var err error
|
|
||||||
|
|
||||||
dir := t.TempDir()
|
|
||||||
|
|
||||||
srcdir = filepath.Join(dir, "src")
|
|
||||||
if err = os.MkdirAll(srcdir, 0755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
dstdir = filepath.Join(dir, "dst")
|
|
||||||
if err = os.MkdirAll(dstdir, 0755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = CopyDir(srcdir, dstdir); err == nil {
|
|
||||||
t.Fatalf("expected error for CopyDir(%s, %s), got none", srcdir, dstdir)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != errDstExist {
|
|
||||||
t.Fatalf("expected %v error for CopyDir(%s, %s), got %s", errDstExist, srcdir, dstdir, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCopyDirFailOpen(t *testing.T) {
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
// XXX: setting permissions works differently in
|
|
||||||
// Microsoft Windows. os.Chmod(..., 0222) below is not
|
|
||||||
// enough for the file to be readonly, and os.Chmod(...,
|
|
||||||
// 0000) returns an invalid argument error. Skipping
|
|
||||||
// this until a compatible implementation is
|
|
||||||
// provided.
|
|
||||||
t.Skip("skipping on windows")
|
|
||||||
}
|
|
||||||
|
|
||||||
var currentUID = os.Getuid()
|
|
||||||
|
|
||||||
if currentUID == 0 {
|
|
||||||
// Skipping if root, because all files are accessible
|
|
||||||
t.Skip("Skipping for root user")
|
|
||||||
}
|
|
||||||
|
|
||||||
var srcdir, dstdir string
|
|
||||||
|
|
||||||
dir := t.TempDir()
|
|
||||||
|
|
||||||
srcdir = filepath.Join(dir, "src")
|
|
||||||
if err := os.MkdirAll(srcdir, 0755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
srcfn := filepath.Join(srcdir, "file")
|
|
||||||
srcf, err := os.Create(srcfn)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
srcf.Close()
|
|
||||||
|
|
||||||
// setup source file so that it cannot be read
|
|
||||||
if err = os.Chmod(srcfn, 0222); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
dstdir = filepath.Join(dir, "dst")
|
|
||||||
|
|
||||||
if err = CopyDir(srcdir, dstdir); err == nil {
|
|
||||||
t.Fatalf("expected error for CopyDir(%s, %s), got none", srcdir, dstdir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCopyFile(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
|
|
||||||
srcf, err := os.Create(filepath.Join(dir, "srcfile"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
want := "hello world"
|
|
||||||
if _, err := srcf.Write([]byte(want)); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
srcf.Close()
|
|
||||||
|
|
||||||
destf := filepath.Join(dir, "destf")
|
|
||||||
if err := copyFile(srcf.Name(), destf); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
got, err := ioutil.ReadFile(destf)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if want != string(got) {
|
|
||||||
t.Fatalf("expected: %s, got: %s", want, string(got))
|
|
||||||
}
|
|
||||||
|
|
||||||
wantinfo, err := os.Stat(srcf.Name())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
gotinfo, err := os.Stat(destf)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if wantinfo.Mode() != gotinfo.Mode() {
|
|
||||||
t.Fatalf("expected %s: %#v\n to be the same mode as %s: %#v", srcf.Name(), wantinfo.Mode(), destf, gotinfo.Mode())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func cleanUpDir(dir string) {
|
|
||||||
// NOTE(mattn): It seems that sometimes git.exe is not dead
|
|
||||||
// when cleanUpDir() is called. But we do not know any way to wait for it.
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
mu.Lock()
|
|
||||||
exec.Command(`taskkill`, `/F`, `/IM`, `git.exe`).Run()
|
|
||||||
mu.Unlock()
|
|
||||||
}
|
|
||||||
if dir != "" {
|
|
||||||
os.RemoveAll(dir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCopyFileSymlink(t *testing.T) {
|
|
||||||
tempdir := t.TempDir()
|
|
||||||
|
|
||||||
testcases := map[string]string{
|
|
||||||
filepath.Join("./testdata/symlinks/file-symlink"): filepath.Join(tempdir, "dst-file"),
|
|
||||||
filepath.Join("./testdata/symlinks/windows-file-symlink"): filepath.Join(tempdir, "windows-dst-file"),
|
|
||||||
filepath.Join("./testdata/symlinks/invalid-symlink"): filepath.Join(tempdir, "invalid-symlink"),
|
|
||||||
}
|
|
||||||
|
|
||||||
for symlink, dst := range testcases {
|
|
||||||
t.Run(symlink, func(t *testing.T) {
|
|
||||||
var err error
|
|
||||||
if err = copyFile(symlink, dst); err != nil {
|
|
||||||
t.Fatalf("failed to copy symlink: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var want, got string
|
|
||||||
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
// Creating symlinks on Windows require an additional permission
|
|
||||||
// regular users aren't granted usually. So we copy the file
|
|
||||||
// content as a fall back instead of creating a real symlink.
|
|
||||||
srcb, err := ioutil.ReadFile(symlink)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%+v", err)
|
|
||||||
}
|
|
||||||
dstb, err := ioutil.ReadFile(dst)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%+v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
want = string(srcb)
|
|
||||||
got = string(dstb)
|
|
||||||
} else {
|
|
||||||
want, err = os.Readlink(symlink)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%+v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
got, err = os.Readlink(dst)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("could not resolve symlink: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if want != got {
|
|
||||||
t.Fatalf("resolved path is incorrect. expected %s, got %s", want, got)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCopyFileFail(t *testing.T) {
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
// XXX: setting permissions works differently in
|
|
||||||
// Microsoft Windows. Skipping this until a
|
|
||||||
// compatible implementation is provided.
|
|
||||||
t.Skip("skipping on windows")
|
|
||||||
}
|
|
||||||
|
|
||||||
var currentUID = os.Getuid()
|
|
||||||
|
|
||||||
if currentUID == 0 {
|
|
||||||
// Skipping if root, because all files are accessible
|
|
||||||
t.Skip("Skipping for root user")
|
|
||||||
}
|
|
||||||
|
|
||||||
dir := t.TempDir()
|
|
||||||
|
|
||||||
srcf, err := os.Create(filepath.Join(dir, "srcfile"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
srcf.Close()
|
|
||||||
|
|
||||||
var dstdir string
|
|
||||||
|
|
||||||
cleanup := setupInaccessibleDir(t, func(dir string) error {
|
|
||||||
dstdir = filepath.Join(dir, "dir")
|
|
||||||
return os.Mkdir(dstdir, 0777)
|
|
||||||
})
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
fn := filepath.Join(dstdir, "file")
|
|
||||||
if err := copyFile(srcf.Name(), fn); err == nil {
|
|
||||||
t.Fatalf("expected error for %s, got none", fn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// setupInaccessibleDir creates a temporary location with a single
|
|
||||||
// directory in it, in such a way that directory is not accessible
|
|
||||||
// after this function returns.
|
|
||||||
//
|
|
||||||
// op is called with the directory as argument, so that it can create
|
|
||||||
// files or other test artifacts.
|
|
||||||
//
|
|
||||||
// If setupInaccessibleDir fails in its preparation, or op fails, t.Fatal
|
|
||||||
// will be invoked.
|
|
||||||
//
|
|
||||||
// This function returns a cleanup function that removes all the temporary
|
|
||||||
// files this function creates. It is the caller's responsibility to call
|
|
||||||
// this function before the test is done running, whether there's an error or not.
|
|
||||||
func setupInaccessibleDir(t *testing.T, op func(dir string) error) func() {
|
|
||||||
dir := t.TempDir()
|
|
||||||
|
|
||||||
subdir := filepath.Join(dir, "dir")
|
|
||||||
|
|
||||||
cleanup := func() {
|
|
||||||
if err := os.Chmod(subdir, 0777); err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.Mkdir(subdir, 0777); err != nil {
|
|
||||||
cleanup()
|
|
||||||
t.Fatal(err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := op(subdir); err != nil {
|
|
||||||
cleanup()
|
|
||||||
t.Fatal(err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.Chmod(subdir, 0666); err != nil {
|
|
||||||
cleanup()
|
|
||||||
t.Fatal(err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return cleanup
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIsDir(t *testing.T) {
|
|
||||||
|
|
||||||
var currentUID = os.Getuid()
|
|
||||||
|
|
||||||
if currentUID == 0 {
|
|
||||||
// Skipping if root, because all files are accessible
|
|
||||||
t.Skip("Skipping for root user")
|
|
||||||
}
|
|
||||||
|
|
||||||
wd, err := os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var dn string
|
|
||||||
|
|
||||||
cleanup := setupInaccessibleDir(t, func(dir string) error {
|
|
||||||
dn = filepath.Join(dir, "dir")
|
|
||||||
return os.Mkdir(dn, 0777)
|
|
||||||
})
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
tests := map[string]struct {
|
|
||||||
exists bool
|
|
||||||
err bool
|
|
||||||
}{
|
|
||||||
wd: {true, false},
|
|
||||||
filepath.Join(wd, "testdata"): {true, false},
|
|
||||||
filepath.Join(wd, "main.go"): {false, true},
|
|
||||||
filepath.Join(wd, "this_file_does_not_exist.thing"): {false, true},
|
|
||||||
dn: {false, true},
|
|
||||||
}
|
|
||||||
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
// This test doesn't work on Microsoft Windows because
|
|
||||||
// of the differences in how file permissions are
|
|
||||||
// implemented. For this to work, the directory where
|
|
||||||
// the directory exists should be inaccessible.
|
|
||||||
delete(tests, dn)
|
|
||||||
}
|
|
||||||
|
|
||||||
for f, want := range tests {
|
|
||||||
got, err := IsDir(f)
|
|
||||||
if err != nil && !want.err {
|
|
||||||
t.Fatalf("expected no error, got %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if got != want.exists {
|
|
||||||
t.Fatalf("expected %t for %s, got %t", want.exists, f, got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIsSymlink(t *testing.T) {
|
|
||||||
|
|
||||||
var currentUID = os.Getuid()
|
|
||||||
|
|
||||||
if currentUID == 0 {
|
|
||||||
// Skipping if root, because all files are accessible
|
|
||||||
t.Skip("Skipping for root user")
|
|
||||||
}
|
|
||||||
|
|
||||||
dir := t.TempDir()
|
|
||||||
|
|
||||||
dirPath := filepath.Join(dir, "directory")
|
|
||||||
if err := os.MkdirAll(dirPath, 0777); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
filePath := filepath.Join(dir, "file")
|
|
||||||
f, err := os.Create(filePath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
f.Close()
|
|
||||||
|
|
||||||
dirSymlink := filepath.Join(dir, "dirSymlink")
|
|
||||||
fileSymlink := filepath.Join(dir, "fileSymlink")
|
|
||||||
|
|
||||||
if err = os.Symlink(dirPath, dirSymlink); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err = os.Symlink(filePath, fileSymlink); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
inaccessibleFile string
|
|
||||||
inaccessibleSymlink string
|
|
||||||
)
|
|
||||||
|
|
||||||
cleanup := setupInaccessibleDir(t, func(dir string) error {
|
|
||||||
inaccessibleFile = filepath.Join(dir, "file")
|
|
||||||
if fh, err := os.Create(inaccessibleFile); err != nil {
|
|
||||||
return err
|
|
||||||
} else if err = fh.Close(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
inaccessibleSymlink = filepath.Join(dir, "symlink")
|
|
||||||
return os.Symlink(inaccessibleFile, inaccessibleSymlink)
|
|
||||||
})
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
tests := map[string]struct{ expected, err bool }{
|
|
||||||
dirPath: {false, false},
|
|
||||||
filePath: {false, false},
|
|
||||||
dirSymlink: {true, false},
|
|
||||||
fileSymlink: {true, false},
|
|
||||||
inaccessibleFile: {false, true},
|
|
||||||
inaccessibleSymlink: {false, true},
|
|
||||||
}
|
|
||||||
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
// XXX: setting permissions works differently in Windows. Skipping
|
|
||||||
// these cases until a compatible implementation is provided.
|
|
||||||
delete(tests, inaccessibleFile)
|
|
||||||
delete(tests, inaccessibleSymlink)
|
|
||||||
}
|
|
||||||
|
|
||||||
for path, want := range tests {
|
|
||||||
got, err := IsSymlink(path)
|
|
||||||
if err != nil {
|
|
||||||
if !want.err {
|
|
||||||
t.Errorf("expected no error, got %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if got != want.expected {
|
|
||||||
t.Errorf("expected %t for %s, got %t", want.expected, path, got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,58 +0,0 @@
|
|||||||
//go:build !windows
|
|
||||||
|
|
||||||
/*
|
|
||||||
Copyright (c) for portions of rename.go are held by The Go Authors, 2016 and are provided under
|
|
||||||
the BSD license.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions are
|
|
||||||
met:
|
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer.
|
|
||||||
* Redistributions in binary form must reproduce the above
|
|
||||||
copyright notice, this list of conditions and the following disclaimer
|
|
||||||
in the documentation and/or other materials provided with the
|
|
||||||
distribution.
|
|
||||||
* Neither the name of Google Inc. nor the names of its
|
|
||||||
contributors may be used to endorse or promote products derived from
|
|
||||||
this software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
||||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
||||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
||||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
||||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
||||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
||||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package fs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// renameFallback attempts to determine the appropriate fallback to failed rename
|
|
||||||
// operation depending on the resulting error.
|
|
||||||
func renameFallback(err error, src, dst string) error {
|
|
||||||
// Rename may fail if src and dst are on different devices; fall back to
|
|
||||||
// copy if we detect that case. syscall.EXDEV is the common name for the
|
|
||||||
// cross device link error which has varying output text across different
|
|
||||||
// operating systems.
|
|
||||||
terr, ok := err.(*os.LinkError)
|
|
||||||
if !ok {
|
|
||||||
return err
|
|
||||||
} else if terr.Err != syscall.EXDEV {
|
|
||||||
return errors.Wrapf(terr, "link error: cannot rename %s to %s", src, dst)
|
|
||||||
}
|
|
||||||
|
|
||||||
return renameByCopy(src, dst)
|
|
||||||
}
|
|
@ -1,69 +0,0 @@
|
|||||||
//go:build windows
|
|
||||||
|
|
||||||
/*
|
|
||||||
Copyright (c) for portions of rename_windows.go are held by The Go Authors, 2016 and are provided under
|
|
||||||
the BSD license.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions are
|
|
||||||
met:
|
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer.
|
|
||||||
* Redistributions in binary form must reproduce the above
|
|
||||||
copyright notice, this list of conditions and the following disclaimer
|
|
||||||
in the documentation and/or other materials provided with the
|
|
||||||
distribution.
|
|
||||||
* Neither the name of Google Inc. nor the names of its
|
|
||||||
contributors may be used to endorse or promote products derived from
|
|
||||||
this software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
||||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
||||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
||||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
||||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
||||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
||||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package fs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// renameFallback attempts to determine the appropriate fallback to failed rename
|
|
||||||
// operation depending on the resulting error.
|
|
||||||
func renameFallback(err error, src, dst string) error {
|
|
||||||
// Rename may fail if src and dst are on different devices; fall back to
|
|
||||||
// copy if we detect that case. syscall.EXDEV is the common name for the
|
|
||||||
// cross device link error which has varying output text across different
|
|
||||||
// operating systems.
|
|
||||||
terr, ok := err.(*os.LinkError)
|
|
||||||
if !ok {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if terr.Err != syscall.EXDEV {
|
|
||||||
// In windows it can drop down to an operating system call that
|
|
||||||
// returns an operating system error with a different number and
|
|
||||||
// message. Checking for that as a fall back.
|
|
||||||
noerr, ok := terr.Err.(syscall.Errno)
|
|
||||||
|
|
||||||
// 0x11 (ERROR_NOT_SAME_DEVICE) is the windows error.
|
|
||||||
// See https://msdn.microsoft.com/en-us/library/cc231199.aspx
|
|
||||||
if ok && noerr != 0x11 {
|
|
||||||
return errors.Wrapf(terr, "link error: cannot rename %s to %s", src, dst)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return renameByCopy(src, dst)
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
../test.file
|
|
@ -1 +0,0 @@
|
|||||||
/non/existing/file
|
|
@ -1 +0,0 @@
|
|||||||
C:/Users/ibrahim/go/src/github.com/golang/dep/internal/fs/testdata/test.file
|
|
@ -1,178 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2016 The Kubernetes 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 util
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
apps "k8s.io/api/apps/v1"
|
|
||||||
v1 "k8s.io/api/core/v1"
|
|
||||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
intstrutil "k8s.io/apimachinery/pkg/util/intstr"
|
|
||||||
appsclient "k8s.io/client-go/kubernetes/typed/apps/v1"
|
|
||||||
)
|
|
||||||
|
|
||||||
// deploymentutil contains a copy of a few functions from Kubernetes controller code to avoid a dependency on k8s.io/kubernetes.
|
|
||||||
// This code is copied from https://github.com/kubernetes/kubernetes/blob/e856613dd5bb00bcfaca6974431151b5c06cbed5/pkg/controller/deployment/util/deployment_util.go
|
|
||||||
// No changes to the code were made other than removing some unused functions
|
|
||||||
|
|
||||||
// RsListFunc returns the ReplicaSet from the ReplicaSet namespace and the List metav1.ListOptions.
|
|
||||||
type RsListFunc func(string, metav1.ListOptions) ([]*apps.ReplicaSet, error)
|
|
||||||
|
|
||||||
// ListReplicaSets returns a slice of RSes the given deployment targets.
|
|
||||||
// Note that this does NOT attempt to reconcile ControllerRef (adopt/orphan),
|
|
||||||
// because only the controller itself should do that.
|
|
||||||
// However, it does filter out anything whose ControllerRef doesn't match.
|
|
||||||
func ListReplicaSets(deployment *apps.Deployment, getRSList RsListFunc) ([]*apps.ReplicaSet, error) {
|
|
||||||
// TODO: Right now we list replica sets by their labels. We should list them by selector, i.e. the replica set's selector
|
|
||||||
// should be a superset of the deployment's selector, see https://github.com/kubernetes/kubernetes/issues/19830.
|
|
||||||
namespace := deployment.Namespace
|
|
||||||
selector, err := metav1.LabelSelectorAsSelector(deployment.Spec.Selector)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
options := metav1.ListOptions{LabelSelector: selector.String()}
|
|
||||||
all, err := getRSList(namespace, options)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// Only include those whose ControllerRef matches the Deployment.
|
|
||||||
owned := make([]*apps.ReplicaSet, 0, len(all))
|
|
||||||
for _, rs := range all {
|
|
||||||
if metav1.IsControlledBy(rs, deployment) {
|
|
||||||
owned = append(owned, rs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return owned, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReplicaSetsByCreationTimestamp sorts a list of ReplicaSet by creation timestamp, using their names as a tie breaker.
|
|
||||||
type ReplicaSetsByCreationTimestamp []*apps.ReplicaSet
|
|
||||||
|
|
||||||
func (o ReplicaSetsByCreationTimestamp) Len() int { return len(o) }
|
|
||||||
func (o ReplicaSetsByCreationTimestamp) Swap(i, j int) { o[i], o[j] = o[j], o[i] }
|
|
||||||
func (o ReplicaSetsByCreationTimestamp) Less(i, j int) bool {
|
|
||||||
if o[i].CreationTimestamp.Equal(&o[j].CreationTimestamp) {
|
|
||||||
return o[i].Name < o[j].Name
|
|
||||||
}
|
|
||||||
return o[i].CreationTimestamp.Before(&o[j].CreationTimestamp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindNewReplicaSet returns the new RS this given deployment targets (the one with the same pod template).
|
|
||||||
func FindNewReplicaSet(deployment *apps.Deployment, rsList []*apps.ReplicaSet) *apps.ReplicaSet {
|
|
||||||
sort.Sort(ReplicaSetsByCreationTimestamp(rsList))
|
|
||||||
for i := range rsList {
|
|
||||||
if EqualIgnoreHash(&rsList[i].Spec.Template, &deployment.Spec.Template) {
|
|
||||||
// In rare cases, such as after cluster upgrades, Deployment may end up with
|
|
||||||
// having more than one new ReplicaSets that have the same template as its template,
|
|
||||||
// see https://github.com/kubernetes/kubernetes/issues/40415
|
|
||||||
// We deterministically choose the oldest new ReplicaSet.
|
|
||||||
return rsList[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// new ReplicaSet does not exist.
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// EqualIgnoreHash returns true if two given podTemplateSpec are equal, ignoring the diff in value of Labels[pod-template-hash]
|
|
||||||
// We ignore pod-template-hash because:
|
|
||||||
// 1. The hash result would be different upon podTemplateSpec API changes
|
|
||||||
// (e.g. the addition of a new field will cause the hash code to change)
|
|
||||||
// 2. The deployment template won't have hash labels
|
|
||||||
func EqualIgnoreHash(template1, template2 *v1.PodTemplateSpec) bool {
|
|
||||||
t1Copy := template1.DeepCopy()
|
|
||||||
t2Copy := template2.DeepCopy()
|
|
||||||
// Remove hash labels from template.Labels before comparing
|
|
||||||
delete(t1Copy.Labels, apps.DefaultDeploymentUniqueLabelKey)
|
|
||||||
delete(t2Copy.Labels, apps.DefaultDeploymentUniqueLabelKey)
|
|
||||||
return apiequality.Semantic.DeepEqual(t1Copy, t2Copy)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNewReplicaSet returns a replica set that matches the intent of the given deployment; get ReplicaSetList from client interface.
|
|
||||||
// Returns nil if the new replica set doesn't exist yet.
|
|
||||||
func GetNewReplicaSet(deployment *apps.Deployment, c appsclient.AppsV1Interface) (*apps.ReplicaSet, error) {
|
|
||||||
rsList, err := ListReplicaSets(deployment, RsListFromClient(c))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return FindNewReplicaSet(deployment, rsList), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RsListFromClient returns an rsListFunc that wraps the given client.
|
|
||||||
func RsListFromClient(c appsclient.AppsV1Interface) RsListFunc {
|
|
||||||
return func(namespace string, options metav1.ListOptions) ([]*apps.ReplicaSet, error) {
|
|
||||||
rsList, err := c.ReplicaSets(namespace).List(context.Background(), options)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var ret []*apps.ReplicaSet
|
|
||||||
for i := range rsList.Items {
|
|
||||||
ret = append(ret, &rsList.Items[i])
|
|
||||||
}
|
|
||||||
return ret, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsRollingUpdate returns true if the strategy type is a rolling update.
|
|
||||||
func IsRollingUpdate(deployment *apps.Deployment) bool {
|
|
||||||
return deployment.Spec.Strategy.Type == apps.RollingUpdateDeploymentStrategyType
|
|
||||||
}
|
|
||||||
|
|
||||||
// MaxUnavailable returns the maximum unavailable pods a rolling deployment can take.
|
|
||||||
func MaxUnavailable(deployment apps.Deployment) int32 {
|
|
||||||
if !IsRollingUpdate(&deployment) || *(deployment.Spec.Replicas) == 0 {
|
|
||||||
return int32(0)
|
|
||||||
}
|
|
||||||
// Error caught by validation
|
|
||||||
_, maxUnavailable, _ := ResolveFenceposts(deployment.Spec.Strategy.RollingUpdate.MaxSurge, deployment.Spec.Strategy.RollingUpdate.MaxUnavailable, *(deployment.Spec.Replicas))
|
|
||||||
if maxUnavailable > *deployment.Spec.Replicas {
|
|
||||||
return *deployment.Spec.Replicas
|
|
||||||
}
|
|
||||||
return maxUnavailable
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResolveFenceposts resolves both maxSurge and maxUnavailable. This needs to happen in one
|
|
||||||
// step. For example:
|
|
||||||
//
|
|
||||||
// 2 desired, max unavailable 1%, surge 0% - should scale old(-1), then new(+1), then old(-1), then new(+1)
|
|
||||||
// 1 desired, max unavailable 1%, surge 0% - should scale old(-1), then new(+1)
|
|
||||||
// 2 desired, max unavailable 25%, surge 1% - should scale new(+1), then old(-1), then new(+1), then old(-1)
|
|
||||||
// 1 desired, max unavailable 25%, surge 1% - should scale new(+1), then old(-1)
|
|
||||||
// 2 desired, max unavailable 0%, surge 1% - should scale new(+1), then old(-1), then new(+1), then old(-1)
|
|
||||||
// 1 desired, max unavailable 0%, surge 1% - should scale new(+1), then old(-1)
|
|
||||||
func ResolveFenceposts(maxSurge, maxUnavailable *intstrutil.IntOrString, desired int32) (int32, int32, error) {
|
|
||||||
surge, err := intstrutil.GetValueFromIntOrPercent(intstrutil.ValueOrDefault(maxSurge, intstrutil.FromInt(0)), int(desired), true)
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, err
|
|
||||||
}
|
|
||||||
unavailable, err := intstrutil.GetValueFromIntOrPercent(intstrutil.ValueOrDefault(maxUnavailable, intstrutil.FromInt(0)), int(desired), false)
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if surge == 0 && unavailable == 0 {
|
|
||||||
// Validation should never allow the user to explicitly use zero values for both maxSurge
|
|
||||||
// maxUnavailable. Due to rounding down maxUnavailable though, it may resolve to zero.
|
|
||||||
// If both fenceposts resolve to zero, then we should set maxUnavailable to 1 on the
|
|
||||||
// theory that surge might not work due to quota.
|
|
||||||
unavailable = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
return int32(surge), int32(unavailable), nil
|
|
||||||
}
|
|
@ -1,58 +0,0 @@
|
|||||||
/*
|
|
||||||
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 tlsutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Options represents configurable options used to create client and server TLS configurations.
|
|
||||||
type Options struct {
|
|
||||||
CaCertFile string
|
|
||||||
// If either the KeyFile or CertFile is empty, ClientConfig() will not load them.
|
|
||||||
KeyFile string
|
|
||||||
CertFile string
|
|
||||||
// Client-only options
|
|
||||||
InsecureSkipVerify bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClientConfig returns a TLS configuration for use by a Helm client.
|
|
||||||
func ClientConfig(opts Options) (cfg *tls.Config, err error) {
|
|
||||||
var cert *tls.Certificate
|
|
||||||
var pool *x509.CertPool
|
|
||||||
|
|
||||||
if opts.CertFile != "" || opts.KeyFile != "" {
|
|
||||||
if cert, err = CertFromFilePair(opts.CertFile, opts.KeyFile); err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return nil, errors.Wrapf(err, "could not load x509 key pair (cert: %q, key: %q)", opts.CertFile, opts.KeyFile)
|
|
||||||
}
|
|
||||||
return nil, errors.Wrapf(err, "could not read x509 key pair (cert: %q, key: %q)", opts.CertFile, opts.KeyFile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !opts.InsecureSkipVerify && opts.CaCertFile != "" {
|
|
||||||
if pool, err = CertPoolFromFile(opts.CaCertFile); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg = &tls.Config{InsecureSkipVerify: opts.InsecureSkipVerify, Certificates: []tls.Certificate{*cert}, RootCAs: pool}
|
|
||||||
return cfg, nil
|
|
||||||
}
|
|
@ -1,76 +0,0 @@
|
|||||||
/*
|
|
||||||
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 tlsutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"io/ioutil"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewClientTLS returns tls.Config appropriate for client auth.
|
|
||||||
func NewClientTLS(certFile, keyFile, caFile string) (*tls.Config, error) {
|
|
||||||
config := tls.Config{}
|
|
||||||
|
|
||||||
if certFile != "" && keyFile != "" {
|
|
||||||
cert, err := CertFromFilePair(certFile, keyFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
config.Certificates = []tls.Certificate{*cert}
|
|
||||||
}
|
|
||||||
|
|
||||||
if caFile != "" {
|
|
||||||
cp, err := CertPoolFromFile(caFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
config.RootCAs = cp
|
|
||||||
}
|
|
||||||
|
|
||||||
return &config, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CertPoolFromFile returns an x509.CertPool containing the certificates
|
|
||||||
// in the given PEM-encoded file.
|
|
||||||
// Returns an error if the file could not be read, a certificate could not
|
|
||||||
// be parsed, or if the file does not contain any certificates
|
|
||||||
func CertPoolFromFile(filename string) (*x509.CertPool, error) {
|
|
||||||
b, err := ioutil.ReadFile(filename)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Errorf("can't read CA file: %v", filename)
|
|
||||||
}
|
|
||||||
cp := x509.NewCertPool()
|
|
||||||
if !cp.AppendCertsFromPEM(b) {
|
|
||||||
return nil, errors.Errorf("failed to append certificates from file: %s", filename)
|
|
||||||
}
|
|
||||||
return cp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CertFromFilePair returns an tls.Certificate containing the
|
|
||||||
// certificates public/private key pair from a pair of given PEM-encoded files.
|
|
||||||
// Returns an error if the file could not be read, a certificate could not
|
|
||||||
// be parsed, or if the file does not contain any certificates
|
|
||||||
func CertFromFilePair(certFile, keyFile string) (*tls.Certificate, error) {
|
|
||||||
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(err, "can't load key pair from cert %s and key %s", certFile, keyFile)
|
|
||||||
}
|
|
||||||
return &cert, err
|
|
||||||
}
|
|
@ -1,113 +0,0 @@
|
|||||||
/*
|
|
||||||
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 tlsutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
const tlsTestDir = "../../testdata"
|
|
||||||
|
|
||||||
const (
|
|
||||||
testCaCertFile = "rootca.crt"
|
|
||||||
testCertFile = "crt.pem"
|
|
||||||
testKeyFile = "key.pem"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestClientConfig(t *testing.T) {
|
|
||||||
opts := Options{
|
|
||||||
CaCertFile: testfile(t, testCaCertFile),
|
|
||||||
CertFile: testfile(t, testCertFile),
|
|
||||||
KeyFile: testfile(t, testKeyFile),
|
|
||||||
InsecureSkipVerify: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg, err := ClientConfig(opts)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error building tls client config: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if got := len(cfg.Certificates); got != 1 {
|
|
||||||
t.Fatalf("expecting 1 client certificates, got %d", got)
|
|
||||||
}
|
|
||||||
if cfg.InsecureSkipVerify {
|
|
||||||
t.Fatalf("insecure skip verify mismatch, expecting false")
|
|
||||||
}
|
|
||||||
if cfg.RootCAs == nil {
|
|
||||||
t.Fatalf("mismatch tls RootCAs, expecting non-nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testfile(t *testing.T, file string) (path string) {
|
|
||||||
var err error
|
|
||||||
if path, err = filepath.Abs(filepath.Join(tlsTestDir, file)); err != nil {
|
|
||||||
t.Fatalf("error getting absolute path to test file %q: %v", file, err)
|
|
||||||
}
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewClientTLS(t *testing.T) {
|
|
||||||
certFile := testfile(t, testCertFile)
|
|
||||||
keyFile := testfile(t, testKeyFile)
|
|
||||||
caCertFile := testfile(t, testCaCertFile)
|
|
||||||
|
|
||||||
cfg, err := NewClientTLS(certFile, keyFile, caCertFile)
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if got := len(cfg.Certificates); got != 1 {
|
|
||||||
t.Fatalf("expecting 1 client certificates, got %d", got)
|
|
||||||
}
|
|
||||||
if cfg.InsecureSkipVerify {
|
|
||||||
t.Fatalf("insecure skip verify mismatch, expecting false")
|
|
||||||
}
|
|
||||||
if cfg.RootCAs == nil {
|
|
||||||
t.Fatalf("mismatch tls RootCAs, expecting non-nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg, err = NewClientTLS("", "", caCertFile)
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if got := len(cfg.Certificates); got != 0 {
|
|
||||||
t.Fatalf("expecting 0 client certificates, got %d", got)
|
|
||||||
}
|
|
||||||
if cfg.InsecureSkipVerify {
|
|
||||||
t.Fatalf("insecure skip verify mismatch, expecting false")
|
|
||||||
}
|
|
||||||
if cfg.RootCAs == nil {
|
|
||||||
t.Fatalf("mismatch tls RootCAs, expecting non-nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg, err = NewClientTLS(certFile, keyFile, "")
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if got := len(cfg.Certificates); got != 1 {
|
|
||||||
t.Fatalf("expecting 1 client certificates, got %d", got)
|
|
||||||
}
|
|
||||||
if cfg.InsecureSkipVerify {
|
|
||||||
t.Fatalf("insecure skip verify mismatch, expecting false")
|
|
||||||
}
|
|
||||||
if cfg.RootCAs != nil {
|
|
||||||
t.Fatalf("mismatch tls RootCAs, expecting nil")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,73 +0,0 @@
|
|||||||
/*
|
|
||||||
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 urlutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/url"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
// URLJoin joins a base URL to one or more path components.
|
|
||||||
//
|
|
||||||
// It's like filepath.Join for URLs. If the baseURL is pathish, this will still
|
|
||||||
// perform a join.
|
|
||||||
//
|
|
||||||
// If the URL is unparsable, this returns an error.
|
|
||||||
func URLJoin(baseURL string, paths ...string) (string, error) {
|
|
||||||
u, err := url.Parse(baseURL)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
// We want path instead of filepath because path always uses /.
|
|
||||||
all := []string{u.Path}
|
|
||||||
all = append(all, paths...)
|
|
||||||
u.Path = path.Join(all...)
|
|
||||||
return u.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Equal normalizes two URLs and then compares for equality.
|
|
||||||
func Equal(a, b string) bool {
|
|
||||||
au, err := url.Parse(a)
|
|
||||||
if err != nil {
|
|
||||||
a = filepath.Clean(a)
|
|
||||||
b = filepath.Clean(b)
|
|
||||||
// If urls are paths, return true only if they are an exact match
|
|
||||||
return a == b
|
|
||||||
}
|
|
||||||
bu, err := url.Parse(b)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, u := range []*url.URL{au, bu} {
|
|
||||||
if u.Path == "" {
|
|
||||||
u.Path = "/"
|
|
||||||
}
|
|
||||||
u.Path = filepath.Clean(u.Path)
|
|
||||||
}
|
|
||||||
return au.String() == bu.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExtractHostname returns hostname from URL
|
|
||||||
func ExtractHostname(addr string) (string, error) {
|
|
||||||
u, err := url.Parse(addr)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return u.Hostname(), nil
|
|
||||||
}
|
|
@ -1,81 +0,0 @@
|
|||||||
/*
|
|
||||||
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 urlutil
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestURLJoin(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name, url, expect string
|
|
||||||
paths []string
|
|
||||||
}{
|
|
||||||
{name: "URL, one path", url: "http://example.com", paths: []string{"hello"}, expect: "http://example.com/hello"},
|
|
||||||
{name: "Long URL, one path", url: "http://example.com/but/first", paths: []string{"slurm"}, expect: "http://example.com/but/first/slurm"},
|
|
||||||
{name: "URL, two paths", url: "http://example.com", paths: []string{"hello", "world"}, expect: "http://example.com/hello/world"},
|
|
||||||
{name: "URL, no paths", url: "http://example.com", paths: []string{}, expect: "http://example.com"},
|
|
||||||
{name: "basepath, two paths", url: "../example.com", paths: []string{"hello", "world"}, expect: "../example.com/hello/world"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
if got, err := URLJoin(tt.url, tt.paths...); err != nil {
|
|
||||||
t.Errorf("%s: error %q", tt.name, err)
|
|
||||||
} else if got != tt.expect {
|
|
||||||
t.Errorf("%s: expected %q, got %q", tt.name, tt.expect, got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEqual(t *testing.T) {
|
|
||||||
for _, tt := range []struct {
|
|
||||||
a, b string
|
|
||||||
match bool
|
|
||||||
}{
|
|
||||||
{"http://example.com", "http://example.com", true},
|
|
||||||
{"http://example.com", "http://another.example.com", false},
|
|
||||||
{"https://example.com", "https://example.com", true},
|
|
||||||
{"http://example.com/", "http://example.com", true},
|
|
||||||
{"https://example.com", "http://example.com", false},
|
|
||||||
{"http://example.com/foo", "http://example.com/foo/", true},
|
|
||||||
{"http://example.com/foo//", "http://example.com/foo/", true},
|
|
||||||
{"http://example.com/./foo/", "http://example.com/foo/", true},
|
|
||||||
{"http://example.com/bar/../foo/", "http://example.com/foo/", true},
|
|
||||||
{"/foo", "/foo", true},
|
|
||||||
{"/foo", "/foo/", true},
|
|
||||||
{"/foo/.", "/foo/", true},
|
|
||||||
{"%/1234", "%/1234", true},
|
|
||||||
{"%/1234", "%/123", false},
|
|
||||||
{"/1234", "%/1234", false},
|
|
||||||
} {
|
|
||||||
if tt.match != Equal(tt.a, tt.b) {
|
|
||||||
t.Errorf("Expected %q==%q to be %t", tt.a, tt.b, tt.match)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExtractHostname(t *testing.T) {
|
|
||||||
tests := map[string]string{
|
|
||||||
"http://example.com": "example.com",
|
|
||||||
"https://example.com/foo": "example.com",
|
|
||||||
|
|
||||||
"https://example.com:31337/not/with/a/bang/but/a/whimper": "example.com",
|
|
||||||
}
|
|
||||||
for start, expect := range tests {
|
|
||||||
if got, _ := ExtractHostname(start); got != expect {
|
|
||||||
t.Errorf("Got %q, expected %q", got, expect)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,81 +0,0 @@
|
|||||||
/*
|
|
||||||
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 "helm.sh/helm/v3/internal/version"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
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 = "v3.10"
|
|
||||||
|
|
||||||
// metadata is extra build time data
|
|
||||||
metadata = ""
|
|
||||||
// gitCommit is the git sha1
|
|
||||||
gitCommit = ""
|
|
||||||
// gitTreeState is the state of the git tree
|
|
||||||
gitTreeState = ""
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
v := BuildInfo{
|
|
||||||
Version: GetVersion(),
|
|
||||||
GitCommit: gitCommit,
|
|
||||||
GitTreeState: gitTreeState,
|
|
||||||
GoVersion: runtime.Version(),
|
|
||||||
}
|
|
||||||
|
|
||||||
// HACK(bacongobbler): strip out GoVersion during a test run for consistent test output
|
|
||||||
if flag.Lookup("test.v") != nil {
|
|
||||||
v.GoVersion = ""
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
Loading…
Reference in new issue