diff --git a/content/en/docs/next/applications/external.md b/content/en/docs/next/applications/external.md index 76317be3..4ada18f6 100644 --- a/content/en/docs/next/applications/external.md +++ b/content/en/docs/next/applications/external.md @@ -12,6 +12,12 @@ This guide explains the structure of an external application package and how to For a complete working example, see [github.com/cozystack/external-apps-example](https://github.com/cozystack/external-apps-example). +{{% note %}} + +This page describes the Git-and-HelmRelease bootstrap. A repository can also be published as an OCI artifact and connected with one command, using the `PackageSource` model described in [Application Marketplace]({{% ref "/docs/next/marketplace" %}}). The two coexist on a cluster; the pipeline on this page is untouched. + +{{% /note %}} + Just like standard Cozystack applications, this external application package uses Helm and FluxCD. To learn more about developing application packages, read the Cozystack [Developer Guide]({{% ref "/docs/next/development" %}}). diff --git a/content/en/docs/next/marketplace/_index.md b/content/en/docs/next/marketplace/_index.md new file mode 100644 index 00000000..c9622184 --- /dev/null +++ b/content/en/docs/next/marketplace/_index.md @@ -0,0 +1,52 @@ +--- +title: "Application Marketplace" +linkTitle: "Marketplace" +description: "Extend the Cozystack application catalog with external repositories using the PackageSource model and the cozypkg CLI." +weight: 48 +--- + +The Cozystack marketplace lets an administrator extend the built-in application catalog with applications published in external repositories. Connecting a repository to a cluster makes its applications available to install; once one is installed, it appears in the same dashboard catalog and behaves like the standard managed applications platform users already know. + +A repository is a self-contained, versioned bundle published as an OCI artifact. It is authored and validated with the `cozypkg` CLI, connected to a cluster with a single command (or from the dashboard), and, optionally, listed in a community index so operators can discover it. + +{{% note %}} + +The marketplace is built on the `PackageSource` model, a newer mechanism alongside the Git-and-HelmRelease bootstrap described in [Adding External Applications]({{% ref "/docs/next/applications/external" %}}). The two coexist on a cluster; the existing External-Apps pipeline is untouched. + +{{% /note %}} + +## How it works + +A marketplace repository ships one or more `PackageSource` resources. Each `PackageSource` declares variants and components, and every component is a Helm chart. A user-installable application takes two of them: the application chart itself, and a paired registration chart whose `ApplicationDefinition` advertises the application to the Cozystack API and dashboard. + +The lifecycle has two sides: + +- **Publishing** turns a repository into an OCI artifact: `cozypkg init` scaffolds it, `cozypkg validate` lints it offline, and `cozypkg push` bundles the `packages/` tree into a single versioned artifact in any OCI registry. +- **Connecting** registers that artifact on a cluster: `cozypkg tap` (or the dashboard) creates a Flux `OCIRepository` and materializes the repository's `PackageSource` resources. `cozypkg add` then installs individual applications from the connected repository, and they show up in the catalog. + +## Key objects + +| Object | Group | Role | +| --- | --- | --- | +| `PackageSource` | `cozystack.io/v1alpha1` | Declares a repository's variants and components. | +| `ApplicationDefinition` | `cozystack.io/v1alpha1` | Registers a component as a user-installable application in the API and dashboard. | +| `Tap` | `core.cozystack.io/v1alpha1` | Virtual resource backing the dashboard "Repositories" view: connect, list, and disconnect repositories. | +| `OCIRepository` | `source.toolkit.fluxcd.io/v1` | Flux source Cozystack creates for a connected repository's artifact. | + +A connected repository keeps its own declared `PackageSource` name. If that name would collide with a core component or with another connected repository, the connect is rejected instead of overwriting it, so an external repository cannot take over an official `PackageSource`. Only the name is compared: the `ApplicationDefinition` resources a repository registers are not checked against the core catalog. + +## Trust model + +Installing an application from a connected repository runs its charts in your management cluster, so connect only sources you trust. (Tapping itself installs nothing; it registers the source.) + +Connecting is a standing subscription rather than a one-time fetch. Both connect paths create an `OCIRepository` pinned to a **tag**, with `interval: 5m`, so Flux re-resolves that tag every five minutes and a new revision behind it is pulled and its `PackageSource` resources re-applied. What the new revision drops is removed as well: a package the publisher stops shipping disappears from every connected cluster, and an installed `Package` left behind goes `Ready=False` with `PackageSourceNotFound`. Whoever controls the tag controls what your cluster reads from then on: re-pushing the same tag reaches every connected cluster within the interval. `cozypkg tap` rejects digest references, so pinning an immutable digest at connect time is not an option. + +Signature verification happens at **publication** time, not at connect time. `cozypkg tap` validates an artifact's structure before it creates anything; the dashboard connect does not, it records the source, and the pull, the digest check and the materialization of the `PackageSource` resources all happen in the cluster afterwards, so a broken chart surfaces later as a failed install. Neither verifies a cosign signature, and neither sets `spec.verify` on the `OCIRepository`. The verification point is the community index CI gate, which pins each release to the entry's recorded cosign identity; it attests the artifact as of the moment the entry merged, not whatever the tag resolves to later. + +Setting `spec.verify` on the `OCIRepository` in `cozy-system` by hand is what closes that gap: Flux then checks the signature on every pull, including the ones the interval triggers. Connecting the same repository again from the dashboard replaces the source's whole spec and drops the field, so re-add it after a reconnect. See [Publishing a Repository]({{% ref "/docs/next/marketplace/publishing" %}}#the-community-index) for the publication side. + +## Where to go next + +- [Publishing a Repository]({{% ref "/docs/next/marketplace/publishing" %}}): scaffold, validate, push, and list a repository in the community index. +- [Connecting a Repository]({{% ref "/docs/next/marketplace/connecting" %}}): discover, connect, install, and disconnect repositories on a cluster. +- [`cozypkg` Reference]({{% ref "/docs/next/marketplace/cozypkg" %}}): every command and flag. diff --git a/content/en/docs/next/marketplace/connecting.md b/content/en/docs/next/marketplace/connecting.md new file mode 100644 index 00000000..654e1b11 --- /dev/null +++ b/content/en/docs/next/marketplace/connecting.md @@ -0,0 +1,147 @@ +--- +title: "Connecting a Repository" +linkTitle: "Connecting" +description: "Discover, connect, install from, and disconnect external application repositories on a Cozystack cluster." +weight: 20 +--- + +This guide is for **operators**: cluster administrators who connect external repositories and install their applications. It covers discovering repositories, connecting them from the CLI or the dashboard, installing applications, and disconnecting. + +For the publisher side (packaging and pushing a repository), see [Publishing a Repository]({{% ref "/docs/next/marketplace/publishing" %}}). + +## Prerequisites + +- The `cozypkg` CLI (see [Install cozypkg]({{% ref "/docs/next/install/cozystack/kubernetes-distribution" %}}#2-install-cozypkg)) and a kubeconfig for the target cluster. Creating cluster-scoped resources requires cluster-admin. +- The `flux` CLI on your `PATH`. `cozypkg tap` pulls the artifact to your own machine before it creates anything on the cluster, and it shells out to `flux pull artifact` to do it. `--skip-validate` skips the validation, not the pull, so `flux` is needed either way. +- A Cozystack release that includes the marketplace. There is no switch to enable: the `taps` API and the tap materializer are always present. + +{{% warning %}} + +Installing an application from a connected repository runs its charts in your management cluster (tapping itself installs nothing). Connect only sources you trust, or repositories listed in a curated index whose gate verifies signatures. See the [Trust model]({{% ref "/docs/next/marketplace" %}}#trust-model). + +{{% /warning %}} + +## Discover repositories + +`cozypkg search` queries the community index and lists matching repositories without connecting them. Point it at an index with `--index` or the `COZYPKG_INDEX` environment variable (a local directory or an `oci://` reference): + +```bash +export COZYPKG_INDEX=oci://ghcr.io/cozystack/packages-index:latest +cozypkg search database +``` + +The community index is not published yet (see [Publishing]({{% ref "/docs/next/marketplace/publishing" %}}#the-community-index)), so the `oci://` reference above is illustrative. Until it exists, point `--index` at a local checkout of an index repository. + +## Connect from the CLI + +`cozypkg tap` registers a published repository. It creates a Flux `OCIRepository` pointing at the artifact and materializes the `PackageSource` resources the artifact carries under their declared names. If a name would collide with a core component or another connected repository, the tap is rejected instead of overwriting it. Only the `PackageSource` name is compared; the `ApplicationDefinition` resources the repository registers are not. Nothing is installed yet: + +```bash +cozypkg tap oci://ghcr.io/acme/hello:v1.0.0 +``` + +The Flux source takes its name from the last two path segments of the reference, as `tap--`, and the registry host is not part of it. A tap whose derived name already exists pointing at a different URL is refused as well. Two registries serving the same repository path collide that way, which is exactly what mirroring produces: untap the first before pointing the same path at the second. + +Re-tapping the same repository is safe only if you repeat every flag the first tap used. A tap is a replacement, not an addition: it applies the whole `OCIRepository` built from that invocation's flags, so a flag left off a later run is dropped from the source. `--secret` is the one that hurts. Re-tapping a private repository without it removes the pull credential, the command still reports success, because its own pull used your local login, and the breakage only shows up later as the cluster failing to pull. Repeat every flag on every tap of the same repository. + +Use `--tag` to move a direct `oci://` tap to a new release, and `--skip-validate` to skip validating the artifact structure before tapping (not recommended). + +If the repository is listed in an index, you can tap it by its short name and let the index resolve the reference: + +```bash +cozypkg tap acme.hello --index "$COZYPKG_INDEX" +``` + +A short name resolves to the entry's recorded `version`, which is the release the index gate validated and checked the signature of. Do not add `--tag` to it: the flag overrides the resolved version, and what you connect is then a release the gate never saw. To move an indexed tap to a newer release, re-run the short-name tap after the entry's new `version` merges. + +### Private repositories + +A private repository is pulled twice, and each pull authenticates on its own. + +The first pull runs on your machine, when `cozypkg tap` fetches the artifact through the `flux` CLI. It uses the registry credentials that machine already has, so log in before tapping; otherwise the tap fails before it creates anything: + +```bash +docker login ghcr.io +``` + +The cluster then pulls the same artifact for itself. For that, pre-create a pull-credential `Secret` in the `cozy-system` namespace and point the tap at it with `--secret`. Cozystack attaches it as the `OCIRepository`'s `secretRef`; it covers the cluster-side pull only, and `cozypkg` never reads it: + +```bash +printf 'Registry token: ' +read -rs REGISTRY_TOKEN +echo + +kubectl create secret docker-registry acme-pull \ + --namespace cozy-system \ + --docker-server=ghcr.io \ + --docker-username= \ + --docker-password="$REGISTRY_TOKEN" + +unset REGISTRY_TOKEN + +cozypkg tap oci://ghcr.io/acme/hello:v1.0.0 --secret acme-pull +``` + +Reading the token from a prompt keeps it out of your shell history. It is still visible in the process list while `kubectl` runs; where that matters, write the `dockerconfigjson` yourself and create the `Secret` from it with `--from-file`. + +`--secret` is not one-time setup. Pass it again on every later tap of the same repository, including a `--tag` bump, and check the source afterwards: + +```bash +kubectl get ocirepositories --namespace cozy-system \ + --selector apps.cozystack.io/marketplace-tap=true \ + --output custom-columns=NAME:.metadata.name,URL:.spec.url,SECRET:.spec.secretRef.name +``` + +A `SECRET` column reading `` on a private repository means the credential was dropped; tapping again with `--secret` restores it. + +## Connect from the dashboard + +The dashboard "Repositories" view is backed by the `Tap` resource and covers the same flow without the CLI. Open it from the sidebar, choose **Connect**, and provide the `oci://` reference and, for a private repository, the name of a pull-credential `Secret` in `cozy-system`. Connected repositories are listed with their status; a tapped repository still connecting or blocked by a name collision shows its message there, and tapped repositories can be disconnected from the same view. + +Two differences from the CLI. The dashboard skips the structural validation `cozypkg tap` runs, so a repository whose charts are broken connects cleanly and fails later, when an application from it is installed. And connecting here only records the intent: the call writes the Flux source and returns `connecting`, and the cluster then pulls the artifact, checks its digest against the source, reads the `PackageSource` resources and applies the same name-collision check in the background. So an unreachable reference is accepted rather than refused up front the way the CLI refuses it; the outcome arrives as the repository's status in the view, and `cozypkg list` does not show the source until that has finished. + +## Install applications + +Once a repository is connected, install an application from it with `cozypkg add`, naming the materialized `PackageSource`. A tapped repository keeps its own declared name, so run `cozypkg list` first to see the exact name to use: + +```bash +cozypkg list +cozypkg add acme.hello +``` + +`cozypkg add` creates a `Package` from the named `PackageSource`, plus one for every source it depends on that is not installed already. The reconciler then creates a HelmRelease for each component that carries an `install:` block, which is how the paired registration chart puts the application in the catalog. `add` needs a terminal: it asks which variant to install even when a `PackageSource` declares only one, and asks again for confirmation when a component is privileged. `--allow-privileged` answers the second question; nothing answers the first. To install without a terminal, write the `Package` manifest yourself and pass it with `-f`, which creates it directly and skips both prompts, along with the dependency resolution that comes with them. + +Installed applications appear in the dashboard catalog alongside the built-in ones, and platform users deploy them the same way. + +## List what is connected and installed + +`cozypkg list` shows every `PackageSource` on the cluster, so the platform's own `cozystack.*` sources are listed alongside the tapped ones. `--installed` shows installed `Package` resources instead, and `--components` breaks components onto separate lines: + +```bash +cozypkg list +cozypkg list --installed +``` + +## Disconnect + +Disconnecting has two independent steps, mirroring the two connect steps. + +Remove installed applications with `cozypkg del`. It deletes the named `Package` and its resources, and every installed `Package` that depends on it goes too: the command prints the whole set, requested and dependent kept apart, and asks for confirmation before it removes anything. The connected source stays in place: + +```bash +cozypkg del acme.hello +``` + +Then remove the source itself with `cozypkg untap`. It deletes the tapped `PackageSource` (identified by its `apps.cozystack.io/marketplace-tap` marker label, not a name prefix) and refuses official sources. If a `Package` of that name is still installed it refuses and deletes nothing; pass `--yes` to untap anyway (the `Package` stays installed): + +```bash +cozypkg untap acme.hello +``` + +The Flux `OCIRepository` is removed only when no other `PackageSource` still references it. So for an artifact that carries several `PackageSource` resources, untap each one; the shared source is deleted with the last. + +Finish that sequence promptly. The source stays on its interval until its last `PackageSource` is gone, and a new revision arriving mid-sequence re-materializes everything the artifact carries, putting back the ones already untapped. + +From the dashboard, disconnecting a tapped repository in the "Repositories" view follows the same rule: it removes the named `PackageSource`, and the Flux source only once no other `PackageSource` references it. It does not remove already-installed applications, and, unlike `cozypkg untap`, it does not stop to ask when a `Package` from that source is still installed. + +An application left installed after its source is gone is not left running as it was. Its `Package` goes `Ready=False` with `PackageSourceNotFound` and stops being reconciled, so the release drifts from then on. Remove it with `cozypkg del` if that is what you meant, or tap the repository again to bring the source back. diff --git a/content/en/docs/next/marketplace/cozypkg.md b/content/en/docs/next/marketplace/cozypkg.md new file mode 100644 index 00000000..4ce014e1 --- /dev/null +++ b/content/en/docs/next/marketplace/cozypkg.md @@ -0,0 +1,172 @@ +--- +title: "cozypkg Reference" +linkTitle: "cozypkg Reference" +description: "Command and flag reference for the cozypkg marketplace CLI." +weight: 30 +--- + +`cozypkg` is the CLI for authoring, publishing, and managing Cozystack marketplace repositories. This page is a reference for its commands; for task-oriented walkthroughs see [Publishing a Repository]({{% ref "/docs/next/marketplace/publishing" %}}) and [Connecting a Repository]({{% ref "/docs/next/marketplace/connecting" %}}). + +Commands that create or read cluster resources accept `--kubeconfig` and otherwise fall back to `~/.kube/config` or the `KUBECONFIG` environment variable. Creating cluster-scoped resources requires cluster-admin. + +`push` shells out to the `flux` binary, and so does anything that reads an `oci://` reference: `tap`, `validate` against an artifact, and `search` or short-name `tap` against an `oci://` index. `flux` must be on your `PATH` for those. Its pulls run on your machine and use the registry credentials found there. + +## Environment variables + +- `COZYPKG_INDEX`: default index location for `search` and short-name `tap`. A local directory or an `oci://` reference. Overridden by `--index`. + +## Authoring and publishing + +### `cozypkg init [directory]` + +Scaffold a new repository built around the `PackageSource` model: a `PackageSource` with one variant and a paired app / `-rd` component, ready to validate and push. The generated tree passes `cozypkg validate` as-is. + +| Flag | Description | +| --- | --- | +| `--app