From 9aa8bd674021d1a3b62dcb2c138cfd12200da760 Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Wed, 11 Jun 2025 16:38:50 +0200 Subject: [PATCH] [cozypkg] Introduce cozypkg CLI tool Signed-off-by: Andrei Kvapil --- cmd/cozypkg/main.go | 1188 +++++++++++++++++ go.mod | 13 +- go.sum | 28 +- .../installer/images/cozystack/Dockerfile | 6 +- .../images/cozystack-api/Dockerfile | 2 +- .../images/cozystack-controller/Dockerfile | 2 +- 6 files changed, 1213 insertions(+), 26 deletions(-) create mode 100644 cmd/cozypkg/main.go diff --git a/cmd/cozypkg/main.go b/cmd/cozypkg/main.go new file mode 100644 index 00000000..29b28529 --- /dev/null +++ b/cmd/cozypkg/main.go @@ -0,0 +1,1188 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "log" + "os" + "path/filepath" + "regexp" + "sort" + "strings" + "time" + + // Kubernetes auth plugins (Azure, GCP, OIDC, ...). + _ "k8s.io/client-go/plugin/pkg/client/auth" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/discovery" + "k8s.io/client-go/dynamic" + "k8s.io/utils/pointer" + + helmaction "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chart/loader" + helmcfg "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/releaseutil" + + v2 "github.com/fluxcd/helm-controller/api/v2" + "github.com/imdario/mergo" + "github.com/opencontainers/go-digest" + "github.com/spf13/cobra" + clientsetscheme "k8s.io/client-go/kubernetes/scheme" + + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/tools/record" + + "github.com/databus23/helm-diff/v3/diff" + "github.com/databus23/helm-diff/v3/manifest" + + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + sigsyaml "sigs.k8s.io/yaml" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + "k8s.io/cli-runtime/pkg/printers" + + fluxmeta "github.com/fluxcd/pkg/apis/meta" + fluxchartutil "github.com/fluxcd/pkg/chartutil" + "github.com/fluxcd/pkg/runtime/conditions" + hchart "helm.sh/helm/v3/pkg/chartutil" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// Global CLI flags. +var Version = "dev" + +var ( + kubeCfgPath string + ns string + plain bool // render without talking to the API server + showOnly []string // template globs for `cozypkg show` + extraVals []string // additional -f/--values files +) + +func init() { + _ = v2.AddToScheme(clientsetscheme.Scheme) + _ = metav1.AddMetaToScheme(clientsetscheme.Scheme) +} + +// main is the application entry-point. +func main() { + log.SetFlags(0) + + root := &cobra.Command{ + Use: "cozypkg", + Short: "Cozy wrapper around Helm and Flux CD for local development", + Version: Version, + } + root.SetVersionTemplate("cozypkg version {{.Version}}\n") + + root.PersistentFlags().StringVar(&kubeCfgPath, "kubeconfig", "", "Path to kubeconfig") + root.PersistentFlags().StringVarP(&ns, "namespace", "n", "", "Kubernetes namespace (defaults to the current context)") + + _ = root.RegisterFlagCompletionFunc("namespace", completeNamespaces) + + root.AddCommand( + cmdShow(), + cmdApply(), + cmdDiff(), + cmdSuspend(), + cmdResume(), + cmdDelete(), + cmdList(), + cmdGet(), + cmdCompletion(), + cmdReconcile(), + ) + + root.AddCommand(&cobra.Command{ + Use: "version", + Short: "Print version", + Run: func(_ *cobra.Command, _ []string) { + fmt.Println("cozypkg", Version) + }, + }) + + root.SilenceErrors = true + root.SilenceUsage = true + if err := root.Execute(); err != nil { + log.Fatalf("error: %v", err) + } +} + +// restConfig builds a *rest.Config from the --kubeconfig flag or $KUBECONFIG. +func restConfig() (*rest.Config, error) { + cfg := kubeCfgPath + if cfg == "" { + cfg = os.Getenv("KUBECONFIG") + } + return clientcmd.BuildConfigFromFlags("", cfg) +} + +// helmCfg returns an initialised Helm configuration bound to a namespace. +func helmCfg(rc *rest.Config, namespace string) (*helmaction.Configuration, *helmcfg.EnvSettings, error) { + env := helmcfg.New() + if kubeCfgPath != "" { + env.KubeConfig = kubeCfgPath + } + env.SetNamespace(namespace) + + cfg := new(helmaction.Configuration) + if err := cfg.Init(env.RESTClientGetter(), namespace, "secret", log.Printf); err != nil { + return nil, nil, err + } + return cfg, env, nil +} + +// fluxPostRenderer injects Flux labels so that rendered manifests match the +// server-side state. +type fluxPostRenderer struct{ name, ns string } + +// Run adds Flux labels to every object. +func (f *fluxPostRenderer) Run(in *bytes.Buffer) (*bytes.Buffer, error) { + docs := bytes.Split(in.Bytes(), []byte("\n---")) + var out [][]byte + + for _, d := range docs { + if len(bytes.TrimSpace(d)) == 0 { + continue + } + + var obj map[string]interface{} + if err := sigsyaml.Unmarshal(d, &obj); err != nil || obj == nil { + // keep lists / empty docs intact + out = append(out, d) + continue + } + + md := ensureMap(obj, "metadata") + lbl := ensureMap(md, "labels") + lbl["helm.toolkit.fluxcd.io/name"] = f.name + lbl["helm.toolkit.fluxcd.io/namespace"] = f.ns + + rendered, err := sigsyaml.Marshal(obj) + if err != nil { + return nil, err + } + out = append(out, rendered) + } + + return bytes.NewBuffer(bytes.Join(out, []byte("\n---\n"))), nil +} + +// ensureMap returns the map under parent[key], creating it if needed. +func ensureMap(parent map[string]interface{}, key string) map[string]interface{} { + if parent == nil { + return map[string]interface{}{} + } + if v, ok := parent[key]; ok { + if m, ok := v.(map[string]interface{}); ok { + return m + } + } + m := map[string]interface{}{} + parent[key] = m + return m +} + +// mergedValues merges valuesFiles and inline .spec.values in the given HelmRelease. +func mergedValues(hr *v2.HelmRelease, chartDir string) (map[string]interface{}, error) { + vals := map[string]interface{}{} + + for _, vf := range hr.Spec.Chart.Spec.ValuesFiles { + if vf == "-" { // stdin placeholder – not applicable here + continue + } + data, err := os.ReadFile(filepath.Join(chartDir, vf)) + if err != nil { + return nil, err + } + var mv map[string]interface{} + if err := sigsyaml.Unmarshal(data, &mv); err != nil { + return nil, err + } + if err := mergo.Merge(&vals, mv, mergo.WithOverride); err != nil { + return nil, err + } + } + + if hr.Spec.Values != nil && len(hr.Spec.Values.Raw) > 0 { + var mv map[string]interface{} + if err := sigsyaml.Unmarshal(hr.Spec.Values.Raw, &mv); err != nil { + return nil, err + } + if err := mergo.Merge(&vals, mv, mergo.WithOverride); err != nil { + return nil, err + } + } + + return vals, nil +} + +// renderManifests performs a Helm dry-run render and returns the manifest text. +func renderManifests(cfg *helmaction.Configuration, hr *v2.HelmRelease, chartDir string, vals map[string]interface{}, rc *rest.Config) (string, error) { + inst := helmaction.NewInstall(cfg) + inst.DryRun = true + inst.DryRunOption = "server" + inst.ReleaseName = hr.Name + inst.Namespace = hr.Namespace + inst.DisableHooks = true + + if plain { + vers, err := discoverAPIVersions(rc) + if err != nil { + return "", err + } + inst.APIVersions = vers + inst.ClientOnly = true + } else { + inst.PostRenderer = &fluxPostRenderer{name: hr.Name, ns: hr.Namespace} + } + + ch, err := loader.Load(chartDir) + if err != nil { + return "", err + } + rel, err := inst.Run(ch, vals) + if err != nil { + return "", err + } + return rel.Manifest, nil +} + +// upgradeRelease runs a Helm upgrade (installing if necessary). +func upgradeRelease(cfg *helmaction.Configuration, hr *v2.HelmRelease, chartDir string, vals map[string]interface{}) error { + up := helmaction.NewUpgrade(cfg) + up.Namespace = hr.Namespace + up.Install = true + if !plain { + up.PostRenderer = &fluxPostRenderer{name: hr.Name, ns: hr.Namespace} + } + + ch, err := loader.Load(chartDir) + if err != nil { + return err + } + _, err = up.Run(hr.Name, ch, vals) + return err +} + +// realHelmDiff returns a textual diff between the live release and desired state. +func realHelmDiff(cfg *helmaction.Configuration, hr *v2.HelmRelease, chartDir string, vals map[string]interface{}) (string, error) { + var buf bytes.Buffer + + get := helmaction.NewGet(cfg) + rel, err := get.Run(hr.Name) + var current []byte + if err == nil { + current = []byte(rel.Manifest) + } else if !strings.Contains(err.Error(), "release: not found") { + return "", fmt.Errorf("failed to get release: %w", err) + } + + inst := helmaction.NewInstall(cfg) + inst.DryRun = true + inst.DryRunOption = "server" + inst.ClientOnly = true + inst.ReleaseName = hr.Name + inst.Namespace = hr.Namespace + inst.DisableHooks = true + inst.PostRenderer = &fluxPostRenderer{name: hr.Name, ns: hr.Namespace} + + ch, err := loader.Load(chartDir) + if err != nil { + return "", err + } + dry, err := inst.Run(ch, vals) + if err != nil { + return "", err + } + desired := []byte(dry.Manifest) + + curSpecs := manifest.Parse(string(current), hr.Namespace, false, manifest.Helm3TestHook, manifest.Helm2TestSuccessHook) + newSpecs := manifest.Parse(string(desired), hr.Namespace, false, manifest.Helm3TestHook, manifest.Helm2TestSuccessHook) + + _ = diff.Manifests(curSpecs, newSpecs, &diff.Options{OutputContext: -1}, &buf) + return buf.String(), nil +} + +// runFn is a helper signature passed to cmdFactory. +type runFn func(*helmaction.Configuration, *v2.HelmRelease, string) error + +// cmdFactory fetches (or stubs) a HelmRelease and invokes runFn. +func cmdFactory(name string, fn runFn) *cobra.Command { + cmd := &cobra.Command{ + Use: name + " ", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + relName := args[0] + + if name == "show" || name == "diff" || name == "apply" { + chartDir, err := filepath.Abs(".") + if err != nil { + return fmt.Errorf("unable to determine current directory: %w", err) + } + if ok, err := hchart.IsChartDir(chartDir); err != nil || !ok { + return fmt.Errorf("invalid Helm chart in %q: %w", chartDir, err) + } + } + + // Offline mode: create a minimal stub. + if plain { + if ns == "" { + ns = "default" + } + stub := &v2.HelmRelease{ObjectMeta: metav1.ObjectMeta{Name: relName, Namespace: ns}} + rc, _ := restConfig() + cfg, _, err := helmCfg(rc, ns) + if err != nil { + return err + } + return fn(cfg, stub, ".") + } + + rc, err := restConfig() + if err != nil { + return err + } + + if ns == "" { + ns, _, err = defaultNamespace() + if err != nil { + return err + } + } + + cfg, _, err := helmCfg(rc, ns) + if err != nil { + return err + } + + cl, err := ctrlclient.New(rc, ctrlclient.Options{}) + if err != nil { + return err + } + + var hr v2.HelmRelease + if err := cl.Get(context.TODO(), ctrlclient.ObjectKey{Namespace: ns, Name: relName}, &hr); err != nil { + return err + } + + return fn(cfg, &hr, ".") + }, + } + cmd.ValidArgsFunction = completeHelmReleases + return cmd +} + +// cmdShow returns the `cozypkg show` command. +func cmdShow() *cobra.Command { + cmd := cmdFactory("show", func(cfg *helmaction.Configuration, hr *v2.HelmRelease, chartDir string) error { + var vals map[string]interface{} + if plain { + vals = map[string]interface{}{} + } else { + var err error + vals, err = mergedValues(hr, chartDir) + if err != nil { + return err + } + } + + if add, err := loadExtraValues(); err != nil { + return err + } else if err := mergo.Merge(&vals, add, mergo.WithOverride); err != nil { + return err + } + + rc, _ := restConfig() + mani, err := renderManifests(cfg, hr, chartDir, vals, rc) + if err != nil { + return err + } + + if len(showOnly) == 0 { + fmt.Print(mani) + return nil + } + + split := releaseutil.SplitManifests(mani) + keys := make([]string, 0, len(split)) + for k := range split { + keys = append(keys, k) + } + sort.Sort(releaseutil.BySplitManifestsOrder(keys)) + + reSrc := regexp.MustCompile(`# Source: [^/]+/(.+)`) // emulate helm --show-only + for _, glob := range showOnly { + found := false + for _, k := range keys { + m := split[k] + sm := reSrc.FindStringSubmatch(m) + if len(sm) == 0 { + continue + } + if ok, _ := filepath.Match(filepath.ToSlash(glob), sm[1]); !ok { + continue + } + fmt.Printf("---\n%s\n", m) + found = true + } + if !found { + return fmt.Errorf("template %s not found in chart", glob) + } + } + return nil + }) + cmd.Short = "Render manifests like helm template" + cmd.Flags().StringSliceVarP(&showOnly, "show-only", "s", nil, "Render only templates matching glob(s)") + cmd.Flags().StringSliceVarP(&extraVals, "values", "f", nil, "Additional values files (may be repeated)") + cmd.Flags().BoolVar(&plain, "plain", false, "Render chart without querying values from the HelmRelease") + return cmd +} + +// cmdApply returns the `cozypkg apply` command. +func cmdApply() *cobra.Command { + var autoResume bool + + cmd := cmdFactory("apply", func(cfg *helmaction.Configuration, hr *v2.HelmRelease, chartDir string) error { + ctx := context.Background() + + rc, _ := restConfig() + cl, _ := ctrlclient.New(rc, ctrlclient.Options{}) + + bc := record.NewBroadcaster() + defer bc.Shutdown() + rec := bc.NewRecorder(clientsetscheme.Scheme, corev1.EventSource{Component: "cozypkg"}) + + // Suspend before touching Helm. + if err := patchSuspend(ctx, cl, hr.Namespace, hr.Name, pointer.Bool(true)); err != nil { + return err + } + fmt.Fprintf(os.Stderr, "HelmRelease %s/%s suspended\n", hr.Namespace, hr.Name) + + vals, err := mergedValues(hr, chartDir) + if err != nil { + return err + } + if add, err := loadExtraValues(); err != nil { + return err + } else if err := mergo.Merge(&vals, add, mergo.WithOverride); err != nil { + return err + } + + cfgDigest := fluxchartutil.DigestValues(digest.Canonical, vals).String() + chartVer := hr.Spec.Chart.Spec.Version + + hr.Status.LastAttemptedGeneration = hr.Generation + hr.Status.LastAttemptedRevision = chartVer + hr.Status.LastAttemptedConfigDigest = cfgDigest + hr.Status.LastAttemptedReleaseAction = v2.ReleaseActionUpgrade + _ = cl.Status().Update(ctx, hr) + + if err := upgradeRelease(cfg, hr, chartDir, vals); err != nil { + markFailure(ctx, cl, nil, hr, err) + return err + } + + if autoResume { + if err := patchSuspend(ctx, cl, hr.Namespace, hr.Name, nil); err != nil { + return err + } + } + + markSuccess(ctx, cl, rec, hr, chartVer, cfgDigest) + return nil + }) + + cmd.Short = "Upgrade or install HelmRelease and sync status" + cmd.Flags().BoolVar(&plain, "plain", false, "Install chart without querying values from the HelmRelease") + cmd.Flags().BoolVar(&autoResume, "resume", false, "Automatically clear spec.suspend after successful apply") + cmd.Flags().StringSliceVarP(&extraVals, "values", "f", nil, "Additional values files (may be repeated)") + return cmd +} + +// cmdDiff returns the `cozypkg diff` command. +func cmdDiff() *cobra.Command { + cmd := cmdFactory("diff", func(cfg *helmaction.Configuration, hr *v2.HelmRelease, chartDir string) error { + var vals map[string]interface{} + if plain { + vals = map[string]interface{}{} + } else { + var err error + vals, err = mergedValues(hr, chartDir) + if err != nil { + return err + } + } + + if add, err := loadExtraValues(); err != nil { + return err + } else if err := mergo.Merge(&vals, add, mergo.WithOverride); err != nil { + return err + } + + out, err := realHelmDiff(cfg, hr, chartDir, vals) + if err != nil { + return err + } + fmt.Print(out) + return nil + }) + + cmd.Short = "Show a diff between live and desired manifests" + cmd.Flags().StringSliceVarP(&extraVals, "values", "f", nil, "Additional values files (may be repeated)") + cmd.Flags().BoolVar(&plain, "plain", false, "Render chart without querying values from the HelmRelease") + return cmd +} + +// cmdSuspend returns the `cozypkg suspend` command. +func cmdSuspend() *cobra.Command { + cmd := cmdFactory("suspend", func(_ *helmaction.Configuration, hr *v2.HelmRelease, _ string) error { + rc, _ := restConfig() + cl, _ := ctrlclient.New(rc, ctrlclient.Options{}) + return patchSuspend(context.Background(), cl, hr.Namespace, hr.Name, pointer.Bool(true)) + }) + cmd.Short = "Suspend Flux HelmRelease" + return cmd +} + +// cmdResume returns the `cozypkg resume` command. +func cmdResume() *cobra.Command { + cmd := cmdFactory("resume", func(_ *helmaction.Configuration, hr *v2.HelmRelease, _ string) error { + rc, _ := restConfig() + cl, _ := ctrlclient.New(rc, ctrlclient.Options{}) + return patchSuspend(context.Background(), cl, hr.Namespace, hr.Name, nil) + }) + cmd.Short = "Resume Flux HelmRelease" + return cmd +} + +// cmdDelete returns the `cozypkg delete` command. +func cmdDelete() *cobra.Command { + cmd := cmdFactory("delete", func(cfg *helmaction.Configuration, hr *v2.HelmRelease, _ string) error { + un := helmaction.NewUninstall(cfg) + _, err := un.Run(hr.Name) + return err + }) + cmd.Short = "Uninstall the Helm release" + return cmd +} + +// defaultNamespace returns the namespace from the kubeconfig or "default". +func defaultNamespace() (string, bool, error) { + cfg := kubeCfgPath + if cfg == "" { + cfg = os.Getenv("KUBECONFIG") + } + loading := &clientcmd.ClientConfigLoadingRules{ExplicitPath: cfg} + cc := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loading, &clientcmd.ConfigOverrides{}) + return cc.Namespace() +} + +// serverTable retrieves a metav1.Table from the API server. +func serverTable(cfg *rest.Config, gvr schema.GroupVersionResource, namespace, name string) (*metav1.Table, error) { + rcfg := rest.CopyConfig(cfg) + rcfg.APIPath = "/apis" + rcfg.GroupVersion = &schema.GroupVersion{Group: gvr.Group, Version: gvr.Version} + rcfg.NegotiatedSerializer = clientsetscheme.Codecs.WithoutConversion() + + rc, err := rest.RESTClientFor(rcfg) + if err != nil { + return nil, err + } + + req := rc.Get() + if namespace != "" { + req = req.Namespace(namespace) + } + req = req.Resource(gvr.Resource) + if name != "" { + req = req.Name(name) + } + + req.SetHeader("Accept", "application/json;as=Table;g=meta.k8s.io;v=v1,application/json") + req.Param("includeObject", "Object") + + raw, err := req.DoRaw(context.TODO()) + if err != nil { + return nil, err + } + + obj, _, err := clientsetscheme.Codecs.UniversalDeserializer().Decode(raw, nil, nil) + if err != nil { + return nil, err + } + + table, ok := obj.(*metav1.Table) + if !ok { + return nil, fmt.Errorf("unexpected object kind: %T", obj) + } + return table, nil +} + +// prependNamespaceColumn adds the NAMESPACE column when listing across all namespaces. +func prependNamespaceColumn(t *metav1.Table) error { + if len(t.ColumnDefinitions) > 0 && strings.EqualFold(t.ColumnDefinitions[0].Name, "NAMESPACE") { + return nil + } + + nsCol := metav1.TableColumnDefinition{Name: "NAMESPACE", Type: "string"} + t.ColumnDefinitions = append([]metav1.TableColumnDefinition{nsCol}, t.ColumnDefinitions...) + + for i, row := range t.Rows { + ns := "" + if row.Object.Object != nil { + if acc, err := meta.Accessor(row.Object.Object); err == nil { + ns = acc.GetNamespace() + } + } else if len(row.Object.Raw) > 0 { + if decoded, _, err := clientsetscheme.Codecs.UniversalDeserializer().Decode(row.Object.Raw, nil, nil); err == nil { + if acc, err := meta.Accessor(decoded); err == nil { + ns = acc.GetNamespace() + } + } + } + row.Cells = append([]interface{}{ns}, row.Cells...) + t.Rows[i] = row + } + return nil +} + +// runHRCommand implements shared logic for `get` and `list`. +func runHRCommand(cmd *cobra.Command, args []string, allNS bool, output *string) error { + if allNS && len(args) > 0 { + return fmt.Errorf("-A/--all-namespaces may be used only when no names are specified") + } + + rc, err := restConfig() + if err != nil { + return err + } + + nsLocal := ns + if nsLocal == "" && !allNS { + nsLocal, _, err = defaultNamespace() + if err != nil { + return err + } + } + + gvr := schema.GroupVersionResource{Group: "helm.toolkit.fluxcd.io", Version: "v2", Resource: "helmreleases"} + wantTable := *output == "" || *output == "wide" + + obj, err := collectHR(cmd.Context(), rc, gvr, nsLocal, allNS, args, wantTable) + if err != nil { + return err + } + + if wantTable { + tp := printers.NewTablePrinter(printers.PrintOptions{Wide: *output == "wide"}) + return tp.PrintObj(obj, cmd.OutOrStdout()) + } + + pf := genericclioptions.NewPrintFlags("").WithTypeSetter(clientsetscheme.Scheme) + pf.OutputFormat = output + pr, _ := pf.ToPrinter() + return pr.PrintObj(obj, cmd.OutOrStdout()) +} + +// collectHR aggregates HelmRelease objects (raw or as tables) depending on the +// requested output. +func collectHR( + ctx context.Context, + rc *rest.Config, + gvr schema.GroupVersionResource, + ns string, + allNS bool, + names []string, // nil | len==0 → list current NS / cluster + wantTable bool, +) (runtime.Object, error) { + + // ─── helpers ------------------------------------------------------------ + dc, _ := dynamic.NewForConfig(rc) + + fetchTable := func(targetNS, name string) (*metav1.Table, error) { + return serverTable(rc, gvr, targetNS, name) + } + fetchObj := func(targetNS, name string) (*unstructured.Unstructured, error) { + if name == "" { // list + list, err := dc.Resource(gvr).Namespace(targetNS).List(ctx, metav1.ListOptions{}) + if err != nil { + return nil, err + } + ul := &unstructured.Unstructured{} + ul.Object = list.Object + return ul, nil + } + return dc.Resource(gvr).Namespace(targetNS).Get(ctx, name, metav1.GetOptions{}) + } + + // ─── table branch ------------------------------------------------------- + if wantTable { + var agg *metav1.Table + operate := func(targetNS, name string) error { + tbl, err := fetchTable(targetNS, name) + if err != nil { + return err + } + if allNS { + _ = prependNamespaceColumn(tbl) + } + if agg == nil { + agg = tbl + } else { + agg.Rows = append(agg.Rows, tbl.Rows...) + } + return nil + } + + if len(names) == 0 { // list + targetNS := "" + if !allNS { + targetNS = ns + } + if err := operate(targetNS, ""); err != nil { + return nil, err + } + return agg, nil + } + + for _, n := range names { + targetNS := ns + if allNS { + targetNS = "" + } + if err := operate(targetNS, n); err != nil { + return nil, err + } + } + return agg, nil + } + + // ─── raw branch --------------------------------------------------------- + ulist := &unstructured.UnstructuredList{Object: map[string]interface{}{ + "apiVersion": "v1", "kind": "List", + }} + + if len(names) == 0 { // list + targetNS := "" + if !allNS { + targetNS = ns + } + u, err := fetchObj(targetNS, "") + if err != nil { + return nil, err + } + for i := range u.Object["items"].([]interface{}) { + item := u.Object["items"].([]interface{})[i].(map[string]interface{}) + ulist.Items = append(ulist.Items, unstructured.Unstructured{Object: item}) + } + return ulist, nil + } + + if len(names) == 1 { + targetNS := ns + if allNS { + targetNS = "" + } + return fetchObj(targetNS, names[0]) + } + + for _, n := range names { + targetNS := ns + if allNS { + targetNS = "" + } + u, err := fetchObj(targetNS, n) + if err != nil { + return nil, err + } + ulist.Items = append(ulist.Items, *u) + } + return ulist, nil +} + +// cmdGet exposes `cozypkg get`. +func cmdGet() *cobra.Command { + var ( + allNS bool + output string + ) + + cmd := &cobra.Command{ + Use: "get [release...]", + Short: "Get one or many HelmReleases", + Args: cobra.ArbitraryArgs, + RunE: func(c *cobra.Command, args []string) error { return runHRCommand(c, args, allNS, &output) }, + } + cmd.ValidArgsFunction = completeHelmReleases + + cmd.Flags().BoolVarP(&allNS, "all-namespaces", "A", false, "Across all namespaces") + cmd.Flags().StringVarP(&output, "output", "o", "", "json|yaml|wide|name|custom-columns=<...>") + return cmd +} + +// cmdList exposes `cozypkg list` (alias: ls). +func cmdList() *cobra.Command { + var ( + allNS bool + output string + ) + + cmd := &cobra.Command{ + Use: "list [release...]", + Aliases: []string{"ls"}, + Short: "List HelmReleases", + Args: cobra.ArbitraryArgs, + RunE: func(c *cobra.Command, args []string) error { return runHRCommand(c, args, allNS, &output) }, + } + cmd.ValidArgsFunction = completeHelmReleases + + cmd.Flags().BoolVarP(&allNS, "all-namespaces", "A", false, "Across all namespaces (only when no names are given)") + cmd.Flags().StringVarP(&output, "output", "o", "", "json|yaml|wide|name|custom-columns=<...>") + return cmd +} + +// newHistoryEntry creates a v2.Snapshot for status.history. +func newHistoryEntry(hr *v2.HelmRelease, chartVersion, cfgDigest string) *v2.Snapshot { + return &v2.Snapshot{ + Name: hr.Spec.Chart.Spec.Chart, + Namespace: hr.Namespace, + Version: 1, + ChartName: hr.Spec.Chart.Spec.Chart, + ChartVersion: chartVersion, + ConfigDigest: cfgDigest, + FirstDeployed: metav1.Now(), + LastDeployed: metav1.Now(), + Status: "deployed", + } +} + +// markSuccess sets Ready=True and emits a normal event. +func markSuccess(ctx context.Context, cl ctrlclient.Client, rec record.EventRecorder, hr *v2.HelmRelease, chartVer, cfgDigest string) { + msg := fmt.Sprintf("Helm upgrade succeeded for %s/%s with chart %s@%s", hr.Namespace, hr.Name, hr.Spec.Chart.Spec.Chart, chartVer) + + conditions.MarkTrue(hr, v2.ReleasedCondition, v2.UpgradeSucceededReason, msg) + conditions.MarkTrue(hr, fluxmeta.ReadyCondition, v2.UpgradeSucceededReason, msg) + + hr.Status.History = append(hr.Status.History, newHistoryEntry(hr, chartVer, cfgDigest)) + hr.Status.Failures = 0 + hr.Status.ObservedGeneration = hr.Generation + _ = cl.Status().Update(ctx, hr) + if rec != nil { + rec.Event(hr, corev1.EventTypeNormal, v2.UpgradeSucceededReason, msg) + } +} + +// markFailure sets Ready=False and emits a warning event. +func markFailure(ctx context.Context, cl ctrlclient.Client, rec record.EventRecorder, hr *v2.HelmRelease, err error) { + msg := fmt.Sprintf("Helm upgrade failed for %s/%s: %s", hr.Namespace, hr.Name, err.Error()) + + conditions.MarkFalse(hr, v2.ReleasedCondition, v2.UpgradeFailedReason, err.Error()) + conditions.MarkFalse(hr, fluxmeta.ReadyCondition, v2.UpgradeFailedReason, err.Error()) + + hr.Status.Failures++ + hr.Status.ObservedGeneration = hr.Generation + _ = cl.Status().Update(ctx, hr) + if rec != nil { + rec.Event(hr, corev1.EventTypeWarning, v2.UpgradeFailedReason, msg) + } +} + +// patchSuspend toggles spec.suspend using a merge-patch with a Flux field owner. +func patchSuspend(ctx context.Context, cl ctrlclient.Client, ns, name string, val *bool) error { + var payload []byte + switch { + case val == nil: + payload = []byte(`{"spec":{"suspend":null}}`) + case *val: + payload = []byte(`{"spec":{"suspend":true}}`) + default: + payload = []byte(`{"spec":{"suspend":false}}`) + } + + p := client.RawPatch(types.MergePatchType, payload) + obj := &v2.HelmRelease{ObjectMeta: metav1.ObjectMeta{Namespace: ns, Name: name}} + return cl.Patch(ctx, obj, p, client.FieldOwner("flux-client-side-apply")) +} + +// discoverAPIVersions enumerates all apiVersions advertised by the cluster. +func discoverAPIVersions(rc *rest.Config) (hchart.VersionSet, error) { + dc, err := discovery.NewDiscoveryClientForConfig(rc) + if err != nil { + return nil, err + } + grps, err := dc.ServerGroups() + if err != nil { + return nil, err + } + var vers []string + for _, g := range grps.Groups { + for _, v := range g.Versions { + if g.Name == "" { + vers = append(vers, v.Version) // core: v1 + } else { + vers = append(vers, g.Name+"/"+v.Version) + } + } + } + return hchart.VersionSet(vers), nil +} + +// loadExtraValues merges all files passed via -f/--values. +func loadExtraValues() (map[string]interface{}, error) { + merged := map[string]interface{}{} + for _, vf := range extraVals { + data, err := os.ReadFile(vf) + if err != nil { + return nil, err + } + var mv map[string]interface{} + if err := sigsyaml.Unmarshal(data, &mv); err != nil { + return nil, err + } + if err := mergo.Merge(&merged, mv, mergo.WithOverride); err != nil { + return nil, err + } + } + return merged, nil +} + +func cmdCompletion() *cobra.Command { + return &cobra.Command{ + Use: "completion [bash|zsh|fish|powershell]", + Short: "Generate the autocompletion script for the specified shell", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + root := cmd.Root() + switch args[0] { + case "bash": + return root.GenBashCompletion(os.Stdout) + case "zsh": + return root.GenZshCompletion(os.Stdout) + case "fish": + return root.GenFishCompletion(os.Stdout, true) + case "powershell": + return root.GenPowerShellCompletionWithDesc(os.Stdout) + default: + return fmt.Errorf("unknown shell: %s", args[0]) + } + }, + } +} + +func completeNamespaces(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + rc, err := restConfig() + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + cl, err := ctrlclient.New(rc, ctrlclient.Options{}) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + var nsList corev1.NamespaceList + if err := cl.List(context.TODO(), &nsList); err != nil { + return nil, cobra.ShellCompDirectiveError + } + + var suggestions []string + for _, ns := range nsList.Items { + if strings.HasPrefix(ns.Name, toComplete) { + suggestions = append(suggestions, ns.Name) + } + } + return suggestions, cobra.ShellCompDirectiveNoFileComp +} + +func completeHelmReleases(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + rc, err := restConfig() + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + cl, err := ctrlclient.New(rc, ctrlclient.Options{}) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + nsLocal := ns + if nsLocal == "" { + nsLocal, _, err = defaultNamespace() + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + } + + var list v2.HelmReleaseList + if err := cl.List(context.TODO(), &list, &client.ListOptions{Namespace: nsLocal}); err != nil { + return nil, cobra.ShellCompDirectiveError + } + + var out []string + for _, hr := range list.Items { + if strings.HasPrefix(hr.Name, toComplete) { + out = append(out, hr.Name) + } + } + return out, cobra.ShellCompDirectiveNoFileComp +} + +// cmdReconcile returns the `cozypkg reconcile` command. +func cmdReconcile() *cobra.Command { + var ( + withSource bool + force bool + ) + + cmd := cmdFactory("reconcile", func(_ *helmaction.Configuration, hr *v2.HelmRelease, _ string) error { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + // ------------------------------------------------------------------ // + // Clients & helpers // + // ------------------------------------------------------------------ // + rc, err := restConfig() + if err != nil { + return err + } + cl, err := ctrlclient.New(rc, ctrlclient.Options{}) + if err != nil { + return err + } + dyn, err := dynamic.NewForConfig(rc) + if err != nil { + return err + } + + waitByWatch := func(gvr schema.GroupVersionResource, ns, name, field, want string) error { + w, err := dyn.Resource(gvr).Namespace(ns). + Watch(ctx, metav1.ListOptions{FieldSelector: "metadata.name=" + name}) + if err != nil { + return err + } + for ev := range w.ResultChan() { + u := ev.Object.(*unstructured.Unstructured) + + v, _, _ := unstructured.NestedString(u.Object, "status", field) + if v == want { + // — After match, check for failure + conds, found, _ := unstructured.NestedSlice(u.Object, "status", "conditions") + if found { + for _, c := range conds { + m, ok := c.(map[string]interface{}) + if !ok { + continue + } + if m["type"] == "Ready" { + status := m["status"] + if status == "True" { + return nil + } + msg, _ := m["message"].(string) + if msg == "" { + msg = "unknown failure" + } + return fmt.Errorf("%s/%s: reconciliation failed: %s", gvr.Resource, name, msg) + } + } + } + return fmt.Errorf("%s/%s: reconciliation did not report Ready=True", gvr.Resource, name) + } + } + return fmt.Errorf("%s/%s: timeout waiting for %s=%s", gvr.Resource, name, field, want) + } + + // ------------------------------------------------------------------ // + // 1. (optional) HelmChart // + // ------------------------------------------------------------------ // + if withSource { + chartNS := hr.Spec.Chart.Spec.SourceRef.Namespace + if chartNS == "" { + chartNS = hr.Namespace + } + chartName := fmt.Sprintf("%s-%s", hr.Namespace, hr.Name) + chartGVR := schema.GroupVersionResource{ + Group: "source.toolkit.fluxcd.io", Version: "v1", Resource: "helmcharts", + } + + // annotate + chart := &unstructured.Unstructured{} + chart.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "source.toolkit.fluxcd.io", + Version: "v1", + Kind: "HelmChart", + }) + if err := cl.Get(ctx, types.NamespacedName{Namespace: chartNS, Name: chartName}, chart); err != nil { + return fmt.Errorf("HelmChart %s/%s not found: %w", chartNS, chartName, err) + } + ts := time.Now().Format(time.RFC3339Nano) + patch := client.MergeFrom(chart.DeepCopy()) + ann := chart.GetAnnotations() + if ann == nil { + ann = map[string]string{} + } + ann["reconcile.fluxcd.io/requestedAt"] = ts + chart.SetAnnotations(ann) + if err := cl.Patch(ctx, chart, patch); err != nil { + return fmt.Errorf("patch HelmChart: %w", err) + } + fmt.Fprintf(os.Stderr, "✔ HelmChart %s annotated\n", chartName) + + fmt.Fprintln(os.Stderr, "◎ waiting for HelmChart reconciliation") + if err := waitByWatch(chartGVR, chartNS, chartName, "lastHandledReconcileAt", ts); err != nil { + return err + } + fmt.Fprintln(os.Stderr, "✔ HelmChart reconciled") + } + + // ------------------------------------------------------------------ // + // 2. HelmRelease // + // ------------------------------------------------------------------ // + ts := time.Now().Format(time.RFC3339Nano) + ann := map[string]interface{}{ + "reconcile.fluxcd.io/requestedAt": ts, + } + if force { + ann["reconcile.fluxcd.io/forceAt"] = ts + } + patch := map[string]interface{}{"metadata": map[string]interface{}{"annotations": ann}} + pbytes, _ := json.Marshal(patch) + if err := cl.Patch(ctx, hr, + ctrlclient.RawPatch(types.MergePatchType, pbytes)); err != nil { + return fmt.Errorf("patch HelmRelease: %w", err) + } + fmt.Fprintf(os.Stderr, "✔ HelmRelease %s annotated\n", hr.Name) + + fmt.Fprintln(os.Stderr, "◎ waiting for HelmRelease reconciliation") + hrGVR := schema.GroupVersionResource{ + Group: "helm.toolkit.fluxcd.io", Version: "v2", Resource: "helmreleases", + } + if err := waitByWatch(hrGVR, hr.Namespace, hr.Name, "lastHandledReconcileAt", ts); err != nil { + return err + } + if force { + if err := waitByWatch(hrGVR, hr.Namespace, hr.Name, "lastHandledForceAt", ts); err != nil { + return err + } + } + fmt.Fprintln(os.Stderr, "✔ HelmRelease reconciled") + return nil + }) + + cmd.Short = "Trigger HelmRelease reconciliation (optionally its HelmChart)" + cmd.Flags().BoolVar(&withSource, "with-source", false, + "Reconcile the source HelmChart before the HelmRelease") + cmd.Flags().BoolVar(&force, "force", false, + "Force a one-off upgrade of the HelmRelease") + return cmd +} diff --git a/go.mod b/go.mod index 35f9bc60..c0ce39cf 100644 --- a/go.mod +++ b/go.mod @@ -37,7 +37,6 @@ require ( github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect - github.com/evanphx/json-patch v4.12.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.9.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fluxcd/pkg/apis/kustomize v1.6.1 // indirect @@ -92,14 +91,14 @@ require ( go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/crypto v0.31.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/net v0.33.0 // indirect + golang.org/x/net v0.30.0 // indirect golang.org/x/oauth2 v0.23.0 // indirect - golang.org/x/sync v0.10.0 // indirect - golang.org/x/sys v0.28.0 // indirect - golang.org/x/term v0.27.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/term v0.25.0 // indirect + golang.org/x/text v0.19.0 // indirect golang.org/x/time v0.7.0 // indirect golang.org/x/tools v0.26.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index c7aaa336..382420fe 100644 --- a/go.sum +++ b/go.sum @@ -26,8 +26,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= -github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= +github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= @@ -212,8 +212,8 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -222,26 +222,26 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= -golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/packages/core/installer/images/cozystack/Dockerfile b/packages/core/installer/images/cozystack/Dockerfile index a81b43a5..f4cdb508 100644 --- a/packages/core/installer/images/cozystack/Dockerfile +++ b/packages/core/installer/images/cozystack/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:alpine3.21 as k8s-await-election-builder +FROM golang:1.24-alpine as k8s-await-election-builder ARG K8S_AWAIT_ELECTION_GITREPO=https://github.com/LINBIT/k8s-await-election ARG K8S_AWAIT_ELECTION_VERSION=0.4.1 @@ -13,7 +13,7 @@ RUN git clone ${K8S_AWAIT_ELECTION_GITREPO} /usr/local/go/k8s-await-election/ \ && make \ && mv ./out/k8s-await-election-${TARGETARCH} /k8s-await-election -FROM golang:alpine3.21 as builder +FROM golang:1.24-alpine as builder RUN apk add --no-cache make git RUN apk add helm --repository=https://dl-cdn.alpinelinux.org/alpine/edge/community @@ -26,7 +26,7 @@ RUN go build -o /cozystack-assets-server -ldflags '-extldflags "-static" -w -s' # Check that versions_map is not changed RUN make repos -FROM alpine:3.21 +FROM alpine:3.22 RUN apk add --no-cache make RUN apk add helm kubectl --repository=https://dl-cdn.alpinelinux.org/alpine/edge/community diff --git a/packages/system/cozystack-api/images/cozystack-api/Dockerfile b/packages/system/cozystack-api/images/cozystack-api/Dockerfile index c3f90f01..ea7bdc10 100644 --- a/packages/system/cozystack-api/images/cozystack-api/Dockerfile +++ b/packages/system/cozystack-api/images/cozystack-api/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.23-alpine AS builder +FROM golang:1.24-alpine AS builder ARG TARGETOS ARG TARGETARCH diff --git a/packages/system/cozystack-controller/images/cozystack-controller/Dockerfile b/packages/system/cozystack-controller/images/cozystack-controller/Dockerfile index b44d5ac8..c7ada9ef 100644 --- a/packages/system/cozystack-controller/images/cozystack-controller/Dockerfile +++ b/packages/system/cozystack-controller/images/cozystack-controller/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.23-alpine AS builder +FROM golang:1.24-alpine AS builder ARG TARGETOS ARG TARGETARCH