Skip to content

Commit

Permalink
Install operator instance from in-cluster operator version resource (#…
Browse files Browse the repository at this point in the history
…1680)

Summary:
The current `kudo install ...` command accepts either a remote repo operator name, a URL or a local folder/tgz. This PR allows the user to install an instance of an existing in-cluster operator version using the newly introduced `--in-cluster` option:

```
❯ ./bin/kubectl-kudo install first-operator --operator-version=0.2.0 --in-cluster
operatorversion default/first-operator-0.2.0 already installed
instance default/first-operator-instance created
```

This way `kudoctl` will only look for already installed operator versions (one can list them using `kudoctl get operatorversions`) and does not need access to the repo/package files.

Fixes #1678

Signed-off-by: Aleksey Dukhovniy <[email protected]>
  • Loading branch information
Aleksey Dukhovniy authored Sep 18, 2020
1 parent b4b17b7 commit 248272f
Show file tree
Hide file tree
Showing 12 changed files with 352 additions and 45 deletions.
13 changes: 10 additions & 3 deletions pkg/controller/instance/resolver_incluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,16 @@ import (
"github.com/kudobuilder/kudo/pkg/kudoctl/packages"
)

// InClusterResolver resolves packages that are already installed in the cluster. Note, that unlike other resolvers, the
// resulting 'packages.Package' struct does not contain package 'packages.Files' (we don't have the original files) and
// doesn't have an Instance resource because multiple Instances of the same Operator/OperatorVersion can exist
// InClusterResolver is a server-side package resolver for packages that are already installed in the cluster. It is a simpler
// version of the client-side pkg/kudoctl/packages/resolver/resolver_incluster.go. The client-side version would search
// the installed OperatorVersions and try to resolve any valid combination of the operator name and its app and operator versions,
// same as we would search in the repository.
// This resolver is only used to make sure that all the dependencies of an operator exist and that referenced operator versions
// are installed and uniquely identifiable by the passed operator name, appVersion and operatorVersion parameters
// (see pkg/apis/kudo/v1beta1/operatorversion_types_helpers.go::OperatorVersionName method).
// Also, note that unlike other resolvers, the resulting 'packages.Package' struct does not contain package 'packages.Files'
// (we don't have the original files) and doesn't have an Instance resource because multiple Instances of the same
// Operator/OperatorVersion can exist.
type InClusterResolver struct {
c client.Client
ns string
Expand Down
12 changes: 6 additions & 6 deletions pkg/kudoctl/cmd/get/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@ func Run(args []string, opts CmdOpts) error {
var objs []runtime.Object
switch args[0] {
case Instances:
objs, err = opts.Client.ListInstances(opts.Namespace)
objs, err = opts.Client.ListInstancesAsRuntimeObject(opts.Namespace)
case Operators:
objs, err = opts.Client.ListOperators(opts.Namespace)
objs, err = opts.Client.ListOperatorsAsRuntimeObject(opts.Namespace)
case OperatorVersions:
objs, err = opts.Client.ListOperatorVersions(opts.Namespace)
objs, err = opts.Client.ListOperatorVersionsAsRuntimeObject(opts.Namespace)
case All:
return runGetAll(opts)
}
Expand Down Expand Up @@ -80,15 +80,15 @@ func Run(args []string, opts CmdOpts) error {
}

func runGetAll(opts CmdOpts) error {
instances, err := opts.Client.ListInstances(opts.Namespace)
instances, err := opts.Client.ListInstancesAsRuntimeObject(opts.Namespace)
if err != nil {
return fmt.Errorf("failed to get instances")
}
operatorversions, err := opts.Client.ListOperatorVersions(opts.Namespace)
operatorversions, err := opts.Client.ListOperatorVersionsAsRuntimeObject(opts.Namespace)
if err != nil {
return fmt.Errorf("failed to get operatorversions")
}
operators, err := opts.Client.ListOperators(opts.Namespace)
operators, err := opts.Client.ListOperatorsAsRuntimeObject(opts.Namespace)
if err != nil {
return fmt.Errorf("failed to get operators")
}
Expand Down
4 changes: 4 additions & 0 deletions pkg/kudoctl/cmd/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ var (
# Install operator from tarball at URL
kubectl kudo install http://kudo.dev/zk.tgz
# Install operator from an in-cluster operator version
kubectl kudo install zookeeper --operator-version=0.3.0 --in-cluster
# Specify an operator version of Kafka to install to your cluster
kubectl kudo install kafka --operator-version=1.1.1`
)
Expand Down Expand Up @@ -62,6 +65,7 @@ func newInstallCmd(fs afero.Fs) *cobra.Command {
installCmd.Flags().BoolVar(&options.Wait, "wait", false, "Specify if the CLI should wait for the install to complete before returning (default \"false\")")
installCmd.Flags().Int64Var(&options.WaitTime, "wait-time", 300, "Specify the max wait time in seconds for CLI for the install to complete before returning (default \"300\")")
installCmd.Flags().BoolVar(&options.CreateNameSpace, "create-namespace", false, "If set, install will create the specified namespace and will fail if it exists. (default \"false\")")
installCmd.Flags().BoolVar(&options.InCluster, "in-cluster", false, "Specify if the CLI should resolve the package using the operator version already installed in the cluster. (default \"false\")")

return installCmd
}
26 changes: 19 additions & 7 deletions pkg/kudoctl/cmd/install/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type Options struct {
Wait bool
WaitTime int64
CreateNameSpace bool
InCluster bool
}

// DefaultOptions initializes the install command options to its defaults
Expand All @@ -38,7 +39,7 @@ var DefaultOptions = &Options{}
// Run returns the errors associated with cmd env
func Run(args []string, options *Options, fs afero.Fs, settings *env.Settings) error {

err := validate(args)
err := validate(args, options)
if err != nil {
return err
}
Expand All @@ -47,24 +48,29 @@ func Run(args []string, options *Options, fs afero.Fs, settings *env.Settings) e
return err
}

func validate(args []string) error {
func validate(args []string, opts *Options) error {
if len(args) != 1 {
return clog.Errorf("expecting exactly one argument - name of the package or path to install")
}

if opts.InCluster {
if opts.SkipInstance || opts.RepoName != "" {
return clog.Errorf("you can't use skip-instance or repo option when installing from in-cluster operators")
}
}
return nil
}

// installOperator is installing single operator into cluster and returns error in case of error
func installOperator(operatorArgument string, options *Options, fs afero.Fs, settings *env.Settings) error {

repository, err := repo.ClientFromSettings(fs, settings.Home, options.RepoName)
repoClient, err := repo.ClientFromSettings(fs, settings.Home, options.RepoName)
if err != nil {
return fmt.Errorf("could not build operator repository: %w", err)
}
clog.V(4).Printf("repository used %s", repository)
clog.V(4).Printf("repository used %s", repoClient)

kc, err := env.GetClient(settings)
kudoClient, err := env.GetClient(settings)
clog.V(3).Printf("acquiring kudo client")
if err != nil {
clog.V(3).Printf("failed to acquire client")
Expand All @@ -73,7 +79,13 @@ func installOperator(operatorArgument string, options *Options, fs afero.Fs, set

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

resolver := pkgresolver.New(repository)
var resolver pkgresolver.Resolver
if options.InCluster {
resolver = pkgresolver.NewInClusterResolver(kudoClient, settings.Namespace)
} else {
resolver = pkgresolver.New(repoClient)
}

pkg, err := resolver.Resolve(operatorArgument, options.AppVersion, options.OperatorVersion)
if err != nil {
return fmt.Errorf("failed to resolve operator package for: %s %w", operatorArgument, err)
Expand All @@ -90,7 +102,7 @@ func installOperator(operatorArgument string, options *Options, fs afero.Fs, set
}

return install.Package(
kc,
kudoClient,
options.InstanceName,
settings.Namespace,
*pkg.Resources,
Expand Down
17 changes: 11 additions & 6 deletions pkg/kudoctl/cmd/install/install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,21 @@ import (
func TestValidate(t *testing.T) {

tests := []struct {
arg []string
err string
args []string
opts *Options
err string
}{
{nil, "expecting exactly one argument - name of the package or path to install"}, // 1
{[]string{"arg", "arg2"}, "expecting exactly one argument - name of the package or path to install"}, // 2
{[]string{}, "expecting exactly one argument - name of the package or path to install"}, // 3
{args: nil, opts: &Options{}, err: "expecting exactly one argument - name of the package or path to install"},
{args: []string{"arg", "arg2"}, opts: &Options{}, err: "expecting exactly one argument - name of the package or path to install"},
{args: []string{}, opts: &Options{}, err: "expecting exactly one argument - name of the package or path to install"},
{args: []string{"arg"}, opts: &Options{
SkipInstance: true,
InCluster: true,
}, err: "you can't use skip-instance or repo option when installing from in-cluster operators"},
}

for _, tt := range tests {
err := validate(tt.arg)
err := validate(tt.args, tt.opts)
if tt.err != "" {
assert.EqualError(t, err, tt.err)
}
Expand Down
24 changes: 14 additions & 10 deletions pkg/kudoctl/packages/convert/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,28 +78,32 @@ func FilesToResources(files *packages.Files) (*packages.Resources, error) {
Status: kudoapi.OperatorVersionStatus{},
}

instance := &kudoapi.Instance{
instance := BuildInstanceResource(files.Operator.Name, files.Operator.OperatorVersion)

return &packages.Resources{
Operator: operator,
OperatorVersion: fv,
Instance: instance,
}, nil
}

func BuildInstanceResource(operatorName, operatorVersion string) *kudoapi.Instance {
return &kudoapi.Instance{
TypeMeta: metav1.TypeMeta{
Kind: "Instance",
APIVersion: packages.APIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: kudoapi.OperatorInstanceName(files.Operator.Name),
Labels: map[string]string{kudo.OperatorLabel: files.Operator.Name},
Name: kudoapi.OperatorInstanceName(operatorName),
Labels: map[string]string{kudo.OperatorLabel: operatorName},
},
Spec: kudoapi.InstanceSpec{
OperatorVersion: corev1.ObjectReference{
Name: kudoapi.OperatorVersionName(files.Operator.Name, files.Operator.OperatorVersion),
Name: kudoapi.OperatorVersionName(operatorName, operatorVersion),
},
},
Status: kudoapi.InstanceStatus{},
}

return &packages.Resources{
Operator: operator,
OperatorVersion: fv,
Instance: instance,
}, nil
}

func validateTask(t kudoapi.Task, templates map[string]string) []string {
Expand Down
8 changes: 7 additions & 1 deletion pkg/kudoctl/packages/resolver/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/kudobuilder/kudo/pkg/kudoctl/clog"
"github.com/kudobuilder/kudo/pkg/kudoctl/http"
"github.com/kudobuilder/kudo/pkg/kudoctl/packages"
"github.com/kudobuilder/kudo/pkg/kudoctl/util/kudo"
"github.com/kudobuilder/kudo/pkg/kudoctl/util/repo"
)

Expand All @@ -23,7 +24,7 @@ type PackageResolver struct {
}

// New creates an operator package resolver for non-repository packages
func New(repo *repo.Client) *PackageResolver {
func New(repo *repo.Client) Resolver {
lf := NewLocal()
uf := NewURL()
return &PackageResolver{
Expand All @@ -33,6 +34,11 @@ func New(repo *repo.Client) *PackageResolver {
}
}

// NewInClusterResolver returns an initialized InClusterResolver for resolving already installed packages
func NewInClusterResolver(c *kudo.Client, ns string) Resolver {
return &InClusterResolver{c: c, ns: ns}
}

// Resolve provides a one stop to acquire any non-repo packages by trying to look for package files
// resolving the operator name to:
// - a local tgz file
Expand Down
86 changes: 86 additions & 0 deletions pkg/kudoctl/packages/resolver/resolver_incluster.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package resolver

import (
"fmt"
"sort"

"github.com/kudobuilder/kudo/pkg/kudoctl/packages"
"github.com/kudobuilder/kudo/pkg/kudoctl/packages/convert"
"github.com/kudobuilder/kudo/pkg/kudoctl/util/kudo"
"github.com/kudobuilder/kudo/pkg/kudoctl/util/repo"
)

// InClusterResolver resolves packages that are already installed in the cluster on the client-side. Note, that unlike
// other resolvers, the resulting 'packages.Package' struct does not contain package 'packages.Files' (we don't have
// the original files).
type InClusterResolver struct {
c *kudo.Client
ns string
}

func (r InClusterResolver) Resolve(name string, appVersion string, operatorVersion string) (*packages.Package, error) {
// 1. find all in-cluster operator versions with the passed operator name
versions, err := r.FindInClusterOperatorVersions(name)
if err != nil {
return nil, err
}

//2. sorting packages in descending order same as the repo does it: pkg/kudoctl/util/repo/index.go::sortPackages
// to preserve the selection rules. See sortPackages method description for more details.
sort.Sort(sort.Reverse(versions))

// 3. find first matching operator version
version, err := repo.FindFirstMatchForEntries(versions, name, appVersion, operatorVersion)
if err != nil {
return nil, err
}

// 4. fetch the existing O/OV and make the instance to install
ovn := version.Name
operatorVersion = version.OperatorVersion

ov, err := r.c.GetOperatorVersion(ovn, r.ns)
if err != nil {
return nil, fmt.Errorf("failed to resolve operator version %s/%s:%s", r.ns, ovn, appVersion)
}

o, err := r.c.GetOperator(name, r.ns)
if err != nil {
return nil, fmt.Errorf("failed to resolve operator %s/%s", r.ns, name)
}

i := convert.BuildInstanceResource(name, operatorVersion)

return &packages.Package{
Resources: &packages.Resources{
Operator: o,
OperatorVersion: ov,
Instance: i,
},
Files: nil,
}, nil
}

// FindInClusterOperatorVersions method searches for all in-cluster operator versions for the passed operator name
// and returns them as an []*PackageVersion array
func (r InClusterResolver) FindInClusterOperatorVersions(operatorName string) (repo.PackageVersions, error) {
ovs, err := r.c.ListOperatorVersions(r.ns)
if err != nil {
return nil, fmt.Errorf("failed to list in-cluster operator %s versions: %v", operatorName, err)
}

versions := repo.PackageVersions{}
for _, ov := range ovs {
if ov.Spec.Operator.Name == operatorName {
versions = append(versions, &repo.PackageVersion{
Metadata: &repo.Metadata{
Name: ov.Name,
OperatorVersion: ov.Spec.Version,
AppVersion: ov.Spec.AppVersion,
},
})
}
}

return versions, nil
}
Loading

0 comments on commit 248272f

Please sign in to comment.