Skip to content

Commit 63fbe53

Browse files
zen-dogANeumann82
andauthored
Tarball packages can now contain local dependencies (#1728)
* Tarball packages can now contain local dependencies Summary: Currently, KUDO allows local dependencies like `package: ../child` or `../child.tgz` however, only if the parent operator is *also* in a local directory. This PR adds the possibility for a packaged operator (a tarball) to contain all the dependencies it needs. While, the preferred way is to keep the individual operators in the repo, sometimes a self-contained operator package (like an uber-jar) is more desirable e.g.: - an air-gaped environment needs a functioning repo before operator installation which significantly increases the overall deployment overhead - some dependency operators might be heavily customized official ones, which makes it hard to contribute them back to the official repo To keep the packages backward-compatible, we continue to expect the parent operator at the top-level of the tarball, so that all the dependencies must share the same base path and be at least one level deeper e.g.: ``` . └── child │ └── operator.yaml └── operator.yaml ``` A dependency operator (`child` above) can have its dependencies which are always relative to the `operator.yaml` which declares them. See [this commit](c237347) for more information. Fixes #1701 Signed-off-by: Andreas Neumann <[email protected]> Co-authored-by: Andreas Neumann <[email protected]>
1 parent 9f86f0f commit 63fbe53

File tree

29 files changed

+366
-322
lines changed

29 files changed

+366
-322
lines changed

pkg/controller/instance/instance_controller.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -327,9 +327,9 @@ func (r *Reconciler) resolveDependencies(i *kudoapi.Instance, ov *kudoapi.Operat
327327
if i.IsChildInstance() {
328328
return nil
329329
}
330-
resolver := &InClusterResolver{ns: ov.Namespace, c: r.Client}
330+
resolver := NewInClusterResolver(r.Client, ov.Namespace)
331331

332-
_, err := dependencies.Resolve(ov.Name, ov, resolver)
332+
_, err := dependencies.Resolve(ov, resolver)
333333
if err != nil {
334334
return engine.ExecutionError{Err: fmt.Errorf("%w%v", engine.ErrFatalExecution, err), EventName: "CircularDependency"}
335335
}

pkg/controller/instance/resolver_incluster.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,14 @@ type InClusterResolver struct {
2121
ns string
2222
}
2323

24-
func (r InClusterResolver) Resolve(name string, appVersion string, operatorVersion string) (*packages.Resources, error) {
24+
func NewInClusterResolver(client client.Client, ns string) *InClusterResolver {
25+
return &InClusterResolver{
26+
c: client,
27+
ns: ns,
28+
}
29+
}
30+
31+
func (r InClusterResolver) Resolve(name string, appVersion string, operatorVersion string) (*packages.PackageScope, error) {
2532
ovn := kudoapi.OperatorVersionName(name, appVersion, operatorVersion)
2633

2734
ov, err := kudoapi.GetOperatorVersionByName(ovn, r.ns, r.c)
@@ -34,9 +41,11 @@ func (r InClusterResolver) Resolve(name string, appVersion string, operatorVersi
3441
return nil, fmt.Errorf("failed to resolve operator %s/%s", r.ns, name)
3542
}
3643

37-
return &packages.Resources{
44+
res := &packages.Resources{
3845
Operator: o,
3946
OperatorVersion: ov,
4047
Instance: nil,
41-
}, nil
48+
}
49+
50+
return &packages.PackageScope{Resources: res, DependenciesResolver: r}, nil
4251
}

pkg/kudoctl/cmd/install/install.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ package install
22

33
import (
44
"fmt"
5+
"os"
56
"time"
67

78
"github.com/spf13/afero"
89

910
"github.com/kudobuilder/kudo/pkg/kudoctl/clog"
1011
"github.com/kudobuilder/kudo/pkg/kudoctl/env"
12+
"github.com/kudobuilder/kudo/pkg/kudoctl/packages"
1113
"github.com/kudobuilder/kudo/pkg/kudoctl/packages/install"
1214
pkgresolver "github.com/kudobuilder/kudo/pkg/kudoctl/packages/resolver"
1315
deps "github.com/kudobuilder/kudo/pkg/kudoctl/resources/dependencies"
@@ -80,19 +82,24 @@ func installOperator(operatorArgument string, options *Options, fs afero.Fs, set
8082

8183
clog.V(3).Printf("getting operator package")
8284

83-
var resolver pkgresolver.Resolver
85+
var resolver packages.Resolver
8486
if options.InCluster {
8587
resolver = pkgresolver.NewInClusterResolver(kudoClient, settings.Namespace)
8688
} else {
87-
resolver = pkgresolver.New(repoClient)
89+
wd, err := os.Getwd()
90+
if err != nil {
91+
return fmt.Errorf("failed to get current working directory: %v", err)
92+
}
93+
94+
resolver = pkgresolver.NewPackageResolver(repoClient, wd)
8895
}
8996

9097
pr, err := resolver.Resolve(operatorArgument, options.AppVersion, options.OperatorVersion)
9198
if err != nil {
9299
return fmt.Errorf("failed to resolve operator package for: %s %w", operatorArgument, err)
93100
}
94101

95-
dependencies, err := deps.Resolve(operatorArgument, pr.OperatorVersion, resolver)
102+
dependencies, err := deps.Resolve(pr.Resources.OperatorVersion, pr.DependenciesResolver)
96103
if err != nil {
97104
return err
98105
}
@@ -111,7 +118,7 @@ func installOperator(operatorArgument string, options *Options, fs afero.Fs, set
111118
kudoClient,
112119
options.InstanceName,
113120
settings.Namespace,
114-
*pr,
121+
*pr.Resources,
115122
options.Parameters,
116123
dependencies,
117124
installOpts)

pkg/kudoctl/cmd/package_list.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cmd
33
import (
44
"fmt"
55
"io"
6+
"os"
67

78
"github.com/spf13/afero"
89
"github.com/spf13/cobra"
@@ -27,19 +28,19 @@ For list commands, the argument passed represents an operator. That representa
2728
`
2829

2930
const packageListExamples = ` # show list of parameters from local operator folder
30-
kubectl kudo package list parameters local-folder
31+
kubectl kudo package list parameters ./local-folder
3132
3233
# show list of parameters from zookeeper (where zookeeper is name of package in KUDO repository)
3334
kubectl kudo package list parameters zookeeper
3435
3536
# show list of tasks from local operator folder
36-
kubectl kudo package list tasks local-folder
37+
kubectl kudo package list tasks ./local-folder
3738
3839
# show list of tasks from zookeeper (where zookeeper is name of package in KUDO repository)
3940
kubectl kudo package list tasks zookeeper
4041
4142
# show list of plans from local operator folder
42-
kubectl kudo package list plans local-folder
43+
kubectl kudo package list plans ./local-folder
4344
4445
# show plans from zookeeper (where zookeeper is name of package in KUDO repository)
4546
kubectl kudo package list plans zookeeper
@@ -69,10 +70,15 @@ func packageDiscovery(fs afero.Fs, settings *env.Settings, repoName, pathOrName,
6970
clog.V(3).Printf("repository used %s", repository)
7071

7172
clog.V(3).Printf("getting package pkg files for %v with version: %v_%v", pathOrName, appVersion, operatorVersion)
72-
resolver := pkgresolver.New(repository)
73+
wd, err := os.Getwd()
74+
if err != nil {
75+
return nil, fmt.Errorf("failed to get current working directory: %v", err)
76+
}
77+
78+
resolver := pkgresolver.NewPackageResolver(repository, wd)
7379
pr, err := resolver.Resolve(pathOrName, appVersion, operatorVersion)
7480
if err != nil {
7581
return nil, fmt.Errorf("failed to resolve package files for operator: %s: %w", pathOrName, err)
7682
}
77-
return pr, nil
83+
return pr.Resources, nil
7884
}

pkg/kudoctl/cmd/upgrade.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cmd
33
import (
44
"errors"
55
"fmt"
6+
"os"
67

78
"github.com/spf13/afero"
89
"github.com/spf13/cobra"
@@ -103,18 +104,23 @@ func runUpgrade(args []string, options *options, fs afero.Fs, settings *env.Sett
103104
return fmt.Errorf("could not build operator repository: %w", err)
104105
}
105106

106-
resolver := pkgresolver.New(repository)
107+
wd, err := os.Getwd()
108+
if err != nil {
109+
return fmt.Errorf("failed to get current working directory: %v", err)
110+
}
111+
112+
resolver := pkgresolver.NewPackageResolver(repository, wd)
107113
pr, err := resolver.Resolve(packageToUpgrade, options.AppVersion, options.OperatorVersion)
108114
if err != nil {
109115
return fmt.Errorf("failed to resolve operator package for: %s: %w", packageToUpgrade, err)
110116
}
111117

112-
pr.OperatorVersion.SetNamespace(settings.Namespace)
118+
pr.Resources.OperatorVersion.SetNamespace(settings.Namespace)
113119

114-
dependencies, err := deps.Resolve(packageToUpgrade, pr.OperatorVersion, resolver)
120+
dependencies, err := deps.Resolve(pr.Resources.OperatorVersion, pr.DependenciesResolver)
115121
if err != nil {
116122
return err
117123
}
118124

119-
return upgrade.OperatorVersion(kc, pr.OperatorVersion, options.InstanceName, options.Parameters, dependencies)
125+
return upgrade.OperatorVersion(kc, pr.Resources.OperatorVersion, options.InstanceName, options.Parameters, dependencies)
120126
}

pkg/kudoctl/packages/reader/parser.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,8 @@ func parsePackageFile(filePath string, fileBytes []byte, currentPackage *package
9898
}
9999
currentPackage.Params = &paramsFile
100100
default:
101-
return fmt.Errorf("unexpected file when reading package from filesystem: %s", filePath)
101+
// we ignore unexpected files as they might belong to a dependency operator
102+
return nil
102103
}
103104
return nil
104105
}

pkg/kudoctl/packages/reader/parser_test.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,6 @@ func TestParsePackageFile(t *testing.T) {
4343
{filePath: "templates/some/nested/template2.yaml", isTemplate: true, fileContent: "not-empty"},
4444
{filePath: "./templates/some-template.yaml", isTemplate: true, fileContent: "not-empty"},
4545
{filePath: "./templates/with/subdirectory/some-template.yaml", isTemplate: true, fileContent: "not-empty"},
46-
{filePath: "templates_without_path.yaml", expectedError: errors.New("unexpected file when reading package from filesystem: templates_without_path.yaml"), fileContent: "not-empty"},
47-
{filePath: "invalid.yaml", expectedError: errors.New("unexpected file when reading package from filesystem: invalid.yaml")},
4846
{filePath: "operator.yaml", isOperator: true, expectedError: errors.New("failed to parse yaml into valid operator operator.yaml")},
4947
}
5048

pkg/kudoctl/packages/reader/reader.go

Lines changed: 0 additions & 34 deletions
This file was deleted.

pkg/kudoctl/packages/reader/reader_tar.go

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"fmt"
88
"io"
99
"io/ioutil"
10+
"path"
1011

1112
"github.com/spf13/afero"
1213

@@ -15,16 +16,19 @@ import (
1516
"github.com/kudobuilder/kudo/pkg/kudoctl/packages/convert"
1617
)
1718

18-
func ReadTar(fs afero.Fs, path string) (*packages.Resources, error) {
19+
// ResourcesFromTar extracts files from the tar provides by he `inFs` and the `path` and converts
20+
// them to resources. All the extracted files are saved in the `outFs` for later use (e.g. searching
21+
// for local dependencies)
22+
func ResourcesFromTar(in afero.Fs, out afero.Fs, path string) (*packages.Resources, error) {
1923
// 1. read the tarball
20-
b, err := afero.ReadFile(fs, path)
24+
b, err := afero.ReadFile(in, path)
2125
if err != nil {
2226
return nil, err
2327
}
2428
buf := bytes.NewBuffer(b)
2529

26-
// 2. ParseTgz tar files
27-
files, err := ParseTgz(buf)
30+
// 2. extract and parse tar files
31+
files, err := PackageFilesFromTar(out, buf)
2832
if err != nil {
2933
return nil, fmt.Errorf("while parsing package files from %s: %v", path, err)
3034
}
@@ -38,11 +42,30 @@ func ReadTar(fs afero.Fs, path string) (*packages.Resources, error) {
3842
return resources, nil
3943
}
4044

41-
func ParseTgz(r io.Reader) (*packages.Files, error) {
42-
gzr, err := gzip.NewReader(r)
45+
// PackageFilesFromTar extracts a tgz archive held by passed reader and returns the package files.
46+
// Additionally, all the files are saved in the `out` Fs (in the root `/` folder).
47+
func PackageFilesFromTar(out afero.Fs, r io.Reader) (*packages.Files, error) {
48+
err := ExtractTar(out, r)
4349
if err != nil {
4450
return nil, err
4551
}
52+
53+
pf, err := PackageFilesFromDir(out, "/")
54+
return pf, err
55+
}
56+
57+
// ExtractTar extract a tgz archive into the given filesystem. This is a generic extract method
58+
// so no package parsing is performed.
59+
// *Note:* all file paths are prepended by `/` and are extracted into the root of the passed Fs.
60+
// By default tar strips out the leading slash, but leaves `./` when packing a folder and doesn't
61+
// add it when packing a file so that depending on how it was packed the same file might have a path
62+
// like `templates/foo.yaml` or `./templates/foo.yaml`. Since we're extracting into the empty MemFs,
63+
// we can avoid the inconsistency and just extract into the root.
64+
func ExtractTar(out afero.Fs, r io.Reader) error {
65+
gzr, err := gzip.NewReader(r)
66+
if err != nil {
67+
return err
68+
}
4669
defer func() {
4770
err := gzr.Close()
4871
if err != nil {
@@ -52,19 +75,18 @@ func ParseTgz(r io.Reader) (*packages.Files, error) {
5275

5376
tr := tar.NewReader(gzr)
5477

55-
result := newPackageFiles()
5678
for {
5779
header, err := tr.Next()
5880

5981
switch {
6082

6183
// if no more files are found return
6284
case err == io.EOF:
63-
return &result, nil
85+
return nil
6486

6587
// return any other error
6688
case err != nil:
67-
return nil, err
89+
return err
6890

6991
// if the header is nil, just skip it (not sure how this happens)
7092
case header == nil:
@@ -74,18 +96,23 @@ func ParseTgz(r io.Reader) (*packages.Files, error) {
7496
// check the file type
7597
switch header.Typeflag {
7698

99+
// there are no folders in the tar, only files with nested file names e.g. `templates/foo.yaml` ¯\_(ツ)_/¯
77100
case tar.TypeDir:
78-
// we don't need to handle folders, files have folder name in their names and that should be enough
101+
clog.Printf("Tar file contained directory. Did not expect this: %s", header.Name)
102+
continue
79103

80104
case tar.TypeReg:
105+
// read the file
81106
buf, err := ioutil.ReadAll(tr)
82107
if err != nil {
83-
return nil, fmt.Errorf("while reading file %s from package tarball: %v", header.Name, err)
108+
return fmt.Errorf("while reading file %s from package tarball: %v", header.Name, err)
84109
}
85110

86-
err = parsePackageFile(header.Name, buf, &result)
111+
// copy over contents. the files are extracted into the root of the passed Fs
112+
// nolint:gosec
113+
err = afero.WriteFile(out, path.Join("/", header.Name), buf, 0644)
87114
if err != nil {
88-
return nil, err
115+
return fmt.Errorf("while writing file %s: %v", header.Name, err)
89116
}
90117
}
91118
}

pkg/kudoctl/packages/reader/reader_test.go

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,33 +34,32 @@ func TestReadFileSystemPackage(t *testing.T) {
3434

3535
t.Run(fmt.Sprintf("%s-from-%s", tt.name, tt.path), func(t *testing.T) {
3636
var err error
37-
var pr *packages.Resources
37+
var got *packages.Resources
3838

3939
if strings.HasSuffix(tt.path, ".tgz") {
40-
pr, err = ReadTar(fs, tt.path)
40+
got, err = ResourcesFromTar(fs, afero.NewMemMapFs(), tt.path)
4141
} else {
42-
pr, err = ResourcesFromDir(fs, tt.path)
42+
got, err = ResourcesFromDir(fs, tt.path)
4343
}
4444

4545
assert.NoError(t, err, "unexpected error while reading the package")
46-
actual := pr
4746

48-
actual.Instance.ObjectMeta.Name = tt.instanceName
47+
got.Instance.ObjectMeta.Name = tt.instanceName
4948
golden, err := loadResourcesFromPath(tt.goldenFiles)
5049
if err != nil {
5150
t.Errorf("Found unexpected error when loading golden files: %v", err)
5251
}
5352

5453
// we need to sort here because current yaml parsing is not preserving the order of fields
5554
// at the same time, the deep library we use for equality does not support ignoring order
56-
sort.Slice(actual.OperatorVersion.Spec.Parameters, func(i, j int) bool {
57-
return actual.OperatorVersion.Spec.Parameters[i].Name < actual.OperatorVersion.Spec.Parameters[j].Name
55+
sort.Slice(got.OperatorVersion.Spec.Parameters, func(i, j int) bool {
56+
return got.OperatorVersion.Spec.Parameters[i].Name < got.OperatorVersion.Spec.Parameters[j].Name
5857
})
5958
sort.Slice(golden.OperatorVersion.Spec.Parameters, func(i, j int) bool {
6059
return golden.OperatorVersion.Spec.Parameters[i].Name < golden.OperatorVersion.Spec.Parameters[j].Name
6160
})
6261

63-
assert.Equal(t, golden, actual)
62+
assert.Equal(t, golden, got)
6463
})
6564
}
6665
}

0 commit comments

Comments
 (0)