Compare commits

..

1 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
73 changed files with 1317 additions and 561 deletions

View File

@@ -28,7 +28,7 @@ jobs:
- name: Install generate
run: |
curl -sSL https://github.com/cozystack/cozyvalues-gen/releases/download/v0.9.0/cozyvalues-gen-linux-amd64.tar.gz | tar -xzvf- -C /usr/local/bin/ cozyvalues-gen
curl -sSL https://github.com/cozystack/cozyvalues-gen/releases/download/v0.8.5/cozyvalues-gen-linux-amd64.tar.gz | tar -xzvf- -C /usr/local/bin/ cozyvalues-gen
- name: Run pre-commit hooks
run: |

View File

@@ -30,5 +30,3 @@ This list is sorted in chronological order, based on the submission date.
| [Bootstack](https://bootstack.app/) | @mrkhachaturov | 2024-08-01| At Bootstack, we utilize a Kubernetes operator specifically designed to simplify and streamline cloud infrastructure creation.|
| [gohost](https://gohost.kz/) | @karabass_off | 2024-02-01 | Our company has been working in the market of Kazakhstan for more than 15 years, providing clients with a standard set of services: VPS/VDC, IaaS, shared hosting, etc. Now we are expanding the lineup by introducing Bare Metal Kubenetes cluster under Cozystack management. |
| [Urmanac](https://urmanac.com) | @kingdonb | 2024-12-04 | Urmanac is the future home of a hosting platform for the knowledge base of a community of personal server enthusiasts. We use Cozystack to provide support services for web sites hosted using both conventional deployments and on SpinKube, with WASM. |
| [Hidora](https://hikube.cloud) | @matthieu-robin | 2025-09-17 | Hidora is a Swiss cloud provider delivering managed services and infrastructure solutions through datacenters located in Switzerland, ensuring data sovereignty and reliability. Its sovereign cloud platform, Hikube, is designed to run workloads with high availability across multiple datacenters, providing enterprises with a secure and scalable foundation for their applications based on Cozystack. |
|

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

@@ -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

@@ -27,7 +27,7 @@ For more details, read [Restic: Effective Backup from Stdin](https://blog.aenix.
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------- |
| `replicas` | Number of Clickhouse replicas | `int` | `2` |
| `shards` | Number of Clickhouse shards | `int` | `1` |
| `resources` | Explicit CPU and memory configuration for each Clickhouse replica. When left empty, the preset defined in `resourcesPreset` is applied. | `*object` | `null` |
| `resources` | Explicit CPU and memory configuration for each Clickhouse 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` | `small` |
@@ -65,7 +65,7 @@ For more details, read [Restic: Effective Backup from Stdin](https://blog.aenix.
| Name | Description | Type | Value |
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------- |
| `clickhouseKeeper` | Clickhouse Keeper configuration | `*object` | `null` |
| `clickhouseKeeper` | Clickhouse Keeper configuration | `*object` | `{}` |
| `clickhouseKeeper.enabled` | Deploy ClickHouse Keeper for cluster coordination | `*bool` | `true` |
| `clickhouseKeeper.size` | Persistent Volume Claim size, available for application data | `*quantity` | `1Gi` |
| `clickhouseKeeper.resourcesPreset` | Default sizing preset used when `resources` is omitted. Allowed values: `nano`, `micro`, `small`, `medium`, `large`, `xlarge`, `2xlarge`. | `string` | `micro` |

View File

@@ -5,7 +5,16 @@
"backup": {
"description": "Backup configuration",
"type": "object",
"default": {},
"default": {
"cleanupStrategy": "--keep-last=3 --keep-daily=3 --keep-within-weekly=1m",
"enabled": false,
"resticPassword": "\u003cpassword\u003e",
"s3AccessKey": "\u003cyour-access-key\u003e",
"s3Bucket": "s3.example.org/clickhouse-backups",
"s3Region": "us-east-1",
"s3SecretKey": "\u003cyour-secret-key\u003e",
"schedule": "0 2 * * *"
},
"required": [
"cleanupStrategy",
"enabled",
@@ -62,7 +71,12 @@
"clickhouseKeeper": {
"description": "Clickhouse Keeper configuration",
"type": "object",
"default": {},
"default": {
"enabled": true,
"replicas": 3,
"resourcesPreset": "micro",
"size": "1Gi"
},
"required": [
"resourcesPreset"
],

View File

@@ -11,7 +11,7 @@ Internally, FerretDB service is backed by Postgres.
| Name | Description | Type | Value |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------- |
| `replicas` | Number of replicas | `int` | `2` |
| `resources` | Explicit CPU and memory configuration for each FerretDB replica. When left empty, the preset defined in `resourcesPreset` is applied. | `*object` | `null` |
| `resources` | Explicit CPU and memory configuration for each FerretDB 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

@@ -5,7 +5,15 @@
"backup": {
"description": "Backup configuration",
"type": "object",
"default": {},
"default": {
"destinationPath": "s3://bucket/path/to/folder/",
"enabled": false,
"endpointURL": "http://minio-gateway-service:9000",
"retentionPolicy": "30d",
"s3AccessKey": "\u003cyour-access-key\u003e",
"s3SecretKey": "\u003cyour-secret-key\u003e",
"schedule": "0 2 * * * *"
},
"required": [
"destinationPath",
"enabled",
@@ -56,7 +64,11 @@
"bootstrap": {
"description": "Bootstrap (recovery) configuration",
"type": "object",
"default": {},
"default": {
"enabled": false,
"oldName": "",
"recoveryTime": ""
},
"properties": {
"enabled": {
"description": "Restore database cluster from a backup",
@@ -81,7 +93,10 @@
"quorum": {
"description": "Configuration for the quorum-based synchronous replication",
"type": "object",
"default": {},
"default": {
"maxSyncReplicas": 0,
"minSyncReplicas": 0
},
"required": [
"maxSyncReplicas",
"minSyncReplicas"

View File

@@ -1,17 +0,0 @@
<svg width="144" height="144" viewBox="0 0 144 144" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="144" height="144" rx="24" fill="url(#paint0_radial_858_3072)"/>
<path d="M135.784 75.6446L135.939 87.7638L89.6846 81.5362L62.0868 84.5079L35.3417 81.4329L8.75167 84.5854L8.72583 81.5104L35.3676 77.5826V64.1713L62.2935 70.7348L62.3452 81.2778L89.4779 77.686L89.4004 64.1972L135.784 75.6446Z" fill="white"/>
<path d="M89.4778 86.0325L135.888 90.8388V102.726H8.64825L8.51904 99.573H35.2641C35.2641 99.573 35.2641 90.7355 35.2641 86.0583C44.2567 86.9369 62.0867 88.6941 62.0867 88.6941V99.2629H89.4778V86.0325Z" fill="white"/>
<path d="M62.2934 66.8846L62.2158 63.6286C62.2158 63.6286 79.8133 58.3571 88.9092 55.6697C88.9092 51.3026 88.9092 47.0906 88.9092 42C104.879 48.4085 120.228 54.6102 135.733 60.8378C135.733 64.7139 135.733 68.435 135.733 72.5695C119.841 68.2024 104.284 63.9129 89.1676 59.7525C79.9684 62.2074 62.2934 66.8846 62.2934 66.8846Z" fill="white"/>
<path d="M35.3962 81.7073L8.80612 84.8598L8.78027 81.7848L35.422 77.857V64.4457L62.348 71.0093L62.3996 81.5522L89.5323 77.9604L89.4548 64.4716L135.839 75.919L135.994 88.0382L89.7391 81.8106L62.1412 84.7823L35.3962 81.7073Z" fill="white"/>
<path d="M89.5323 86.3069L135.942 91.1133V103H8.7027L8.57349 99.8474H35.3186C35.3186 99.8474 35.3186 91.0099 35.3186 86.3328C44.3111 87.2114 62.1412 88.9685 62.1412 88.9685V99.5373H89.5323V86.3069Z" fill="white"/>
<path d="M62.3483 67.159L62.2708 63.9031C62.2708 63.9031 79.8682 58.6316 88.9642 55.9442C88.9642 51.5771 88.9642 47.3651 88.9642 42.2744C104.934 48.6829 120.283 54.8847 135.787 61.1123C135.787 64.9884 135.787 68.7094 135.787 72.8439C119.895 68.4769 104.339 64.1873 89.2226 60.027C80.0233 62.4818 62.3483 67.159 62.3483 67.159Z" fill="white"/>
<defs>
<radialGradient id="paint0_radial_858_3072" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(-29.5 -18) rotate(39.6963) scale(302.168 275.271)">
<stop stop-color="#BEDDFF"/>
<stop offset="0.259615" stop-color="#9ECCFD"/>
<stop offset="0.591346" stop-color="#3F9AFB"/>
<stop offset="1" stop-color="#0B70E0"/>
</radialGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

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

View File

@@ -18,7 +18,11 @@
"haproxy": {
"description": "HAProxy configuration",
"type": "object",
"default": {},
"default": {
"replicas": 2,
"resources": {},
"resourcesPreset": "nano"
},
"required": [
"replicas",
"resources",
@@ -82,7 +86,11 @@
"nginx": {
"description": "Nginx configuration",
"type": "object",
"default": {},
"default": {
"replicas": 2,
"resources": {},
"resourcesPreset": "nano"
},
"required": [
"replicas",
"resourcesPreset"

View File

@@ -10,7 +10,13 @@
"kafka": {
"description": "Kafka configuration",
"type": "object",
"default": {},
"default": {
"replicas": 3,
"resources": {},
"resourcesPreset": "small",
"size": "10Gi",
"storageClass": ""
},
"required": [
"replicas",
"resourcesPreset",
@@ -126,7 +132,13 @@
"zookeeper": {
"description": "Zookeeper configuration",
"type": "object",
"default": {},
"default": {
"replicas": 3,
"resources": {},
"resourcesPreset": "small",
"size": "5Gi",
"storageClass": ""
},
"required": [
"replicas",
"resourcesPreset",

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.29.1
version: 0.29.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

@@ -91,21 +91,21 @@ See the reference for components utilized in this service:
### Application-specific parameters
| Name | Description | Type | Value |
| ----------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ------------------- | ----------- |
| `version` | Kubernetes version given as vMAJOR.MINOR. Available are versions from 1.28 to 1.33. | `string` | `v1.33` |
| `host` | Hostname used to access the Kubernetes cluster externally. Defaults to `<cluster-name>.<tenant-host>` when empty. | `string` | `""` |
| `nodeGroups` | Worker nodes configuration | `map[string]object` | `{...}` |
| `nodeGroups[name].minReplicas` | Minimum amount of replicas | `int` | `0` |
| `nodeGroups[name].maxReplicas` | Maximum amount of replicas | `int` | `10` |
| `nodeGroups[name].instanceType` | Virtual machine instance type | `string` | `u1.medium` |
| `nodeGroups[name].ephemeralStorage` | Ephemeral storage size | `quantity` | `20Gi` |
| `nodeGroups[name].roles` | List of node's roles | `[]string` | `[]` |
| `nodeGroups[name].resources` | Resources available to each worker node | `object` | `{}` |
| `nodeGroups[name].resources.cpu` | CPU available to each worker node | `*quantity` | `null` |
| `nodeGroups[name].resources.memory` | Memory (RAM) available to each worker node | `*quantity` | `null` |
| `nodeGroups[name].gpus` | List of GPUs to attach (WARN: NVIDIA driver requires at least 4 GiB of RAM) | `[]object` | `{}` |
| `nodeGroups[name].gpus[i].name` | Name of GPU, such as "nvidia.com/AD102GL_L40S" | `string` | `""` |
| Name | Description | Type | Value |
| ----------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ------------------- | ------- |
| `version` | Kubernetes version given as vMAJOR.MINOR. Available are versions from 1.28 to 1.33. | `string` | `v1.33` |
| `host` | Hostname used to access the Kubernetes cluster externally. Defaults to `<cluster-name>.<tenant-host>` when empty. | `string` | `""` |
| `nodeGroups` | Worker nodes configuration | `map[string]object` | `{...}` |
| `nodeGroups[name].minReplicas` | Minimum amount of replicas | `int` | `0` |
| `nodeGroups[name].maxReplicas` | Maximum amount of replicas | `int` | `0` |
| `nodeGroups[name].instanceType` | Virtual machine instance type | `string` | `""` |
| `nodeGroups[name].ephemeralStorage` | Ephemeral storage size | `quantity` | `""` |
| `nodeGroups[name].roles` | List of node's roles | `[]string` | `[]` |
| `nodeGroups[name].resources` | Resources available to each worker node | `object` | `{}` |
| `nodeGroups[name].resources.cpu` | CPU available to each worker node | `*quantity` | `null` |
| `nodeGroups[name].resources.memory` | Memory (RAM) available to each worker node | `*quantity` | `null` |
| `nodeGroups[name].gpus` | List of GPUs to attach (WARN: NVIDIA driver requires at least 4 GiB of RAM) | `[]object` | `[]` |
| `nodeGroups[name].gpus.name` | Name of GPU, such as "nvidia.com/AD102GL_L40S" | `string` | `""` |
### Cluster Addons

View File

@@ -1 +1 @@
ghcr.io/cozystack/cozystack/cluster-autoscaler:0.29.1@sha256:2d39989846c3579dd020b9f6c77e6e314cc81aa344eaac0f6d633e723c17196d
ghcr.io/cozystack/cozystack/cluster-autoscaler:0.29.0@sha256:2d39989846c3579dd020b9f6c77e6e314cc81aa344eaac0f6d633e723c17196d

View File

@@ -1 +1 @@
ghcr.io/cozystack/cozystack/kubevirt-cloud-provider:0.29.1@sha256:5335c044313b69ee13b30ca4941687e509005e55f4ae25723861edbf2fbd6dd2
ghcr.io/cozystack/cozystack/kubevirt-cloud-provider:0.29.0@sha256:5335c044313b69ee13b30ca4941687e509005e55f4ae25723861edbf2fbd6dd2

View File

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

View File

@@ -5,7 +5,46 @@
"addons": {
"description": "Cluster addons configuration",
"type": "object",
"default": {},
"default": {
"certManager": {
"enabled": false,
"valuesOverride": {}
},
"cilium": {
"valuesOverride": {}
},
"coredns": {
"valuesOverride": {}
},
"fluxcd": {
"enabled": false,
"valuesOverride": {}
},
"gatewayAPI": {
"enabled": false
},
"gpuOperator": {
"enabled": false,
"valuesOverride": {}
},
"ingressNginx": {
"enabled": false,
"exposeMethod": "Proxied",
"hosts": {},
"valuesOverride": {}
},
"monitoringAgents": {
"enabled": false,
"valuesOverride": {}
},
"velero": {
"enabled": false,
"valuesOverride": {}
},
"verticalPodAutoscaler": {
"valuesOverride": {}
}
},
"required": [
"certManager",
"cilium",
@@ -22,7 +61,10 @@
"certManager": {
"description": "Cert-manager: automatically creates and manages SSL/TLS certificate",
"type": "object",
"default": {},
"default": {
"enabled": false,
"valuesOverride": {}
},
"required": [
"enabled",
"valuesOverride"
@@ -44,7 +86,9 @@
"cilium": {
"description": "Cilium CNI plugin",
"type": "object",
"default": {},
"default": {
"valuesOverride": {}
},
"required": [
"valuesOverride"
],
@@ -60,7 +104,9 @@
"coredns": {
"description": "Coredns",
"type": "object",
"default": {},
"default": {
"valuesOverride": {}
},
"required": [
"valuesOverride"
],
@@ -76,7 +122,10 @@
"fluxcd": {
"description": "Flux CD",
"type": "object",
"default": {},
"default": {
"enabled": false,
"valuesOverride": {}
},
"required": [
"enabled",
"valuesOverride"
@@ -98,7 +147,9 @@
"gatewayAPI": {
"description": "Gateway API",
"type": "object",
"default": {},
"default": {
"enabled": false
},
"required": [
"enabled"
],
@@ -113,7 +164,10 @@
"gpuOperator": {
"description": "GPU-operator: NVIDIA GPU Operator",
"type": "object",
"default": {},
"default": {
"enabled": false,
"valuesOverride": {}
},
"required": [
"enabled",
"valuesOverride"
@@ -135,7 +189,12 @@
"ingressNginx": {
"description": "Ingress-NGINX Controller",
"type": "object",
"default": {},
"default": {
"enabled": false,
"exposeMethod": "Proxied",
"hosts": {},
"valuesOverride": {}
},
"required": [
"enabled",
"exposeMethod",
@@ -175,7 +234,10 @@
"monitoringAgents": {
"description": "MonitoringAgents",
"type": "object",
"default": {},
"default": {
"enabled": false,
"valuesOverride": {}
},
"required": [
"enabled",
"valuesOverride"
@@ -197,7 +259,10 @@
"velero": {
"description": "Velero",
"type": "object",
"default": {},
"default": {
"enabled": false,
"valuesOverride": {}
},
"required": [
"enabled",
"valuesOverride"
@@ -219,7 +284,9 @@
"verticalPodAutoscaler": {
"description": "VerticalPodAutoscaler",
"type": "object",
"default": {},
"default": {
"valuesOverride": {}
},
"required": [
"valuesOverride"
],
@@ -237,7 +304,27 @@
"controlPlane": {
"description": "Control Plane Configuration",
"type": "object",
"default": {},
"default": {
"apiServer": {
"resources": {},
"resourcesPreset": "medium"
},
"controllerManager": {
"resources": {},
"resourcesPreset": "micro"
},
"konnectivity": {
"server": {
"resources": {},
"resourcesPreset": "micro"
}
},
"replicas": 2,
"scheduler": {
"resources": {},
"resourcesPreset": "micro"
}
},
"required": [
"apiServer",
"controllerManager",
@@ -249,7 +336,10 @@
"apiServer": {
"description": "Control plane API server configuration.",
"type": "object",
"default": {},
"default": {
"resources": {},
"resourcesPreset": "medium"
},
"required": [
"resources",
"resourcesPreset"
@@ -307,7 +397,10 @@
"controllerManager": {
"description": "Controller Manager configuration.",
"type": "object",
"default": {},
"default": {
"resources": {},
"resourcesPreset": "micro"
},
"required": [
"resources",
"resourcesPreset"
@@ -365,7 +458,12 @@
"konnectivity": {
"description": "Konnectivity configuration.",
"type": "object",
"default": {},
"default": {
"server": {
"resources": {},
"resourcesPreset": "micro"
}
},
"required": [
"server"
],
@@ -373,7 +471,10 @@
"server": {
"description": "Konnectivity server configuration.",
"type": "object",
"default": {},
"default": {
"resources": {},
"resourcesPreset": "micro"
},
"required": [
"resources",
"resourcesPreset"
@@ -438,7 +539,10 @@
"scheduler": {
"description": "Scheduler configuration.",
"type": "object",
"default": {},
"default": {
"resources": {},
"resourcesPreset": "micro"
},
"required": [
"resources",
"resourcesPreset"
@@ -505,7 +609,7 @@
"default": {
"md0": {
"ephemeralStorage": "20Gi",
"gpus": [],
"gpus": {},
"instanceType": "u1.medium",
"maxReplicas": 10,
"minReplicas": 0,
@@ -527,7 +631,6 @@
"properties": {
"ephemeralStorage": {
"description": "Ephemeral storage size",
"default": "20Gi",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
@@ -542,7 +645,6 @@
"gpus": {
"description": "List of GPUs to attach (WARN: NVIDIA driver requires at least 4 GiB of RAM)",
"type": "array",
"default": [],
"items": {
"type": "object",
"required": [
@@ -558,23 +660,19 @@
},
"instanceType": {
"description": "Virtual machine instance type",
"type": "string",
"default": "u1.medium"
"type": "string"
},
"maxReplicas": {
"description": "Maximum amount of replicas",
"type": "integer",
"default": 10
"type": "integer"
},
"minReplicas": {
"description": "Minimum amount of replicas",
"type": "integer",
"default": 0
"type": "integer"
},
"resources": {
"description": "Resources available to each worker node",
"type": "object",
"default": {},
"properties": {
"cpu": {
"description": "CPU available to each worker node",

View File

@@ -9,17 +9,17 @@ version: "v1.33"
## @param host {string} Hostname used to access the Kubernetes cluster externally. Defaults to `<cluster-name>.<tenant-host>` when empty.
host: ""
## @param nodeGroups {map[string]nodeGroup} Worker nodes configuration
## @field nodeGroup {nodeGroup} Node configuration
## @field nodeGroup.minReplicas {int default=0} Minimum amount of replicas
## @field nodeGroup.maxReplicas {int default=10} Maximum amount of replicas
## @field nodeGroup.instanceType {string default="u1.medium"} Virtual machine instance type
## @field nodeGroup.ephemeralStorage {quantity default="20Gi"} Ephemeral storage size
## @field nodeGroup.roles {[]string default=[]} List of node's roles
## @field nodeGroup.resources {resources default={}} Resources available to each worker node
## @param nodeGroups {map[string]node} Worker nodes configuration
## @field node {node} Node configuration
## @field node.minReplicas {int} Minimum amount of replicas
## @field node.maxReplicas {int} Maximum amount of replicas
## @field node.instanceType {string} Virtual machine instance type
## @field node.ephemeralStorage {quantity} Ephemeral storage size
## @field node.roles {[]string} List of node's roles
## @field node.resources {resources} Resources available to each worker node
## @field resources.cpu {*quantity} CPU available to each worker node
## @field resources.memory {*quantity} Memory (RAM) available to each worker node
## @field nodeGroup.gpus {[]gpu default={}} List of GPUs to attach (WARN: NVIDIA driver requires at least 4 GiB of RAM)
## @field node.gpus {[]gpu} List of GPUs to attach (WARN: NVIDIA driver requires at least 4 GiB of RAM)
## @field gpu.name {string} Name of GPU, such as "nvidia.com/AD102GL_L40S"
##
nodeGroups:

View File

@@ -72,7 +72,7 @@ more details:
| Name | Description | Type | Value |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------- |
| `replicas` | Number of MariaDB replicas | `int` | `2` |
| `resources` | Explicit CPU and memory configuration for each MariaDB replica. When left empty, the preset defined in `resourcesPreset` is applied. | `*object` | `null` |
| `resources` | Explicit CPU and memory configuration for each MariaDB 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` | `nano` |

View File

@@ -5,7 +5,16 @@
"backup": {
"description": "Backup configuration",
"type": "object",
"default": {},
"default": {
"cleanupStrategy": "--keep-last=3 --keep-daily=3 --keep-within-weekly=1m",
"enabled": false,
"resticPassword": "\u003cpassword\u003e",
"s3AccessKey": "\u003cyour-access-key\u003e",
"s3Bucket": "s3.example.org/mysql-backups",
"s3Region": "us-east-1",
"s3SecretKey": "\u003cyour-secret-key\u003e",
"schedule": "0 2 * * *"
},
"required": [
"cleanupStrategy",
"enabled",

View File

@@ -10,7 +10,7 @@ It provides a data layer for cloud native applications, IoT messaging, and micro
| Name | Description | Type | Value |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------- |
| `replicas` | Number of replicas | `int` | `2` |
| `resources` | Explicit CPU and memory configuration for each NATS replica. When left empty, the preset defined in `resourcesPreset` is applied. | `*object` | `null` |
| `resources` | Explicit CPU and memory configuration for each NATS 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` | `nano` |

View File

@@ -5,7 +5,10 @@
"config": {
"description": "NATS configuration",
"type": "object",
"default": {},
"default": {
"merge": {},
"resolver": {}
},
"properties": {
"merge": {
"description": "Additional configuration to merge into NATS config (see example)",
@@ -29,7 +32,10 @@
"jetstream": {
"description": "Jetstream configuration",
"type": "object",
"default": {},
"default": {
"enabled": true,
"size": "10Gi"
},
"required": [
"enabled",
"size"

View File

@@ -69,7 +69,7 @@ See:
| Name | Description | Type | Value |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------- |
| `replicas` | Number of Postgres replicas | `int` | `2` |
| `resources` | Explicit CPU and memory configuration for each PostgreSQL replica. When left empty, the preset defined in `resourcesPreset` is applied. | `*object` | `null` |
| `resources` | Explicit CPU and memory configuration for each PostgreSQL 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

@@ -5,7 +5,15 @@
"backup": {
"description": "Backup configuration",
"type": "object",
"default": {},
"default": {
"destinationPath": "s3://bucket/path/to/folder/",
"enabled": false,
"endpointURL": "http://minio-gateway-service:9000",
"retentionPolicy": "30d",
"s3AccessKey": "\u003cyour-access-key\u003e",
"s3SecretKey": "\u003cyour-secret-key\u003e",
"schedule": "0 2 * * * *"
},
"properties": {
"destinationPath": {
"description": "Path to store the backup (i.e. s3://bucket/path/to/folder)",
@@ -47,7 +55,11 @@
"bootstrap": {
"description": "Bootstrap configuration",
"type": "object",
"default": {},
"default": {
"enabled": false,
"oldName": "",
"recoveryTime": ""
},
"required": [
"enabled",
"oldName"
@@ -113,7 +125,11 @@
"postgresql": {
"description": "PostgreSQL server configuration",
"type": "object",
"default": {},
"default": {
"parameters": {
"max_connections": 100
}
},
"required": [
"parameters"
],
@@ -121,7 +137,9 @@
"parameters": {
"description": "PostgreSQL server parameters",
"type": "object",
"default": {},
"default": {
"max_connections": 100
},
"required": [
"max_connections"
],
@@ -138,7 +156,10 @@
"quorum": {
"description": "Quorum configuration for synchronous replication",
"type": "object",
"default": {},
"default": {
"maxSyncReplicas": 0,
"minSyncReplicas": 0
},
"required": [
"maxSyncReplicas",
"minSyncReplicas"

View File

@@ -16,7 +16,7 @@ The service utilizes official RabbitMQ operator. This ensures the reliability an
| Name | Description | Type | Value |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------- |
| `replicas` | Number of RabbitMQ replicas | `int` | `3` |
| `resources` | Explicit CPU and memory configuration for each RabbitMQ replica. When left empty, the preset defined in `resourcesPreset` is applied. | `*object` | `null` |
| `resources` | Explicit CPU and memory configuration for each RabbitMQ 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` | `nano` |

View File

@@ -16,7 +16,7 @@ Service utilizes the Spotahome Redis Operator for efficient management and orche
| Name | Description | Type | Value |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------- |
| `replicas` | Number of Redis replicas | `int` | `2` |
| `resources` | Explicit CPU and memory configuration for each Redis replica. When left empty, the preset defined in `resourcesPreset` is applied. | `*object` | `null` |
| `resources` | Explicit CPU and memory configuration for each Redis 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` | `nano` |

View File

@@ -10,7 +10,14 @@
"httpAndHttps": {
"description": "HTTP and HTTPS configuration",
"type": "object",
"default": {},
"default": {
"endpoints": {},
"mode": "tcp",
"targetPorts": {
"http": 80,
"https": 443
}
},
"required": [
"mode",
"targetPorts"
@@ -36,7 +43,10 @@
"targetPorts": {
"description": "Target ports configuration",
"type": "object",
"default": {},
"default": {
"http": 80,
"https": 443
},
"required": [
"http",
"https"

View File

@@ -71,8 +71,7 @@ kubernetes 0.26.2 8ddbe32e
kubernetes 0.26.3 c02a3818
kubernetes 0.27.0 6cd5e746
kubernetes 0.28.0 7f477eec
kubernetes 0.29.0 87b23161
kubernetes 0.29.1 HEAD
kubernetes 0.29.0 HEAD
mysql 0.1.0 263e47be
mysql 0.2.0 c24a103f
mysql 0.3.0 53f2365e

View File

@@ -50,7 +50,7 @@ virtctl ssh <user>@<vm>
| `systemDisk.storageClass` | StorageClass used to store the data | `*string` | `replicated` |
| `gpus` | List of GPUs to attach | `[]object` | `[]` |
| `gpus[i].name` | The name of the GPU to attach. This should match the GPU resource name in the cluster. | `string` | `""` |
| `resources` | Resources | `*object` | `null` |
| `resources` | Resources | `*object` | `{}` |
| `resources.cpu` | The number of CPU cores allocated to the virtual machine | `*quantity` | `null` |
| `resources.sockets` | The number of CPU sockets allocated to the virtual machine (used to define vCPU topology) | `*quantity` | `null` |
| `resources.memory` | The amount of memory allocated to the virtual machine | `*quantity` | `null` |

View File

@@ -168,7 +168,11 @@
"systemDisk": {
"description": "System disk configuration",
"type": "object",
"default": {},
"default": {
"image": "ubuntu",
"storage": "5Gi",
"storageClass": "replicated"
},
"required": [
"image",
"storage"

View File

@@ -49,7 +49,7 @@ virtctl ssh <user>@<vm>
| `disks[i].bus` | Disk bus type, such as "sata" | `*string` | `null` |
| `gpus` | List of GPUs to attach (WARN: NVIDIA driver requires at least 4 GiB of RAM) | `[]object` | `[]` |
| `gpus[i].name` | Name of GPU, such as "nvidia.com/AD102GL_L40S" | `string` | `""` |
| `resources` | Resources | `*object` | `null` |
| `resources` | Resources | `*object` | `{}` |
| `resources.cpu` | The number of CPU cores allocated to the virtual machine | `*quantity` | `null` |
| `resources.memory` | The amount of memory allocated to the virtual machine | `*quantity` | `null` |
| `resources.sockets` | The number of CPU sockets allocated to the virtual machine (used to define vCPU topology) | `*quantity` | `null` |

View File

@@ -22,7 +22,7 @@ Furthermore, Shadowbox is compatible with standard Shadowsocks clients, providin
| Name | Description | Type | Value |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------- |
| `replicas` | Number of VPN server replicas | `int` | `2` |
| `resources` | Explicit CPU and memory configuration for each VPN server replica. When left empty, the preset defined in `resourcesPreset` is applied. | `*object` | `null` |
| `resources` | Explicit CPU and memory configuration for each VPN server 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` | `nano` |

View File

@@ -1,2 +1,2 @@
cozystack:
image: ghcr.io/cozystack/cozystack/installer:v0.36.1@sha256:1579855349bef729209e8668b96dbb03e0fc80b74bcae2c25f3ed5f3ae6d2f7f
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.1@sha256:150efd626321c9389415da5779504be4f10e70beafaeb1b7c162b08b3d50b51f
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.1@sha256:ecf30f70d9a4b708f68fab52ba3a2ecc0787bb2e79906d76b770bb51f8d6ad6c
ghcr.io/cozystack/cozystack/matchbox:v0.36.0-beta.4@sha256:764c547a352c9c1d0442e43cdfd0ef50b216bd7f6e5514c777b1d90c4d95da92

View File

@@ -4,12 +4,12 @@
### Common parameters
| Name | Description | Type | Value |
| ------------------ | ----------------------------------- | ----------- | ------ |
| `size` | Persistent Volume size | `*quantity` | `4Gi` |
| `storageClass` | StorageClass used to store the data | `*string` | `""` |
| `replicas` | Number of etcd replicas | `*int` | `3` |
| `resources` | Resource configuration for etcd | `*object` | `null` |
| `resources.cpu` | The number of CPU cores allocated | `*quantity` | `4` |
| `resources.memory` | The amount of memory allocated | `*quantity` | `1Gi` |
| Name | Description | Type | Value |
| ------------------ | ----------------------------------- | ----------- | ----- |
| `size` | Persistent Volume size | `*quantity` | `4Gi` |
| `storageClass` | StorageClass used to store the data | `*string` | `""` |
| `replicas` | Number of etcd replicas | `*int` | `3` |
| `resources` | Resource configuration for etcd | `*object` | `{}` |
| `resources.cpu` | The number of CPU cores allocated | `*quantity` | `4` |
| `resources.memory` | The amount of memory allocated | `*quantity` | `1Gi` |

View File

@@ -10,7 +10,10 @@
"resources": {
"description": "Resource configuration for etcd",
"type": "object",
"default": {},
"default": {
"cpu": 4,
"memory": "1Gi"
},
"properties": {
"cpu": {
"description": "The number of CPU cores allocated",

View File

@@ -9,7 +9,7 @@
| `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` | `null` |
| `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

@@ -3,4 +3,4 @@ name: monitoring
description: Monitoring and observability stack
icon: /logos/monitoring.svg
type: application
version: 1.13.1
version: 1.13.0

View File

@@ -17,7 +17,7 @@
| `metricsStorages[i].name` | Name of the storage instance | `string` | `""` |
| `metricsStorages[i].retentionPeriod` | Retention period for the metrics in the storage instance | `string` | `""` |
| `metricsStorages[i].deduplicationInterval` | Deduplication interval for the metrics in the storage instance | `string` | `""` |
| `metricsStorages[i].storage` | Persistent Volume size for the storage instance | `string` | `10Gi` |
| `metricsStorages[i].storage` | Persistent Volume size for the storage instance | `string` | `""` |
| `metricsStorages[i].storageClassName` | StorageClass used to store the data | `*string` | `null` |
| `metricsStorages[i].vminsert` | Configuration for vminsert component of the storage instance | `*object` | `null` |
| `metricsStorages[i].vminsert.minAllowed` | Requests (minimum allowed/available resources) | `*object` | `null` |
@@ -44,13 +44,13 @@
### Logs storage configuration
| Name | Description | Type | Value |
| ---------------------------------- | ----------------------------------------------------- | ---------- | ------------ |
| `logsStorages` | Configuration of logs storage instances | `[]object` | `[...]` |
| `logsStorages[i].name` | Name of the storage instance | `string` | `""` |
| `logsStorages[i].retentionPeriod` | Retention period for the logs in the storage instance | `string` | `1` |
| `logsStorages[i].storage` | Persistent Volume size for the storage instance | `string` | `10Gi` |
| `logsStorages[i].storageClassName` | StorageClass used to store the data | `*string` | `replicated` |
| Name | Description | Type | Value |
| ---------------------------------- | ----------------------------------------------------- | ---------- | ------- |
| `logsStorages` | Configuration of logs storage instances | `[]object` | `[...]` |
| `logsStorages[i].name` | Name of the storage instance | `string` | `""` |
| `logsStorages[i].retentionPeriod` | Retention period for the logs in the storage instance | `string` | `""` |
| `logsStorages[i].storage` | Persistent Volume size for the storage instance | `string` | `""` |
| `logsStorages[i].storageClassName` | StorageClass used to store the data | `*string` | `null` |
### Alerta configuration

View File

@@ -1 +1 @@
ghcr.io/cozystack/cozystack/grafana:1.13.1@sha256:c63978e1ed0304e8518b31ddee56c4e8115541b997d8efbe1c0a74da57140399
ghcr.io/cozystack/cozystack/grafana:1.13.0@sha256:c63978e1ed0304e8518b31ddee56c4e8115541b997d8efbe1c0a74da57140399

View File

@@ -5,17 +5,47 @@
"alerta": {
"description": "Configuration for Alerta service",
"type": "object",
"default": {},
"default": {
"alerts": {
"telegram": {
"chatID": "",
"disabledSeverity": "",
"token": ""
}
},
"resources": {
"limits": {
"cpu": "1",
"memory": "1Gi"
},
"requests": {
"cpu": "100m",
"memory": "256Mi"
}
},
"storage": "10Gi",
"storageClassName": ""
},
"properties": {
"alerts": {
"description": "Configuration for alerts",
"type": "object",
"default": {},
"default": {
"telegram": {
"chatID": "",
"disabledSeverity": "",
"token": ""
}
},
"properties": {
"telegram": {
"description": "Configuration for Telegram alerts",
"type": "object",
"default": {},
"default": {
"chatID": "",
"disabledSeverity": "",
"token": ""
},
"required": [
"chatID",
"disabledSeverity",
@@ -41,11 +71,23 @@
"resources": {
"description": "Resources configuration",
"type": "object",
"default": {},
"default": {
"limits": {
"cpu": "1",
"memory": "1Gi"
},
"requests": {
"cpu": "100m",
"memory": "256Mi"
}
},
"properties": {
"limits": {
"type": "object",
"default": {},
"default": {
"cpu": "1",
"memory": "1Gi"
},
"properties": {
"cpu": {
"description": "CPU limit (maximum available CPU)",
@@ -79,7 +121,10 @@
},
"requests": {
"type": "object",
"default": {},
"default": {
"cpu": "100m",
"memory": "256Mi"
},
"properties": {
"cpu": {
"description": "CPU request (minimum available CPU)",
@@ -127,12 +172,28 @@
"grafana": {
"description": "Configuration for Grafana",
"type": "object",
"default": {},
"default": {
"db": {
"size": "10Gi"
},
"resources": {
"limits": {
"cpu": "1",
"memory": "1Gi"
},
"requests": {
"cpu": "100m",
"memory": "256Mi"
}
}
},
"properties": {
"db": {
"description": "Database configuration",
"type": "object",
"default": {},
"default": {
"size": "10Gi"
},
"properties": {
"size": {
"description": "Persistent Volume size for the database",
@@ -144,11 +205,23 @@
"resources": {
"description": "Resources configuration",
"type": "object",
"default": {},
"default": {
"limits": {
"cpu": "1",
"memory": "1Gi"
},
"requests": {
"cpu": "100m",
"memory": "256Mi"
}
},
"properties": {
"limits": {
"type": "object",
"default": {},
"default": {
"cpu": "1",
"memory": "1Gi"
},
"properties": {
"cpu": {
"description": "CPU limit (maximum available CPU)",
@@ -182,7 +255,10 @@
},
"requests": {
"type": "object",
"default": {},
"default": {
"cpu": "100m",
"memory": "256Mi"
},
"properties": {
"cpu": {
"description": "CPU request (minimum available CPU)",
@@ -247,18 +323,15 @@
},
"retentionPeriod": {
"description": "Retention period for the logs in the storage instance",
"type": "string",
"default": "1"
"type": "string"
},
"storage": {
"description": "Persistent Volume size for the storage instance",
"type": "string",
"default": "10Gi"
"type": "string"
},
"storageClassName": {
"description": "StorageClass used to store the data",
"type": "string",
"default": "replicated"
"type": "string"
}
}
}
@@ -305,8 +378,7 @@
},
"storage": {
"description": "Persistent Volume size for the storage instance",
"type": "string",
"default": "10Gi"
"type": "string"
},
"storageClassName": {
"description": "StorageClass used to store the data",

View File

@@ -9,7 +9,7 @@ host: ""
## @field metricsStorage.name {string} Name of the storage instance
## @field metricsStorage.retentionPeriod {string} Retention period for the metrics in the storage instance
## @field metricsStorage.deduplicationInterval {string} Deduplication interval for the metrics in the storage instance
## @field metricsStorage.storage {string default="10Gi"} Persistent Volume size for the storage instance
## @field metricsStorage.storage {string} Persistent Volume size for the storage instance
## @field metricsStorage.storageClassName {*string} StorageClass used to store the data
## @field metricsStorage.vminsert {*vmcomponent} Configuration for vminsert component of the storage instance
## @field metricsStorage.vmselect {*vmcomponent} Configuration for vmselect component of the storage instance
@@ -69,9 +69,9 @@ metricsStorages:
## @param logsStorages {[]logsStorage} Configuration of logs storage instances
## @field logsStorage.name {string} Name of the storage instance
## @field logsStorage.retentionPeriod {string default=1} Retention period for the logs in the storage instance
## @field logsStorage.storage {string default="10Gi"} Persistent Volume size for the storage instance
## @field logsStorage.storageClassName {*string default="replicated"} StorageClass used to store the data
## @field logsStorage.retentionPeriod {string} Retention period for the logs in the storage instance
## @field logsStorage.storage {string} Persistent Volume size for the storage instance
## @field logsStorage.storageClassName {*string} StorageClass used to store the data
##
logsStorages:
- name: generic

View File

@@ -13,46 +13,46 @@
### 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` | `null` |
| `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` | `null` |
| `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` | `null` |
| `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[name].replicas` | Number of replicas in the zone | `*int` | `null` |
| `volume.zones[name].size` | Zone storage size | `*quantity` | `null` |
| `s3` | S3 service configuration | `*object` | `null` |
| `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` |
| 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.1@sha256:890c6f38d22fa8cba423d086686bd55c20b3d0c27881cf4b7a7801f1b0685112
ghcr.io/cozystack/cozystack/objectstorage-sidecar:v0.36.0-beta.4@sha256:24451989b15b6801b33ad355a5507307d0333bf9afd240f1db0aca9c92f6b2ad

View File

@@ -5,7 +5,13 @@
"db": {
"description": "Database Configuration",
"type": "object",
"default": {},
"default": {
"replicas": 2,
"resources": {},
"resourcesPreset": "small",
"size": "10Gi",
"storageClass": ""
},
"required": [
"resources",
"resourcesPreset"
@@ -86,7 +92,14 @@
"filer": {
"description": "Filer service configuration",
"type": "object",
"default": {},
"default": {
"grpcHost": "",
"grpcPort": 443,
"replicas": 2,
"resources": {},
"resourcesPreset": "small",
"whitelist": {}
},
"required": [
"resources",
"resourcesPreset"
@@ -170,7 +183,11 @@
"master": {
"description": "Master service configuration",
"type": "object",
"default": {},
"default": {
"replicas": 3,
"resources": {},
"resourcesPreset": "small"
},
"required": [
"resources",
"resourcesPreset"
@@ -238,7 +255,11 @@
"s3": {
"description": "S3 service configuration",
"type": "object",
"default": {},
"default": {
"replicas": 2,
"resources": {},
"resourcesPreset": "small"
},
"required": [
"resources",
"resourcesPreset"
@@ -311,7 +332,14 @@
"volume": {
"description": "Volume service configuration",
"type": "object",
"default": {},
"default": {
"replicas": 2,
"resources": {},
"resourcesPreset": "small",
"size": "10Gi",
"storageClass": "",
"zones": {}
},
"required": [
"resources",
"resourcesPreset"

View File

@@ -56,8 +56,7 @@ monitoring 1.10.1 8c86905b
monitoring 1.11.0 4369b031
monitoring 1.12.0 0e47e1e8
monitoring 1.12.1 c02a3818
monitoring 1.13.0 87b23161
monitoring 1.13.1 HEAD
monitoring 1.13.0 HEAD
seaweedfs 0.1.0 71514249
seaweedfs 0.2.0 5fb9cfe3
seaweedfs 0.2.1 fde4bcfa

View File

@@ -1 +1 @@
ghcr.io/cozystack/cozystack/s3manager:v0.5.0@sha256:899ea667b3e575244d512cade23f30cc93d768b070f9c2bebcb440e443444bdb
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.1@sha256:a9ce8848b0a46e52ce47ad10fcccc4e848740f5cf1d4e6cb78fc4a196e167e1b
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

@@ -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.1@sha256:6a6144430bdec901b4046840a484f0d9effc570f4f7cef2e5740332b98e3077a
image: ghcr.io/cozystack/cozystack/cozystack-controller:v0.36.0-beta.4@sha256:f4f8fa8e2f33f66d90b99398025d0da1328b530a622a9b420c993e51e1302992
debug: false
disableTelemetry: false
cozystackVersion: "v0.36.1"
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.1",
"appVersion": "v0.36.0-beta.4",
"authProxyEnabled": {{ .Values.authProxy.enabled }},
"oauthLoginURI": {{ .Values.authProxy.oauthLoginURI | quote }},
"oauthLogoutURI": {{ .Values.authProxy.oauthLogoutURI | quote }},

View File

@@ -1,4 +1,4 @@
FROM bitnamilegacy/node:20.15.1 AS build
FROM bitnami/node:20.15.1 AS build
WORKDIR /app
ARG COMMIT_REF=4926bc68fabb0914afab574006643c85a597b371
@@ -26,5 +26,5 @@ RUN sed -i \
-e 's/#1b2b32/#2a2d2f/g' \
$(grep -rl "#2d4048\|#25333d\|#fcfdfd\|#f1f6f8\|#e3eaed\|#cbd4d8\|#aeb8bc\|#859399\|#6a7a81\|#4f6169\|#3a4d55\|#2d4048\|#21333b\|#1b2b32")
FROM bitnamilegacy/nginx:1.25.2
FROM bitnami/nginx:1.25.2
COPY --from=build /app/build /app

View File

@@ -9,7 +9,7 @@ RUN apk add --no-cache patch
WORKDIR /source
RUN wget -O- https://github.com/cozystack/kubeapps/archive/${COMMIT_REF}.tar.gz | tar xzf - --strip-components=1
FROM bitnamilegacy/golang:1.23.4 AS builder
FROM bitnami/golang:1.23.4 AS builder
WORKDIR /go/src/github.com/vmware-tanzu/kubeapps
COPY --from=source /source/go.mod /source/go.sum ./
ARG TARGETOS

View File

@@ -19,7 +19,7 @@ kubeapps:
image:
registry: ghcr.io/cozystack/cozystack
repository: dashboard
tag: v0.36.1
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.1
digest: "sha256:95cec1059acc1e307f9346bdf570cfc1ca5962bdb803ff08ccd17712d1d8f485"
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.1@sha256:61bd1a046d0ad2bc90bf4507aa41073989001d13606e9b8cf8f318edcfadf2c7
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.1@sha256:61bd1a046d0ad2bc90bf4507aa41073989001d13606e9b8cf8f318edcfadf2c7
- --migrate-image=ghcr.io/cozystack/cozystack/kamaji:v0.36.0-beta.4@sha256:bcf8ccde72e5f8619b626a290c1e4c81018d9a10497e66948afebabd80a64023

View File

@@ -1,4 +1,4 @@
portSecurity: true
routes: ""
image: ghcr.io/cozystack/cozystack/kubeovn-plunger:v0.36.1@sha256:b38c0610e9648d21326be30a773173757897d2ba9ec438272c8ae3d738956b66
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.1@sha256:8fa755cdb024c7bdeff390d01d6b8569d60a4b244a7371209be4c4851df5fad4
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:7035b06406493c3385645319a50f2198c6bfb343d2e8c3f0707e769db1e960f7
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.1@sha256:cae43eae09fc39e5f2140d30ef55253f871cc565b8b7a564a54077b7cbd92212
image: ghcr.io/cozystack/cozystack/kubevirt-csi-driver:0.29.0@sha256:3a3bc912f70ccba1e9f92a0754179dbdc4c01f24073467b6d1406c77da794863

View File

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

View File

@@ -118,7 +118,7 @@ seaweedfs:
bucketClassName: "seaweedfs"
region: ""
sidecar:
image: "ghcr.io/cozystack/cozystack/objectstorage-sidecar:v0.36.1@sha256:890c6f38d22fa8cba423d086686bd55c20b3d0c27881cf4b7a7801f1b0685112"
image: "ghcr.io/cozystack/cozystack/objectstorage-sidecar:v0.36.0-beta.4@sha256:24451989b15b6801b33ad355a5507307d0333bf9afd240f1db0aca9c92f6b2ad"
certificates:
commonName: "SeaweedFS CA"
ipAddresses: []

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
}

View File

@@ -45,6 +45,7 @@ import (
internalapiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
schemadefault "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting"
// Importing API errors package to construct appropriate error responses
apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -88,24 +89,12 @@ type REST struct {
// NewREST creates a new REST storage for Application with specific configuration
func NewREST(dynamicClient dynamic.Interface, config *config.Resource) *REST {
var specSchema *structuralschema.Structural
if raw := strings.TrimSpace(config.Application.OpenAPISchema); raw != "" {
var v1js apiextv1.JSONSchemaProps
if err := json.Unmarshal([]byte(raw), &v1js); err != nil {
klog.Errorf("Failed to unmarshal v1 OpenAPI schema: %v", err)
} else {
scheme := runtime.NewScheme()
_ = internalapiext.AddToScheme(scheme)
_ = apiextv1.AddToScheme(scheme)
var ijs internalapiext.JSONSchemaProps
if err := scheme.Convert(&v1js, &ijs, nil); err != nil {
klog.Errorf("Failed to convert v1->internal JSONSchemaProps: %v", err)
} else if s, err := structuralschema.NewStructural(&ijs); err != nil {
klog.Errorf("Failed to create structural schema: %v", err)
} else {
specSchema = s
}
var js internalapiext.JSONSchemaProps
if err := json.Unmarshal([]byte(raw), &js); err != nil {
klog.Errorf("Failed to unmarshal OpenAPI schema: %v", err)
} else if specSchema, err = structuralschema.NewStructural(&js); err != nil {
klog.Errorf("Failed to create structural schema: %v", err)
}
}
@@ -1216,3 +1205,28 @@ func (e errNotAcceptable) Status() metav1.Status {
Message: e.Error(),
}
}
// applySpecDefaults applies default values to the Application spec based on the schema
func (r *REST) applySpecDefaults(app *appsv1alpha1.Application) error {
if r.specSchema == nil {
return nil
}
var m map[string]any
if app.Spec != nil && len(app.Spec.Raw) > 0 {
if err := json.Unmarshal(app.Spec.Raw, &m); err != nil {
return err
}
}
if m == nil {
m = map[string]any{}
}
schemadefault.Default(m, r.specSchema)
raw, err := json.Marshal(m)
if err != nil {
return err
}
app.Spec = &apiextv1.JSON{Raw: raw}
return nil
}

View File

@@ -1,187 +0,0 @@
/*
Copyright 2024 The Cozystack Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package application
import (
"encoding/json"
"fmt"
appsv1alpha1 "github.com/cozystack/cozystack/pkg/apis/apps/v1alpha1"
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
)
// applySpecDefaults applies default values to the Application spec based on the schema
func (r *REST) applySpecDefaults(app *appsv1alpha1.Application) error {
if r.specSchema == nil {
return nil
}
var m map[string]any
if app.Spec != nil && len(app.Spec.Raw) > 0 {
if err := json.Unmarshal(app.Spec.Raw, &m); err != nil {
return err
}
}
if m == nil {
m = map[string]any{}
}
if err := defaultLikeKubernetes(&m, r.specSchema); err != nil {
return err
}
raw, err := json.Marshal(m)
if err != nil {
return err
}
app.Spec = &apiextv1.JSON{Raw: raw}
return nil
}
func defaultLikeKubernetes(root *map[string]any, s *structuralschema.Structural) error {
v := any(*root)
nv, err := applyDefaults(v, s, true)
if err != nil {
return err
}
obj, ok := nv.(map[string]any)
if !ok && nv != nil {
return fmt.Errorf("internal error: applyDefaults returned non-map type %T for object root", nv)
}
if obj == nil {
obj = map[string]any{}
}
*root = obj
return nil
}
func applyDefaults(v any, s *structuralschema.Structural, top bool) (any, error) {
if s == nil {
return v, nil
}
effType := s.Generic.Type
if effType == "" {
switch {
case len(s.Properties) > 0 || (s.AdditionalProperties != nil && s.AdditionalProperties.Structural != nil):
effType = "object"
case s.Items != nil:
effType = "array"
default:
// scalar
}
}
switch effType {
case "object":
mv, isMap := v.(map[string]any)
if !isMap || v == nil {
if s.Generic.Default.Object != nil && !top {
if dm, ok := s.Generic.Default.Object.(map[string]any); ok {
mv = cloneMap(dm)
}
}
if mv == nil {
mv = map[string]any{}
}
}
for name, ps := range s.Properties {
if _, ok := mv[name]; !ok {
if ps.Generic.Default.Object != nil {
mv[name] = clone(ps.Generic.Default.Object)
}
}
if cur, ok := mv[name]; ok {
cv, err := applyDefaults(cur, &ps, false)
if err != nil {
return nil, err
}
mv[name] = cv
}
}
if s.AdditionalProperties != nil && s.AdditionalProperties.Structural != nil {
ap := s.AdditionalProperties.Structural
for k, cur := range mv {
if _, isKnown := s.Properties[k]; isKnown {
continue
}
cv, err := applyDefaults(cur, ap, false)
if err != nil {
return nil, err
}
mv[k] = cv
}
}
return mv, nil
case "array":
sl, isSlice := v.([]any)
if !isSlice || v == nil {
if s.Generic.Default.Object != nil {
if ds, ok := s.Generic.Default.Object.([]any); ok {
sl = cloneSlice(ds)
}
}
if sl == nil {
sl = []any{}
}
}
if s.Items != nil {
for i := range sl {
cv, err := applyDefaults(sl[i], s.Items, false)
if err != nil {
return nil, err
}
sl[i] = cv
}
}
return sl, nil
default:
if v == nil && s.Generic.Default.Object != nil {
return clone(s.Generic.Default.Object), nil
}
return v, nil
}
}
func clone(x any) any {
switch t := x.(type) {
case map[string]any:
return cloneMap(t)
case []any:
return cloneSlice(t)
default:
return t
}
}
func cloneMap(m map[string]any) map[string]any {
out := make(map[string]any, len(m))
for k, v := range m {
out[k] = clone(v)
}
return out
}
func cloneSlice(s []any) []any {
out := make([]any, len(s))
for i := range s {
out[i] = clone(s[i])
}
return out
}

View File

@@ -1,124 +0,0 @@
package application
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
apischema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
)
func TestApplication(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Application defaulting Suite")
}
var _ = Describe("defaultLikeKubernetes", func() {
var rootSchema *apischema.Structural
BeforeEach(func() {
rootSchema = buildTestSchema()
})
It("applies value-schema defaults to existing map key without merging parent object default", func() {
spec := map[string]any{
"nodeGroups": map[string]any{
"md0": map[string]any{
"minReplicas": 3,
},
},
}
err := defaultLikeKubernetes(&spec, rootSchema)
Expect(err).NotTo(HaveOccurred())
ng := spec["nodeGroups"].(map[string]any)["md0"].(map[string]any)
Expect(ng).To(HaveKeyWithValue("minReplicas", BeNumerically("==", 3)))
Expect(ng).To(HaveKeyWithValue("instanceType", "u1.medium"))
Expect(ng["roles"]).To(ConsistOf("ingress-nginx"))
Expect(ng).NotTo(HaveKey("ephemeralStorage"))
Expect(ng).NotTo(HaveKey("maxReplicas"))
Expect(ng).NotTo(HaveKey("resources"))
})
It("does not create new map keys from parent object default", func() {
spec := map[string]any{
"nodeGroups": map[string]any{},
}
err := defaultLikeKubernetes(&spec, rootSchema)
Expect(err).NotTo(HaveOccurred())
ng := spec["nodeGroups"].(map[string]any)
Expect(ng).NotTo(HaveKey("md0"))
})
})
func buildTestSchema() *apischema.Structural {
instanceType := apischema.Structural{
Generic: apischema.Generic{
Type: "string",
Default: apischema.JSON{Object: "u1.medium"},
},
}
roles := apischema.Structural{
Generic: apischema.Generic{
Type: "array",
Default: apischema.JSON{Object: []any{"ingress-nginx"}},
},
Items: &apischema.Structural{
Generic: apischema.Generic{Type: "string"},
},
}
minReplicas := apischema.Structural{
Generic: apischema.Generic{Type: "integer"},
}
ephemeralStorage := apischema.Structural{
Generic: apischema.Generic{Type: "string"},
}
maxReplicas := apischema.Structural{
Generic: apischema.Generic{Type: "integer"},
}
resources := apischema.Structural{
Generic: apischema.Generic{Type: "object"},
Properties: map[string]apischema.Structural{},
}
nodeGroupsValue := &apischema.Structural{
Generic: apischema.Generic{Type: "object"},
Properties: map[string]apischema.Structural{
"instanceType": instanceType,
"roles": roles,
"minReplicas": minReplicas,
"ephemeralStorage": ephemeralStorage,
"maxReplicas": maxReplicas,
"resources": resources,
},
}
nodeGroups := apischema.Structural{
Generic: apischema.Generic{
Type: "object",
Default: apischema.JSON{Object: map[string]any{
"md0": map[string]any{
"ephemeralStorage": "20Gi",
"maxReplicas": 10,
"minReplicas": 0,
"resources": map[string]any{},
},
}},
},
AdditionalProperties: &apischema.StructuralOrBool{
Structural: nodeGroupsValue,
},
}
return &apischema.Structural{
Generic: apischema.Generic{Type: "object"},
Properties: map[string]apischema.Structural{
"nodeGroups": nodeGroups,
},
}
}

View File

@@ -22,15 +22,9 @@ kubectl get helmreleases.helm.toolkit.fluxcd.io -A \
# JSON Patch
| [
(if $s.chart.spec.version? then
{op:"replace", path:"/spec/chart/spec/version", value:"0.7.0"}
else
{op:"add", path:"/spec/chart/spec/version", value:"0.7.0"}
end),
{ op:"add", path:"/spec/chart/spec/version", value:"0.7.0" },
(if ($v|type) != "object" then {op:"add", path:"/spec/values", value:{}} else empty end),
(if ($v.volume?|type) != "object" then {op:"add", path:"/spec/values/volume", value:{}} else empty end),
(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),