From 5d82d85b273dd5ee790be5e8d90701ce0bd34857 Mon Sep 17 00:00:00 2001 From: Bruno Michel Date: Tue, 24 Oct 2023 10:41:17 +0200 Subject: [PATCH] Remove the updates worker It was deprecated in 2019, has not been maintained, and is probably broken. --- client/instances.go | 76 ----- cmd/instances.go | 49 --- cozy.example.yaml | 1 - docs/cli/cozy-stack_instances.md | 1 - docs/cli/cozy-stack_instances_update.md | 40 --- docs/workers.md | 7 - pkg/config/config/config_test.go | 2 +- pkg/config/config/testdata/full_config.yaml | 2 +- scripts/completion/cozy-stack.bash | 51 --- web/instances/instances.go | 32 -- web/jobs/jobs.go | 1 - worker/updates/updates.go | 350 -------------------- 12 files changed, 2 insertions(+), 610 deletions(-) delete mode 100644 docs/cli/cozy-stack_instances_update.md delete mode 100644 worker/updates/updates.go diff --git a/client/instances.go b/client/instances.go index 05afdfaa58e..03373144a17 100644 --- a/client/instances.go +++ b/client/instances.go @@ -106,16 +106,6 @@ type OAuthClientOptions struct { OnboardingState string } -// UpdatesOptions is a struct holding all the options to launch an update. -type UpdatesOptions struct { - Domain string - DomainsWithContext string - Slugs []string - ForceRegistry bool - OnlyRegistry bool - Logs chan *JobLog -} - type ExportOptions struct { Domain string LocalPath string @@ -407,72 +397,6 @@ func (ac *AdminClient) RegisterOAuthClient(opts *OAuthClientOptions) (map[string return client, nil } -// Updates launch the updating process of the applications. When no Domain is -// specified, the updates are launched for all the existing instances. -func (ac *AdminClient) Updates(opts *UpdatesOptions) error { - q := url.Values{ - "Domain": {opts.Domain}, - "DomainsWithContext": {opts.DomainsWithContext}, - "Slugs": {strings.Join(opts.Slugs, ",")}, - "ForceRegistry": {strconv.FormatBool(opts.ForceRegistry)}, - "OnlyRegistry": {strconv.FormatBool(opts.OnlyRegistry)}, - } - channel, err := ac.RealtimeClient(RealtimeOptions{ - DocTypes: []string{"io.cozy.jobs", "io.cozy.jobs.logs"}, - }) - if err != nil { - return err - } - defer func() { - if opts.Logs != nil { - close(opts.Logs) - } - channel.Close() - }() - res, err := ac.Req(&request.Options{ - Method: "POST", - Path: "/instances/updates", - Queries: q, - }) - if err != nil { - return err - } - defer res.Body.Close() - var job job.Job - if err = json.NewDecoder(res.Body).Decode(&job); err != nil { - return err - } - for evt := range channel.Channel() { - if evt.Event == "error" { - return fmt.Errorf("realtime: %s", evt.Payload.Title) - } - if evt.Payload.ID != job.ID() { - continue - } - switch evt.Payload.Type { - case "io.cozy.jobs": - if err = json.Unmarshal(evt.Payload.Doc, &job); err != nil { - return err - } - if job.State == "errored" { - return fmt.Errorf("error executing updates: %s", job.Error) - } - if job.State == "done" { - return nil - } - case "io.cozy.jobs.logs": - if opts.Logs != nil { - var log JobLog - if err = json.Unmarshal(evt.Payload.Doc, &log); err != nil { - return err - } - opts.Logs <- &log - } - } - } - return err -} - // Export launch the creation of a tarball to export data from an instance. func (ac *AdminClient) Export(opts *ExportOptions) error { if !validDomain(opts.Domain) { diff --git a/cmd/instances.go b/cmd/instances.go index 9ea756ba1c5..b4dc5f4aef1 100644 --- a/cmd/instances.go +++ b/cmd/instances.go @@ -42,8 +42,6 @@ var flagTrace bool var flagPassphrase string var flagForce bool var flagJSON bool -var flagForceRegistry bool -var flagOnlyRegistry bool var flagSwiftLayout int var flagCouchCluster int var flagUUID string @@ -871,47 +869,6 @@ var findOauthClientCmd = &cobra.Command{ }, } -var updateCmd = &cobra.Command{ - Use: "update [slugs...]", - Short: "Start the updates for the specified domain instance.", - Long: `Start the updates for the specified domain instance. Use whether the --domain -flag to specify the instance or the --all-domains flags to updates all domains. -The slugs arguments can be used to select which applications should be -updated.`, - Aliases: []string{"updates"}, - RunE: func(cmd *cobra.Command, args []string) error { - ac := newAdminClient() - if flagAllDomains { - logs := make(chan *client.JobLog) - go func() { - for log := range logs { - fmt.Fprintf(os.Stdout, "[%s][time:%s]", log.Level, log.Time.Format(time.RFC3339)) - for k, v := range log.Data { - fmt.Fprintf(os.Stdout, "[%s:%s]", k, v) - } - fmt.Fprintf(os.Stdout, " %s\n", log.Message) - } - }() - return ac.Updates(&client.UpdatesOptions{ - Slugs: args, - ForceRegistry: flagForceRegistry, - OnlyRegistry: flagOnlyRegistry, - Logs: logs, - }) - } - if flagDomain == "" { - return errMissingDomain - } - return ac.Updates(&client.UpdatesOptions{ - Domain: flagDomain, - DomainsWithContext: flagContextName, - Slugs: args, - ForceRegistry: flagForceRegistry, - OnlyRegistry: flagOnlyRegistry, - }) - }, -} - var exportCmd = &cobra.Command{ Use: "export", Short: "Export an instance", @@ -1105,7 +1062,6 @@ func init() { instanceCmdGroup.AddCommand(oauthRefreshTokenInstanceCmd) instanceCmdGroup.AddCommand(oauthClientInstanceCmd) instanceCmdGroup.AddCommand(findOauthClientCmd) - instanceCmdGroup.AddCommand(updateCmd) instanceCmdGroup.AddCommand(exportCmd) instanceCmdGroup.AddCommand(importCmd) instanceCmdGroup.AddCommand(showSwiftPrefixInstanceCmd) @@ -1168,11 +1124,6 @@ func init() { lsInstanceCmd.Flags().BoolVar(&flagJSON, "json", false, "Show each line as a json representation of the instance") lsInstanceCmd.Flags().StringSliceVar(&flagListFields, "fields", nil, "Arguments shown for each line in the list") lsInstanceCmd.Flags().BoolVar(&flagAvailableFields, "available-fields", false, "List available fields for --fields option") - updateCmd.Flags().BoolVar(&flagAllDomains, "all-domains", false, "Work on all domains iteratively") - updateCmd.Flags().StringVar(&flagDomain, "domain", "", "Specify the domain name of the instance") - updateCmd.Flags().StringVar(&flagContextName, "context-name", "", "Work only on the instances with the given context name") - updateCmd.Flags().BoolVar(&flagForceRegistry, "force-registry", false, "Force to update all applications sources from git to the registry") - updateCmd.Flags().BoolVar(&flagOnlyRegistry, "only-registry", false, "Only update applications installed from the registry") exportCmd.Flags().StringVar(&flagDomain, "domain", "", "Specify the domain name of the instance") exportCmd.Flags().StringVar(&flagPath, "path", "", "Specify the local path where to store the export archive") importCmd.Flags().StringVar(&flagDomain, "domain", "", "Specify the domain name of the instance") diff --git a/cozy.example.yaml b/cozy.example.yaml index 61911e89498..fdae0c654e8 100644 --- a/cozy.example.yaml +++ b/cozy.example.yaml @@ -154,7 +154,6 @@ jobs: # - "trash-files": async deletion of files in the trash # - "clean-old-trashed": deletion of old files and directories after some time # - "unzip": unzipping tarball - # - "updates": run updates for installed applications (deprecated) # - "zip": creating a zip tarball # # When no configuration is given for a worker, a default configuration is diff --git a/docs/cli/cozy-stack_instances.md b/docs/cli/cozy-stack_instances.md index 50c50665fbf..8a47bfcb0a8 100644 --- a/docs/cli/cozy-stack_instances.md +++ b/docs/cli/cozy-stack_instances.md @@ -62,5 +62,4 @@ cozy-stack instances [flags] * [cozy-stack instances token-cli](cozy-stack_instances_token-cli.md) - Generate a new CLI access token (global access) * [cozy-stack instances token-konnector](cozy-stack_instances_token-konnector.md) - Generate a new konnector token * [cozy-stack instances token-oauth](cozy-stack_instances_token-oauth.md) - Generate a new OAuth access token -* [cozy-stack instances update](cozy-stack_instances_update.md) - Start the updates for the specified domain instance. diff --git a/docs/cli/cozy-stack_instances_update.md b/docs/cli/cozy-stack_instances_update.md deleted file mode 100644 index 84a23334f78..00000000000 --- a/docs/cli/cozy-stack_instances_update.md +++ /dev/null @@ -1,40 +0,0 @@ -## cozy-stack instances update - -Start the updates for the specified domain instance. - -### Synopsis - -Start the updates for the specified domain instance. Use whether the --domain -flag to specify the instance or the --all-domains flags to updates all domains. -The slugs arguments can be used to select which applications should be -updated. - -``` -cozy-stack instances update [slugs...] [flags] -``` - -### Options - -``` - --all-domains Work on all domains iteratively - --context-name string Work only on the instances with the given context name - --domain string Specify the domain name of the instance - --force-registry Force to update all applications sources from git to the registry - -h, --help help for update - --only-registry Only update applications installed from the registry -``` - -### Options inherited from parent commands - -``` - --admin-host string administration server host (default "localhost") - --admin-port int administration server port (default 6060) - -c, --config string configuration file (default "$HOME/.cozy.yaml") - --host string server host (default "localhost") - -p, --port int server port (default 8080) -``` - -### SEE ALSO - -* [cozy-stack instances](cozy-stack_instances.md) - Manage instances of a stack - diff --git a/docs/workers.md b/docs/workers.md index bb7e3801cea..606a124fab4 100644 --- a/docs/workers.md +++ b/docs/workers.md @@ -395,10 +395,3 @@ It can be launched from command-line with: ```sh $ cozy-stack jobs run migrations --domain example.mycozy.cloud --json '{"type": "to-swift-v3"}' ``` - -## Deprecated workers - -### updates - -The `updates` worker was used for updating applications when there were no -auto-update mechanism. diff --git a/pkg/config/config/config_test.go b/pkg/config/config/config_test.go index 0bc6fcbda34..e507111c8e0 100644 --- a/pkg/config/config/config_test.go +++ b/pkg/config/config/config_test.go @@ -56,7 +56,7 @@ func TestConfigUnmarshal(t *testing.T) { assert.Equal(t, true, cfg.Jobs.AllowList) assert.EqualValues(t, []Worker{ { - WorkerType: "updates", + WorkerType: "zip", Concurrency: &one, MaxExecCount: &one, Timeout: &oneHour, diff --git a/pkg/config/config/testdata/full_config.yaml b/pkg/config/config/testdata/full_config.yaml index bb53734f6de..4010f231cb7 100644 --- a/pkg/config/config/testdata/full_config.yaml +++ b/pkg/config/config/testdata/full_config.yaml @@ -49,7 +49,7 @@ jobs: defaultDurationToKeep: 1H imagemagick_convert_cmd: some-cmd workers: - updates: + zip: concurrency: 1 max_exec_count: 1 timeout: 1h diff --git a/scripts/completion/cozy-stack.bash b/scripts/completion/cozy-stack.bash index 84697e60338..e35aac851fb 100644 --- a/scripts/completion/cozy-stack.bash +++ b/scripts/completion/cozy-stack.bash @@ -3274,52 +3274,6 @@ _cozy-stack_instances_token-oauth() noun_aliases=() } -_cozy-stack_instances_update() -{ - last_command="cozy-stack_instances_update" - - command_aliases=() - - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--all-domains") - local_nonpersistent_flags+=("--all-domains") - flags+=("--context-name=") - two_word_flags+=("--context-name") - local_nonpersistent_flags+=("--context-name") - local_nonpersistent_flags+=("--context-name=") - flags+=("--domain=") - two_word_flags+=("--domain") - local_nonpersistent_flags+=("--domain") - local_nonpersistent_flags+=("--domain=") - flags+=("--force-registry") - local_nonpersistent_flags+=("--force-registry") - flags+=("--only-registry") - local_nonpersistent_flags+=("--only-registry") - flags+=("--admin-host=") - two_word_flags+=("--admin-host") - flags+=("--admin-port=") - two_word_flags+=("--admin-port") - flags+=("--config=") - two_word_flags+=("--config") - two_word_flags+=("-c") - flags+=("--host=") - two_word_flags+=("--host") - flags+=("--port=") - two_word_flags+=("--port") - two_word_flags+=("-p") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - _cozy-stack_instances() { last_command="cozy-stack_instances" @@ -3367,11 +3321,6 @@ _cozy-stack_instances() commands+=("token-cli") commands+=("token-konnector") commands+=("token-oauth") - commands+=("update") - if [[ -z "${BASH_VERSION:-}" || "${BASH_VERSINFO[0]:-}" -gt 3 ]]; then - command_aliases+=("updates") - aliashash["updates"]="update" - fi flags=() two_word_flags=() diff --git a/web/instances/instances.go b/web/instances/instances.go index eb6d1a2678b..dbe306654c3 100644 --- a/web/instances/instances.go +++ b/web/instances/instances.go @@ -12,7 +12,6 @@ import ( "github.com/cozy/cozy-stack/model/app" "github.com/cozy/cozy-stack/model/instance" "github.com/cozy/cozy-stack/model/instance/lifecycle" - "github.com/cozy/cozy-stack/model/job" "github.com/cozy/cozy-stack/model/oauth" "github.com/cozy/cozy-stack/model/session" "github.com/cozy/cozy-stack/model/sharing" @@ -22,7 +21,6 @@ import ( "github.com/cozy/cozy-stack/pkg/jsonapi" "github.com/cozy/cozy-stack/pkg/prefixer" "github.com/cozy/cozy-stack/pkg/utils" - "github.com/cozy/cozy-stack/worker/updates" "github.com/labstack/echo/v4" ) @@ -282,35 +280,6 @@ func deleteHandler(c echo.Context) error { return c.NoContent(http.StatusNoContent) } -func updatesHandler(c echo.Context) error { - slugs := utils.SplitTrimString(c.QueryParam("Slugs"), ",") - domain := c.QueryParam("Domain") - domainsWithContext := c.QueryParam("DomainsWithContext") - forceRegistry, _ := strconv.ParseBool(c.QueryParam("ForceRegistry")) - onlyRegistry, _ := strconv.ParseBool(c.QueryParam("OnlyRegistry")) - msg, err := job.NewMessage(&updates.Options{ - Slugs: slugs, - Force: true, - ForceRegistry: forceRegistry, - OnlyRegistry: onlyRegistry, - Domain: domain, - DomainsWithContext: domainsWithContext, - AllDomains: domain == "", - }) - if err != nil { - return err - } - j, err := job.System().PushJob(prefixer.GlobalPrefixer, &job.JobRequest{ - WorkerType: "updates", - Message: msg, - ForwardLogs: true, - }) - if err != nil { - return wrapError(err) - } - return c.JSON(http.StatusOK, j) -} - func setAuthMode(c echo.Context) error { domain := c.Param("domain") inst, err := lifecycle.GetInstance(domain) @@ -684,7 +653,6 @@ func Routes(router *echo.Group) { router.DELETE("/:domain/sessions", cleanSessions) // Advanced features for instances - router.POST("/updates", updatesHandler) router.GET("/:domain/last-activity", lastActivity) router.POST("/:domain/export", exporter) router.GET("/:domain/exports/:export-id/data", dataExporter) diff --git a/web/jobs/jobs.go b/web/jobs/jobs.go index 1b5f0fd53a2..880ca4cbd41 100644 --- a/web/jobs/jobs.go +++ b/web/jobs/jobs.go @@ -41,7 +41,6 @@ import ( _ "github.com/cozy/cozy-stack/worker/sms" _ "github.com/cozy/cozy-stack/worker/thumbnail" _ "github.com/cozy/cozy-stack/worker/trash" - _ "github.com/cozy/cozy-stack/worker/updates" ) type ( diff --git a/worker/updates/updates.go b/worker/updates/updates.go deleted file mode 100644 index d94f7f75607..00000000000 --- a/worker/updates/updates.go +++ /dev/null @@ -1,350 +0,0 @@ -package updates - -import ( - "fmt" - "net/url" - "strings" - "sync" - "time" - - "github.com/cozy/cozy-stack/model/app" - "github.com/cozy/cozy-stack/model/instance" - "github.com/cozy/cozy-stack/model/instance/lifecycle" - "github.com/cozy/cozy-stack/model/job" - "github.com/cozy/cozy-stack/pkg/consts" - "github.com/cozy/cozy-stack/pkg/couchdb" - "github.com/cozy/cozy-stack/pkg/logger" - "github.com/cozy/cozy-stack/pkg/prefixer" - "github.com/cozy/cozy-stack/pkg/registry" -) - -const ( - numUpdaters = 4 - numUpdatersSingleInstance = 4 -) - -func init() { - job.AddWorker(&job.WorkerConfig{ - WorkerType: "updates", - Reserved: true, - Concurrency: 1, - MaxExecCount: 1, - Timeout: 1 * time.Hour, - WorkerFunc: Worker, - }) -} - -type updateError struct { - domain string - slug string - step string - reason error -} - -func (u *updateError) toFields() logger.Fields { - fields := make(logger.Fields, 4) - fields["step"] = u.step - fields["reason"] = u.reason.Error() - if u.domain != "" { - fields["domain"] = u.domain - } - if u.slug != "" { - fields["slug"] = u.slug - } - return fields -} - -func updateErrorFromInstaller(inst *app.Installer, step string, reason error) *updateError { - return &updateError{ - domain: inst.Domain(), - slug: inst.Slug(), - step: step, - reason: reason, - } -} - -// Options is the option handler for updates: -// - Slugs: allow to filter the application's slugs to update, if empty, all -// applications are updated -// - Force: forces the update, even if the user has not activated the auto- -// update -// - ForceRegistry: translates the git:// sourced application into -// registry:// -type Options struct { - Slugs []string `json:"slugs,omitempty"` - Domain string `json:"domain,omitempty"` - DomainsWithContext string `json:"domains_with_context,omitempty"` - AllDomains bool `json:"all_domains"` - Force bool `json:"force"` - ForceRegistry bool `json:"force_registry"` - OnlyRegistry bool `json:"only_registry"` -} - -// Worker is the worker method to launch the updates. -func Worker(ctx *job.WorkerContext) error { - var opts Options - if err := ctx.UnmarshalMessage(&opts); err != nil { - return err - } - if opts.AllDomains { - return UpdateAll(ctx, &opts) - } - if opts.Domain != "" { - inst, err := lifecycle.GetInstance(opts.Domain) - if err != nil { - return err - } - return UpdateInstance(ctx, inst, &opts) - } - return nil -} - -// UpdateAll starts the auto-updates process for all instances. The slugs -// parameters can be used optionnaly to filter (allowlist) the applications' -// slug to update. -func UpdateAll(ctx *job.WorkerContext, opts *Options) error { - insc := make(chan *app.Installer) - errc := make(chan *updateError) - - totalInstances, err := couchdb.CountAllDocs(prefixer.GlobalPrefixer, consts.Instances) - if err != nil { - return err - } - totalInstances -= 1 - - // log a message for every hundredth instances updated, rounded to the - // closest multiple of 100. - countMark := totalInstances / 100 - countMark = ((countMark + 100 - 1) / 100) * 100 - - var g sync.WaitGroup - g.Add(numUpdaters) - - for i := 0; i < numUpdaters; i++ { - go func() { - for installer := range insc { - _, err := installer.RunSync() - if err != nil { - errc <- updateErrorFromInstaller(installer, "RunSync", err) - } else { - errc <- nil - } - } - g.Done() - }() - } - - go func() { - count := 0 - errf := instance.ForeachInstances(func(inst *instance.Instance) error { - if opts.DomainsWithContext != "" && - inst.ContextName != opts.DomainsWithContext { - return nil - } - select { - case <-ctx.Done(): - return ctx.Err() - default: - } - count++ - if opts.Force || !inst.NoAutoUpdate { - installerPush(inst, insc, errc, opts) - } - if count == totalInstances { - ctx.Logger().Infof("updated %d instances -- finished", count) - } else if countMark > 0 && count%countMark == 0 { - ctx.Logger().Infof("updated %d instances", count) - } - return nil - }) - if errf != nil { - errc <- &updateError{step: "ForeachInstances", reason: errf} - } - close(insc) - g.Wait() - close(errc) - }() - - errors := 0 - totals := 0 - for err := range errc { - if err != nil { - ctx.Logger().WithFields(err.toFields()).Error("") - errors++ - } - totals++ - } - - if errors > 0 { - return fmt.Errorf("At least one error has happened during the updates: "+ - "%d errors for %d updates", errors, totals) - } - return nil -} - -// UpdateInstance starts the auto-update process on the given instance. The -// slugs parameters can be used to filter (allowlist) the applications' slug -func UpdateInstance(ctx *job.WorkerContext, inst *instance.Instance, opts *Options) error { - insc := make(chan *app.Installer) - errc := make(chan *updateError) - - if opts.DomainsWithContext != "" && - inst.ContextName != opts.DomainsWithContext { - return nil - } - - var g sync.WaitGroup - g.Add(numUpdatersSingleInstance) - - for i := 0; i < numUpdatersSingleInstance; i++ { - go func() { - for installer := range insc { - _, err := installer.RunSync() - if err != nil { - errc <- updateErrorFromInstaller(installer, "RunSync", err) - } else { - errc <- nil - } - } - g.Done() - }() - } - - go func() { - installerPush(inst, insc, errc, opts) - close(insc) - g.Wait() - close(errc) - }() - - errors := 0 - totals := 0 - for err := range errc { - if err != nil { - ctx.Logger().WithFields(err.toFields()).Error("") - errors++ - } - totals++ - } - - if errors > 0 { - return fmt.Errorf("At least one error has happened during the updates: "+ - "%d errors for %d updates", errors, totals) - } - return nil -} - -func installerPush(inst *instance.Instance, insc chan *app.Installer, errc chan *updateError, opts *Options) { - registries := inst.Registries() - - var g sync.WaitGroup - g.Add(2) - - go func() { - defer g.Done() - webapps, _, err := app.ListWebappsWithPagination(inst, 0, "") - if err != nil { - errc <- &updateError{ - domain: inst.Domain, - step: "ListWebapps", - reason: err, - } - return - } - for _, webapp := range webapps { - if filterSlug(webapp.Slug(), opts.Slugs) { - continue - } - if opts.OnlyRegistry && strings.HasPrefix(webapp.Source(), "registry://") { - continue - } - installer, err := createInstaller(inst, registries, webapp, opts) - if err != nil { - errc <- &updateError{ - domain: inst.Domain, - slug: webapp.Slug(), - step: "CreateInstaller", - reason: err, - } - } else { - insc <- installer - } - } - }() - - go func() { - defer g.Done() - konnectors, _, err := app.ListKonnectorsWithPagination(inst, 0, "") - if err != nil { - errc <- &updateError{ - domain: inst.Domain, - step: "ListKonnectors", - reason: err, - } - return - } - for _, konn := range konnectors { - if filterSlug(konn.Slug(), opts.Slugs) { - continue - } - if opts.OnlyRegistry && strings.HasPrefix(konn.Source(), "registry://") { - continue - } - installer, err := createInstaller(inst, registries, konn, opts) - if err != nil { - errc <- &updateError{ - domain: inst.Domain, - slug: konn.Slug(), - step: "CreateInstaller", - reason: err, - } - } else { - insc <- installer - } - } - }() - - g.Wait() -} - -func filterSlug(slug string, slugs []string) bool { - if len(slugs) == 0 { - return false - } - for _, s := range slugs { - if s == slug { - return false - } - } - return true -} - -func createInstaller(inst *instance.Instance, registries []*url.URL, man app.Manifest, opts *Options) (*app.Installer, error) { - var sourceURL string - if opts.ForceRegistry { - originalSourceURL, err := url.Parse(man.Source()) - if err == nil && (originalSourceURL.Scheme == "git" || - originalSourceURL.Scheme == "git+ssh" || - originalSourceURL.Scheme == "ssh+git") { - var channel string - if man.AppType() == consts.WebappType && strings.HasPrefix(originalSourceURL.Fragment, "build") { - channel = "dev" - } else { - channel = "stable" - } - _, err := registry.GetLatestVersion(man.Slug(), channel, registries) - if err == nil { - sourceURL = fmt.Sprintf("registry://%s/%s", man.Slug(), channel) - } - } - } - return app.NewInstaller(inst, app.Copier(man.AppType(), inst), - &app.InstallerOptions{ - Operation: app.Update, - Manifest: man, - Registries: registries, - SourceURL: sourceURL, - PermissionsAcked: true, - }, - ) -}