From 6000d23ca318f704e5ae1deca669e98e90829af5 Mon Sep 17 00:00:00 2001 From: Philip Laine Date: Mon, 13 May 2024 14:08:17 +0200 Subject: [PATCH] Add new wait for resource command that uses kstatus --- go.mod | 8 ++++++- go.sum | 7 ++++++ src/cmd/tools/wait.go | 37 ++++++++++++++++++++++++++++++ src/pkg/utils/wait.go | 53 ++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 103 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 9fe6c7a968..598c84b91c 100644 --- a/go.mod +++ b/go.mod @@ -56,11 +56,17 @@ require ( k8s.io/klog/v2 v2.120.1 k8s.io/kubectl v0.29.1 oras.land/oras-go/v2 v2.5.0 + sigs.k8s.io/cli-utils v0.36.0 sigs.k8s.io/kustomize/api v0.16.0 sigs.k8s.io/kustomize/kyaml v0.16.0 sigs.k8s.io/yaml v1.4.0 ) +require ( + github.com/evanphx/json-patch/v5 v5.6.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect +) + require ( atomicgo.dev/cursor v0.2.0 // indirect atomicgo.dev/keyboard v0.2.9 // indirect @@ -509,7 +515,7 @@ require ( modernc.org/memory v1.7.2 // indirect modernc.org/sqlite v1.28.0 // indirect oras.land/oras-go v1.2.4 // indirect - sigs.k8s.io/controller-runtime v0.16.3 // indirect + sigs.k8s.io/controller-runtime v0.16.3 sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/kustomize/kustomize/v5 v5.0.4-0.20230601165947-6ce0bf390ce3 // indirect sigs.k8s.io/release-utils v0.7.7 // indirect diff --git a/go.sum b/go.sum index cb92210e44..c2c4787ffd 100644 --- a/go.sum +++ b/go.sum @@ -684,6 +684,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws= github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI= github.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= +github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM= github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= github.com/facebookincubator/flog v0.0.0-20190930132826-d2511d0ce33c/go.mod h1:QGzNH9ujQ2ZUr/CjDGZGWeDAVStrWNjHeEcjJL96Nuk= @@ -1109,6 +1111,7 @@ github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267 h1:TMtDYDHKYY github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267/go.mod h1:h1nSAbGFqGVzn6Jyl1R/iCcBUHN4g+gW1u9CoBTrb9E= github.com/jellydator/ttlcache/v3 v3.1.1 h1:RCgYJqo3jgvhl+fEWvjNW8thxGWsgxi+TPhRir1Y9y8= github.com/jellydator/ttlcache/v3 v3.1.1/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= @@ -2178,6 +2181,8 @@ golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNq golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -2497,6 +2502,8 @@ oras.land/oras-go/v2 v2.5.0/go.mod h1:z4eisnLP530vwIOUOJeBIj0aGI0L1C3d53atvCBqZH rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/cli-utils v0.36.0 h1:k7GM6LmIMydtvM6Ad91XuqKk0QEVL9bVbaiX1uvWIrA= +sigs.k8s.io/cli-utils v0.36.0/go.mod h1:uCFC3BPXB3xHFQyKkWUlTrncVDCKzbdDfqZqRTCrk24= sigs.k8s.io/controller-runtime v0.16.3 h1:2TuvuokmfXvDUamSx1SuAOO3eTyye+47mJCigwG62c4= sigs.k8s.io/controller-runtime v0.16.3/go.mod h1:j7bialYoSn142nv9sCOJmQgDXQXxnroFU4VnX/brVJ0= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= diff --git a/src/cmd/tools/wait.go b/src/cmd/tools/wait.go index 9977c58011..e0c9b98a19 100644 --- a/src/cmd/tools/wait.go +++ b/src/cmd/tools/wait.go @@ -5,6 +5,7 @@ package tools import ( + "context" "time" "github.com/defenseunicorns/zarf/src/config/lang" @@ -13,12 +14,17 @@ import ( "github.com/spf13/cobra" // Import to initialize client auth plugins. + "k8s.io/apimachinery/pkg/runtime/schema" _ "k8s.io/client-go/plugin/pkg/client/auth" ) var ( waitTimeout string waitNamespace string + + waitResourceTimeout string + waitResourceNamespace string + waitResourceKubeconfig string ) var waitForCmd = &cobra.Command{ @@ -56,9 +62,40 @@ var waitForCmd = &cobra.Command{ }, } +var waitForResourceCmd = &cobra.Command{ + Use: "wait-for-resource { kind.group | kind.version.group } { NAME }", + Short: lang.CmdToolsWaitForShort, + Long: lang.CmdToolsWaitForLong, + Example: lang.CmdToolsWaitForExample, + Args: cobra.MinimumNArgs(2), + Run: func(cmd *cobra.Command, args []string) { + timeout, err := time.ParseDuration(waitResourceTimeout) + if err != nil { + message.Fatalf(err, lang.CmdToolsWaitForErrTimeoutString, waitResourceTimeout) + } + ctx, cancel := context.WithTimeout(cmd.Context(), timeout) + defer cancel() + // Accept passing version even if we ignore it. + gvk, gk := schema.ParseKindArg(args[0]) + if gvk != nil { + gk = gvk.GroupKind() + } + err = utils.ExecuteWaitResource(ctx, gk, waitResourceNamespace, args[1]) + if err != nil { + message.Fatal(err, err.Error()) + } + }, +} + func init() { toolsCmd.AddCommand(waitForCmd) waitForCmd.Flags().StringVar(&waitTimeout, "timeout", "5m", lang.CmdToolsWaitForFlagTimeout) waitForCmd.Flags().StringVarP(&waitNamespace, "namespace", "n", "", lang.CmdToolsWaitForFlagNamespace) waitForCmd.Flags().BoolVar(&message.NoProgress, "no-progress", false, lang.RootCmdFlagNoProgress) + + toolsCmd.AddCommand(waitForResourceCmd) + waitForResourceCmd.Flags().StringVar(&waitResourceTimeout, "timeout", "5m", lang.CmdToolsWaitForFlagTimeout) + waitForResourceCmd.Flags().StringVarP(&waitResourceKubeconfig, "kubeconfig", "f", "", lang.CmdToolsWaitForFlagNamespace) + waitForResourceCmd.Flags().StringVarP(&waitResourceNamespace, "namespace", "n", "", lang.CmdToolsWaitForFlagNamespace) + waitForResourceCmd.Flags().BoolVar(&message.NoProgress, "no-progress", false, lang.RootCmdFlagNoProgress) } diff --git a/src/pkg/utils/wait.go b/src/pkg/utils/wait.go index a4815e5ea5..eb78ef8cc3 100644 --- a/src/pkg/utils/wait.go +++ b/src/pkg/utils/wait.go @@ -5,6 +5,7 @@ package utils import ( + "context" "fmt" "net" "net/http" @@ -13,10 +14,17 @@ import ( "strings" "time" - "github.com/defenseunicorns/zarf/src/pkg/utils/exec" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/cli-utils/pkg/kstatus/polling" + "sigs.k8s.io/cli-utils/pkg/kstatus/polling/event" + "sigs.k8s.io/cli-utils/pkg/kstatus/status" + "sigs.k8s.io/cli-utils/pkg/object" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client/config" "github.com/defenseunicorns/zarf/src/config/lang" "github.com/defenseunicorns/zarf/src/pkg/message" + "github.com/defenseunicorns/zarf/src/pkg/utils/exec" ) // isJSONPathWaitType checks if the condition is a JSONPath or condition. @@ -28,6 +36,49 @@ func isJSONPathWaitType(condition string) bool { return true } +func ExecuteWaitResource(ctx context.Context, gk schema.GroupKind, namespace, name string) error { + cfg, err := config.GetConfig() + if err != nil { + return err + } + mgr, err := ctrl.NewManager(cfg, ctrl.Options{}) + if err != nil { + return err + } + identifiers := []object.ObjMetadata{ + { + GroupKind: gk, + Namespace: namespace, + Name: name, + }, + } + poller := polling.NewStatusPoller(mgr.GetClient(), mgr.GetRESTMapper(), polling.Options{}) + events := poller.Poll(ctx, identifiers, polling.PollOptions{PollInterval: 5 * time.Second}) + + spinnerMsg := fmt.Sprintf("Waiting for %s to be ready.", path.Join(gk.String(), name)) + spinner := message.NewProgressSpinner(spinnerMsg) + defer spinner.Stop() + for { + select { + case <-ctx.Done(): + return ctx.Err() + case e := <-events: + if e.Type == event.ErrorEvent { + return e.Error + } + switch e.Resource.Status { + case status.CurrentStatus: + spinner.Successf(e.Resource.Message) + return nil + case status.InProgressStatus: + spinner.Updatef(e.Resource.Message) + default: + message.Debug(e.Resource.Message) + } + } + } +} + // ExecuteWait executes the wait-for command. func ExecuteWait(waitTimeout, waitNamespace, condition, kind, identifier string, timeout time.Duration) error { // Handle network endpoints.