From 54a5237ffb2088421c6194b1cbac9890b3db8097 Mon Sep 17 00:00:00 2001 From: christoph Date: Tue, 16 Jan 2024 13:56:22 +0100 Subject: [PATCH] feat(cli, client): install package #23 #28 #35 --- .gitignore | 2 + README.md | 2 +- cmd/glasskube/cmd/install.go | 43 +++++++++++++++++++ cmd/glasskube/cmd/root.go | 10 +++-- cmd/glasskube/config/config.go | 3 ++ pkg/client/client.go | 30 ++++++++++++++ pkg/client/package.go | 75 ++++++++++++++++++++++++++++++++++ pkg/install/install.go | 73 +++++++++++++++++++++++++++++++++ website/docs/index.md | 8 +++- 9 files changed, 239 insertions(+), 7 deletions(-) create mode 100644 cmd/glasskube/cmd/install.go create mode 100644 cmd/glasskube/config/config.go create mode 100644 pkg/client/client.go create mode 100644 pkg/client/package.go create mode 100644 pkg/install/install.go diff --git a/.gitignore b/.gitignore index 88fa79867..a762011ad 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,5 @@ go.work bin/ dist/ + +.idea/ diff --git a/README.md b/README.md index 2af229015..97a677a95 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ Start the package manager: glasskube serve ``` -Open [`http://localhost:80805`](http://localhost:80805) and explore available packages. +Open [`http://localhost:8580`](http://localhost:8580) and explore available packages. ## 📦 Supported Packages diff --git a/cmd/glasskube/cmd/install.go b/cmd/glasskube/cmd/install.go new file mode 100644 index 000000000..f5c25eb80 --- /dev/null +++ b/cmd/glasskube/cmd/install.go @@ -0,0 +1,43 @@ +package cmd + +import ( + "fmt" + "github.com/glasskube/glasskube/api/v1alpha1/condition" + "github.com/glasskube/glasskube/cmd/glasskube/config" + "github.com/glasskube/glasskube/pkg/client" + "github.com/glasskube/glasskube/pkg/install" + "github.com/spf13/cobra" +) + +var installCmd = &cobra.Command{ + Use: "install [package-name]", + Short: "Install a package", + Long: `Install a package.`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + pkgClient, err := client.InitKubeClient(config.Kubeconfig) + if err != nil { + return err + } + status, err := install.Install(pkgClient, cmd.Context(), args[0]) + if err != nil { + return err + } + if status != nil { + switch (*status).Status { + case condition.Ready: + fmt.Println("Installed successfully.") + default: + fmt.Printf("Installation has status %v, reason: %v\nMessage: %v\n", + (*status).Status, (*status).Reason, (*status).Message) + } + } else { + fmt.Println("Installation status unknown - no error and no status have been observed.") + } + return nil + }, +} + +func init() { + RootCmd.AddCommand(installCmd) +} diff --git a/cmd/glasskube/cmd/root.go b/cmd/glasskube/cmd/root.go index d3ce289e3..5faa43965 100644 --- a/cmd/glasskube/cmd/root.go +++ b/cmd/glasskube/cmd/root.go @@ -1,7 +1,7 @@ package cmd import ( - "fmt" + "github.com/glasskube/glasskube/cmd/glasskube/config" "github.com/spf13/cobra" ) @@ -11,8 +11,10 @@ var ( Use: "glasskube", Version: "0.0.0", Short: "Kubernetes Package Management the easy way 🔥", - Run: func(cmd *cobra.Command, args []string) { - fmt.Println("glasskube cli stub") - }, } ) + +func init() { + RootCmd.PersistentFlags().StringVar(&config.Kubeconfig, "kubeconfig", "", + "path to the kubeconfig file, whose current-context will be used (defaults to ~/.kube/config)") +} diff --git a/cmd/glasskube/config/config.go b/cmd/glasskube/config/config.go new file mode 100644 index 000000000..5b1c694c7 --- /dev/null +++ b/cmd/glasskube/config/config.go @@ -0,0 +1,3 @@ +package config + +var Kubeconfig string diff --git a/pkg/client/client.go b/pkg/client/client.go new file mode 100644 index 000000000..bf6d5452e --- /dev/null +++ b/pkg/client/client.go @@ -0,0 +1,30 @@ +package client + +import ( + "github.com/glasskube/glasskube/api/v1alpha1" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/tools/clientcmd" +) + +var PackageGVR = v1alpha1.GroupVersion.WithResource("packages") + +func InitKubeClient(kubeconfig string) (*PackageV1Alpha1Client, error) { + loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() + if kubeconfig != "" { + loadingRules.ExplicitPath = kubeconfig + } + clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{}) + config, err := clientConfig.ClientConfig() + if err != nil { + return nil, err + } + err = v1alpha1.AddToScheme(scheme.Scheme) + if err != nil { + return nil, err + } + pkgClient, err := NewPackageClient(config) + if err != nil { + return nil, err + } + return pkgClient, nil +} diff --git a/pkg/client/package.go b/pkg/client/package.go new file mode 100644 index 000000000..dc8d0182c --- /dev/null +++ b/pkg/client/package.go @@ -0,0 +1,75 @@ +package client + +import ( + "context" + "github.com/glasskube/glasskube/api/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" +) + +type PackageV1Alpha1Client struct { + restClient rest.Interface +} + +type PackageInterface interface { + Create(ctx context.Context, p *v1alpha1.Package) error + Watch(ctx context.Context) (watch.Interface, error) +} + +type packageClient struct { + restClient rest.Interface +} + +func NewPackageClient(cfg *rest.Config) (*PackageV1Alpha1Client, error) { + pkgRestConfig := *cfg + pkgRestConfig.ContentConfig.GroupVersion = &v1alpha1.GroupVersion + pkgRestConfig.APIPath = "/apis" + pkgRestConfig.NegotiatedSerializer = scheme.Codecs.WithoutConversion() + restClient, err := rest.RESTClientFor(&pkgRestConfig) + if err != nil { + return nil, err + } + return &PackageV1Alpha1Client{restClient: restClient}, err +} + +func (c *PackageV1Alpha1Client) Packages() PackageInterface { + return &packageClient{ + restClient: c.restClient, + } +} + +func (c *packageClient) Create(ctx context.Context, pkg *v1alpha1.Package) error { + return c.restClient.Post(). + Resource(PackageGVR.Resource). + Body(pkg).Do(ctx).Into(pkg) +} + +func (c *packageClient) Watch(ctx context.Context) (watch.Interface, error) { + opts := metav1.ListOptions{Watch: true} + return c.restClient.Get(). + Resource(PackageGVR.Resource). + VersionedParams(&opts, scheme.ParameterCodec). + Watch(ctx) +} + +// NewPackage instantiates a new v1alpha1.Package struct with the given package name +func NewPackage(packageName string) *v1alpha1.Package { + return &v1alpha1.Package{ + ObjectMeta: metav1.ObjectMeta{ + Name: packageName, + }, + Spec: v1alpha1.PackageSpec{ + PackageInfo: v1alpha1.PackageInfoTemplate{ + Name: packageName, + }, + }, + } +} + +type PackageStatus struct { + Status string + Reason string + Message string +} diff --git a/pkg/install/install.go b/pkg/install/install.go new file mode 100644 index 000000000..09c0a0d77 --- /dev/null +++ b/pkg/install/install.go @@ -0,0 +1,73 @@ +package install + +import ( + "context" + "errors" + "fmt" + "github.com/glasskube/glasskube/api/v1alpha1" + "github.com/glasskube/glasskube/api/v1alpha1/condition" + "github.com/glasskube/glasskube/pkg/client" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/watch" +) + +// Install creates a new v1alpha1.Package custom resource in the cluster, and blocks until this resource has either +// status Ready or Failed. +func Install(pkgClient *client.PackageV1Alpha1Client, ctx context.Context, packageName string) (*client.PackageStatus, error) { + pkg := client.NewPackage(packageName) + err := pkgClient.Packages().Create(ctx, pkg) + if err != nil { + return nil, err + } + fmt.Printf("Installing %v.\n", packageName) + + status, err := awaitInstall(pkgClient, ctx, pkg.GetUID()) + if err != nil { + return nil, err + } + + return status, nil +} + +func awaitInstall(pkgClient *client.PackageV1Alpha1Client, ctx context.Context, pkgUID types.UID) (*client.PackageStatus, error) { + watcher, err := pkgClient.Packages().Watch(ctx) + if err != nil { + return nil, err + } + + defer watcher.Stop() + for event := range watcher.ResultChan() { + if obj, ok := event.Object.(*v1alpha1.Package); ok && obj.GetUID() == pkgUID { + if event.Type == watch.Added || event.Type == watch.Modified { + if status := getStatus(&obj.Status); status != nil { + return status, nil + } + } else if event.Type == watch.Deleted { + return nil, errors.New("created package has been deleted unexpectedly") + } + } + } + return nil, errors.New("failed to confirm package installation status") +} + +func getStatus(status *v1alpha1.PackageStatus) *client.PackageStatus { + readyCnd := meta.FindStatusCondition((*status).Conditions, condition.Ready) + if readyCnd != nil && readyCnd.Status == v1.ConditionTrue { + return newPackageStatus(readyCnd) + } + failedCnd := meta.FindStatusCondition((*status).Conditions, condition.Failed) + if failedCnd != nil && failedCnd.Status == v1.ConditionTrue { + return newPackageStatus(failedCnd) + } + return nil +} + +func newPackageStatus(cnd *v1.Condition) *client.PackageStatus { + return &client.PackageStatus{ + Status: cnd.Type, + Reason: cnd.Reason, + Message: cnd.Message, + } +} diff --git a/website/docs/index.md b/website/docs/index.md index c7c62a21b..45d405ec6 100644 --- a/website/docs/index.md +++ b/website/docs/index.md @@ -70,14 +70,18 @@ Currently only the glasskube packages repository is supported: [`glasskube/packa ## Commands -### `glasskube` +### `glasskube serve` -Will start the UI server and opens a local browser on localhost:80805. +Will start the UI server and opens a local browser on [http://localhost:8580](http://localhost:8580). ### `glasskube bootstrap` ### `glasskube install` +Install the given package in your cluster. +By default, the cluster given in `~/.kube/config` (`current-context`) will be used. +An alternative kube config can be passed with the `--kubeconfig` flag. + ``` glasskube install