Compare commits

...

21 Commits

Author SHA1 Message Date
Andrei Kvapil
ca61c71084 Replace Ancestor tracking webhook with controller
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-09-17 12:48:17 +02:00
Timofei Larkin
65a734bb65 [ci] Get REGISTRY from vars, not secrets (#1423)
## What this PR does

This patch sources the REGISTRY env var from GitHub actions variables
instead of secrets, so pull requests from forked repos work correctly.

### Release note

```release-note
[ci] Source the REGISTRY env var from actions' variables, not secrets,
so external pull requests can work.
```

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Chores**
* Updated CI configuration to source the container registry setting from
organization variables instead of secrets, improving maintainability and
visibility of build settings.
* No impact to application features, functionality, or performance;
builds and deployments continue to operate as before.
* No action required from users or admins; this is an internal workflow
refinement.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-16 15:05:25 +04:00
Timofei Larkin
07384c3605 [ci] Get REGISTRY from vars, not secrets
This patch sources the REGISTRY env var from GitHub actions variables
instead of secrets, so pull requests from forked repos work correctly.

```release-note
[ci] Source the REGISTRY env var from actions' variables, not secrets,
so external pull requests can work.
```

Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2025-09-16 11:36:00 +03:00
Andrei Kvapil
87b2316194 Release v0.36.0-beta.4 (#1422)
This PR prepares the release `v0.36.0-beta.4`.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- Chores
- Upgraded multiple components to v0.36.0-beta.4 (installer, API,
controller, dashboard, Kamaji, objectstorage controller, matchbox, e2e
sandbox, objectstorage-sidecar).
- Refreshed image digests to latest for kubevirt CSI driver,
nginx-cache, kubeovn, and s3manager.
  - Updated dashboard app version and related API images.
  - Pinned kubeovn-plunger to a stable version instead of latest.
- General stability, compatibility, and maintenance improvements with no
functional changes to user workflows.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-16 02:04:15 +02:00
cozystack-bot
585569f285 Prepare release v0.36.0-beta.4
Signed-off-by: cozystack-bot <217169706+cozystack-bot@users.noreply.github.com>
2025-09-15 23:22:50 +00:00
Andrei Kvapil
dbe1df8d27 [seaweedfs] Remove VerticalPodAutoscaler (#1421)
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>

<!-- Thank you for making a contribution! Here are some tips for you:
- Start the PR title with the [label] of Cozystack component:
- For system components: [platform], [system], [linstor], [cilium],
[kube-ovn], [dashboard], [cluster-api], etc.
- For managed apps: [apps], [tenant], [kubernetes], [postgres],
[virtual-machine] etc.
- For development and maintenance: [tests], [ci], [docs], [maintenance].
- If it's a work in progress, consider creating this PR as a draft.
- Don't hesistate to ask for opinion and review in the community chats,
even if it's still a draft.
- Add the label `backport` if it's a bugfix that needs to be backported
to a previous version.
-->

## What this PR does

It does not work well anyway

### Release note

<!--  Write a release note:
- Explain what has changed internally and for users.
- Start with the same [label] as in the PR title
- Follow the guidelines at
https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md.
-->

```release-note
[]
```

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- Breaking Changes
- Vertical Pod Autoscalers for SeaweedFS components (filer, master,
volume) are no longer deployed. Resource autoscaling via VPA is disabled
for new installs and upgrades.
- On upgrade, previously created VPAs may be removed; ensure resource
requests/limits are configured or manage autoscaling via HPA or external
tooling.

- Chores
- Deployment simplified by removing built-in VPA resources for SeaweedFS
components.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-15 21:51:38 +02:00
Andrei Kvapil
17eb1e0ba3 [seaweedfs] Remove VerticalPodAutoscaler
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-09-15 20:54:51 +02:00
Andrei Kvapil
b55c9f616d [kube-ovn] fix plunger: flag provided but not defined: -kube-ovn-namespace (#1418)
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>

<!-- Thank you for making a contribution! Here are some tips for you:
- Start the PR title with the [label] of Cozystack component:
- For system components: [platform], [system], [linstor], [cilium],
[kube-ovn], [dashboard], [cluster-api], etc.
- For managed apps: [apps], [tenant], [kubernetes], [postgres],
[virtual-machine] etc.
- For development and maintenance: [tests], [ci], [docs], [maintenance].
- If it's a work in progress, consider creating this PR as a draft.
- Don't hesistate to ask for opinion and review in the community chats,
even if it's still a draft.
- Add the label `backport` if it's a bugfix that needs to be backported
to a previous version.
-->

## What this PR does

This PR fixes the error:

```
flag provided but not defined: -kube-ovn-namespace
Usage of /kubeovn-plunger:
  -disable-telemetry
        Disable telemetry collection
  -enable-http2
        If set, HTTP/2 will be enabled for the metrics and webhook servers
  -health-probe-bind-address string
        The address the probe endpoint binds to. (default ":8081")
  -kubeconfig string
        Paths to a kubeconfig. Only required if out-of-cluster.
  -leader-elect
        Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager.
  -metrics-bind-address string
        The address the metrics endpoint binds to. Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service. (default "0")
  -metrics-secure
        If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead. (default true)
  -zap-devel
        Development Mode defaults(encoder=consoleEncoder,logLevel=Debug,stackTraceLevel=Warn). Production Mode defaults(encoder=jsonEncoder,logLevel=Info,stackTraceLevel=Error)
  -zap-encoder value
        Zap log encoding (one of 'json' or 'console')
  -zap-log-level value
        Zap Level to configure the verbosity of logging. Can be one of 'debug', 'info', 'error', or any integer value > 0 which corresponds to custom debug levels of increasing verbosity
  -zap-stacktrace-level value
        Zap Level at and above which stacktraces are captured (one of 'info', 'error', 'panic').
  -zap-time-encoding value
        Zap time encoding (one of 'epoch', 'millis', 'nano', 'iso8601', 'rfc3339' or 'rfc3339nano'). Defaults to 'epoch'.
```

### Release note

<!--  Write a release note:
- Explain what has changed internally and for users.
- Start with the same [label] as in the PR title
- Follow the guidelines at
https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md.
-->

```release-note
[]
```

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- New Features
  - None.

- Bug Fixes
- Improved kube-ovn-plunger reliability by removing a redundant
namespace configuration, allowing automatic detection and reducing
potential misconfiguration.
- Preserved existing logging and metrics behavior with no changes
required by users.

- Chores
- Simplified deployment configuration for kube-ovn-plunger by
eliminating an unnecessary parameter, reducing maintenance overhead.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-15 18:43:50 +02:00
Andrei Kvapil
f025845a94 [ingress] make nginx resources configurable (#1416)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- New Features
- Added per-replica CPU and memory configuration for the ingress
controller.
- Introduced resource presets (nano, micro, small, medium, large,
xlarge, 2xlarge) with a default of micro.
- Documentation
- Updated parameters guide to document new resource settings and
presets.
- Chores
  - Bumped ingress chart version to 1.9.0.
- Updated version mapping to include the new chart version and pin the
previous one.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-15 18:42:17 +02:00
Andrei Kvapil
e54fc63af4 [seaweedfs] Refactor config; add resources (#1415)
Co-authored-by: kklinch0 <kklinch0@gmail.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>

<!-- Thank you for making a contribution! Here are some tips for you:
- Start the PR title with the [label] of Cozystack component:
- For system components: [platform], [system], [linstor], [cilium],
[kube-ovn], [dashboard], [cluster-api], etc.
- For managed apps: [apps], [tenant], [kubernetes], [postgres],
[virtual-machine] etc.
- For development and maintenance: [tests], [ci], [docs], [maintenance].
- If it's a work in progress, consider creating this PR as a draft.
- Don't hesistate to ask for opinion and review in the community chats,
even if it's still a draft.
- Add the label `backport` if it's a bugfix that needs to be backported
to a previous version.
-->

## What this PR does


### Release note

<!--  Write a release note:
- Explain what has changed internally and for users.
- Start with the same [label] as in the PR title
- Follow the guidelines at
https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md.
-->

```release-note
[]
```

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- New Features
- Component-based configuration (master, volume with zones, filer, db,
s3) with per-service replicas and resource presets.
  - Per-zone volume monitoring plus new DB and S3 monitors.
- Database replicas/size/storageClass now configurable; S3 defaults to 2
replicas.
- Documentation
  - README updated to the new component-based schema.
- Refactor
- Configuration reorganized from flat to nested; standardized resource
settings.
- Chores
  - Chart version bumped to 0.7.0.
- Automated migration to upgrade releases and relocate existing values.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-15 18:42:02 +02:00
Timofei Larkin
9352861051 [cozystack-controller] Clusterwide read perms (#1419)
## What this PR does

In an earlier patch the Cozystack controller now reads arbitrary objects
in the cluster to establish the lineage of any created pod, service,
pvc, or secret. These objects may be created by various other
controllers, so in general, the controller now requires read permissions
on arbitrary objects in the cluster.

### Release note

```release-note
[cozystack-controler] Fix an RBAC error that prevented the workload
labelling feature from working.
```

Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2025-09-15 20:37:14 +04:00
Andrei Kvapil
b9eec3f261 [installer] Fix: add jq and git to installer image (#1417)
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>

<!-- Thank you for making a contribution! Here are some tips for you:
- Start the PR title with the [label] of Cozystack component:
- For system components: [platform], [system], [linstor], [cilium],
[kube-ovn], [dashboard], [cluster-api], etc.
- For managed apps: [apps], [tenant], [kubernetes], [postgres],
[virtual-machine] etc.
- For development and maintenance: [tests], [ci], [docs], [maintenance].
- If it's a work in progress, consider creating this PR as a draft.
- Don't hesistate to ask for opinion and review in the community chats,
even if it's still a draft.
- Add the label `backport` if it's a bugfix that needs to be backported
to a previous version.
-->

## What this PR does


### Release note

<!--  Write a release note:
- Explain what has changed internally and for users.
- Start with the same [label] as in the PR title
- Follow the guidelines at
https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md.
-->

```release-note
[]
```
2025-09-15 17:51:26 +02:00
Timofei Larkin
f2cfb4f870 [cozystack-controller] Clusterwide read perms
In an earlier patch the Cozystack controller now reads arbitrary objects
in the cluster to establish the lineage of any created pod, service,
pvc, or secret. These objects may be created by various other
controllers, so in general, the controller now requires read permissions
on arbitrary objects in the cluster.

```release-note
[cozystack-controler] Fix an RBAC error that prevented the workload
labelling feature from working.
```

Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2025-09-15 18:49:37 +03:00
Andrei Kvapil
2291d0f7f2 [kube-ovn] fix plunger: flag provided but not defined: -kube-ovn-namespace
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-09-15 17:27:18 +02:00
Andrei Kvapil
15c100d262 [installer] Fix: add jq and git to installer image
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-09-15 16:53:48 +02:00
kklinch0
2c9864bc09 [ingress] make nginx resources configurable
Signed-off-by: kklinch0 <kklinch0@gmail.com>
2025-09-15 16:48:52 +02:00
Andrei Kvapil
bb1e8805dc [seaweedfs] Refactor config; add resources
Co-authored-by: kklinch0 <kklinch0@gmail.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-09-15 16:45:40 +02:00
Andrei Kvapil
08b5217b72 [kubeovn] Fix service scrape for plunger (#1414)
## What this PR does

This patch delivers changes to the monitoring config of Kube-OVN
plunger, which were accidentally omitted in its release, leading to a
duplicate service, broken monitoring agents' helm release and not
actually scraping the plunger.

### Release note

```release-note
[kubeovn-plunger] Fix the VMServiceScrape object for collecting the
plunger's metrics.
```

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- New Features
- Enable metrics scraping for Kube-OVN Plunger, integrating it into the
monitoring stack.

- Chores
- Migrated scraping to a VictoriaMetrics configuration and moved
resources to the monitoring namespace.
- Updated selectors to target the Kube-OVN Plunger workload in the
appropriate namespace.
- Adjusted metric relabeling: node label removed and some label names
simplified; series may appear under kubeovn-plunger instead of kube-dns.
- Standardized scrape port naming to “metrics,” aligning with current
service conventions.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-15 10:49:38 +02:00
Timofei Larkin
08d2d61f1a [kubeovn] Fix service scrape for plunger
This patch delivers changes to the monitoring config of Kube-OVN
plunger, which were accidentally omitted in its release, leading to a
duplicate service, broken monitoring agents' helm release and not
actually scraping the plunger.

```release-note
[kubeovn-plunger] Fix the VMServiceScrape object for collecting the
plunger's metrics.
```

Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2025-09-15 10:50:16 +03:00
Timofei Larkin
356fea6a37 [cozystack-controller] Ancestor tracking webhook (#1400)
## What this PR does

Many resources created as part of managed apps in cozystack (pods,
secrets, etc) do not carry predictable labels that unambiguously
indicate which app originally triggered their creation. Some resources
are managed by controllers and other custom resources and this
indirection can lead to loss of information. Other controllers sometimes
simply do not allow setting labels on controlled resources and the
latter do not inherit labels from the owner. This patch implements a
webhook that sidesteps this problem with a universal solution. On
creation of a pod/secret/PVC etc it walks through the owner references
until a HelmRelease is found that can be matched with a managed app
dynamically registered in the Cozystack API server. The pod is mutated
with labels identifying the managed app.

### Release note

```release-note
[cozystack-controller] Add a mutating webhook to identify the Cozystack
managed app that ultimately owns low-level resources created in the
cluster and label these resources with a reference to said app.
```

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Adds an admission webhook that injects application lineage labels on
resource create/update for improved observability and ownership tracing.
- Adds a runtime-updatable mapping for resolving HelmRelease →
application, and registers both the lineage controller and webhook
during startup.
- Adds Deployment, Service, and cert-manager templates to enable and
secure the webhook (in-cluster TLS, service routing).

- **Tests**
- Adds a test to exercise lineage traversal and validate ownership-graph
resolution and labeling.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2025-09-12 11:44:12 +04:00
Timofei Larkin
e1b97e3727 [cozystack-controller] Ancestor tracking webhook
Many resources created as part of managed apps in cozystack (pods,
secrets, etc) do not carry predictable labels that unambiguously
indicate which app originally triggered their creation. Some resources
are managed by controllers and other custom resources and this
indirection can lead to loss of information. Other controllers sometimes
simply do not allow setting labels on controlled resources and the
latter do not inherit labels from the owner. This patch implements a
webhook that sidesteps this problem with a universal solution. On
creation of a pod/secret/PVC etc it walks through the owner references
until a HelmRelease is found that can be matched with a managed app
dynamically registered in the Cozystack API server. The pod is mutated
with labels identifying the managed app.

```release-note
[cozystack-controller] Add a mutating webhook to identify the Cozystack
managed app that ultimately owns low-level resources created in the
cluster and label these resources with a reference to said app.
```

Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2025-09-11 20:55:33 +03:00
48 changed files with 1748 additions and 328 deletions

View File

@@ -1,7 +1,7 @@
name: Pull Request
env:
REGISTRY: ${{ secrets.OCIR_REPO }}
REGISTRY: ${{ vars.OCIR_REPO }}
on:
pull_request:
types: [opened, synchronize, reopened]

View File

@@ -38,6 +38,7 @@ import (
cozystackiov1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
"github.com/cozystack/cozystack/internal/controller"
"github.com/cozystack/cozystack/internal/controller/lineagelabeler"
"github.com/cozystack/cozystack/internal/telemetry"
helmv2 "github.com/fluxcd/helm-controller/api/v2"
@@ -67,6 +68,7 @@ func main() {
var telemetryEndpoint string
var telemetryInterval string
var cozystackVersion string
var watchResources string
var tlsOpts []func(*tls.Config)
flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
"Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.")
@@ -86,6 +88,9 @@ func main() {
"Interval between telemetry data collection (e.g. 15m, 1h)")
flag.StringVar(&cozystackVersion, "cozystack-version", "unknown",
"Version of Cozystack")
flag.StringVar(&watchResources, "watch-resources",
"v1/Pod,v1/Service,v1/Secret,v1/PersistentVolumeClaim",
"Comma-separated list of resources to watch in the form 'group/version/Kind'.")
opts := zap.Options{
Development: false,
}
@@ -214,6 +219,15 @@ func main() {
os.Exit(1)
}
if err := (&lineagelabeler.LineageLabelerReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
WatchResourceCSV: watchResources,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "LineageLabeler")
os.Exit(1)
}
// +kubebuilder:scaffold:builder
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {

View File

@@ -2,14 +2,25 @@ package controller
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"sort"
"sync"
"time"
"github.com/cozystack/cozystack/internal/shared/crdmem"
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
"github.com/go-logr/logr"
appsv1 "k8s.io/api/apps/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/log"
@@ -20,85 +31,55 @@ type CozystackResourceDefinitionReconciler struct {
client.Client
Scheme *runtime.Scheme
// Configurable debounce duration
Debounce time.Duration
// Internal state for debouncing
mu sync.Mutex
lastEvent time.Time // Time of last CRUD event on CozystackResourceDefinition
lastHandled time.Time // Last time the Deployment was actually restarted
lastEvent time.Time
lastHandled time.Time
mem *crdmem.Memory
}
// Reconcile handles the logic to restart the target Deployment only once,
// even if multiple events occur close together
func (r *CozystackResourceDefinitionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := log.FromContext(ctx)
logger := log.FromContext(ctx)
// Only respond to our target deployment
if req.Namespace != "cozy-system" || req.Name != "cozystack-api" {
crd := &cozyv1alpha1.CozystackResourceDefinition{}
err := r.Get(ctx, types.NamespacedName{Name: req.Name}, crd)
if err == nil {
if r.mem != nil {
r.mem.Upsert(crd)
}
r.mu.Lock()
r.lastEvent = time.Now()
r.mu.Unlock()
return ctrl.Result{}, nil
}
r.mu.Lock()
le := r.lastEvent
lh := r.lastHandled
debounce := r.Debounce
r.mu.Unlock()
if debounce <= 0 {
debounce = 5 * time.Second
}
// No events received yet — nothing to do
if le.IsZero() {
return ctrl.Result{}, nil
}
// Wait until the debounce duration has passed since the last event
if d := time.Since(le); d < debounce {
return ctrl.Result{RequeueAfter: debounce - d}, nil
}
// Already handled this event — skip restart
if !lh.Before(le) {
return ctrl.Result{}, nil
}
// Perform the restart by patching the deployment annotation
deploy := &appsv1.Deployment{}
if err := r.Get(ctx, types.NamespacedName{Namespace: "cozy-system", Name: "cozystack-api"}, deploy); err != nil {
log.Error(err, "Failed to get Deployment cozy-system/cozystack-api")
return ctrl.Result{}, client.IgnoreNotFound(err)
}
patch := client.MergeFrom(deploy.DeepCopy())
if deploy.Spec.Template.Annotations == nil {
deploy.Spec.Template.Annotations = make(map[string]string)
}
deploy.Spec.Template.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339)
if err := r.Patch(ctx, deploy, patch); err != nil {
log.Error(err, "Failed to patch Deployment annotation")
if err != nil && !apierrors.IsNotFound(err) {
return ctrl.Result{}, err
}
// Mark this event as handled
r.mu.Lock()
r.lastHandled = le
r.mu.Unlock()
log.Info("Deployment cozy-system/cozystack-api successfully restarted")
if apierrors.IsNotFound(err) && r.mem != nil {
r.mem.Delete(req.Name)
}
if req.Namespace == "cozy-system" && req.Name == "cozystack-api" {
return r.debouncedRestart(ctx, logger)
}
return ctrl.Result{}, nil
}
// SetupWithManager configures how the controller listens to events
func (r *CozystackResourceDefinitionReconciler) SetupWithManager(mgr ctrl.Manager) error {
if r.Debounce == 0 {
r.Debounce = 5 * time.Second
}
if r.mem == nil {
r.mem = crdmem.Global()
}
if err := r.mem.EnsurePrimingWithManager(mgr); err != nil {
return err
}
return ctrl.NewControllerManagedBy(mgr).
Named("cozystack-restart-controller").
Named("cozystackresource-controller").
For(&cozyv1alpha1.CozystackResourceDefinition{}, builder.WithPredicates()).
Watches(
&cozyv1alpha1.CozystackResourceDefinition{},
handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
@@ -115,3 +96,88 @@ func (r *CozystackResourceDefinitionReconciler) SetupWithManager(mgr ctrl.Manage
).
Complete(r)
}
type crdHashView struct {
Name string `json:"name"`
Spec cozyv1alpha1.CozystackResourceDefinitionSpec `json:"spec"`
}
func (r *CozystackResourceDefinitionReconciler) computeConfigHash() (string, error) {
if r.mem == nil {
return "", nil
}
snapshot := r.mem.Snapshot()
sort.Slice(snapshot, func(i, j int) bool { return snapshot[i].Name < snapshot[j].Name })
views := make([]crdHashView, 0, len(snapshot))
for i := range snapshot {
views = append(views, crdHashView{
Name: snapshot[i].Name,
Spec: snapshot[i].Spec,
})
}
b, err := json.Marshal(views)
if err != nil {
return "", err
}
sum := sha256.Sum256(b)
return hex.EncodeToString(sum[:]), nil
}
func (r *CozystackResourceDefinitionReconciler) debouncedRestart(ctx context.Context, logger logr.Logger) (ctrl.Result, error) {
r.mu.Lock()
le := r.lastEvent
lh := r.lastHandled
debounce := r.Debounce
r.mu.Unlock()
if debounce <= 0 {
debounce = 5 * time.Second
}
if le.IsZero() {
return ctrl.Result{}, nil
}
if d := time.Since(le); d < debounce {
return ctrl.Result{RequeueAfter: debounce - d}, nil
}
if !lh.Before(le) {
return ctrl.Result{}, nil
}
newHash, err := r.computeConfigHash()
if err != nil {
return ctrl.Result{}, err
}
deploy := &appsv1.Deployment{}
if err := r.Get(ctx, types.NamespacedName{Namespace: "cozy-system", Name: "cozystack-api"}, deploy); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
if deploy.Spec.Template.Annotations == nil {
deploy.Spec.Template.Annotations = map[string]string{}
}
oldHash := deploy.Spec.Template.Annotations["cozystack.io/config-hash"]
if oldHash == newHash && oldHash != "" {
r.mu.Lock()
r.lastHandled = le
r.mu.Unlock()
logger.Info("No changes in CRD config; skipping restart", "hash", newHash)
return ctrl.Result{}, nil
}
patch := client.MergeFrom(deploy.DeepCopy())
deploy.Spec.Template.Annotations["cozystack.io/config-hash"] = newHash
if err := r.Patch(ctx, deploy, patch); err != nil {
return ctrl.Result{}, err
}
r.mu.Lock()
r.lastHandled = le
r.mu.Unlock()
logger.Info("Updated cozystack-api podTemplate config-hash; rollout triggered",
"old", oldHash, "new", newHash)
return ctrl.Result{}, nil
}

View File

@@ -0,0 +1,367 @@
package lineagelabeler
import (
"context"
"errors"
"fmt"
"strings"
"sync"
"sync/atomic"
"time"
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
"github.com/cozystack/cozystack/internal/shared/crdmem"
"github.com/cozystack/cozystack/pkg/lineage"
helmv2 "github.com/fluxcd/helm-controller/api/v2"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/discovery"
"k8s.io/client-go/discovery/cached/memory"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
"k8s.io/client-go/restmapper"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
var ErrNoAncestors = errors.New("no ancestors")
type LineageLabelerReconciler struct {
client.Client
Scheme *runtime.Scheme
WatchResourceCSV string
dynClient dynamic.Interface
mapper meta.RESTMapper
appMap atomic.Value
once sync.Once
mem *crdmem.Memory
}
type chartRef struct{ repo, chart string }
type appRef struct{ groupVersion, kind, prefix string }
func (r *LineageLabelerReconciler) initMapping() {
r.once.Do(func() {
r.appMap.Store(make(map[chartRef]appRef))
})
}
func (r *LineageLabelerReconciler) currentMap() map[chartRef]appRef {
val := r.appMap.Load()
if val == nil {
return map[chartRef]appRef{}
}
return val.(map[chartRef]appRef)
}
func (r *LineageLabelerReconciler) Map(hr *helmv2.HelmRelease) (string, string, string, error) {
cfg := r.currentMap()
s := hr.Spec.Chart.Spec
key := chartRef{s.SourceRef.Name, s.Chart}
if v, ok := cfg[key]; ok {
return v.groupVersion, v.kind, v.prefix, nil
}
return "", "", "", fmt.Errorf("cannot map helm release %s/%s to dynamic app", hr.Namespace, hr.Name)
}
func parseGVKList(csv string) ([]schema.GroupVersionKind, error) {
csv = strings.TrimSpace(csv)
if csv == "" {
return nil, fmt.Errorf("watch resource list is empty")
}
parts := strings.Split(csv, ",")
out := make([]schema.GroupVersionKind, 0, len(parts))
for _, p := range parts {
p = strings.TrimSpace(p)
s := strings.Split(p, "/")
if len(s) == 2 {
out = append(out, schema.GroupVersionKind{Group: "", Version: s[0], Kind: s[1]})
continue
}
if len(s) == 3 {
out = append(out, schema.GroupVersionKind{Group: s[0], Version: s[1], Kind: s[2]})
continue
}
return nil, fmt.Errorf("invalid resource token %q, expected 'group/version/Kind' or 'v1/Kind'", p)
}
return out, nil
}
func (r *LineageLabelerReconciler) SetupWithManager(mgr ctrl.Manager) error {
r.initMapping()
cfg := rest.CopyConfig(mgr.GetConfig())
dc, err := dynamic.NewForConfig(cfg)
if err != nil {
return err
}
disco, err := discovery.NewDiscoveryClientForConfig(cfg)
if err != nil {
return err
}
cached := memory.NewMemCacheClient(disco)
r.dynClient = dc
r.mapper = restmapper.NewDeferredDiscoveryRESTMapper(cached)
if r.mem == nil {
r.mem = crdmem.Global()
}
if err := r.mem.EnsurePrimingWithManager(mgr); err != nil {
return err
}
gvks, err := parseGVKList(r.WatchResourceCSV)
if err != nil {
return err
}
if len(gvks) == 0 {
return fmt.Errorf("no resources to watch")
}
b := ctrl.NewControllerManagedBy(mgr).Named("lineage-labeler")
nsPred := predicate.NewPredicateFuncs(func(obj client.Object) bool {
ns := obj.GetNamespace()
return ns != "" && strings.HasPrefix(ns, "tenant-")
})
primary := gvks[0]
primaryObj := &unstructured.Unstructured{}
primaryObj.SetGroupVersionKind(primary)
b = b.For(primaryObj,
builder.WithPredicates(
predicate.And(
nsPred,
predicate.Or(
predicate.GenerationChangedPredicate{},
predicate.ResourceVersionChangedPredicate{},
),
),
),
)
for _, gvk := range gvks[1:] {
u := &unstructured.Unstructured{}
u.SetGroupVersionKind(gvk)
b = b.Watches(u,
&handler.EnqueueRequestForObject{},
builder.WithPredicates(
predicate.And(
nsPred,
predicate.Or(
predicate.GenerationChangedPredicate{},
predicate.ResourceVersionChangedPredicate{},
),
),
),
)
}
b = b.Watches(
&cozyv1alpha1.CozystackResourceDefinition{},
handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
_ = r.refreshAppMap(ctx)
return nil
}),
)
_ = r.refreshAppMap(context.Background())
return b.Complete(r)
}
func (r *LineageLabelerReconciler) refreshAppMap(ctx context.Context) error {
var items []cozyv1alpha1.CozystackResourceDefinition
var err error
if r.mem != nil {
items, err = r.mem.ListFromCacheOrAPI(ctx, r.Client)
} else {
var list cozyv1alpha1.CozystackResourceDefinitionList
err = r.Client.List(ctx, &list)
items = list.Items
}
if err != nil {
return err
}
newMap := make(map[chartRef]appRef, len(items))
for _, crd := range items {
k := chartRef{
repo: crd.Spec.Release.Chart.SourceRef.Name,
chart: crd.Spec.Release.Chart.Name,
}
v := appRef{
groupVersion: "apps.cozystack.io/v1alpha1",
kind: crd.Spec.Application.Kind,
prefix: crd.Spec.Release.Prefix,
}
if _, exists := newMap[k]; exists {
continue
}
newMap[k] = v
}
r.appMap.Store(newMap)
return nil
}
func (r *LineageLabelerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
l := log.FromContext(ctx)
if req.Namespace == "" || !strings.HasPrefix(req.Namespace, "tenant-") {
return ctrl.Result{}, nil
}
if len(r.currentMap()) == 0 {
_ = r.refreshAppMap(ctx)
if len(r.currentMap()) == 0 {
return ctrl.Result{RequeueAfter: 2 * time.Second}, nil
}
}
gvks, err := parseGVKList(r.WatchResourceCSV)
if err != nil {
return ctrl.Result{}, err
}
var obj *unstructured.Unstructured
found := false
for _, gvk := range gvks {
mapping, mErr := r.mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
if mErr != nil {
continue
}
ns := req.Namespace
if mapping.Scope.Name() != meta.RESTScopeNameNamespace {
ns = ""
}
res, gErr := r.dynClient.Resource(mapping.Resource).Namespace(ns).Get(ctx, req.Name, metav1.GetOptions{})
if gErr != nil {
if apierrors.IsNotFound(gErr) {
continue
}
continue
}
obj = res
found = true
break
}
if !found || obj == nil {
return ctrl.Result{}, nil
}
existing := obj.GetLabels()
if existing == nil {
existing = map[string]string{}
}
keys := []string{
"apps.cozystack.io/application.group",
"apps.cozystack.io/application.kind",
"apps.cozystack.io/application.name",
}
allPresent := true
for _, k := range keys {
if _, ok := existing[k]; !ok {
allPresent = false
break
}
}
if allPresent {
return ctrl.Result{}, nil
}
labels, warn, err := r.computeLabels(ctx, obj)
if err != nil {
if errors.Is(err, ErrNoAncestors) {
return ctrl.Result{}, nil
}
return ctrl.Result{}, client.IgnoreNotFound(err)
}
if warn != "" {
l.V(1).Info("lineage ambiguous; using first ancestor", "name", req.NamespacedName)
}
for k, v := range labels {
existing[k] = v
}
obj.SetLabels(existing)
// Server-Side Apply: claim ownership of our label keys
gvk := obj.GroupVersionKind()
patch := &unstructured.Unstructured{}
patch.SetGroupVersionKind(gvk)
patch.SetNamespace(obj.GetNamespace())
patch.SetName(obj.GetName())
patch.SetLabels(map[string]string{
"apps.cozystack.io/application.group": existing["apps.cozystack.io/application.group"],
"apps.cozystack.io/application.kind": existing["apps.cozystack.io/application.kind"],
"apps.cozystack.io/application.name": existing["apps.cozystack.io/application.name"],
})
// Use controller-runtime client with Apply patch type and field owner
if err := r.Patch(ctx, patch,
client.Apply,
client.FieldOwner("cozystack/lineage"),
client.ForceOwnership(false),
); err != nil {
if apierrors.IsConflict(err) {
return ctrl.Result{RequeueAfter: 500 * time.Millisecond}, nil
}
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
func (r *LineageLabelerReconciler) computeLabels(ctx context.Context, o *unstructured.Unstructured) (map[string]string, string, error) {
owners := lineage.WalkOwnershipGraph(ctx, r.dynClient, r.mapper, r, o)
if len(owners) == 0 {
return nil, "", ErrNoAncestors
}
obj, err := owners[0].GetUnstructured(ctx, r.dynClient, r.mapper)
if err != nil {
return nil, "", err
}
gv, err := schema.ParseGroupVersion(obj.GetAPIVersion())
if err != nil {
return nil, "", fmt.Errorf("invalid APIVersion %s: %w", obj.GetAPIVersion(), err)
}
var warn string
if len(owners) > 1 {
warn = "ambiguous"
}
group := gv.Group
if len(group) > 63 {
group = trimDNSLabel(group[:63])
}
return map[string]string{
"apps.cozystack.io/application.group": group,
"apps.cozystack.io/application.kind": obj.GetKind(),
"apps.cozystack.io/application.name": obj.GetName(),
}, warn, nil
}
func trimDNSLabel(s string) string {
for len(s) > 0 {
b := s[len(s)-1]
if (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9') {
return s
}
s = s[:len(s)-1]
}
return s
}

View File

@@ -0,0 +1,99 @@
package crdmem
import (
"context"
"sync"
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
)
type Memory struct {
mu sync.RWMutex
data map[string]cozyv1alpha1.CozystackResourceDefinition
primed bool
primeOnce sync.Once
}
func New() *Memory {
return &Memory{data: make(map[string]cozyv1alpha1.CozystackResourceDefinition)}
}
var (
global *Memory
globalOnce sync.Once
)
func Global() *Memory {
globalOnce.Do(func() { global = New() })
return global
}
func (m *Memory) Upsert(obj *cozyv1alpha1.CozystackResourceDefinition) {
if obj == nil {
return
}
m.mu.Lock()
m.data[obj.Name] = *obj.DeepCopy()
m.mu.Unlock()
}
func (m *Memory) Delete(name string) {
m.mu.Lock()
delete(m.data, name)
m.mu.Unlock()
}
func (m *Memory) Snapshot() []cozyv1alpha1.CozystackResourceDefinition {
m.mu.RLock()
defer m.mu.RUnlock()
out := make([]cozyv1alpha1.CozystackResourceDefinition, 0, len(m.data))
for _, v := range m.data {
out = append(out, v)
}
return out
}
func (m *Memory) IsPrimed() bool {
m.mu.RLock()
defer m.mu.RUnlock()
return m.primed
}
type runnable func(context.Context) error
func (r runnable) Start(ctx context.Context) error { return r(ctx) }
func (m *Memory) EnsurePrimingWithManager(mgr ctrl.Manager) error {
var errOut error
m.primeOnce.Do(func() {
errOut = mgr.Add(runnable(func(ctx context.Context) error {
if ok := mgr.GetCache().WaitForCacheSync(ctx); !ok {
return nil
}
var list cozyv1alpha1.CozystackResourceDefinitionList
if err := mgr.GetClient().List(ctx, &list); err == nil {
for i := range list.Items {
m.Upsert(&list.Items[i])
}
m.mu.Lock()
m.primed = true
m.mu.Unlock()
}
return nil
}))
})
return errOut
}
func (m *Memory) ListFromCacheOrAPI(ctx context.Context, c client.Client) ([]cozyv1alpha1.CozystackResourceDefinition, error) {
if m.IsPrimed() {
return m.Snapshot(), nil
}
var list cozyv1alpha1.CozystackResourceDefinitionList
if err := c.List(ctx, &list); err != nil {
return nil, err
}
return list.Items, nil
}

View File

@@ -1 +1 @@
ghcr.io/cozystack/cozystack/nginx-cache:0.7.0@sha256:c1944c60a449e36e29153a38db6feee41139d38b02fe3670efb673feb3bc0ee6
ghcr.io/cozystack/cozystack/nginx-cache:0.7.0@sha256:e0a07082bb6fc6aeaae2315f335386f1705a646c72f9e0af512aebbca5cb2b15

View File

@@ -1 +1 @@
ghcr.io/cozystack/cozystack/kubevirt-csi-driver:0.29.0@sha256:8eb9803aa1b38e1b2db98237bf0d1046f0ba90be0157c22da1efc3811bb25ecf
ghcr.io/cozystack/cozystack/kubevirt-csi-driver:0.29.0@sha256:3a3bc912f70ccba1e9f92a0754179dbdc4c01f24073467b6d1406c77da794863

View File

@@ -34,7 +34,7 @@ FROM alpine:3.22
RUN wget -O- https://github.com/cozystack/cozypkg/raw/refs/heads/main/hack/install.sh | sh -s -- -v 1.1.0
RUN apk add --no-cache make kubectl coreutils
RUN apk add --no-cache make kubectl coreutils git jq
COPY --from=builder /src/scripts /cozystack/scripts
COPY --from=builder /src/packages/core /cozystack/packages/core

View File

@@ -1,2 +1,2 @@
cozystack:
image: ghcr.io/cozystack/cozystack/installer:v0.36.0-beta.3@sha256:275a20255a04c4cc5850fada5b7b15fbe95cd75c5eef518679ca192800a9f916
image: ghcr.io/cozystack/cozystack/installer:v0.36.0-beta.4@sha256:51cb9828af3bdceac289de3b1161625065db22535a961958530e9c751880ee96

View File

@@ -1,2 +1,2 @@
e2e:
image: ghcr.io/cozystack/cozystack/e2e-sandbox:v0.36.0-beta.3@sha256:813d9b07350e2aaa5b5f34821bc66752f3e23385eac30557ea8c023014d2fbd7
image: ghcr.io/cozystack/cozystack/e2e-sandbox:v0.36.0-beta.4@sha256:c70ad3321fc14ca831c84bf6e7e6e5409bfe8130703173c277ca51db740c6cb3

View File

@@ -1 +1 @@
ghcr.io/cozystack/cozystack/matchbox:v0.36.0-beta.3@sha256:772422d29b91c185edf030a5e0be18b2183464db9a8230cb042227aa7e716ea0
ghcr.io/cozystack/cozystack/matchbox:v0.36.0-beta.4@sha256:764c547a352c9c1d0442e43cdfd0ef50b216bd7f6e5514c777b1d90c4d95da92

View File

@@ -3,4 +3,4 @@ name: ingress
description: NGINX Ingress Controller
icon: /logos/ingress-nginx.svg
type: application
version: 1.8.0
version: 1.9.0

View File

@@ -4,9 +4,13 @@
### Common parameters
| Name | Description | Type | Value |
| ---------------- | ----------------------------------------------------------------- | ----------- | ------- |
| `replicas` | Number of ingress-nginx replicas | `int` | `2` |
| `whitelist` | List of client networks | `[]*string` | `[]` |
| `clouflareProxy` | Restoring original visitor IPs when Cloudflare proxied is enabled | `bool` | `false` |
| Name | Description | Type | Value |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | ----------- | ------- |
| `replicas` | Number of ingress-nginx replicas | `int` | `2` |
| `whitelist` | List of client networks | `[]*string` | `[]` |
| `cloudflareProxy` | Restoring original visitor IPs when Cloudflare proxied is enabled | `bool` | `false` |
| `resources` | Explicit CPU and memory configuration for each ingress-nginx replica. When left empty, the preset defined in `resourcesPreset` is applied. | `*object` | `{}` |
| `resources.cpu` | CPU available to each replica | `*quantity` | `null` |
| `resources.memory` | Memory (RAM) available to each replica | `*quantity` | `null` |
| `resourcesPreset` | Default sizing preset used when `resources` is omitted. Allowed values: `nano`, `micro`, `small`, `medium`, `large`, `xlarge`, `2xlarge`. | `string` | `micro` |

View File

@@ -29,6 +29,7 @@ spec:
controller:
replicaCount: {{ .Values.replicas }}
ingressClass: {{ .Release.Namespace }}
resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.resourcesPreset .Values.resources $) | nindent 10 }}
ingressClassResource:
name: {{ .Release.Namespace }}
controllerValue: k8s.io/ingress-nginx-{{ .Release.Namespace }}
@@ -49,12 +50,12 @@ spec:
type: LoadBalancer
externalTrafficPolicy: Local
{{- end }}
{{- if or .Values.whitelist .Values.clouflareProxy }}
{{- if or .Values.whitelist .Values.cloudflareProxy }}
config:
{{- with .Values.whitelist }}
whitelist-source-range: "{{ join "," . }}"
{{- end }}
{{- if .Values.clouflareProxy }}
{{- if .Values.cloudflareProxy }}
set_real_ip_from: "{{ include "ingress.cloudflare-ips" . }}"
use-forwarded-headers: "true"
server-snippet: "real_ip_header CF-Connecting-IP;"

View File

@@ -2,7 +2,7 @@
"title": "Chart Values",
"type": "object",
"properties": {
"clouflareProxy": {
"cloudflareProxy": {
"description": "Restoring original visitor IPs when Cloudflare proxied is enabled",
"type": "boolean",
"default": false
@@ -12,6 +12,53 @@
"type": "integer",
"default": 2
},
"resources": {
"description": "Explicit CPU and memory configuration for each ingress-nginx replica. When left empty, the preset defined in `resourcesPreset` is applied.",
"type": "object",
"default": {},
"properties": {
"cpu": {
"description": "CPU available to each replica",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
},
"memory": {
"description": "Memory (RAM) available to each replica",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
}
}
},
"resourcesPreset": {
"description": "Default sizing preset used when `resources` is omitted. Allowed values: `nano`, `micro`, `small`, `medium`, `large`, `xlarge`, `2xlarge`.",
"type": "string",
"default": "micro",
"enum": [
"nano",
"micro",
"small",
"medium",
"large",
"xlarge",
"2xlarge"
]
},
"whitelist": {
"description": "List of client networks",
"type": "array",

View File

@@ -11,5 +11,16 @@ replicas: 2
## - "10.100.0.0/16"
whitelist: []
## @param clouflareProxy {bool} Restoring original visitor IPs when Cloudflare proxied is enabled
clouflareProxy: false
## @param cloudflareProxy {bool} Restoring original visitor IPs when Cloudflare proxied is enabled
cloudflareProxy: false
## @param resources {*resources} Explicit CPU and memory configuration for each ingress-nginx replica. When left empty, the preset defined in `resourcesPreset` is applied.
## @field resources.cpu {*quantity} CPU available to each replica
## @field resources.memory {*quantity} Memory (RAM) available to each replica
## Example:
## resources:
## cpu: 4000m
## memory: 4Gi
resources: {}
## @param resourcesPreset {string enum:"nano,micro,small,medium,large,xlarge,2xlarge"} Default sizing preset used when `resources` is omitted. Allowed values: `nano`, `micro`, `small`, `medium`, `large`, `xlarge`, `2xlarge`.
resourcesPreset: "micro"

View File

@@ -16,7 +16,7 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.6.0
version: 0.7.0
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to

View File

@@ -4,19 +4,55 @@
### Common parameters
| Name | Description | Type | Value |
| ---------------------- | ------------------------------------------------------------------------------------------------------ | ------------------- | -------- |
| `host` | The hostname used to access the SeaweedFS externally (defaults to 's3' subdomain for the tenant host). | `*string` | `""` |
| `topology` | The topology of the SeaweedFS cluster. (allowed values: Simple, MultiZone, Client) | `string` | `Simple` |
| `replicationFactor` | Replication factor: number of replicas for each volume in the SeaweedFS cluster. | `int` | `2` |
| `replicas` | Number of replicas | `int` | `2` |
| `size` | Persistent Volume size | `quantity` | `10Gi` |
| `storageClass` | StorageClass used to store the data | `*string` | `""` |
| `zones` | A map of zones for MultiZone topology. Each zone can have its own number of replicas and size. | `map[string]object` | `{...}` |
| `zones[name].replicas` | Number of replicas in the zone | `int` | `0` |
| `zones[name].size` | Zone storage size | `quantity` | `""` |
| `filer` | Filer service configuration | `*object` | `{}` |
| `filer.grpcHost` | The hostname used to expose or access the filer service externally. | `*string` | `""` |
| `filer.grpcPort` | The port used to access the filer service externally. | `*int` | `443` |
| `filer.whitelist` | A list of IP addresses or CIDR ranges that are allowed to access the filer service. | `[]*string` | `[]` |
| Name | Description | Type | Value |
| ------------------- | ------------------------------------------------------------------------------------------------------ | --------- | -------- |
| `host` | The hostname used to access the SeaweedFS externally (defaults to 's3' subdomain for the tenant host). | `*string` | `""` |
| `topology` | The topology of the SeaweedFS cluster. (allowed values: Simple, MultiZone, Client) | `string` | `Simple` |
| `replicationFactor` | Replication factor: number of replicas for each volume in the SeaweedFS cluster. | `int` | `2` |
### SeaweedFS Components Configuration
| Name | Description | Type | Value |
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | ------- |
| `db` | Database Configuration | `object` | `{}` |
| `db.replicas` | Number of database replicas | `*int` | `2` |
| `db.size` | Persistent Volume size | `*quantity` | `10Gi` |
| `db.storageClass` | StorageClass used to store the data | `*string` | `""` |
| `db.resources` | Explicit CPU and memory configuration for the database. When left empty, the preset defined in `resourcesPreset` is applied. | `object` | `{}` |
| `db.resources.cpu` | The number of CPU cores allocated | `*quantity` | `null` |
| `db.resources.memory` | The amount of memory allocated | `*quantity` | `null` |
| `db.resourcesPreset` | Default sizing preset used when `resources` is omitted. Allowed values: `nano`, `micro`, `small`, `medium`, `large`, `xlarge`, `2xlarge`. | `string` | `small` |
| `master` | Master service configuration | `*object` | `{}` |
| `master.replicas` | Number of master replicas | `*int` | `3` |
| `master.resources` | Explicit CPU and memory configuration for the master. When left empty, the preset defined in `resourcesPreset` is applied. | `object` | `{}` |
| `master.resources.cpu` | The number of CPU cores allocated | `*quantity` | `null` |
| `master.resources.memory` | The amount of memory allocated | `*quantity` | `null` |
| `master.resourcesPreset` | Default sizing preset used when `resources` is omitted. Allowed values: `nano`, `micro`, `small`, `medium`, `large`, `xlarge`, `2xlarge`. | `string` | `small` |
| `filer` | Filer service configuration | `*object` | `{}` |
| `filer.replicas` | Number of filer replicas | `*int` | `2` |
| `filer.resources` | Explicit CPU and memory configuration for the filer. When left empty, the preset defined in `resourcesPreset` is applied. | `object` | `{}` |
| `filer.resources.cpu` | The number of CPU cores allocated | `*quantity` | `null` |
| `filer.resources.memory` | The amount of memory allocated | `*quantity` | `null` |
| `filer.resourcesPreset` | Default sizing preset used when `resources` is omitted. Allowed values: `nano`, `micro`, `small`, `medium`, `large`, `xlarge`, `2xlarge`. | `string` | `small` |
| `filer.grpcHost` | The hostname used to expose or access the filer service externally. | `*string` | `""` |
| `filer.grpcPort` | The port used to access the filer service externally. | `*int` | `443` |
| `filer.whitelist` | A list of IP addresses or CIDR ranges that are allowed to access the filer service. | `[]*string` | `[]` |
| `volume` | Volume service configuration | `*object` | `{}` |
| `volume.replicas` | Number of volume replicas | `*int` | `2` |
| `volume.size` | Persistent Volume size | `*quantity` | `10Gi` |
| `volume.storageClass` | StorageClass used to store the data | `*string` | `""` |
| `volume.resources` | Explicit CPU and memory configuration for the volume. When left empty, the preset defined in `resourcesPreset` is applied. | `object` | `{}` |
| `volume.resources.cpu` | The number of CPU cores allocated | `*quantity` | `null` |
| `volume.resources.memory` | The amount of memory allocated | `*quantity` | `null` |
| `volume.resourcesPreset` | Default sizing preset used when `resources` is omitted. Allowed values: `nano`, `micro`, `small`, `medium`, `large`, `xlarge`, `2xlarge`. | `string` | `small` |
| `volume.zones` | A map of zones for MultiZone topology. Each zone can have its own number of replicas and size. | `map[string]object` | `{}` |
| `volume.zones.replicas` | Number of replicas in the zone | `*int` | `null` |
| `volume.zones.size` | Zone storage size | `*quantity` | `null` |
| `s3` | S3 service configuration | `*object` | `{}` |
| `s3.replicas` | Number of s3 replicas | `*int` | `2` |
| `s3.resources` | Explicit CPU and memory configuration for the s3. When left empty, the preset defined in `resourcesPreset` is applied. | `object` | `{}` |
| `s3.resources.cpu` | The number of CPU cores allocated | `*quantity` | `null` |
| `s3.resources.memory` | The amount of memory allocated | `*quantity` | `null` |
| `s3.resourcesPreset` | Default sizing preset used when `resources` is omitted. Allowed values: `nano`, `micro`, `small`, `medium`, `large`, `xlarge`, `2xlarge`. | `string` | `small` |

View File

@@ -1 +1 @@
ghcr.io/cozystack/cozystack/objectstorage-sidecar:v0.36.0-beta.3@sha256:c8db3d458f65c6a3ccd25651a670e549a6a5601b7832f25bb873fdaf3113008c
ghcr.io/cozystack/cozystack/objectstorage-sidecar:v0.36.0-beta.4@sha256:24451989b15b6801b33ad355a5507307d0333bf9afd240f1db0aca9c92f6b2ad

View File

@@ -9,11 +9,11 @@
{{- fail "Invalid value for .Values.replicationFactor. Must be at least 1." }}
{{- end }}
{{- if eq .Values.topology "MultiZone" }}
{{- if (eq (len .Values.zones) 0) }}
{{- if (eq (len .Values.volume.zones) 0) }}
{{- fail "Zones must be defined for MultiZone topology." }}
{{- end }}
{{- if and (hasKey .Values "zones") (gt (int .Values.replicationFactor) (len .Values.zones)) }}
{{- fail "replicationFactor must be less than or equal to the number of zones defined in .Values.zones." }}
{{- if and (hasKey .Values.volume "zones") (gt (int .Values.replicationFactor) (len .Values.volume.zones)) }}
{{- fail "replicationFactor must be less than or equal to the number of zones defined in .Values.volume.zones." }}
{{- end }}
{{- end }}
@@ -62,6 +62,13 @@ spec:
values:
global:
serviceAccountName: "{{ .Release.Namespace }}-seaweedfs"
db:
replicas: {{ .Values.db.replicas }}
resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.db.resourcesPreset .Values.db.resources $) | nindent 8 }}
size: {{ .Values.db.size }}
{{- with .Values.db.storageClass }}
storageClass: {{ . }}
{{- end }}
seaweedfs:
master:
{{ if eq .Values.topology "Simple" }}
@@ -69,36 +76,25 @@ spec:
{{- else if eq .Values.topology "MultiZone" }}
defaultReplicaPlacement: "{{ sub .Values.replicationFactor 1 }}00"
{{- end }}
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "512Mi"
replicas: {{ .Values.master.replicas }}
resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.master.resourcesPreset .Values.master.resources $) | nindent 10 }}
volume:
{{ if eq .Values.topology "MultiZone" }}
enabled: false
{{- end }}
replicas: {{ .Values.replicas }}
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "512Mi"
replicas: {{ .Values.volume.replicas }}
resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.volume.resourcesPreset .Values.volume.resources $) | nindent 10 }}
dataDirs:
- name: data1
type: "persistentVolumeClaim"
size: "{{ .Values.size }}"
{{- with .Values.storageClass }}
size: "{{ .Values.volume.size }}"
{{- with .Values.volume.storageClass }}
storageClass: {{ . }}
{{- end }}
maxVolumes: 0
{{ if eq .Values.topology "MultiZone" }}
volumes:
{{- range $zoneName, $zone := .Values.zones }}
{{- range $zoneName, $zone := .Values.volume.zones }}
{{ $zoneName }}:
{{ with $zone.replicas }}
replicas: {{ . }}
@@ -113,8 +109,8 @@ spec:
{{- end }}
{{- if $zone.storageClass }}
storageClass: {{ $zone.storageClass }}
{{- else if $.Values.storageClass }}
storageClass: {{ $.Values.storageClass }}
{{- else if $.Values.volume.storageClass }}
storageClass: {{ $.Values.volume.storageClass }}
{{- end }}
maxVolumes: 0
nodeSelector: |
@@ -130,13 +126,7 @@ spec:
{{- end }}
s3:
domainName: {{ .Values.host | default (printf "s3.%s" $host) }}
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "512Mi"
resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.filer.resourcesPreset .Values.filer.resources $) | nindent 10 }}
s3:
ingress:
className: {{ $ingress }}
@@ -150,6 +140,7 @@ spec:
- hosts:
- {{ .Values.host | default (printf "s3.%s" $host) }}
secretName: {{ .Release.Name }}-s3-ingress-tls
resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.s3.resourcesPreset .Values.s3.resources $) | nindent 10 }}
cosi:
driverName: "{{ .Release.Namespace }}.seaweedfs.objectstorage.k8s.io"
bucketClassName: "{{ .Release.Namespace }}"
@@ -166,8 +157,8 @@ kind: WorkloadMonitor
metadata:
name: {{ $.Release.Name }}-master
spec:
replicas: 3
minReplicas: 2
replicas: {{ .Values.master.replicas }}
minReplicas: {{ div .Values.master.replicas 2 | add1 }}
kind: seaweedfs
type: master
selector:
@@ -180,7 +171,7 @@ kind: WorkloadMonitor
metadata:
name: {{ $.Release.Name }}-filer
spec:
replicas: 2
replicas: {{ .Values.filer.replicas }}
minReplicas: 1
kind: seaweedfs
type: filer
@@ -188,27 +179,47 @@ spec:
app.kubernetes.io/component: filer
app.kubernetes.io/name: seaweedfs
version: {{ $.Chart.Version }}
{{ if eq .Values.topology "Simple" }}
---
apiVersion: cozystack.io/v1alpha1
kind: WorkloadMonitor
metadata:
name: {{ $.Release.Name }}-volume
spec:
replicas: {{ .Values.replicas }}
minReplicas: {{ div .Values.replicas 2 | add1 }}
replicas: {{ .Values.volume.replicas }}
minReplicas: 1
kind: seaweedfs
type: volume
selector:
app.kubernetes.io/component: volume
app.kubernetes.io/name: seaweedfs
version: {{ $.Chart.Version }}
{{- else if eq .Values.topology "MultiZone" }}
{{- range $zoneName, $zoneSpec := .Values.volume.zones }}
---
apiVersion: cozystack.io/v1alpha1
kind: WorkloadMonitor
metadata:
name: {{ $.Release.Name }}-volume-{{ $zoneName }}
spec:
replicas: {{ default $.Values.volume.replicas $zoneSpec.replicas }}
minReplicas: 1
kind: seaweedfs
type: volume
selector:
app.kubernetes.io/component: volume-{{ $zoneName }}
app.kubernetes.io/name: seaweedfs
version: {{ $.Chart.Version }}
{{- end }}
{{- end }}
---
apiVersion: cozystack.io/v1alpha1
kind: WorkloadMonitor
metadata:
name: {{ $.Release.Name }}-db
spec:
replicas: 2
replicas: {{ .Values.db.replicas }}
minReplicas: 1
kind: seaweedfs
type: postgres
@@ -216,4 +227,18 @@ spec:
cnpg.io/cluster: seaweedfs-db
cnpg.io/podRole: instance
version: {{ $.Chart.Version }}
---
apiVersion: cozystack.io/v1alpha1
kind: WorkloadMonitor
metadata:
name: {{ $.Release.Name }}-s3
spec:
replicas: {{ .Values.s3.replicas }}
minReplicas: 1
kind: seaweedfs
type: s3
selector:
app.kubernetes.io/component: s3
app.kubernetes.io/name: seaweedfs
version: {{ $.Chart.Version }}
{{- end }}

View File

@@ -1,68 +0,0 @@
{{- if not (eq .Values.topology "Client") }}
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: {{ .Release.Name }}-filer
spec:
targetRef:
apiVersion: apps/v1
kind: StatefulSet
name: {{ .Release.Name }}-filer
updatePolicy:
updateMode: Auto
resourcePolicy:
containerPolicies:
- containerName: seaweedfs
minAllowed:
cpu: 25m
memory: 64Mi
maxAllowed:
cpu: "1"
memory: 2048Mi
---
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: {{ .Release.Name }}-master
spec:
targetRef:
apiVersion: apps/v1
kind: StatefulSet
name: {{ .Release.Name }}-master
updatePolicy:
updateMode: Auto
resourcePolicy:
containerPolicies:
- containerName: seaweedfs
minAllowed:
cpu: 25m
memory: 64Mi
maxAllowed:
cpu: "1"
memory: 2048Mi
---
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: {{ .Release.Name }}-volume
spec:
targetRef:
apiVersion: apps/v1
kind: StatefulSet
name: {{ .Release.Name }}-volume
updatePolicy:
updateMode: Auto
resourcePolicy:
containerPolicies:
- containerName: seaweedfs
minAllowed:
cpu: 25m
memory: 64Mi
maxAllowed:
cpu: "1"
memory: 2048Mi
{{- end }}

View File

@@ -2,14 +2,108 @@
"title": "Chart Values",
"type": "object",
"properties": {
"db": {
"description": "Database Configuration",
"type": "object",
"default": {
"replicas": 2,
"resources": {},
"resourcesPreset": "small",
"size": "10Gi",
"storageClass": ""
},
"required": [
"resources",
"resourcesPreset"
],
"properties": {
"replicas": {
"description": "Number of database replicas",
"type": "integer",
"default": 2
},
"resources": {
"description": "Explicit CPU and memory configuration for the database. When left empty, the preset defined in `resourcesPreset` is applied.",
"type": "object",
"default": {},
"properties": {
"cpu": {
"description": "The number of CPU cores allocated",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
},
"memory": {
"description": "The amount of memory allocated",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
}
}
},
"resourcesPreset": {
"description": "Default sizing preset used when `resources` is omitted. Allowed values: `nano`, `micro`, `small`, `medium`, `large`, `xlarge`, `2xlarge`.",
"type": "string",
"default": "small",
"enum": [
"nano",
"micro",
"small",
"medium",
"large",
"xlarge",
"2xlarge"
]
},
"size": {
"description": "Persistent Volume size",
"default": "10Gi",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
},
"storageClass": {
"description": "StorageClass used to store the data",
"type": "string"
}
}
},
"filer": {
"description": "Filer service configuration",
"type": "object",
"default": {
"grpcHost": "",
"grpcPort": 443,
"replicas": 2,
"resources": {},
"resourcesPreset": "small",
"whitelist": {}
},
"required": [
"resources",
"resourcesPreset"
],
"properties": {
"grpcHost": {
"description": "The hostname used to expose or access the filer service externally.",
@@ -20,6 +114,58 @@
"type": "integer",
"default": 443
},
"replicas": {
"description": "Number of filer replicas",
"type": "integer",
"default": 2
},
"resources": {
"description": "Explicit CPU and memory configuration for the filer. When left empty, the preset defined in `resourcesPreset` is applied.",
"type": "object",
"default": {},
"properties": {
"cpu": {
"description": "The number of CPU cores allocated",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
},
"memory": {
"description": "The amount of memory allocated",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
}
}
},
"resourcesPreset": {
"description": "Default sizing preset used when `resources` is omitted. Allowed values: `nano`, `micro`, `small`, `medium`, `large`, `xlarge`, `2xlarge`.",
"type": "string",
"default": "small",
"enum": [
"nano",
"micro",
"small",
"medium",
"large",
"xlarge",
"2xlarge"
]
},
"whitelist": {
"description": "A list of IP addresses or CIDR ranges that are allowed to access the filer service.",
"type": "array",
@@ -34,33 +180,144 @@
"description": "The hostname used to access the SeaweedFS externally (defaults to 's3' subdomain for the tenant host).",
"type": "string"
},
"replicas": {
"description": "Number of replicas",
"type": "integer",
"default": 2
"master": {
"description": "Master service configuration",
"type": "object",
"default": {
"replicas": 3,
"resources": {},
"resourcesPreset": "small"
},
"required": [
"resources",
"resourcesPreset"
],
"properties": {
"replicas": {
"description": "Number of master replicas",
"type": "integer",
"default": 3
},
"resources": {
"description": "Explicit CPU and memory configuration for the master. When left empty, the preset defined in `resourcesPreset` is applied.",
"type": "object",
"default": {},
"properties": {
"cpu": {
"description": "The number of CPU cores allocated",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
},
"memory": {
"description": "The amount of memory allocated",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
}
}
},
"resourcesPreset": {
"description": "Default sizing preset used when `resources` is omitted. Allowed values: `nano`, `micro`, `small`, `medium`, `large`, `xlarge`, `2xlarge`.",
"type": "string",
"default": "small",
"enum": [
"nano",
"micro",
"small",
"medium",
"large",
"xlarge",
"2xlarge"
]
}
}
},
"replicationFactor": {
"description": "Replication factor: number of replicas for each volume in the SeaweedFS cluster.",
"type": "integer",
"default": 2
},
"size": {
"description": "Persistent Volume size",
"default": "10Gi",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
"s3": {
"description": "S3 service configuration",
"type": "object",
"default": {
"replicas": 2,
"resources": {},
"resourcesPreset": "small"
},
"required": [
"resources",
"resourcesPreset"
],
"x-kubernetes-int-or-string": true
},
"storageClass": {
"description": "StorageClass used to store the data",
"type": "string"
"properties": {
"replicas": {
"description": "Number of s3 replicas",
"type": "integer",
"default": 2
},
"resources": {
"description": "Explicit CPU and memory configuration for the s3. When left empty, the preset defined in `resourcesPreset` is applied.",
"type": "object",
"default": {},
"properties": {
"cpu": {
"description": "The number of CPU cores allocated",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
},
"memory": {
"description": "The amount of memory allocated",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
}
}
},
"resourcesPreset": {
"description": "Default sizing preset used when `resources` is omitted. Allowed values: `nano`, `micro`, `small`, `medium`, `large`, `xlarge`, `2xlarge`.",
"type": "string",
"default": "small",
"enum": [
"nano",
"micro",
"small",
"medium",
"large",
"xlarge",
"2xlarge"
]
}
}
},
"topology": {
"description": "The topology of the SeaweedFS cluster. (allowed values: Simple, MultiZone, Client)",
@@ -72,33 +329,117 @@
"Client"
]
},
"zones": {
"description": "A map of zones for MultiZone topology. Each zone can have its own number of replicas and size.",
"volume": {
"description": "Volume service configuration",
"type": "object",
"default": {},
"additionalProperties": {
"type": "object",
"required": [
"replicas",
"size"
],
"properties": {
"replicas": {
"description": "Number of replicas in the zone",
"type": "integer"
},
"size": {
"description": "Zone storage size",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"default": {
"replicas": 2,
"resources": {},
"resourcesPreset": "small",
"size": "10Gi",
"storageClass": "",
"zones": {}
},
"required": [
"resources",
"resourcesPreset"
],
"properties": {
"replicas": {
"description": "Number of volume replicas",
"type": "integer",
"default": 2
},
"resources": {
"description": "Explicit CPU and memory configuration for the volume. When left empty, the preset defined in `resourcesPreset` is applied.",
"type": "object",
"default": {},
"properties": {
"cpu": {
"description": "The number of CPU cores allocated",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
},
"memory": {
"description": "The amount of memory allocated",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
}
}
},
"resourcesPreset": {
"description": "Default sizing preset used when `resources` is omitted. Allowed values: `nano`, `micro`, `small`, `medium`, `large`, `xlarge`, `2xlarge`.",
"type": "string",
"default": "small",
"enum": [
"nano",
"micro",
"small",
"medium",
"large",
"xlarge",
"2xlarge"
]
},
"size": {
"description": "Persistent Volume size",
"default": "10Gi",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
},
"storageClass": {
"description": "StorageClass used to store the data",
"type": "string"
},
"zones": {
"description": "A map of zones for MultiZone topology. Each zone can have its own number of replicas and size.",
"type": "object",
"default": {},
"additionalProperties": {
"type": "object",
"properties": {
"replicas": {
"description": "Number of replicas in the zone",
"type": "integer"
},
{
"type": "string"
"size": {
"description": "Zone storage size",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
}
],
"x-kubernetes-int-or-string": true
}
}
}
}

View File

@@ -4,42 +4,93 @@
host: ""
## @param topology {string enum:"Simple,MultiZone,Client"} The topology of the SeaweedFS cluster. (allowed values: Simple, MultiZone, Client)
##
topology: Simple
## @param replicationFactor {int} Replication factor: number of replicas for each volume in the SeaweedFS cluster.
replicationFactor: 2
## @param replicas {int} Number of replicas
## @section SeaweedFS Components Configuration
##
replicas: 2
## @param size {quantity} Persistent Volume size
size: 10Gi
## @param storageClass {*string} StorageClass used to store the data
storageClass: ""
## @param db {db} Database Configuration
db:
## @field db.replicas {*int} Number of database replicas
## @field db.size {*quantity} Persistent Volume size
## @field db.storageClass {*string} StorageClass used to store the data
replicas: 2
size: "10Gi"
storageClass: ""
## @field db.resources {resources} Explicit CPU and memory configuration for the database. When left empty, the preset defined in `resourcesPreset` is applied.
## @field resources {*resources} Resource configuration for etcd
## @field resources.cpu {*quantity} The number of CPU cores allocated
## @field resources.memory {*quantity} The amount of memory allocated
## e.g:
## resources:
## cpu: 4000m
## memory: 4Gi
##
## @field db.resourcesPreset {string enum:"nano,micro,small,medium,large,xlarge,2xlarge"} Default sizing preset used when `resources` is omitted. Allowed values: `nano`, `micro`, `small`, `medium`, `large`, `xlarge`, `2xlarge`.
resources: {}
resourcesPreset: "small"
## @param zones {map[string]zone} A map of zones for MultiZone topology. Each zone can have its own number of replicas and size.
## @field zone.replicas {int} Number of replicas in the zone
## @field zone.size {quantity} Zone storage size
## Example:
## zones:
## dc1:
## replicas: 2
## size: 10Gi
## dc2:
## replicas: 2
## size: 10Gi
## dc3:
## replicas: 2
## size: 10Gi
zones: {}
## @param master {*master} Master service configuration
master:
## @field master.replicas {*int} Number of master replicas
## @field master.resources {resources} Explicit CPU and memory configuration for the master. When left empty, the preset defined in `resourcesPreset` is applied.
## @field master.resourcesPreset {string enum:"nano,micro,small,medium,large,xlarge,2xlarge"} Default sizing preset used when `resources` is omitted. Allowed values: `nano`, `micro`, `small`, `medium`, `large`, `xlarge`, `2xlarge`.
replicas: 3
resources: {}
resourcesPreset: "small"
## @param filer {*filer} Filer service configuration
## @field filer.grpcHost {*string} The hostname used to expose or access the filer service externally.
## @field filer.grpcPort {*int} The port used to access the filer service externally.
## TODO: select a more appropriate type after resolving https://github.com/cozystack/cozyvalues-gen/issues/4
## @field filer.whitelist {[]*string} A list of IP addresses or CIDR ranges that are allowed to access the filer service.
filer:
## @field filer.replicas {*int} Number of filer replicas
## @field filer.resources {resources} Explicit CPU and memory configuration for the filer. When left empty, the preset defined in `resourcesPreset` is applied.
## @field filer.resourcesPreset {string enum:"nano,micro,small,medium,large,xlarge,2xlarge"} Default sizing preset used when `resources` is omitted. Allowed values: `nano`, `micro`, `small`, `medium`, `large`, `xlarge`, `2xlarge`.
replicas: 2
resources: {}
resourcesPreset: "small"
## @field filer.grpcHost {*string} The hostname used to expose or access the filer service externally.
## @field filer.grpcPort {*int} The port used to access the filer service externally.
## @field filer.whitelist {[]*string} A list of IP addresses or CIDR ranges that are allowed to access the filer service.
grpcHost: ""
grpcPort: 443
whitelist: []
## @param volume {*volume} Volume service configuration
volume:
## @field volume.replicas {*int} Number of volume replicas
## @field volume.size {*quantity} Persistent Volume size
## @field volume.storageClass {*string} StorageClass used to store the data
## @field volume.resources {resources} Explicit CPU and memory configuration for the volume. When left empty, the preset defined in `resourcesPreset` is applied.
## @field volume.resourcesPreset {string enum:"nano,micro,small,medium,large,xlarge,2xlarge"} Default sizing preset used when `resources` is omitted. Allowed values: `nano`, `micro`, `small`, `medium`, `large`, `xlarge`, `2xlarge`.
replicas: 2
size: 10Gi
storageClass: ""
resources: {}
resourcesPreset: "small"
## @field volume.zones {map[string]zone} A map of zones for MultiZone topology. Each zone can have its own number of replicas and size.
## @field zone.replicas {*int} Number of replicas in the zone
## @field zone.size {*quantity} Zone storage size
## Example:
## zones:
## dc1:
## replicas: 2
## size: 10Gi
## dc2:
## replicas: 2
## size: 10Gi
## dc3:
## replicas: 2
## size: 10Gi
zones: {}
## @param s3 {*s3} S3 service configuration
s3:
## @field s3.replicas {*int} Number of s3 replicas
## @field s3.resources {resources} Explicit CPU and memory configuration for the s3. When left empty, the preset defined in `resourcesPreset` is applied.
## @field s3.resourcesPreset {string enum:"nano,micro,small,medium,large,xlarge,2xlarge"} Default sizing preset used when `resources` is omitted. Allowed values: `nano`, `micro`, `small`, `medium`, `large`, `xlarge`, `2xlarge`.
replicas: 2
resources: {}
resourcesPreset: "small"

View File

@@ -30,7 +30,8 @@ ingress 1.4.0 fd240701
ingress 1.5.0 93bdf411
ingress 1.6.0 632224a3
ingress 1.7.0 c02a3818
ingress 1.8.0 HEAD
ingress 1.8.0 8f1975d1
ingress 1.9.0 HEAD
monitoring 1.0.0 d7cfa53c
monitoring 1.1.0 25221fdc
monitoring 1.2.0 f81be075
@@ -63,4 +64,5 @@ seaweedfs 0.3.0 45a7416c
seaweedfs 0.4.0 632224a3
seaweedfs 0.4.1 8c86905b
seaweedfs 0.5.0 9584e5f5
seaweedfs 0.6.0 HEAD
seaweedfs 0.6.0 8f1975d1
seaweedfs 0.7.0 HEAD

View File

@@ -1 +1 @@
ghcr.io/cozystack/cozystack/s3manager:v0.5.0@sha256:d482319db1f17bf7a6c6369dcfc11871cd84186450440a21a8a2a09b0bc1bc19
ghcr.io/cozystack/cozystack/s3manager:v0.5.0@sha256:b9a401defb90a822087e50e7ab6afd9b4db7e71728030f92c7d320ac46889053

View File

@@ -1,2 +1,2 @@
cozystackAPI:
image: ghcr.io/cozystack/cozystack/cozystack-api:v0.36.0-beta.3@sha256:431abfc03be77459451eb347b7ccd623216adc20862b9e773a73d0bb222368c0
image: ghcr.io/cozystack/cozystack/cozystack-api:v0.36.0-beta.4@sha256:c1d6534b36a24f365d64383fd3deff469a71565200ae1789eabf78e3cd9a3601

View File

@@ -0,0 +1,45 @@
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: cozystack-controller-webhook-selfsigned
namespace: {{ .Release.Namespace }}
spec:
selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: cozystack-controller-webhook-ca
namespace: {{ .Release.Namespace }}
spec:
secretName: cozystack-controller-webhook-ca
duration: 43800h # 5 years
commonName: cozystack-controller-webhook-ca
issuerRef:
name: cozystack-controller-webhook-selfsigned
isCA: true
---
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: cozystack-controller-webhook-ca
namespace: {{ .Release.Namespace }}
spec:
ca:
secretName: cozystack-controller-webhook-ca
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: cozystack-controller-webhook
namespace: {{ .Release.Namespace }}
spec:
secretName: cozystack-controller-webhook-cert
duration: 8760h
renewBefore: 720h
issuerRef:
name: cozystack-controller-webhook-ca
commonName: cozystack-controller
dnsNames:
- cozystack-controller
- cozystack-controller.{{ .Release.Namespace }}.svc

View File

@@ -2,7 +2,6 @@ apiVersion: apps/v1
kind: Deployment
metadata:
name: cozystack-controller
namespace: cozy-system
labels:
app: cozystack-controller
spec:
@@ -29,3 +28,15 @@ spec:
{{- if .Values.cozystackController.disableTelemetry }}
- --disable-telemetry
{{- end }}
ports:
- name: webhook
containerPort: 9443
volumeMounts:
- name: webhook-certs
mountPath: /tmp/k8s-webhook-server/serving-certs
readOnly: true
volumes:
- name: webhook-certs
secret:
secretName: cozystack-controller-webhook-cert
defaultMode: 0400

View File

@@ -0,0 +1,33 @@
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
name: lineage
annotations:
cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/cozystack-controller-webhook
labels:
app: cozystack-controller
webhooks:
- name: lineage.cozystack.io
admissionReviewVersions: ["v1"]
sideEffects: None
clientConfig:
service:
name: cozystack-controller
namespace: {{ .Release.Namespace }}
path: /mutate-lineage
rules:
- operations: ["CREATE"]
apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods","secrets", "services", "persistentvolumeclaims"]
failurePolicy: Fail
namespaceSelector:
matchExpressions:
- key: cozystack.io/system
operator: NotIn
values:
- "true"
- key: kubernetes.io/metadata.name
operator: NotIn
values:
- kube-system

View File

@@ -3,9 +3,6 @@ apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: cozystack-controller
rules:
- apiGroups: [""]
resources: ["configmaps", "pods", "namespaces", "nodes", "services", "persistentvolumes", "persistentvolumeclaims"]
verbs: ["get", "watch", "list"]
- apiGroups: ['cozystack.io']
resources: ['*']
verbs: ['*']
@@ -15,6 +12,6 @@ rules:
- apiGroups: [""]
resources: ["namespaces"]
verbs: ["get", "list", "watch", "patch", "update"]
- apiGroups: ["apps"]
resources: ["deployments"]
- apiGroups: ['*']
resources: ['*']
verbs: ["get", "list", "watch"]

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: cozystack-controller
labels:
app: cozystack-controller
spec:
type: ClusterIP
ports:
- port: 443
targetPort: 9443
protocol: TCP
name: webhook
selector:
app: cozystack-controller

View File

@@ -1,5 +1,5 @@
cozystackController:
image: ghcr.io/cozystack/cozystack/cozystack-controller:v0.36.0-beta.3@sha256:51a59091d53f4a8a16b9ee602d9d2a19240df480cd9fe9d884f6391008d705dd
image: ghcr.io/cozystack/cozystack/cozystack-controller:v0.36.0-beta.4@sha256:f4f8fa8e2f33f66d90b99398025d0da1328b530a622a9b420c993e51e1302992
debug: false
disableTelemetry: false
cozystackVersion: "v0.36.0-beta.3"
cozystackVersion: "v0.36.0-beta.4"

View File

@@ -76,7 +76,7 @@ data:
"kubeappsNamespace": {{ .Release.Namespace | quote }},
"helmGlobalNamespace": {{ include "kubeapps.helmGlobalPackagingNamespace" . | quote }},
"carvelGlobalNamespace": {{ .Values.kubeappsapis.pluginConfig.kappController.packages.v1alpha1.globalPackagingNamespace | quote }},
"appVersion": "v0.36.0-beta.3",
"appVersion": "v0.36.0-beta.4",
"authProxyEnabled": {{ .Values.authProxy.enabled }},
"oauthLoginURI": {{ .Values.authProxy.oauthLoginURI | quote }},
"oauthLogoutURI": {{ .Values.authProxy.oauthLogoutURI | quote }},

View File

@@ -19,7 +19,7 @@ kubeapps:
image:
registry: ghcr.io/cozystack/cozystack
repository: dashboard
tag: v0.36.0-beta.3
tag: v0.36.0-beta.4
digest: "sha256:54906b3d2492c8603a347a5938b6db36e5ed5c4149111cae1804ac9110361947"
frontend:
image:
@@ -48,8 +48,8 @@ kubeapps:
image:
registry: ghcr.io/cozystack/cozystack
repository: kubeapps-apis
tag: v0.36.0-beta.3
digest: "sha256:5b06e4184fa2b4310e6cf830c5d87a45943ab1a2b1a4869e621ae3d7a4401676"
tag: v0.36.0-beta.4
digest: "sha256:c4b268996c96d23bc11b6d109fb7fb51faf05576ee326097889eca72c851b656"
pluginConfig:
flux:
packages:

View File

@@ -3,7 +3,7 @@ kamaji:
deploy: false
image:
pullPolicy: IfNotPresent
tag: v0.36.0-beta.3@sha256:0d74d0a680f4baabf1612c3fb42f0df3b02e2c02d5d913116b6c0d460297fbd3
tag: v0.36.0-beta.4@sha256:bcf8ccde72e5f8619b626a290c1e4c81018d9a10497e66948afebabd80a64023
repository: ghcr.io/cozystack/cozystack/kamaji
resources:
limits:
@@ -13,4 +13,4 @@ kamaji:
cpu: 100m
memory: 100Mi
extraArgs:
- --migrate-image=ghcr.io/cozystack/cozystack/kamaji:v0.36.0-beta.3@sha256:0d74d0a680f4baabf1612c3fb42f0df3b02e2c02d5d913116b6c0d460297fbd3
- --migrate-image=ghcr.io/cozystack/cozystack/kamaji:v0.36.0-beta.4@sha256:bcf8ccde72e5f8619b626a290c1e4c81018d9a10497e66948afebabd80a64023

View File

@@ -29,7 +29,6 @@ spec:
{{- end }}
- --metrics-bind-address=:8080
- --metrics-secure=false
- --kube-ovn-namespace={{ .Release.Namespace }}
ports:
- name: metrics
containerPort: 8080

View File

@@ -1,4 +1,4 @@
portSecurity: true
routes: ""
image: ghcr.io/cozystack/cozystack/kubeovn-plunger:latest@sha256:a3733b86b3c60fa73cb6749e69d6399736f1ab875ec5fc7887caa8b73aa8b0b2
image: ghcr.io/cozystack/cozystack/kubeovn-plunger:v0.36.0-beta.4@sha256:fb334a9cf9b6fa606b2530cb4227e0de303761151ff93aa52209dcbcf8b33ef8
ovnCentralName: ovn-central

View File

@@ -1,3 +1,3 @@
portSecurity: true
routes: ""
image: ghcr.io/cozystack/cozystack/kubeovn-webhook:v0.36.0-beta.3@sha256:63ade678f35bdb467ff9d89bddee5a9224f4be2eb92d4ec95452a73d040903ba
image: ghcr.io/cozystack/cozystack/kubeovn-webhook:v0.36.0-beta.4@sha256:583922648d9e39f4e4d2255a50f5db74b1aa2b1590982e891d0ea6d6cb9ec453

View File

@@ -64,4 +64,4 @@ global:
images:
kubeovn:
repository: kubeovn
tag: v1.14.5@sha256:26865e4020b84ec33fd8947cd9a7b46443b3195ccc7a4ca2953145320ec838b7
tag: v1.14.5@sha256:8968977ba60e1fb14121984899ddf38f7fe4ea800806a7a30db007110064c84b

View File

@@ -1,3 +1,3 @@
storageClass: replicated
csiDriver:
image: ghcr.io/cozystack/cozystack/kubevirt-csi-driver:0.29.0@sha256:8eb9803aa1b38e1b2db98237bf0d1046f0ba90be0157c22da1efc3811bb25ecf
image: ghcr.io/cozystack/cozystack/kubevirt-csi-driver:0.29.0@sha256:3a3bc912f70ccba1e9f92a0754179dbdc4c01f24073467b6d1406c77da794863

View File

@@ -1,42 +1,23 @@
---
apiVersion: v1
kind: Service
metadata:
name: coredns
namespace: kube-system
labels:
app: coredns
spec:
clusterIP: None
ports:
- name: http-metrics
port: 9153
protocol: TCP
targetPort: 9153
selector:
k8s-app: kube-dns
---
apiVersion: operator.victoriametrics.com/v1beta1
kind: VMServiceScrape
metadata:
name: coredns
name: kubeovn-plunger
namespace: cozy-monitoring
spec:
selector:
matchLabels:
app: coredns
app.kubernetes.io/name: kube-ovn-plunger
app.kubernetes.io/instance: kubeovn-plunger
namespaceSelector:
matchNames:
- "kube-system"
- "cozy-kubeovn"
endpoints:
- bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
port: http-metrics
- port: metrics
relabelConfigs:
- action: labeldrop
regex: (endpoint|namespace|pod|container)
- replacement: kube-dns
regex: (endpoint|pod|container)
- replacement: kubeovn-plunger
targetLabel: job
- sourceLabels: [__meta_kubernetes_pod_node_name]
targetLabel: node
- targetLabel: tier
replacement: cluster

View File

@@ -1,3 +1,3 @@
objectstorage:
controller:
image: "ghcr.io/cozystack/cozystack/objectstorage-controller:v0.36.0-beta.3@sha256:a6e922a3e528cf36f2886f22d87ac55958bbd12502b9d864c9f2bbad5c027109"
image: "ghcr.io/cozystack/cozystack/objectstorage-controller:v0.36.0-beta.4@sha256:aa0000265ae58155aebefedac72d0a6acc45437b8668bb9739bf11edefec067a"

View File

@@ -4,9 +4,12 @@ kind: Cluster
metadata:
name: seaweedfs-db
spec:
instances: 2
instances: {{ .Values.db.replicas }}
storage:
size: 10Gi
size: {{ .Values.db.size }}
{{- with .Values.db.storageClass }}
storageClass: {{ . }}
{{- end }}
monitoring:
enablePodMonitor: true

View File

@@ -70,6 +70,7 @@ seaweedfs:
key: password
name: seaweedfs-db-app
s3:
replicas: 2
enabled: true
port: 8333
httpsPort: 0
@@ -109,13 +110,6 @@ seaweedfs:
- hosts:
- seaweedfs.demo.cozystack.io
secretName: seaweedfs-s3-ingress-tls
resources:
limits:
cpu: "2"
memory: "2Gi"
requests:
cpu: "500m"
memory: "1Gi"
cosi:
enabled: true
podLabels:
@@ -124,7 +118,7 @@ seaweedfs:
bucketClassName: "seaweedfs"
region: ""
sidecar:
image: "ghcr.io/cozystack/cozystack/objectstorage-sidecar:v0.36.0-beta.3@sha256:c8db3d458f65c6a3ccd25651a670e549a6a5601b7832f25bb873fdaf3113008c"
image: "ghcr.io/cozystack/cozystack/objectstorage-sidecar:v0.36.0-beta.4@sha256:24451989b15b6801b33ad355a5507307d0333bf9afd240f1db0aca9c92f6b2ad"
certificates:
commonName: "SeaweedFS CA"
ipAddresses: []
@@ -132,3 +126,7 @@ seaweedfs:
keySize: 2048
duration: 2160h # 90d
renewBefore: 360h # 15d
db:
replicas: 2
size: 10Gi
storageClass: ""

193
pkg/lineage/lineage.go Normal file
View File

@@ -0,0 +1,193 @@
package lineage
import (
"context"
"fmt"
"os"
"strings"
helmv2 "github.com/fluxcd/helm-controller/api/v2"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"sigs.k8s.io/controller-runtime/pkg/log"
)
const (
HRAPIVersion = "helm.toolkit.fluxcd.io/v2"
HRKind = "HelmRelease"
HRLabel = "helm.toolkit.fluxcd.io/name"
)
type ObjectID struct {
APIVersion string
Kind string
Namespace string
Name string
}
func (o ObjectID) GetUnstructured(ctx context.Context, client dynamic.Interface, mapper meta.RESTMapper) (*unstructured.Unstructured, error) {
u, err := getUnstructuredObject(ctx, client, mapper, o.APIVersion, o.Kind, o.Namespace, o.Name)
if err != nil {
return nil, err
}
return u, nil
}
func WalkOwnershipGraph(
ctx context.Context,
client dynamic.Interface,
mapper meta.RESTMapper,
appMapper AppMapper,
obj *unstructured.Unstructured,
memory ...interface{},
) (out []ObjectID) {
id := ObjectID{APIVersion: obj.GetAPIVersion(), Kind: obj.GetKind(), Namespace: obj.GetNamespace(), Name: obj.GetName()}
out = []ObjectID{}
l := log.FromContext(ctx)
l.Info("processing object", "apiVersion", obj.GetAPIVersion(), "kind", obj.GetKind(), "name", obj.GetName())
var visited map[ObjectID]bool
var ok bool
if len(memory) == 1 {
visited, ok = memory[0].(map[ObjectID]bool)
if !ok {
l.Error(
fmt.Errorf("invalid argument"), "could not parse visited map in WalkOwnershipGraph call",
"received", memory[0], "expected", "map[ObjectID]bool",
)
return out
}
}
if len(memory) == 0 {
visited = make(map[ObjectID]bool)
}
if len(memory) != 0 && len(memory) != 1 {
l.Error(
fmt.Errorf("invalid argument count"), "could not parse variadic arguments to WalkOwnershipGraph",
"args passed", len(memory)+5, "expected args", "4|5",
)
return out
}
if visited[id] {
return out
}
visited[id] = true
ownerRefs := obj.GetOwnerReferences()
for _, owner := range ownerRefs {
ownerObj, err := getUnstructuredObject(ctx, client, mapper, owner.APIVersion, owner.Kind, obj.GetNamespace(), owner.Name)
if err != nil {
fmt.Fprintf(os.Stderr, "Could not fetch owner %s/%s (%s): %v\n", obj.GetNamespace(), owner.Name, owner.Kind, err)
continue
}
out = append(out, WalkOwnershipGraph(ctx, client, mapper, appMapper, ownerObj, visited)...)
}
// if object has owners, it couldn't be owned directly by the custom app
if len(ownerRefs) > 0 {
return
}
// I want "if err1 != nil go to next block, if err2 != nil, go to next block, etc semantics",
// like an early return from a function, but if all checks succeed, I don't want to do the rest
// of the function, so it's a `for { if err { break } if othererr { break } if allgood { return }
for {
if obj.GetAPIVersion() != HRAPIVersion || obj.GetKind() != HRKind {
break
}
hr := helmReleaseFromUnstructured(obj)
if hr == nil {
break
}
a, k, p, err := appMapper.Map(hr)
if err != nil {
break
}
ownerObj, err := getUnstructuredObject(ctx, client, mapper, a, k, obj.GetNamespace(), strings.TrimPrefix(obj.GetName(), p))
if err != nil {
break
}
// successfully mapped a HelmRelease to a custom app, no need to continue
out = append(out,
ObjectID{
APIVersion: ownerObj.GetAPIVersion(),
Kind: ownerObj.GetKind(),
Namespace: ownerObj.GetNamespace(),
Name: ownerObj.GetName(),
},
)
return
}
labels := obj.GetLabels()
name, ok := labels[HRLabel]
if !ok {
return
}
ownerObj, err := getUnstructuredObject(ctx, client, mapper, HRAPIVersion, HRKind, obj.GetNamespace(), name)
if err != nil {
return
}
out = append(out, WalkOwnershipGraph(ctx, client, mapper, appMapper, ownerObj, visited)...)
return
}
func getUnstructuredObject(
ctx context.Context,
client dynamic.Interface,
mapper meta.RESTMapper,
apiVersion, kind, namespace, name string,
) (*unstructured.Unstructured, error) {
l := log.FromContext(ctx)
gv, err := schema.ParseGroupVersion(apiVersion)
if err != nil {
l.Error(
err, "failed to parse groupversion",
"apiVersion", apiVersion,
)
return nil, err
}
gvk := schema.GroupVersionKind{
Group: gv.Group,
Version: gv.Version,
Kind: kind,
}
mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
if err != nil {
l.Error(err, "Could not map GVK "+gvk.String())
return nil, err
}
ns := namespace
if mapping.Scope.Name() != meta.RESTScopeNameNamespace {
ns = ""
}
ownerObj, err := client.Resource(mapping.Resource).Namespace(ns).Get(ctx, name, metav1.GetOptions{})
if err != nil {
return nil, err
}
return ownerObj, nil
}
func helmReleaseFromUnstructured(obj *unstructured.Unstructured) *helmv2.HelmRelease {
if obj.GetAPIVersion() == HRAPIVersion && obj.GetKind() == HRKind {
hr := &helmv2.HelmRelease{}
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, hr); err == nil {
return hr
}
}
return nil
}

View File

@@ -0,0 +1,53 @@
package lineage
import (
"context"
"fmt"
"os"
"testing"
"github.com/go-logr/logr"
"github.com/go-logr/zapr"
"go.uber.org/zap"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/discovery"
"k8s.io/client-go/discovery/cached/memory"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/restmapper"
"sigs.k8s.io/controller-runtime/pkg/client/config"
)
var (
dynClient dynamic.Interface
mapper meta.RESTMapper
l logr.Logger
ctx context.Context
)
func init() {
cfg := config.GetConfigOrDie()
dynClient, _ = dynamic.NewForConfig(cfg)
discoClient, _ := discovery.NewDiscoveryClientForConfig(cfg)
cachedDisco := memory.NewMemCacheClient(discoClient)
mapper = restmapper.NewDeferredDiscoveryRESTMapper(cachedDisco)
zapLogger, _ := zap.NewDevelopment()
l = zapr.NewLogger(zapLogger)
ctx = logr.NewContext(context.Background(), l)
}
func TestWalkingOwnershipGraph(t *testing.T) {
obj, err := dynClient.Resource(schema.GroupVersionResource{"", "v1", "pods"}).Namespace(os.Args[1]).Get(ctx, os.Args[2], metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
nodes := WalkOwnershipGraph(ctx, dynClient, mapper, obj)
for _, node := range nodes {
fmt.Printf("%#v\n", node)
}
}

49
pkg/lineage/mapper.go Normal file
View File

@@ -0,0 +1,49 @@
package lineage
import (
"fmt"
"strings"
helmv2 "github.com/fluxcd/helm-controller/api/v2"
)
type AppMapper interface {
Map(*helmv2.HelmRelease) (apiVersion, kind, prefix string, err error)
}
type stubMapper struct{}
var stubMapperMap = map[string]string{
"cozystack-extra/bootbox": "apps.cozystack.io/v1alpha1/BootBox/",
"cozystack-apps/bucket": "apps.cozystack.io/v1alpha1/Bucket/bucket-",
"cozystack-apps/clickhouse": "apps.cozystack.io/v1alpha1/ClickHouse/clickhouse-",
"cozystack-extra/etcd": "apps.cozystack.io/v1alpha1/Etcd/",
"cozystack-apps/ferretdb": "apps.cozystack.io/v1alpha1/FerretDB/ferretdb-",
"cozystack-apps/http-cache": "apps.cozystack.io/v1alpha1/HTTPCache/http-cache-",
"cozystack-extra/info": "apps.cozystack.io/v1alpha1/Info/",
"cozystack-extra/ingress": "apps.cozystack.io/v1alpha1/Ingress/",
"cozystack-apps/kafka": "apps.cozystack.io/v1alpha1/Kafka/kafka-",
"cozystack-apps/kubernetes": "apps.cozystack.io/v1alpha1/Kubernetes/kubernetes-",
"cozystack-extra/monitoring": "apps.cozystack.io/v1alpha1/Monitoring/",
"cozystack-apps/mysql": "apps.cozystack.io/v1alpha1/MySQL/mysql-",
"cozystack-apps/nats": "apps.cozystack.io/v1alpha1/NATS/nats-",
"cozystack-apps/postgres": "apps.cozystack.io/v1alpha1/Postgres/postgres-",
"cozystack-apps/rabbitmq": "apps.cozystack.io/v1alpha1/RabbitMQ/rabbitmq-",
"cozystack-apps/redis": "apps.cozystack.io/v1alpha1/Redis/redis-",
"cozystack-extra/seaweedfs": "apps.cozystack.io/v1alpha1/SeaweedFS/",
"cozystack-apps/tcp-balancer": "apps.cozystack.io/v1alpha1/TCPBalancer/tcp-balancer-",
"cozystack-apps/tenant": "apps.cozystack.io/v1alpha1/Tenant/tenant-",
"cozystack-apps/virtual-machine": "apps.cozystack.io/v1alpha1/VirtualMachine/virtual-machine-",
"cozystack-apps/vm-disk": "apps.cozystack.io/v1alpha1/VMDisk/vm-disk-",
"cozystack-apps/vm-instance": "apps.cozystack.io/v1alpha1/VMInstance/vm-instance-",
"cozystack-apps/vpn": "apps.cozystack.io/v1alpha1/VPN/vpn-",
}
func (s *stubMapper) Map(hr *helmv2.HelmRelease) (string, string, string, error) {
val, ok := stubMapperMap[hr.Spec.Chart.Spec.SourceRef.Name+"/"+hr.Spec.Chart.Spec.Chart]
if !ok {
return "", "", "", fmt.Errorf("cannot map helm release %s/%s to dynamic app", hr.Namespace, hr.Name)
}
split := strings.Split(val, "/")
return strings.Join(split[:2], "/"), split[2], split[3], nil
}

47
scripts/migrations/19 Executable file
View File

@@ -0,0 +1,47 @@
#!/bin/sh
# Migration 19 --> 20
set -euo pipefail
kubectl get helmreleases.helm.toolkit.fluxcd.io -A \
--field-selector=metadata.name=seaweedfs -o json | jq -r '
.items[] as $o
| ($o.metadata.namespace // "") as $ns
| $o.metadata.name as $name
| $o.spec as $s
| $o.spec.values as $v
| (
($s.chart.spec.version? // "") != "0.7.0"
or ($v.size? != null)
or ($v.storageClass? != null)
or ($v.replicas? != null)
or ($v.zones? != null)
) as $needsChange
| select($needsChange)
# JSON Patch
| [
{ op:"add", path:"/spec/chart/spec/version", value:"0.7.0" },
(if ($v.volume? | type) != "object" then {op:"add", path:"/spec/values/volume", value:{}} else empty end),
(if $v.size? then {op:"add", path:"/spec/values/volume/size", value:$v.size} else empty end),
(if $v.storageClass? then {op:"add", path:"/spec/values/volume/storageClass", value:$v.storageClass} else empty end),
(if $v.replicas? then {op:"add", path:"/spec/values/volume/replicas", value:$v.replicas} else empty end),
(if $v.zones? then {op:"add", path:"/spec/values/volume/zones", value:$v.zones} else empty end),
(if $v.size? then {op:"remove", path:"/spec/values/size"} else empty end),
(if $v.storageClass? then {op:"remove", path:"/spec/values/storageClass"} else empty end),
(if $v.replicas? then {op:"remove", path:"/spec/values/replicas"} else empty end),
(if $v.zones? then {op:"remove", path:"/spec/values/zones"} else empty end)
] as $patch
| "kubectl " +
(if $ns != "" then "-n \($ns) " else "" end) +
"patch helmreleases.helm.toolkit.fluxcd.io \($name) --type=json -p '\''\($patch|tojson)'\''"
' | sh -ex
# Stamp version
kubectl create configmap -n cozy-system cozystack-version \
--from-literal=version=20 --dry-run=client -o yaml | kubectl apply -f-