mirror of
https://github.com/cozystack/cozystack.git
synced 2026-10-30 16:47:20 +09:00
Connecting a marketplace repository materialized its PackageSource but created no Package, so its ApplicationDefinitions never rendered and its apps never appeared in the dashboard catalog until a manual `cozypkg add` the dashboard cannot drive (the package list is computed from the ApplicationDefinitions, which do not exist yet). The tap materializer now creates a tap-managed registration Package for each materialized PackageSource, the way the platform ships a Package per built-in app, so the ApplicationDefinition registrations deploy on connect and the apps become browsable; the app components carry no install block and are still instantiated per user resource. Privileged install components of an unconfirmed tap auto-registration (a Package that still carries the marketplace-tap label) are refused on several best-effort layers: the Package reconciler skips their HelmRelease and prunes a lingering one at the install site, and the PackageSource reconciler withholds their ExternalArtifact so the helm-controller has no privileged content to deploy, re-running on confirmation via a Package watch. `cozypkg add --allow-privileged` sheds the label as an explicit ownership handover, which is the confirmation; a platform Package never carries the label. A benign component flipped to privileged in a new revision can still be materialized by source-watcher before these layers observe the flip; closing that race fully requires decoupling the catalog registration from install and is tracked in issue #4359. `cozypkg untap` and the dashboard disconnect remove the registration Package (de-registering the apps) under a UID + ResourceVersion precondition, so a concurrent `cozypkg add` handover is never clobbered; running instances are left but unmanaged. `cozypkg add` re-opens a tap auto-registration for variant selection and hands it over. The cozystack-api ClusterRole gains delete on packages for the disconnect path. Assisted-by: LLM Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
101 lines
3.9 KiB
Go
101 lines
3.9 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
// Copyright 2025 The Cozystack Authors.
|
|
|
|
package operator
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
sourcewatcherv1beta1 "github.com/fluxcd/source-watcher/api/v2/v1beta1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
|
|
|
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
|
"github.com/cozystack/cozystack/internal/marketplace/naming"
|
|
"github.com/cozystack/cozystack/internal/marketplace/tapconst"
|
|
)
|
|
|
|
// tapPrivilegedPS builds a PackageSource whose default variant has one
|
|
// privileged component ("op") and one benign component ("web"). When tap is
|
|
// true the source carries the marketplace-tap label, marking it a tap-managed
|
|
// source whose privileged content is withheld until the registration is
|
|
// confirmed.
|
|
func tapPrivilegedPS(tap bool) *cozyv1alpha1.PackageSource {
|
|
ps := &cozyv1alpha1.PackageSource{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "demo.app"},
|
|
Spec: cozyv1alpha1.PackageSourceSpec{
|
|
SourceRef: &cozyv1alpha1.PackageSourceRef{Kind: "OCIRepository", Name: "tap-demo-app", Namespace: "cozy-system"},
|
|
Variants: []cozyv1alpha1.Variant{{
|
|
Name: "default",
|
|
Components: []cozyv1alpha1.Component{
|
|
{Name: "op", Path: "sys/op", Install: &cozyv1alpha1.ComponentInstall{Namespace: "cozy-op", Privileged: true}},
|
|
{Name: "web", Path: "apps/web", Install: &cozyv1alpha1.ComponentInstall{Namespace: "cozy-web"}},
|
|
},
|
|
}},
|
|
},
|
|
}
|
|
if tap {
|
|
ps.Labels = map[string]string{tapconst.Label: "true"}
|
|
}
|
|
return ps
|
|
}
|
|
|
|
func agArtifactNames(t *testing.T, cl client.Client) map[string]bool {
|
|
t.Helper()
|
|
var ag sourcewatcherv1beta1.ArtifactGenerator
|
|
if err := cl.Get(context.Background(), types.NamespacedName{Name: "demo.app", Namespace: "cozy-system"}, &ag); err != nil {
|
|
t.Fatalf("get ArtifactGenerator: %v", err)
|
|
}
|
|
names := map[string]bool{}
|
|
for _, oa := range ag.Spec.OutputArtifacts {
|
|
names[oa.Name] = true
|
|
}
|
|
return names
|
|
}
|
|
|
|
// The ExternalArtifact is what the helm-controller upgrades a HelmRelease from,
|
|
// independently of the Package reconciler. Withholding the privileged
|
|
// component's artifact until the registration is confirmed narrows the window in
|
|
// which unconfirmed privileged content could reach the cluster. The benign
|
|
// component's artifact is always produced (it carries the catalog registration),
|
|
// and a non-tap (platform) source is never gated.
|
|
func TestReconcileArtifactGeneratorsWithholdsUnconfirmedPrivileged(t *testing.T) {
|
|
opName := naming.ArtifactName("demo.app", "default", "op")
|
|
webName := naming.ArtifactName("demo.app", "default", "web")
|
|
|
|
cases := []struct {
|
|
name string
|
|
tapSource bool
|
|
pkg *cozyv1alpha1.Package
|
|
wantOp bool
|
|
}{
|
|
{"tap source, no registration Package", true, nil, false},
|
|
{"tap source, unconfirmed registration", true, &cozyv1alpha1.Package{ObjectMeta: metav1.ObjectMeta{Name: "demo.app", Labels: map[string]string{tapconst.Label: "true"}}}, false},
|
|
{"tap source, confirmed registration", true, &cozyv1alpha1.Package{ObjectMeta: metav1.ObjectMeta{Name: "demo.app"}}, true},
|
|
{"non-tap platform source", false, nil, true},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
objs := []client.Object{tapPrivilegedPS(tc.tapSource)}
|
|
if tc.pkg != nil {
|
|
objs = append(objs, tc.pkg)
|
|
}
|
|
cl := fake.NewClientBuilder().WithScheme(testScheme(t)).WithObjects(objs...).Build()
|
|
r := &PackageSourceReconciler{Client: cl, Scheme: testScheme(t)}
|
|
if err := r.reconcileArtifactGenerators(context.Background(), tapPrivilegedPS(tc.tapSource)); err != nil {
|
|
t.Fatalf("reconcileArtifactGenerators: %v", err)
|
|
}
|
|
names := agArtifactNames(t, cl)
|
|
if !names[webName] {
|
|
t.Errorf("benign component artifact %q must always be produced", webName)
|
|
}
|
|
if names[opName] != tc.wantOp {
|
|
t.Errorf("privileged component artifact %q: produced=%v, want %v", opName, names[opName], tc.wantOp)
|
|
}
|
|
})
|
|
}
|
|
}
|