Skip to content

Add interactive mode to template app command #1335

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
3 changes: 2 additions & 1 deletion .nancy-ignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# pkg:golang/github.com/hashicorp/vault/api
CVE-2024-2660 until=2024-06-30
# see https://github.com/getsops/sops/pull/1519
CVE-2024-2660 until=2024-07-31
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project's packages adheres to [Semantic Versioning](http://semver.org/s

### Added

- Add interactive mode to `kubectl gs template app` command.
- Allow `kubectl gs update app` to update App CR to any version from any catalog.
Also add `--suspend` flag to manage Flux App reconciliation.

Expand Down
16 changes: 13 additions & 3 deletions cmd/template/app/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import (
"github.com/giantswarm/microerror"
"github.com/giantswarm/micrologger"
"github.com/spf13/cobra"
"k8s.io/cli-runtime/pkg/genericclioptions"

"github.com/giantswarm/kubectl-gs/v2/pkg/commonconfig"
)

const (
Expand All @@ -15,15 +18,19 @@ const (
)

type Config struct {
Logger micrologger.Logger
Stderr io.Writer
Stdout io.Writer
Logger micrologger.Logger
ConfigFlags *genericclioptions.RESTClientGetter
Stderr io.Writer
Stdout io.Writer
}

func New(config Config) (*cobra.Command, error) {
if config.Logger == nil {
return nil, microerror.Maskf(invalidConfigError, "%T.Logger must not be empty", config)
}
if config.ConfigFlags == nil {
return nil, microerror.Maskf(invalidConfigError, "%T.ConfigFlags must not be empty", config)
}
if config.Stderr == nil {
config.Stderr = os.Stderr
}
Expand All @@ -34,6 +41,9 @@ func New(config Config) (*cobra.Command, error) {
f := &flag{}

r := &runner{
commonService: &commonconfig.CommonConfig{
ConfigFlags: config.ConfigFlags,
},
flag: f,
logger: config.Logger,
stderr: config.Stderr,
Expand Down
24 changes: 13 additions & 11 deletions cmd/template/app/flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const (
flagDefaultingEnabled = "defaulting-enabled"
flagInCluster = "in-cluster"
flagInstallTimeout = "install-timeout"
flagInteractive = "interactive"
flagName = "name"
flagNamespace = "namespace"
flagTargetNamespace = "target-namespace"
Expand All @@ -45,6 +46,7 @@ type flag struct {
DefaultingEnabled bool
InCluster bool
InstallTimeout time.Duration
Interactive bool
Name string
Namespace string
TargetNamespace string
Expand Down Expand Up @@ -73,6 +75,7 @@ func (f *flag) Init(cmd *cobra.Command) {
cmd.Flags().BoolVar(&f.DefaultingEnabled, flagDefaultingEnabled, true, "Don't template fields that will be defaulted.")
cmd.Flags().BoolVar(&f.InCluster, flagInCluster, false, fmt.Sprintf("Deploy the app in the current management cluster rather than in a workload cluster. If this is set, --%s will be ignored.", flagClusterName))
cmd.Flags().DurationVar(&f.InstallTimeout, flagInstallTimeout, 0, "Timeout for the Helm install.")
cmd.Flags().BoolVar(&f.Interactive, flagInteractive, false, "Run in interactive mode.")
cmd.Flags().DurationVar(&f.RollbackTimeout, flagRollbackTimeout, 0, "Timeout for the Helm rollback.")
cmd.Flags().DurationVar(&f.UninstallTimeout, flagUninstallTimeout, 0, "Timeout for the Helm uninstall.")
cmd.Flags().DurationVar(&f.UpgradeTimeout, flagUpgradeTimeout, 0, "Timeout for the Helm upgrade.")
Expand All @@ -88,20 +91,19 @@ func (f *flag) Init(cmd *cobra.Command) {
}

func (f *flag) Validate() error {
if f.Catalog == "" {
return microerror.Maskf(invalidFlagError, "--%s must not be empty", flagCatalog)
}
if f.Name == "" {
return microerror.Maskf(invalidFlagError, "--%s must not be empty", flagName)
}
if f.Namespace == "" && f.TargetNamespace == "" {
return microerror.Maskf(invalidFlagError, "--%s must not be empty", flagTargetNamespace)
}
if !f.InCluster && f.Cluster == "" && f.ClusterName == "" {
return microerror.Maskf(invalidFlagError, "--%s must not be empty", flagClusterName)
}
if f.Version == "" {
return microerror.Maskf(invalidFlagError, "--%s must not be empty", flagVersion)
if !f.Interactive {
if f.Catalog == "" {
return microerror.Maskf(invalidFlagError, "--%s must not be empty", flagCatalog)
}
if f.Name == "" {
return microerror.Maskf(invalidFlagError, "--%s must not be empty", flagName)
}
if f.Version == "" {
return microerror.Maskf(invalidFlagError, "--%s must not be empty", flagVersion)
}
}

_, err := labels.Parse(f.flagNamespaceConfigLabels)
Expand Down
102 changes: 102 additions & 0 deletions cmd/template/app/interactive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package app

import (
"context"
"fmt"
"sort"
"strings"

applicationv1alpha1 "github.com/giantswarm/apiextensions-application/api/v1alpha1"
"github.com/giantswarm/microerror"
fuzzyfinder "github.com/ktr0731/go-fuzzyfinder"
"github.com/muesli/reflow/wordwrap"
"k8s.io/apimachinery/pkg/labels"

"github.com/giantswarm/kubectl-gs/v2/pkg/data/domain/catalog"
)

func (r *runner) promptCatalog() (*applicationv1alpha1.Catalog, error) {
ctx := context.Background()
o := catalog.GetOptions{}
resource, err := r.catalogService.Get(ctx, o)
if err != nil {
return nil, microerror.Mask(err)
}
catalogs := resource.(*catalog.Collection)
entries := catalogs.Items
sort.Slice(entries, func(i, j int) bool {
return entries[i].CR.GetName() > entries[j].CR.GetName()
})

idx, err := fuzzyfinder.Find(entries, func(i int) string {
return entries[i].CR.GetName()
}, fuzzyfinder.WithPreviewWindow(func(i, w, h int) string {
if i == -1 {
return ""
}
return fmt.Sprintf(" %s\n\n%s", entries[i].CR.Spec.Title, wordwrap.String(entries[i].CR.Spec.Description, h))
}))

if err != nil {
fmt.Println(err)
return nil, err
}
return entries[idx].CR, nil
}

func (r *runner) promptCatalogEntries(catalogName, appName, appVersion string) (*applicationv1alpha1.AppCatalogEntry, error) {
var err error

ctx := context.Background()
o := catalog.ListOptions{}
var selector []string
{
if len(catalogName) > 0 {
selector = append(selector, fmt.Sprintf("application.giantswarm.io/catalog=%s", catalogName))
}
if len(appName) > 0 {
selector = append(selector, fmt.Sprintf("app.kubernetes.io/name=%s", appName))
}
if len(appVersion) > 0 {
selector = append(selector, fmt.Sprintf("app.kubernetes.io/version=%s", appVersion))
}

}
var labelSelector labels.Selector
{
labelSelector, err = labels.Parse(strings.Join(selector, ","))
if err != nil {
return nil, microerror.Mask(err)
}
}
o.LabelSelector = labelSelector

catalogEntries, err := r.catalogService.ListCatalogEntries(ctx, o)
if err != nil {
return nil, microerror.Mask(err)
}
entries := catalogEntries.(*catalog.CatalogEntryList).Items
sort.Slice(entries, func(i, j int) bool {
return entries[j].Spec.DateUpdated.Before(entries[i].Spec.DateUpdated)
})

idx, err := fuzzyfinder.Find(entries, func(i int) string {
return fmt.Sprintf("%s/%s@%s", entries[i].Spec.Catalog.Name, entries[i].Spec.AppName, entries[i].Spec.Version)
}, fuzzyfinder.WithPreviewWindow(func(i, w, h int) string {
if i == -1 {
return ""
}
return fmt.Sprintf(" %s\n\n%s\n\nURL: %s\nUpdated: %s\nUpstream version: %s",
entries[i].Spec.AppName,
wordwrap.String(entries[i].Spec.Chart.Description, h),
entries[i].Spec.Chart.Home,
entries[i].Spec.DateUpdated.Format("2006-01-02 15:04:05"),
entries[i].Spec.AppVersion,
)
}))
if err != nil {
fmt.Println(err)
return nil, err
}
return &entries[idx], nil
}
46 changes: 46 additions & 0 deletions cmd/template/app/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,19 @@ import (

"github.com/giantswarm/kubectl-gs/v2/internal/key"
"github.com/giantswarm/kubectl-gs/v2/pkg/annotations"
"github.com/giantswarm/kubectl-gs/v2/pkg/commonconfig"
"github.com/giantswarm/kubectl-gs/v2/pkg/data/domain/catalog"
"github.com/giantswarm/kubectl-gs/v2/pkg/labels"
templateapp "github.com/giantswarm/kubectl-gs/v2/pkg/template/app"
)

type runner struct {
flag *flag
logger micrologger.Logger

catalogService catalog.Interface
commonService *commonconfig.CommonConfig

stderr io.Writer
stdout io.Writer
}
Expand All @@ -49,6 +55,26 @@ func (r *runner) run(ctx context.Context, cmd *cobra.Command, args []string) err
var userConfigSecretYaml []byte
var err error

client, err := r.commonService.GetClient(r.logger)
if err != nil {
return microerror.Mask(err)
}
c := catalog.Config{
Client: client.CtrlClient(),
}

r.catalogService, err = catalog.New(c)
if err != nil {
return microerror.Mask(err)
}

if r.flag.Interactive {
err = r.runInteractive()
if err != nil {
return microerror.Mask(err)
}
}

appName := r.flag.AppName
if appName == "" {
appName = r.flag.Name
Expand Down Expand Up @@ -204,3 +230,23 @@ func (r *runner) setTimeouts(config *templateapp.Config) {
config.UpgradeTimeout = &metav1.Duration{Duration: r.flag.UpgradeTimeout}
}
}

func (r *runner) runInteractive() (err error) {
if r.flag.Catalog == "" {
catalog, err := r.promptCatalog()
if err != nil {
return microerror.Mask(err)
}
r.flag.Catalog = catalog.GetName()
}

if r.flag.Name == "" || r.flag.Version == "" {
catalogEntry, err := r.promptCatalogEntries(r.flag.Catalog, r.flag.Name, r.flag.Version)
if err != nil {
return microerror.Mask(err)
}
r.flag.Name = catalogEntry.Spec.AppName
r.flag.Version = catalogEntry.Spec.Version
}
return nil
}
7 changes: 4 additions & 3 deletions cmd/template/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,10 @@ func New(config Config) (*cobra.Command, error) {
var appCmd *cobra.Command
{
c := app.Config{
Logger: config.Logger,
Stderr: config.Stderr,
Stdout: config.Stdout,
Logger: config.Logger,
ConfigFlags: config.ConfigFlags,
Stderr: config.Stderr,
Stdout: config.Stdout,
}

appCmd, err = app.New(c)
Expand Down
9 changes: 9 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ require (
github.com/giantswarm/release-operator/v4 v4.2.0
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/go-cmp v0.6.0
github.com/ktr0731/go-fuzzyfinder v0.8.0
github.com/muesli/reflow v0.3.0
github.com/pkg/errors v0.9.1
github.com/rhysd/go-github-selfupdate v1.2.3
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
Expand Down Expand Up @@ -121,6 +123,8 @@ require (
github.com/evanphx/json-patch v5.7.0+incompatible // indirect
github.com/evanphx/json-patch/v5 v5.8.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gdamore/encoding v1.0.0 // indirect
github.com/gdamore/tcell/v2 v2.7.4 // indirect
github.com/getsops/gopgagent v0.0.0-20170926210634-4d7ea76ff71a // indirect
github.com/go-errors/errors v1.4.2 // indirect
github.com/go-jose/go-jose/v4 v4.0.1 // indirect
Expand Down Expand Up @@ -167,12 +171,15 @@ require (
github.com/jongio/azidext/go/azidext v0.5.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/ktr0731/go-ansisgr v0.1.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
Expand All @@ -184,6 +191,7 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nsf/termbox-go v1.1.1 // indirect
github.com/opencontainers/image-spec v1.1.0-rc5 // indirect
github.com/opencontainers/runc v1.1.8 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
Expand All @@ -193,6 +201,7 @@ require (
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
Expand Down
Loading