From 87e394c0c9bd3202bae02df68ac94a2a1b41b9d0 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Mon, 29 Dec 2025 16:32:23 +0300 Subject: [PATCH 001/666] [platform] Add DNS-1035 validation for Application names Add validation to ensure Application names (including Tenants) conform to DNS-1035 format. This prevents creation of resources with names starting with digits, which would cause Kubernetes resource creation failures (e.g., Services, Namespaces). DNS-1035 requires names to: - Start with a lowercase letter [a-z] - Contain only lowercase alphanumeric or hyphens [-a-z0-9] - End with an alphanumeric character [a-z0-9] Also fixes broken validation.go that referenced non-existent internal types (apps.Application, apps.ApplicationSpec). Fixes: https://github.com/cozystack/cozystack/issues/1538 Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- pkg/apis/apps/validation/validation.go | 56 +++++++++++++++++++ pkg/apis/apps/validation/validation_test.go | 61 +++++++++++++++++++++ pkg/registry/apps/application/rest.go | 7 +++ 3 files changed, 124 insertions(+) create mode 100644 pkg/apis/apps/validation/validation.go create mode 100644 pkg/apis/apps/validation/validation_test.go diff --git a/pkg/apis/apps/validation/validation.go b/pkg/apis/apps/validation/validation.go new file mode 100644 index 00000000..ac8e6623 --- /dev/null +++ b/pkg/apis/apps/validation/validation.go @@ -0,0 +1,56 @@ +/* +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 validation + +import ( + "regexp" + + "k8s.io/apimachinery/pkg/util/validation/field" +) + +// dns1035LabelRegex validates DNS-1035 label format. +// DNS-1035 labels must start with a letter, contain only lowercase alphanumeric +// characters or hyphens, and end with an alphanumeric character. +var dns1035LabelRegex = regexp.MustCompile(`^[a-z]([-a-z0-9]*[a-z0-9])?$`) + +// maxDNS1035LabelLength is the maximum length of a DNS-1035 label. +const maxDNS1035LabelLength = 63 + +// ValidateApplicationName validates that an Application name conforms to DNS-1035. +// This is required because Application names are used to create Kubernetes resources +// (Services, Namespaces, etc.) that must have DNS-1035 compliant names. +func ValidateApplicationName(name string, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + if len(name) == 0 { + allErrs = append(allErrs, field.Required(fldPath, "name is required")) + return allErrs + } + + if len(name) > maxDNS1035LabelLength { + allErrs = append(allErrs, field.TooLongMaxLength(fldPath, name, maxDNS1035LabelLength)) + } + + if !dns1035LabelRegex.MatchString(name) { + allErrs = append(allErrs, field.Invalid(fldPath, name, + "a DNS-1035 label must consist of lower case alphanumeric characters or '-', "+ + "start with an alphabetic character, and end with an alphanumeric character "+ + "(e.g. 'my-name', regex used for validation is '[a-z]([-a-z0-9]*[a-z0-9])?')")) + } + + return allErrs +} diff --git a/pkg/apis/apps/validation/validation_test.go b/pkg/apis/apps/validation/validation_test.go new file mode 100644 index 00000000..d1e6b2f8 --- /dev/null +++ b/pkg/apis/apps/validation/validation_test.go @@ -0,0 +1,61 @@ +/* +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 validation + +import ( + "testing" + + "k8s.io/apimachinery/pkg/util/validation/field" +) + +func TestValidateApplicationName(t *testing.T) { + tests := []struct { + name string + appName string + wantError bool + }{ + // Valid names + {"valid simple name", "tenant-one", false}, + {"valid single letter", "a", false}, + {"valid with numbers", "abc-123", false}, + {"valid lowercase", "my-tenant", false}, + {"valid long name", "my-very-long-tenant-name-123", false}, + + // Invalid names + {"starts with digit", "1john", true}, + {"only digits", "123", true}, + {"starts with hyphen", "-tenant", true}, + {"ends with hyphen", "tenant-", true}, + {"uppercase letters", "Tenant", true}, + {"mixed case", "myTenant", true}, + {"underscore", "my_tenant", true}, + {"dot", "my.tenant", true}, + {"empty string", "", true}, + {"space", "my tenant", true}, + {"too long (64 chars)", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", true}, + {"max length (63 chars)", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + errs := ValidateApplicationName(tt.appName, field.NewPath("metadata").Child("name")) + if (len(errs) > 0) != tt.wantError { + t.Errorf("ValidateApplicationName(%q) returned %d errors, wantError = %v", tt.appName, len(errs), tt.wantError) + } + }) + } +} diff --git a/pkg/registry/apps/application/rest.go b/pkg/registry/apps/application/rest.go index 00d22486..d93f55e0 100644 --- a/pkg/registry/apps/application/rest.go +++ b/pkg/registry/apps/application/rest.go @@ -35,6 +35,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/selection" + "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/util/duration" "k8s.io/apimachinery/pkg/watch" "k8s.io/apiserver/pkg/endpoints/request" @@ -43,6 +44,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" appsv1alpha1 "github.com/cozystack/cozystack/pkg/apis/apps/v1alpha1" + "github.com/cozystack/cozystack/pkg/apis/apps/validation" "github.com/cozystack/cozystack/pkg/config" "github.com/cozystack/cozystack/pkg/registry" fieldfilter "github.com/cozystack/cozystack/pkg/registry/fields" @@ -152,6 +154,11 @@ func (r *REST) Create(ctx context.Context, obj runtime.Object, createValidation return nil, fmt.Errorf("expected *appsv1alpha1.Application object, got %T", obj) } + // Validate Application name conforms to DNS-1035 + if errs := validation.ValidateApplicationName(app.Name, field.NewPath("metadata").Child("name")); len(errs) > 0 { + return nil, apierrors.NewInvalid(r.gvk.GroupKind(), app.Name, errs) + } + // Validate that values don't contain reserved keys (starting with "_") if err := validateNoInternalKeys(app.Spec); err != nil { return nil, apierrors.NewBadRequest(err.Error()) From 1cbf1831641f5e695a9e7f12e0f8355832460f93 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Mon, 29 Dec 2025 16:42:05 +0300 Subject: [PATCH 002/666] fix(validation): limit name to 40 chars and add comprehensive tests - Reduce maxApplicationNameLength from 63 to 40 characters to allow room for prefixes like "tenant-" and nested namespaces - Add 27 test cases covering: - Valid names (simple, single letter, with numbers, double hyphen) - Invalid start characters (digit, hyphen) - Invalid end characters (hyphen) - Invalid characters (uppercase, underscore, dot, space, unicode) - Empty/whitespace inputs - Length boundary tests (40 valid, 41+ invalid) Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- pkg/apis/apps/validation/validation.go | 10 ++++--- pkg/apis/apps/validation/validation_test.go | 32 +++++++++++++++++---- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/pkg/apis/apps/validation/validation.go b/pkg/apis/apps/validation/validation.go index ac8e6623..c6c2e535 100644 --- a/pkg/apis/apps/validation/validation.go +++ b/pkg/apis/apps/validation/validation.go @@ -27,8 +27,10 @@ import ( // characters or hyphens, and end with an alphanumeric character. var dns1035LabelRegex = regexp.MustCompile(`^[a-z]([-a-z0-9]*[a-z0-9])?$`) -// maxDNS1035LabelLength is the maximum length of a DNS-1035 label. -const maxDNS1035LabelLength = 63 +// maxApplicationNameLength is the maximum length of an Application name. +// This is set to 40 (not 63) to allow room for prefixes like "tenant-" +// and nested tenant namespaces (e.g., "tenant-parent-child"). +const maxApplicationNameLength = 40 // ValidateApplicationName validates that an Application name conforms to DNS-1035. // This is required because Application names are used to create Kubernetes resources @@ -41,8 +43,8 @@ func ValidateApplicationName(name string, fldPath *field.Path) field.ErrorList { return allErrs } - if len(name) > maxDNS1035LabelLength { - allErrs = append(allErrs, field.TooLongMaxLength(fldPath, name, maxDNS1035LabelLength)) + if len(name) > maxApplicationNameLength { + allErrs = append(allErrs, field.TooLongMaxLength(fldPath, name, maxApplicationNameLength)) } if !dns1035LabelRegex.MatchString(name) { diff --git a/pkg/apis/apps/validation/validation_test.go b/pkg/apis/apps/validation/validation_test.go index d1e6b2f8..87e80fe0 100644 --- a/pkg/apis/apps/validation/validation_test.go +++ b/pkg/apis/apps/validation/validation_test.go @@ -17,6 +17,7 @@ limitations under the License. package validation import ( + "strings" "testing" "k8s.io/apimachinery/pkg/util/validation/field" @@ -33,28 +34,47 @@ func TestValidateApplicationName(t *testing.T) { {"valid single letter", "a", false}, {"valid with numbers", "abc-123", false}, {"valid lowercase", "my-tenant", false}, - {"valid long name", "my-very-long-tenant-name-123", false}, + {"valid long name", "my-very-long-tenant-name", false}, + {"valid double hyphen", "my--tenant", false}, + {"valid max length (40 chars)", strings.Repeat("a", 40), false}, - // Invalid names + // Invalid: starts with wrong character {"starts with digit", "1john", true}, {"only digits", "123", true}, {"starts with hyphen", "-tenant", true}, + + // Invalid: ends with wrong character {"ends with hyphen", "tenant-", true}, + + // Invalid: wrong characters {"uppercase letters", "Tenant", true}, {"mixed case", "myTenant", true}, {"underscore", "my_tenant", true}, {"dot", "my.tenant", true}, - {"empty string", "", true}, {"space", "my tenant", true}, - {"too long (64 chars)", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", true}, - {"max length (63 chars)", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", false}, + {"unicode cyrillic", "тенант", true}, + {"unicode emoji", "tenant🚀", true}, + {"special chars", "tenant@home", true}, + {"colon", "tenant:one", true}, + {"slash", "tenant/one", true}, + + // Invalid: empty or whitespace + {"empty string", "", true}, + {"only spaces", " ", true}, + {"leading space", " tenant", true}, + {"trailing space", "tenant ", true}, + + // Invalid: length + {"too long (41 chars)", strings.Repeat("a", 41), true}, + {"way too long (100 chars)", strings.Repeat("a", 100), true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { errs := ValidateApplicationName(tt.appName, field.NewPath("metadata").Child("name")) if (len(errs) > 0) != tt.wantError { - t.Errorf("ValidateApplicationName(%q) returned %d errors, wantError = %v", tt.appName, len(errs), tt.wantError) + t.Errorf("ValidateApplicationName(%q) returned %d errors, wantError = %v, errors = %v", + tt.appName, len(errs), tt.wantError, errs) } }) } From 9f20771cf8829001b041421145841b452da7ea4b Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Mon, 29 Dec 2025 16:42:57 +0300 Subject: [PATCH 003/666] docs(tenant): update naming requirements in README Clarify DNS-1035 naming rules: - Must start with lowercase letter - Allowed characters: a-z, 0-9, hyphen - Must end with letter or number - Maximum 40 characters Change wording from "not allowed" to "discouraged" for dashes since the validation technically permits them. Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- packages/apps/tenant/README.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/apps/tenant/README.md b/packages/apps/tenant/README.md index 99a973c7..811197f2 100644 --- a/packages/apps/tenant/README.md +++ b/packages/apps/tenant/README.md @@ -6,9 +6,14 @@ Tenants can be created recursively and are subject to the following rules: ### Tenant naming -Tenant names must be alphanumeric. -Using dashes (`-`) in tenant names is not allowed, unlike with other services. -This limitation exists to keep consistent naming in tenants, nested tenants, and services deployed in them. +Tenant names must follow DNS-1035 naming rules: +- Must start with a lowercase letter (`a-z`) +- Can only contain lowercase letters, numbers, and hyphens (`a-z`, `0-9`, `-`) +- Must end with a letter or number (not a hyphen) +- Maximum length: 40 characters + +**Note:** Using dashes (`-`) in tenant names is discouraged, unlike with other services. +This is to keep consistent naming in tenants, nested tenants, and services deployed in them. For example: From 7c0e99e1af2e63ee79f4668ec872c047d23cad0a Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Mon, 29 Dec 2025 20:25:02 +0300 Subject: [PATCH 004/666] [platform] Add OpenAPI schema validation for Application names Add pattern and maxLength constraints to ObjectMeta.name in OpenAPI schema. This enables UI form validation when openapi-k8s-toolkit supports it. - Pattern: ^[a-z]([-a-z0-9]*[a-z0-9])?$ (DNS-1035) - MaxLength: 40 Depends on: cozystack/openapi-k8s-toolkit#1 Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- pkg/cmd/server/openapi.go | 45 ++++++++++++++++ pkg/cmd/server/openapi_test.go | 98 ++++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 pkg/cmd/server/openapi_test.go diff --git a/pkg/cmd/server/openapi.go b/pkg/cmd/server/openapi.go index e6a659c5..e5734b9b 100644 --- a/pkg/cmd/server/openapi.go +++ b/pkg/cmd/server/openapi.go @@ -19,8 +19,47 @@ const ( baseListRef = apiPrefix + ".ApplicationList" baseStatusRef = apiPrefix + ".ApplicationStatus" smp = "application/strategic-merge-patch+json" + + // objectMetaRef is the OpenAPI reference for Kubernetes ObjectMeta. + objectMetaRef = "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta" + // applicationNamePattern is DNS-1035 regex for Application names. + applicationNamePattern = `^[a-z]([-a-z0-9]*[a-z0-9])?$` + // applicationNameMaxLength is the maximum length for Application names. + applicationNameMaxLength = 40 ) +// patchObjectMetaNameValidation adds DNS-1035 validation to metadata.name. +// This ensures UI form validation matches backend validation. +func patchObjectMetaNameValidation(schemas map[string]*spec.Schema) { + objMeta, ok := schemas[objectMetaRef] + if !ok || objMeta == nil { + return + } + + if name, ok := objMeta.Properties["name"]; ok { + name.Pattern = applicationNamePattern + maxLen := int64(applicationNameMaxLength) + name.MaxLength = &maxLen + objMeta.Properties["name"] = name + } +} + +// patchObjectMetaNameValidationV2 adds DNS-1035 validation for Swagger v2. +func patchObjectMetaNameValidationV2(defs map[string]spec.Schema) { + objMeta, ok := defs[objectMetaRef] + if !ok { + return + } + + if name, ok := objMeta.Properties["name"]; ok { + name.Pattern = applicationNamePattern + maxLen := int64(applicationNameMaxLength) + name.MaxLength = &maxLen + objMeta.Properties["name"] = name + } + defs[objectMetaRef] = objMeta +} + // deepCopySchema clones *spec.Schema via JSON-marshal/unmarshal. func deepCopySchema(in *spec.Schema) *spec.Schema { if in == nil { @@ -262,6 +301,9 @@ func buildPostProcessV3(kindSchemas map[string]string) func(*spec3.OpenAPI) (*sp } } + // Add name validation to ObjectMeta for UI form validation + patchObjectMetaNameValidation(doc.Components.Schemas) + // Rewrite all $ref in the document out, err := rewriteDocRefs(doc) if err != nil { @@ -343,6 +385,9 @@ func buildPostProcessV2(kindSchemas map[string]string) func(*spec.Swagger) (*spe return sw, fmt.Errorf("base Application* schemas not found") } + // Add name validation to ObjectMeta for UI form validation + patchObjectMetaNameValidationV2(defs) + for kind, raw := range kindSchemas { ref := apiPrefix + "." + kind statusRef := ref + "Status" diff --git a/pkg/cmd/server/openapi_test.go b/pkg/cmd/server/openapi_test.go new file mode 100644 index 00000000..921b0d30 --- /dev/null +++ b/pkg/cmd/server/openapi_test.go @@ -0,0 +1,98 @@ +/* +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 server + +import ( + "testing" + + "k8s.io/kube-openapi/pkg/validation/spec" +) + +func TestPatchObjectMetaNameValidation(t *testing.T) { + tests := []struct { + name string + schemas map[string]*spec.Schema + wantPattern string + wantMaxLength *int64 + }{ + { + name: "patches ObjectMeta name field", + schemas: map[string]*spec.Schema{ + objectMetaRef: { + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "name": {SchemaProps: spec.SchemaProps{Type: []string{"string"}}}, + }, + }, + }, + }, + wantPattern: applicationNamePattern, + wantMaxLength: ptr(int64(applicationNameMaxLength)), + }, + { + name: "no panic when ObjectMeta missing", + schemas: map[string]*spec.Schema{}, + wantPattern: "", + wantMaxLength: nil, + }, + { + name: "no panic when name field missing", + schemas: map[string]*spec.Schema{ + objectMetaRef: { + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{}, + }, + }, + }, + wantPattern: "", + wantMaxLength: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + patchObjectMetaNameValidation(tt.schemas) + + objMeta, ok := tt.schemas[objectMetaRef] + if !ok { + if tt.wantPattern != "" || tt.wantMaxLength != nil { + t.Error("expected ObjectMeta to exist") + } + return + } + + name, ok := objMeta.Properties["name"] + if !ok { + if tt.wantPattern != "" || tt.wantMaxLength != nil { + t.Error("expected name field to exist") + } + return + } + + if name.Pattern != tt.wantPattern { + t.Errorf("Pattern = %q, want %q", name.Pattern, tt.wantPattern) + } + if (name.MaxLength == nil) != (tt.wantMaxLength == nil) { + t.Errorf("MaxLength nil mismatch") + } else if name.MaxLength != nil && *name.MaxLength != *tt.wantMaxLength { + t.Errorf("MaxLength = %d, want %d", *name.MaxLength, *tt.wantMaxLength) + } + }) + } +} + +func ptr[T any](v T) *T { return &v } From 3685d49c4e3c38360a6ea6d5de186e9b617f4658 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Wed, 11 Feb 2026 12:34:51 +0300 Subject: [PATCH 005/666] feat(api): add dynamic name length validation based on root-host Read root-host from cozystack-values secret at API server startup and use it to compute maximum allowed name length for applications. For all apps: validates prefix + name fits within the Helm release name limit (53 chars). For Tenants: additionally checks that the host label (name + "." + rootHost) fits within the Kubernetes label value limit (63 chars). This replaces the static 40-char limit with a dynamic calculation that accounts for the actual cluster root host length. Ref: https://github.com/cozystack/cozystack/issues/2001 Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- pkg/apiserver/apiserver.go | 2 +- pkg/cmd/server/start.go | 47 ++++++ pkg/cmd/server/start_test.go | 93 ++++++++++- pkg/config/config.go | 1 + pkg/registry/apps/application/rest.go | 47 +++++- .../apps/application/rest_validation_test.go | 153 ++++++++++++++++++ 6 files changed, 339 insertions(+), 4 deletions(-) create mode 100644 pkg/registry/apps/application/rest_validation_test.go diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index bc2b4857..aba9a345 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -208,7 +208,7 @@ func (c completedConfig) New() (*CozyServer, error) { // --- dynamically-configured, per-tenant resources --- appsV1alpha1Storage := map[string]rest.Storage{} for _, resConfig := range c.ResourceConfig.Resources { - storage := applicationstorage.NewREST(cli, watchCli, &resConfig) + storage := applicationstorage.NewREST(cli, watchCli, &resConfig, c.ResourceConfig.RootHost) appsV1alpha1Storage[resConfig.Application.Plural] = cozyregistry.RESTInPeace(storage) } appsApiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(apps.GroupName, Scheme, metav1.ParameterCodec, Codecs) diff --git a/pkg/cmd/server/start.go b/pkg/cmd/server/start.go index 6ff8d2d1..90293b39 100644 --- a/pkg/cmd/server/start.go +++ b/pkg/cmd/server/start.go @@ -33,6 +33,7 @@ import ( "github.com/cozystack/cozystack/pkg/config" sampleopenapi "github.com/cozystack/cozystack/pkg/generated/openapi" "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/apiserver/pkg/endpoints/openapi" @@ -44,6 +45,7 @@ import ( netutils "k8s.io/utils/net" "sigs.k8s.io/controller-runtime/pkg/client" k8sconfig "sigs.k8s.io/controller-runtime/pkg/client/config" + "sigs.k8s.io/yaml" ) // CozyServerOptions holds the state for the Cozy API server @@ -179,9 +181,54 @@ func (o *CozyServerOptions) Complete() error { o.ResourceConfig.Resources = append(o.ResourceConfig.Resources, resource) } + // Read root-host from cozystack-values secret (best-effort, non-fatal) + coreScheme := runtime.NewScheme() + _ = corev1.AddToScheme(coreScheme) + coreClient, err := client.New(cfg, client.Options{Scheme: coreScheme}) + if err != nil { + fmt.Printf("Warning: failed to create client for reading cozystack-values secret: %v\n", err) + } else { + secret := &corev1.Secret{} + err := coreClient.Get(context.Background(), client.ObjectKey{ + Namespace: "cozy-system", + Name: "cozystack-values", + }, secret) + if err != nil { + fmt.Printf("Warning: failed to read cozystack-values secret: %v\n", err) + } else { + o.ResourceConfig.RootHost = parseRootHostFromSecret(secret) + if o.ResourceConfig.RootHost != "" { + fmt.Printf("Loaded root-host: %s\n", o.ResourceConfig.RootHost) + } + } + } + return nil } +// parseRootHostFromSecret extracts _cluster.root-host from the cozystack-values secret. +func parseRootHostFromSecret(secret *corev1.Secret) string { + if secret == nil || secret.Data == nil { + return "" + } + + valuesYAML, ok := secret.Data["values.yaml"] + if !ok || len(valuesYAML) == 0 { + return "" + } + + var values struct { + Cluster struct { + RootHost string `json:"root-host"` + } `json:"_cluster"` + } + if err := yaml.Unmarshal(valuesYAML, &values); err != nil { + return "" + } + + return values.Cluster.RootHost +} + // Validate checks the correctness of the options func (o CozyServerOptions) Validate(args []string) error { var allErrors []error diff --git a/pkg/cmd/server/start_test.go b/pkg/cmd/server/start_test.go index df7c3547..6e19a719 100644 --- a/pkg/cmd/server/start_test.go +++ b/pkg/cmd/server/start_test.go @@ -16,5 +16,94 @@ limitations under the License. package server -// Note: Tests for KEP-4330 component versioning functionality have been removed -// as the functionality is not available in Kubernetes v0.34.1. +import ( + "testing" + + corev1 "k8s.io/api/core/v1" +) + +func TestParseRootHostFromSecret(t *testing.T) { + tests := []struct { + name string + secret *corev1.Secret + expected string + }{ + { + name: "valid secret with root-host", + secret: &corev1.Secret{ + Data: map[string][]byte{ + "values.yaml": []byte("_cluster:\n root-host: \"example.com\"\n bundle-name: \"paas-full\"\n"), + }, + }, + expected: "example.com", + }, + { + name: "valid secret with unquoted root-host", + secret: &corev1.Secret{ + Data: map[string][]byte{ + "values.yaml": []byte("_cluster:\n root-host: my.domain.org\n"), + }, + }, + expected: "my.domain.org", + }, + { + name: "missing values.yaml key", + secret: &corev1.Secret{ + Data: map[string][]byte{ + "other-key": []byte("some data"), + }, + }, + expected: "", + }, + { + name: "malformed YAML", + secret: &corev1.Secret{ + Data: map[string][]byte{ + "values.yaml": []byte("not: valid: yaml: [[["), + }, + }, + expected: "", + }, + { + name: "empty root-host", + secret: &corev1.Secret{ + Data: map[string][]byte{ + "values.yaml": []byte("_cluster:\n root-host: \"\"\n"), + }, + }, + expected: "", + }, + { + name: "no _cluster key", + secret: &corev1.Secret{ + Data: map[string][]byte{ + "values.yaml": []byte("other:\n key: value\n"), + }, + }, + expected: "", + }, + { + name: "_cluster without root-host", + secret: &corev1.Secret{ + Data: map[string][]byte{ + "values.yaml": []byte("_cluster:\n bundle-name: \"paas-full\"\n"), + }, + }, + expected: "", + }, + { + name: "nil data", + secret: &corev1.Secret{}, + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := parseRootHostFromSecret(tt.secret) + if result != tt.expected { + t.Errorf("parseRootHostFromSecret() = %q, want %q", result, tt.expected) + } + }) + } +} diff --git a/pkg/config/config.go b/pkg/config/config.go index 1e123e2c..260d149a 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -19,6 +19,7 @@ package config // ResourceConfig represents the structure of the configuration file. type ResourceConfig struct { Resources []Resource `yaml:"resources"` + RootHost string // cluster root host from cozystack-values secret } // Resource describes an individual resource. diff --git a/pkg/registry/apps/application/rest.go b/pkg/registry/apps/application/rest.go index d93f55e0..45c54b54 100644 --- a/pkg/registry/apps/application/rest.go +++ b/pkg/registry/apps/application/rest.go @@ -91,10 +91,11 @@ type REST struct { singularName string releaseConfig config.ReleaseConfig specSchema *structuralschema.Structural + rootHost string } // NewREST creates a new REST storage for Application with specific configuration -func NewREST(c client.Client, w client.WithWatch, config *config.Resource) *REST { +func NewREST(c client.Client, w client.WithWatch, config *config.Resource, rootHost string) *REST { var specSchema *structuralschema.Structural if raw := strings.TrimSpace(config.Application.OpenAPISchema); raw != "" { @@ -133,6 +134,7 @@ func NewREST(c client.Client, w client.WithWatch, config *config.Resource) *REST singularName: config.Application.Singular, releaseConfig: config.Release, specSchema: specSchema, + rootHost: rootHost, } } @@ -159,6 +161,11 @@ func (r *REST) Create(ctx context.Context, obj runtime.Object, createValidation return nil, apierrors.NewInvalid(r.gvk.GroupKind(), app.Name, errs) } + // Validate name length against Helm release and label limits + if err := r.validateNameLength(app.Name); err != nil { + return nil, apierrors.NewBadRequest(err.Error()) + } + // Validate that values don't contain reserved keys (starting with "_") if err := validateNoInternalKeys(app.Spec); err != nil { return nil, apierrors.NewBadRequest(err.Error()) @@ -477,6 +484,11 @@ func (r *REST) Update(ctx context.Context, name string, objInfo rest.UpdatedObje return nil, false, fmt.Errorf("expected *appsv1alpha1.Application object, got %T", newObj) } + // Validate name length against Helm release and label limits + if err := r.validateNameLength(app.Name); err != nil { + return nil, false, apierrors.NewBadRequest(err.Error()) + } + // Validate that values don't contain reserved keys (starting with "_") if err := validateNoInternalKeys(app.Spec); err != nil { return nil, false, apierrors.NewBadRequest(err.Error()) @@ -1036,6 +1048,39 @@ func validateNoInternalKeys(values *apiextv1.JSON) error { return nil } +// maxHelmReleaseName is the maximum length of a Helm release name (DNS-1123 subdomain). +const maxHelmReleaseName = 53 + +// maxK8sLabelValue is the maximum length of a Kubernetes label value. +const maxK8sLabelValue = 63 + +// validateNameLength checks that the application name won't exceed Kubernetes limits. +// For all apps: prefix + name must fit within the Helm release name limit (53 chars). +// For Tenants: additionally checks that the host label (name + "." + rootHost) fits +// within the Kubernetes label value limit (63 chars). +func (r *REST) validateNameLength(name string) error { + helmMax := maxHelmReleaseName - len(r.releaseConfig.Prefix) + maxLen := helmMax + + if r.kindName == "Tenant" && r.rootHost != "" { + hostLabelMax := maxK8sLabelValue - len(r.rootHost) - 1 // -1 for the dot separator + if hostLabelMax < maxLen { + maxLen = hostLabelMax + } + } + + if maxLen <= 0 { + return fmt.Errorf("configuration error: no valid name length possible (release prefix %q, root host %q)", + r.releaseConfig.Prefix, r.rootHost) + } + + if len(name) > maxLen { + return fmt.Errorf("name %q is too long: maximum %d characters allowed (release prefix %q, root host %q)", + name, maxLen, r.releaseConfig.Prefix, r.rootHost) + } + return nil +} + // convertHelmReleaseToApplication implements the actual conversion logic func (r *REST) convertHelmReleaseToApplication(ctx context.Context, hr *helmv2.HelmRelease) (appsv1alpha1.Application, error) { // Filter out internal keys (starting with "_") from spec diff --git a/pkg/registry/apps/application/rest_validation_test.go b/pkg/registry/apps/application/rest_validation_test.go new file mode 100644 index 00000000..adecc3ad --- /dev/null +++ b/pkg/registry/apps/application/rest_validation_test.go @@ -0,0 +1,153 @@ +/* +Copyright 2025 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 ( + "strings" + "testing" + + "github.com/cozystack/cozystack/pkg/config" +) + +func TestValidateNameLength(t *testing.T) { + tests := []struct { + name string + kindName string + prefix string + rootHost string + appName string + wantError bool + }{ + { + name: "non-tenant short name passes", + kindName: "MySQL", + prefix: "mysql-", + rootHost: "example.com", + appName: "mydb", + wantError: false, + }, + { + name: "non-tenant at helm boundary passes", + kindName: "MySQL", + prefix: "mysql-", + rootHost: "example.com", + appName: strings.Repeat("a", 53-len("mysql-")), // exactly 47 chars + wantError: false, + }, + { + name: "non-tenant exceeding helm limit fails", + kindName: "MySQL", + prefix: "mysql-", + rootHost: "example.com", + appName: strings.Repeat("a", 53-len("mysql-")+1), // 48 chars + wantError: true, + }, + { + name: "tenant no rootHost within helm limit passes", + kindName: "Tenant", + prefix: "tenant-", + rootHost: "", + appName: strings.Repeat("a", 53-len("tenant-")), // 46 chars + wantError: false, + }, + { + name: "tenant no rootHost exceeding helm limit fails", + kindName: "Tenant", + prefix: "tenant-", + rootHost: "", + appName: strings.Repeat("a", 53-len("tenant-")+1), // 47 chars + wantError: true, + }, + { + name: "tenant with rootHost within both limits passes", + kindName: "Tenant", + prefix: "tenant-", + rootHost: "example.com", // 11 chars → host label max = 63-11-1 = 51 + appName: "short", + wantError: false, + }, + { + name: "tenant with short rootHost helm limit is still stricter", + kindName: "Tenant", + prefix: "tenant-", + rootHost: "example.com", // 11 chars → host label max = 51, helm max = 46 + appName: strings.Repeat("a", 53-len("tenant-")), // 46 chars — at Helm boundary + wantError: false, + }, + { + name: "tenant with long rootHost at host label boundary passes", + kindName: "Tenant", + prefix: "tenant-", + rootHost: "long-subdomain.hosting.example.com", // 34 chars → host label max = 63-34-1 = 28 + appName: strings.Repeat("a", 63-len("long-subdomain.hosting.example.com")-1), // exactly 28 chars + wantError: false, + }, + { + name: "tenant with long rootHost exceeding host label limit fails", + kindName: "Tenant", + prefix: "tenant-", + rootHost: "long-subdomain.hosting.example.com", // 34 chars → host label max = 28 + appName: strings.Repeat("a", 63-len("long-subdomain.hosting.example.com")-1+1), // 29 chars + wantError: true, + }, + { + name: "tenant host label limit stricter than helm limit", + kindName: "Tenant", + prefix: "tenant-", + rootHost: "very-long-subdomain.hosting.example.com", // 39 chars → host label max = 63-39-1 = 23 + appName: strings.Repeat("a", 24), // 24 > 23 (host limit) but < 46 (helm limit) + wantError: true, + }, + { + name: "tenant short rootHost where helm limit is stricter", + kindName: "Tenant", + prefix: "tenant-", + rootHost: "x.co", // 4 chars → host label max = 63-4-1 = 58, helm max = 46 + appName: strings.Repeat("a", 53-len("tenant-")+1), // 47 > 46 (helm limit) but < 58 (host limit) + wantError: true, + }, + { + name: "non-tenant ignores rootHost for limit calculation", + kindName: "MySQL", + prefix: "mysql-", + rootHost: "very-long-subdomain.hosting.example.com", + appName: strings.Repeat("a", 53-len("mysql-")), // at helm boundary + wantError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &REST{ + kindName: tt.kindName, + releaseConfig: config.ReleaseConfig{ + Prefix: tt.prefix, + }, + rootHost: tt.rootHost, + } + + err := r.validateNameLength(tt.appName) + + if tt.wantError && err == nil { + t.Errorf("expected error for name %q (len=%d), got nil", tt.appName, len(tt.appName)) + } + if !tt.wantError && err != nil { + t.Errorf("unexpected error for name %q (len=%d): %v", tt.appName, len(tt.appName), err) + } + }) + } +} From dd34fb581ec78db333bc9dd7707d897b9615018a Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Wed, 11 Feb 2026 12:39:51 +0300 Subject: [PATCH 006/666] fix(api): handle edge case when prefix or root host exhaust name capacity Add protection against negative or zero maxLen when release prefix or root host are too long, returning a clear configuration error instead of a confusing "name too long" message. Add corresponding test cases. Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- .../apps/application/rest_validation_test.go | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/pkg/registry/apps/application/rest_validation_test.go b/pkg/registry/apps/application/rest_validation_test.go index adecc3ad..8c93cf37 100644 --- a/pkg/registry/apps/application/rest_validation_test.go +++ b/pkg/registry/apps/application/rest_validation_test.go @@ -128,6 +128,38 @@ func TestValidateNameLength(t *testing.T) { appName: strings.Repeat("a", 53-len("mysql-")), // at helm boundary wantError: false, }, + { + name: "prefix consuming all helm capacity returns config error", + kindName: "MySQL", + prefix: strings.Repeat("x", 53), // prefix == maxHelmReleaseName → helmMax = 0 + rootHost: "", + appName: "a", + wantError: true, + }, + { + name: "prefix exceeding helm capacity returns config error", + kindName: "MySQL", + prefix: strings.Repeat("x", 60), // prefix > maxHelmReleaseName → helmMax < 0 + rootHost: "", + appName: "a", + wantError: true, + }, + { + name: "tenant with rootHost consuming all label capacity returns config error", + kindName: "Tenant", + prefix: "t-", + rootHost: strings.Repeat("h", 62), // 62 chars → hostLabelMax = 63-62-1 = 0, helmMax = 51 + appName: "a", + wantError: true, + }, + { + name: "tenant with rootHost exceeding label capacity returns config error", + kindName: "Tenant", + prefix: "t-", + rootHost: strings.Repeat("h", 70), // 70 chars → hostLabelMax = 63-70-1 = -8, helmMax = 51 + appName: "a", + wantError: true, + }, } for _, tt := range tests { From d4556e4c53ce430d87f5488f3d0f4005163d8342 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Wed, 11 Feb 2026 12:58:44 +0300 Subject: [PATCH 007/666] fix(api): address review feedback for name validation - Add DNS-1035 format validation to Update path (was only in Create) - Simplify Secret reading by reusing existing scheme instead of creating a separate client - Add nil secret test case for parseRootHostFromSecret Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- pkg/cmd/server/start.go | 28 ++++++++++++--------------- pkg/cmd/server/start_test.go | 5 +++++ pkg/registry/apps/application/rest.go | 5 +++++ 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/pkg/cmd/server/start.go b/pkg/cmd/server/start.go index 90293b39..3c1db606 100644 --- a/pkg/cmd/server/start.go +++ b/pkg/cmd/server/start.go @@ -115,6 +115,9 @@ func (o *CozyServerOptions) Complete() error { if err := v1alpha1.AddToScheme(scheme); err != nil { return fmt.Errorf("failed to register types: %w", err) } + if err := corev1.AddToScheme(scheme); err != nil { + return fmt.Errorf("failed to register core types: %w", err) + } cfg, err := k8sconfig.GetConfig() if err != nil { @@ -182,24 +185,17 @@ func (o *CozyServerOptions) Complete() error { } // Read root-host from cozystack-values secret (best-effort, non-fatal) - coreScheme := runtime.NewScheme() - _ = corev1.AddToScheme(coreScheme) - coreClient, err := client.New(cfg, client.Options{Scheme: coreScheme}) + secret := &corev1.Secret{} + err = o.Client.Get(context.Background(), client.ObjectKey{ + Namespace: "cozy-system", + Name: "cozystack-values", + }, secret) if err != nil { - fmt.Printf("Warning: failed to create client for reading cozystack-values secret: %v\n", err) + fmt.Printf("Warning: failed to read cozystack-values secret: %v\n", err) } else { - secret := &corev1.Secret{} - err := coreClient.Get(context.Background(), client.ObjectKey{ - Namespace: "cozy-system", - Name: "cozystack-values", - }, secret) - if err != nil { - fmt.Printf("Warning: failed to read cozystack-values secret: %v\n", err) - } else { - o.ResourceConfig.RootHost = parseRootHostFromSecret(secret) - if o.ResourceConfig.RootHost != "" { - fmt.Printf("Loaded root-host: %s\n", o.ResourceConfig.RootHost) - } + o.ResourceConfig.RootHost = parseRootHostFromSecret(secret) + if o.ResourceConfig.RootHost != "" { + fmt.Printf("Loaded root-host: %s\n", o.ResourceConfig.RootHost) } } diff --git a/pkg/cmd/server/start_test.go b/pkg/cmd/server/start_test.go index 6e19a719..3d4d7856 100644 --- a/pkg/cmd/server/start_test.go +++ b/pkg/cmd/server/start_test.go @@ -96,6 +96,11 @@ func TestParseRootHostFromSecret(t *testing.T) { secret: &corev1.Secret{}, expected: "", }, + { + name: "nil secret", + secret: nil, + expected: "", + }, } for _, tt := range tests { diff --git a/pkg/registry/apps/application/rest.go b/pkg/registry/apps/application/rest.go index 45c54b54..16f0dde4 100644 --- a/pkg/registry/apps/application/rest.go +++ b/pkg/registry/apps/application/rest.go @@ -484,6 +484,11 @@ func (r *REST) Update(ctx context.Context, name string, objInfo rest.UpdatedObje return nil, false, fmt.Errorf("expected *appsv1alpha1.Application object, got %T", newObj) } + // Validate Application name conforms to DNS-1035 + if errs := validation.ValidateApplicationName(app.Name, field.NewPath("metadata").Child("name")); len(errs) > 0 { + return nil, false, apierrors.NewInvalid(r.gvk.GroupKind(), app.Name, errs) + } + // Validate name length against Helm release and label limits if err := r.validateNameLength(app.Name); err != nil { return nil, false, apierrors.NewBadRequest(err.Error()) From 9e47669f686c5adac037eedc65662ab645cb731c Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Wed, 11 Feb 2026 13:07:50 +0300 Subject: [PATCH 008/666] fix(api): remove name validation from Update path and use klog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Skip DNS-1035 and length validation on Update since Kubernetes names are immutable — validating would block updates to pre-existing resources with non-conforming names. Replace fmt.Printf with klog for structured logging consistency. Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- pkg/cmd/server/start.go | 5 +++-- pkg/registry/apps/application/rest.go | 12 +++--------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/pkg/cmd/server/start.go b/pkg/cmd/server/start.go index 3c1db606..bf0d63bd 100644 --- a/pkg/cmd/server/start.go +++ b/pkg/cmd/server/start.go @@ -42,6 +42,7 @@ import ( utilfeature "k8s.io/apiserver/pkg/util/feature" basecompatibility "k8s.io/component-base/compatibility" baseversion "k8s.io/component-base/version" + "k8s.io/klog/v2" netutils "k8s.io/utils/net" "sigs.k8s.io/controller-runtime/pkg/client" k8sconfig "sigs.k8s.io/controller-runtime/pkg/client/config" @@ -191,11 +192,11 @@ func (o *CozyServerOptions) Complete() error { Name: "cozystack-values", }, secret) if err != nil { - fmt.Printf("Warning: failed to read cozystack-values secret: %v\n", err) + klog.Warningf("failed to read cozystack-values secret: %v", err) } else { o.ResourceConfig.RootHost = parseRootHostFromSecret(secret) if o.ResourceConfig.RootHost != "" { - fmt.Printf("Loaded root-host: %s\n", o.ResourceConfig.RootHost) + klog.Infof("Loaded root-host: %s", o.ResourceConfig.RootHost) } } diff --git a/pkg/registry/apps/application/rest.go b/pkg/registry/apps/application/rest.go index 16f0dde4..42734774 100644 --- a/pkg/registry/apps/application/rest.go +++ b/pkg/registry/apps/application/rest.go @@ -484,15 +484,9 @@ func (r *REST) Update(ctx context.Context, name string, objInfo rest.UpdatedObje return nil, false, fmt.Errorf("expected *appsv1alpha1.Application object, got %T", newObj) } - // Validate Application name conforms to DNS-1035 - if errs := validation.ValidateApplicationName(app.Name, field.NewPath("metadata").Child("name")); len(errs) > 0 { - return nil, false, apierrors.NewInvalid(r.gvk.GroupKind(), app.Name, errs) - } - - // Validate name length against Helm release and label limits - if err := r.validateNameLength(app.Name); err != nil { - return nil, false, apierrors.NewBadRequest(err.Error()) - } + // Note: name validation (DNS-1035 format + length) is intentionally skipped on + // Update because Kubernetes names are immutable. Validating here would block + // updates to pre-existing resources whose names don't conform to the new rules. // Validate that values don't contain reserved keys (starting with "_") if err := validateNoInternalKeys(app.Spec); err != nil { From e978e00c7e7b0157c38047c962acb7372ff41004 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Wed, 11 Feb 2026 13:18:45 +0300 Subject: [PATCH 009/666] refactor(api): use standard IsDNS1035Label and remove static length limit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace custom DNS-1035 regex with k8s.io/apimachinery IsDNS1035Label. Remove hardcoded maxApplicationNameLength=40 from both validation and OpenAPI — length validation is now handled entirely by validateNameLength which computes dynamic limits based on Helm release prefix and root-host. Fix README to reflect that max length depends on cluster configuration. Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- packages/apps/tenant/README.md | 6 ++-- pkg/apis/apps/validation/validation.go | 26 ++++------------- pkg/apis/apps/validation/validation_test.go | 6 ++-- pkg/cmd/server/openapi.go | 6 ---- pkg/cmd/server/openapi_test.go | 31 +++++++-------------- 5 files changed, 21 insertions(+), 54 deletions(-) diff --git a/packages/apps/tenant/README.md b/packages/apps/tenant/README.md index 811197f2..472ccce2 100644 --- a/packages/apps/tenant/README.md +++ b/packages/apps/tenant/README.md @@ -10,16 +10,16 @@ Tenant names must follow DNS-1035 naming rules: - Must start with a lowercase letter (`a-z`) - Can only contain lowercase letters, numbers, and hyphens (`a-z`, `0-9`, `-`) - Must end with a letter or number (not a hyphen) -- Maximum length: 40 characters +- Maximum length depends on the cluster configuration (Helm release prefix and root domain) -**Note:** Using dashes (`-`) in tenant names is discouraged, unlike with other services. +**Note:** Using dashes (`-`) in tenant names is **allowed but discouraged**, unlike with other services. This is to keep consistent naming in tenants, nested tenants, and services deployed in them. +Names with dashes (e.g., `foo-bar`) may lead to ambiguous parsing of internal resource names like `tenant-foo-bar`. For example: - The root tenant is named `root`, but internally it's referenced as `tenant-root`. - A nested tenant could be named `foo`, which would result in `tenant-foo` in service names and URLs. -- However, a tenant can not be named `foo-bar`, because parsing names such as `tenant-foo-bar` would be ambiguous. ### Unique domains diff --git a/pkg/apis/apps/validation/validation.go b/pkg/apis/apps/validation/validation.go index c6c2e535..28810b72 100644 --- a/pkg/apis/apps/validation/validation.go +++ b/pkg/apis/apps/validation/validation.go @@ -17,24 +17,15 @@ limitations under the License. package validation import ( - "regexp" - + k8svalidation "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" ) -// dns1035LabelRegex validates DNS-1035 label format. -// DNS-1035 labels must start with a letter, contain only lowercase alphanumeric -// characters or hyphens, and end with an alphanumeric character. -var dns1035LabelRegex = regexp.MustCompile(`^[a-z]([-a-z0-9]*[a-z0-9])?$`) - -// maxApplicationNameLength is the maximum length of an Application name. -// This is set to 40 (not 63) to allow room for prefixes like "tenant-" -// and nested tenant namespaces (e.g., "tenant-parent-child"). -const maxApplicationNameLength = 40 - // ValidateApplicationName validates that an Application name conforms to DNS-1035. // This is required because Application names are used to create Kubernetes resources // (Services, Namespaces, etc.) that must have DNS-1035 compliant names. +// Note: length validation is handled separately by validateNameLength in the REST +// handler, which computes dynamic limits based on Helm release prefix and root-host. func ValidateApplicationName(name string, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} @@ -43,15 +34,8 @@ func ValidateApplicationName(name string, fldPath *field.Path) field.ErrorList { return allErrs } - if len(name) > maxApplicationNameLength { - allErrs = append(allErrs, field.TooLongMaxLength(fldPath, name, maxApplicationNameLength)) - } - - if !dns1035LabelRegex.MatchString(name) { - allErrs = append(allErrs, field.Invalid(fldPath, name, - "a DNS-1035 label must consist of lower case alphanumeric characters or '-', "+ - "start with an alphabetic character, and end with an alphanumeric character "+ - "(e.g. 'my-name', regex used for validation is '[a-z]([-a-z0-9]*[a-z0-9])?')")) + for _, msg := range k8svalidation.IsDNS1035Label(name) { + allErrs = append(allErrs, field.Invalid(fldPath, name, msg)) } return allErrs diff --git a/pkg/apis/apps/validation/validation_test.go b/pkg/apis/apps/validation/validation_test.go index 87e80fe0..7bf194d5 100644 --- a/pkg/apis/apps/validation/validation_test.go +++ b/pkg/apis/apps/validation/validation_test.go @@ -36,7 +36,7 @@ func TestValidateApplicationName(t *testing.T) { {"valid lowercase", "my-tenant", false}, {"valid long name", "my-very-long-tenant-name", false}, {"valid double hyphen", "my--tenant", false}, - {"valid max length (40 chars)", strings.Repeat("a", 40), false}, + {"valid at DNS-1035 max (63 chars)", strings.Repeat("a", 63), false}, // Invalid: starts with wrong character {"starts with digit", "1john", true}, @@ -64,8 +64,8 @@ func TestValidateApplicationName(t *testing.T) { {"leading space", " tenant", true}, {"trailing space", "tenant ", true}, - // Invalid: length - {"too long (41 chars)", strings.Repeat("a", 41), true}, + // Invalid: exceeds DNS-1035 max length (63) + {"too long (64 chars)", strings.Repeat("a", 64), true}, {"way too long (100 chars)", strings.Repeat("a", 100), true}, } diff --git a/pkg/cmd/server/openapi.go b/pkg/cmd/server/openapi.go index e5734b9b..4d89d1fd 100644 --- a/pkg/cmd/server/openapi.go +++ b/pkg/cmd/server/openapi.go @@ -24,8 +24,6 @@ const ( objectMetaRef = "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta" // applicationNamePattern is DNS-1035 regex for Application names. applicationNamePattern = `^[a-z]([-a-z0-9]*[a-z0-9])?$` - // applicationNameMaxLength is the maximum length for Application names. - applicationNameMaxLength = 40 ) // patchObjectMetaNameValidation adds DNS-1035 validation to metadata.name. @@ -38,8 +36,6 @@ func patchObjectMetaNameValidation(schemas map[string]*spec.Schema) { if name, ok := objMeta.Properties["name"]; ok { name.Pattern = applicationNamePattern - maxLen := int64(applicationNameMaxLength) - name.MaxLength = &maxLen objMeta.Properties["name"] = name } } @@ -53,8 +49,6 @@ func patchObjectMetaNameValidationV2(defs map[string]spec.Schema) { if name, ok := objMeta.Properties["name"]; ok { name.Pattern = applicationNamePattern - maxLen := int64(applicationNameMaxLength) - name.MaxLength = &maxLen objMeta.Properties["name"] = name } defs[objectMetaRef] = objMeta diff --git a/pkg/cmd/server/openapi_test.go b/pkg/cmd/server/openapi_test.go index 921b0d30..626581ca 100644 --- a/pkg/cmd/server/openapi_test.go +++ b/pkg/cmd/server/openapi_test.go @@ -24,10 +24,9 @@ import ( func TestPatchObjectMetaNameValidation(t *testing.T) { tests := []struct { - name string - schemas map[string]*spec.Schema - wantPattern string - wantMaxLength *int64 + name string + schemas map[string]*spec.Schema + wantPattern string }{ { name: "patches ObjectMeta name field", @@ -40,14 +39,12 @@ func TestPatchObjectMetaNameValidation(t *testing.T) { }, }, }, - wantPattern: applicationNamePattern, - wantMaxLength: ptr(int64(applicationNameMaxLength)), + wantPattern: applicationNamePattern, }, { - name: "no panic when ObjectMeta missing", - schemas: map[string]*spec.Schema{}, - wantPattern: "", - wantMaxLength: nil, + name: "no panic when ObjectMeta missing", + schemas: map[string]*spec.Schema{}, + wantPattern: "", }, { name: "no panic when name field missing", @@ -58,8 +55,7 @@ func TestPatchObjectMetaNameValidation(t *testing.T) { }, }, }, - wantPattern: "", - wantMaxLength: nil, + wantPattern: "", }, } @@ -69,7 +65,7 @@ func TestPatchObjectMetaNameValidation(t *testing.T) { objMeta, ok := tt.schemas[objectMetaRef] if !ok { - if tt.wantPattern != "" || tt.wantMaxLength != nil { + if tt.wantPattern != "" { t.Error("expected ObjectMeta to exist") } return @@ -77,7 +73,7 @@ func TestPatchObjectMetaNameValidation(t *testing.T) { name, ok := objMeta.Properties["name"] if !ok { - if tt.wantPattern != "" || tt.wantMaxLength != nil { + if tt.wantPattern != "" { t.Error("expected name field to exist") } return @@ -86,13 +82,6 @@ func TestPatchObjectMetaNameValidation(t *testing.T) { if name.Pattern != tt.wantPattern { t.Errorf("Pattern = %q, want %q", name.Pattern, tt.wantPattern) } - if (name.MaxLength == nil) != (tt.wantMaxLength == nil) { - t.Errorf("MaxLength nil mismatch") - } else if name.MaxLength != nil && *name.MaxLength != *tt.wantMaxLength { - t.Errorf("MaxLength = %d, want %d", *name.MaxLength, *tt.wantMaxLength) - } }) } } - -func ptr[T any](v T) *T { return &v } From c932740dc5d09d7a7c3f9d66d13044a8aa35713e Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Wed, 11 Feb 2026 13:24:16 +0300 Subject: [PATCH 010/666] refactor(api): remove global ObjectMeta name patching from OpenAPI Remove patchObjectMetaNameValidation and patchObjectMetaNameValidationV2 functions that were modifying the global ObjectMeta schema. This patching affected ALL resources served by the API server, not just Application resources. Backend validation in Create() is sufficient for enforcing name constraints. Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- pkg/cmd/server/openapi.go | 38 --------------- pkg/cmd/server/openapi_test.go | 87 ---------------------------------- 2 files changed, 125 deletions(-) delete mode 100644 pkg/cmd/server/openapi_test.go diff --git a/pkg/cmd/server/openapi.go b/pkg/cmd/server/openapi.go index 4d89d1fd..b52aa989 100644 --- a/pkg/cmd/server/openapi.go +++ b/pkg/cmd/server/openapi.go @@ -20,40 +20,8 @@ const ( baseStatusRef = apiPrefix + ".ApplicationStatus" smp = "application/strategic-merge-patch+json" - // objectMetaRef is the OpenAPI reference for Kubernetes ObjectMeta. - objectMetaRef = "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta" - // applicationNamePattern is DNS-1035 regex for Application names. - applicationNamePattern = `^[a-z]([-a-z0-9]*[a-z0-9])?$` ) -// patchObjectMetaNameValidation adds DNS-1035 validation to metadata.name. -// This ensures UI form validation matches backend validation. -func patchObjectMetaNameValidation(schemas map[string]*spec.Schema) { - objMeta, ok := schemas[objectMetaRef] - if !ok || objMeta == nil { - return - } - - if name, ok := objMeta.Properties["name"]; ok { - name.Pattern = applicationNamePattern - objMeta.Properties["name"] = name - } -} - -// patchObjectMetaNameValidationV2 adds DNS-1035 validation for Swagger v2. -func patchObjectMetaNameValidationV2(defs map[string]spec.Schema) { - objMeta, ok := defs[objectMetaRef] - if !ok { - return - } - - if name, ok := objMeta.Properties["name"]; ok { - name.Pattern = applicationNamePattern - objMeta.Properties["name"] = name - } - defs[objectMetaRef] = objMeta -} - // deepCopySchema clones *spec.Schema via JSON-marshal/unmarshal. func deepCopySchema(in *spec.Schema) *spec.Schema { if in == nil { @@ -295,9 +263,6 @@ func buildPostProcessV3(kindSchemas map[string]string) func(*spec3.OpenAPI) (*sp } } - // Add name validation to ObjectMeta for UI form validation - patchObjectMetaNameValidation(doc.Components.Schemas) - // Rewrite all $ref in the document out, err := rewriteDocRefs(doc) if err != nil { @@ -379,9 +344,6 @@ func buildPostProcessV2(kindSchemas map[string]string) func(*spec.Swagger) (*spe return sw, fmt.Errorf("base Application* schemas not found") } - // Add name validation to ObjectMeta for UI form validation - patchObjectMetaNameValidationV2(defs) - for kind, raw := range kindSchemas { ref := apiPrefix + "." + kind statusRef := ref + "Status" diff --git a/pkg/cmd/server/openapi_test.go b/pkg/cmd/server/openapi_test.go deleted file mode 100644 index 626581ca..00000000 --- a/pkg/cmd/server/openapi_test.go +++ /dev/null @@ -1,87 +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 server - -import ( - "testing" - - "k8s.io/kube-openapi/pkg/validation/spec" -) - -func TestPatchObjectMetaNameValidation(t *testing.T) { - tests := []struct { - name string - schemas map[string]*spec.Schema - wantPattern string - }{ - { - name: "patches ObjectMeta name field", - schemas: map[string]*spec.Schema{ - objectMetaRef: { - SchemaProps: spec.SchemaProps{ - Properties: map[string]spec.Schema{ - "name": {SchemaProps: spec.SchemaProps{Type: []string{"string"}}}, - }, - }, - }, - }, - wantPattern: applicationNamePattern, - }, - { - name: "no panic when ObjectMeta missing", - schemas: map[string]*spec.Schema{}, - wantPattern: "", - }, - { - name: "no panic when name field missing", - schemas: map[string]*spec.Schema{ - objectMetaRef: { - SchemaProps: spec.SchemaProps{ - Properties: map[string]spec.Schema{}, - }, - }, - }, - wantPattern: "", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - patchObjectMetaNameValidation(tt.schemas) - - objMeta, ok := tt.schemas[objectMetaRef] - if !ok { - if tt.wantPattern != "" { - t.Error("expected ObjectMeta to exist") - } - return - } - - name, ok := objMeta.Properties["name"] - if !ok { - if tt.wantPattern != "" { - t.Error("expected name field to exist") - } - return - } - - if name.Pattern != tt.wantPattern { - t.Errorf("Pattern = %q, want %q", name.Pattern, tt.wantPattern) - } - }) - } -} From e267cfcf9dd30aef98f2fa0a4a479db5d0b305a9 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Wed, 11 Feb 2026 13:29:42 +0300 Subject: [PATCH 011/666] fix(api): address review feedback for validation consistency - Return field.ErrorList from validateNameLength for consistent apierrors.NewInvalid error shape (was NewBadRequest) - Add klog warning when YAML parsing fails in parseRootHostFromSecret - Fix maxHelmReleaseName comment to accurately describe Helm convention - Add note that root-host changes require API server restart - Replace interface{} with any throughout openapi.go and rest.go - Remove trailing blank line in const block Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- pkg/cmd/server/openapi.go | 31 ++++++++-------- pkg/cmd/server/start.go | 1 + pkg/registry/apps/application/rest.go | 36 ++++++++++++------- .../apps/application/rest_validation_test.go | 10 +++--- 4 files changed, 44 insertions(+), 34 deletions(-) diff --git a/pkg/cmd/server/openapi.go b/pkg/cmd/server/openapi.go index b52aa989..616709ca 100644 --- a/pkg/cmd/server/openapi.go +++ b/pkg/cmd/server/openapi.go @@ -19,7 +19,6 @@ const ( baseListRef = apiPrefix + ".ApplicationList" baseStatusRef = apiPrefix + ".ApplicationStatus" smp = "application/strategic-merge-patch+json" - ) // deepCopySchema clones *spec.Schema via JSON-marshal/unmarshal. @@ -102,9 +101,9 @@ func cloneKindSchemas(kind string, base, baseStatus, baseList *spec.Schema, v3 b // GVK-extensions setGVK := func(s *spec.Schema, k string) { - s.Extensions = map[string]interface{}{ - "x-kubernetes-group-version-kind": []interface{}{ - map[string]interface{}{"group": "apps.cozystack.io", "version": "v1alpha1", "kind": k}, + s.Extensions = map[string]any{ + "x-kubernetes-group-version-kind": []any{ + map[string]any{"group": "apps.cozystack.io", "version": "v1alpha1", "kind": k}, }, } } @@ -133,35 +132,35 @@ func cloneKindSchemas(kind string, base, baseStatus, baseList *spec.Schema, v3 b } // rewriteDocRefs rewrites all $ref in the OpenAPI document -func rewriteDocRefs(doc interface{}) ([]byte, error) { +func rewriteDocRefs(doc any) ([]byte, error) { raw, err := json.Marshal(doc) if err != nil { return nil, fmt.Errorf("failed to marshal OpenAPI document: %w", err) } - var any interface{} - if err := json.Unmarshal(raw, &any); err != nil { + var parsed any + if err := json.Unmarshal(raw, &parsed); err != nil { return nil, err } - walkAndRewriteRefs(any, "") - return json.Marshal(any) + walkAndRewriteRefs(parsed, "") + return json.Marshal(parsed) } // walkAndRewriteRefs walks arbitrary JSON (map/array) and // - when encountering x-kubernetes-group-version-kind, extracts kind, // updating the currentKind context; // - rewrites all $ref inside the current context from Application* → kind*. -func walkAndRewriteRefs(node interface{}, currentKind string) { +func walkAndRewriteRefs(node any, currentKind string) { switch n := node.(type) { - case map[string]interface{}: + case map[string]any: if gvk, ok := n["x-kubernetes-group-version-kind"]; ok { switch g := gvk.(type) { - case map[string]interface{}: + case map[string]any: if k, ok := g["kind"].(string); ok { currentKind = k } - case []interface{}: + case []any: if len(g) > 0 { - if mm, ok := g[0].(map[string]interface{}); ok { + if mm, ok := g[0].(map[string]any); ok { if k, ok := mm["kind"].(string); ok { currentKind = k } @@ -178,7 +177,7 @@ func walkAndRewriteRefs(node interface{}, currentKind string) { } walkAndRewriteRefs(v, currentKind) } - case []interface{}: + case []any: for _, v := range n { walkAndRewriteRefs(v, currentKind) } @@ -293,7 +292,7 @@ func sanitizeForV2(s *spec.Schema) { if hasIntAndStringAnyOf(s.AnyOf) { s.Type = spec.StringOrArray{"string"} if s.Extensions == nil { - s.Extensions = map[string]interface{}{} + s.Extensions = map[string]any{} } s.Extensions["x-kubernetes-int-or-string"] = true } diff --git a/pkg/cmd/server/start.go b/pkg/cmd/server/start.go index bf0d63bd..09c9485c 100644 --- a/pkg/cmd/server/start.go +++ b/pkg/cmd/server/start.go @@ -220,6 +220,7 @@ func parseRootHostFromSecret(secret *corev1.Secret) string { } `json:"_cluster"` } if err := yaml.Unmarshal(valuesYAML, &values); err != nil { + klog.Warningf("failed to parse values.yaml from cozystack-values secret: %v", err) return "" } diff --git a/pkg/registry/apps/application/rest.go b/pkg/registry/apps/application/rest.go index 42734774..26e8429f 100644 --- a/pkg/registry/apps/application/rest.go +++ b/pkg/registry/apps/application/rest.go @@ -162,8 +162,8 @@ func (r *REST) Create(ctx context.Context, obj runtime.Object, createValidation } // Validate name length against Helm release and label limits - if err := r.validateNameLength(app.Name); err != nil { - return nil, apierrors.NewBadRequest(err.Error()) + if nameLenErrs := r.validateNameLength(app.Name); len(nameLenErrs) > 0 { + return nil, apierrors.NewInvalid(r.gvk.GroupKind(), app.Name, nameLenErrs) } // Validate that values don't contain reserved keys (starting with "_") @@ -1014,7 +1014,7 @@ func filterInternalKeys(values *apiextv1.JSON) *apiextv1.JSON { if values == nil || len(values.Raw) == 0 { return values } - var data map[string]interface{} + var data map[string]any if err := json.Unmarshal(values.Raw, &data); err != nil { return values } @@ -1035,7 +1035,7 @@ func validateNoInternalKeys(values *apiextv1.JSON) error { if values == nil || len(values.Raw) == 0 { return nil } - var data map[string]interface{} + var data map[string]any if err := json.Unmarshal(values.Raw, &data); err != nil { return err } @@ -1047,7 +1047,8 @@ func validateNoInternalKeys(values *apiextv1.JSON) error { return nil } -// maxHelmReleaseName is the maximum length of a Helm release name (DNS-1123 subdomain). +// maxHelmReleaseName is the Helm release name limit. Helm reserves room for +// chart-generated resource suffixes within the 63-char DNS-1035 label limit. const maxHelmReleaseName = 53 // maxK8sLabelValue is the maximum length of a Kubernetes label value. @@ -1057,7 +1058,13 @@ const maxK8sLabelValue = 63 // For all apps: prefix + name must fit within the Helm release name limit (53 chars). // For Tenants: additionally checks that the host label (name + "." + rootHost) fits // within the Kubernetes label value limit (63 chars). -func (r *REST) validateNameLength(name string) error { +// +// Note: rootHost is read once at API server startup from the cozystack-values Secret. +// Changing root-host requires an API server restart for validation to reflect the new value. +func (r *REST) validateNameLength(name string) field.ErrorList { + fldPath := field.NewPath("metadata").Child("name") + allErrs := field.ErrorList{} + helmMax := maxHelmReleaseName - len(r.releaseConfig.Prefix) maxLen := helmMax @@ -1069,15 +1076,18 @@ func (r *REST) validateNameLength(name string) error { } if maxLen <= 0 { - return fmt.Errorf("configuration error: no valid name length possible (release prefix %q, root host %q)", - r.releaseConfig.Prefix, r.rootHost) + allErrs = append(allErrs, field.Invalid(fldPath, name, + fmt.Sprintf("configuration error: no valid name length possible (release prefix %q, root host %q)", + r.releaseConfig.Prefix, r.rootHost))) + return allErrs } if len(name) > maxLen { - return fmt.Errorf("name %q is too long: maximum %d characters allowed (release prefix %q, root host %q)", - name, maxLen, r.releaseConfig.Prefix, r.rootHost) + allErrs = append(allErrs, field.Invalid(fldPath, name, + fmt.Sprintf("must be no more than %d characters (release prefix %q, root host %q)", + maxLen, r.releaseConfig.Prefix, r.rootHost))) } - return nil + return allErrs } // convertHelmReleaseToApplication implements the actual conversion logic @@ -1233,7 +1243,7 @@ func (r *REST) buildTableFromApplications(apps []appsv1alpha1.Application) metav for i := range apps { app := &apps[i] row := metav1.TableRow{ - Cells: []interface{}{app.GetName(), getReadyStatus(app.Status.Conditions), computeAge(app.GetCreationTimestamp().Time, now), getVersion(app.Status.Version)}, + Cells: []any{app.GetName(), getReadyStatus(app.Status.Conditions), computeAge(app.GetCreationTimestamp().Time, now), getVersion(app.Status.Version)}, Object: runtime.RawExtension{Object: app}, } table.Rows = append(table.Rows, row) @@ -1257,7 +1267,7 @@ func (r *REST) buildTableFromApplication(app appsv1alpha1.Application) metav1.Ta a := app row := metav1.TableRow{ - Cells: []interface{}{app.GetName(), getReadyStatus(app.Status.Conditions), computeAge(app.GetCreationTimestamp().Time, now), getVersion(app.Status.Version)}, + Cells: []any{app.GetName(), getReadyStatus(app.Status.Conditions), computeAge(app.GetCreationTimestamp().Time, now), getVersion(app.Status.Version)}, Object: runtime.RawExtension{Object: &a}, } table.Rows = append(table.Rows, row) diff --git a/pkg/registry/apps/application/rest_validation_test.go b/pkg/registry/apps/application/rest_validation_test.go index 8c93cf37..ee6d66da 100644 --- a/pkg/registry/apps/application/rest_validation_test.go +++ b/pkg/registry/apps/application/rest_validation_test.go @@ -172,13 +172,13 @@ func TestValidateNameLength(t *testing.T) { rootHost: tt.rootHost, } - err := r.validateNameLength(tt.appName) + errs := r.validateNameLength(tt.appName) - if tt.wantError && err == nil { - t.Errorf("expected error for name %q (len=%d), got nil", tt.appName, len(tt.appName)) + if tt.wantError && len(errs) == 0 { + t.Errorf("expected error for name %q (len=%d), got none", tt.appName, len(tt.appName)) } - if !tt.wantError && err != nil { - t.Errorf("unexpected error for name %q (len=%d): %v", tt.appName, len(tt.appName), err) + if !tt.wantError && len(errs) > 0 { + t.Errorf("unexpected error for name %q (len=%d): %v", tt.appName, len(tt.appName), errs) } }) } From d5e713a4e79a773ae9729c47c14f86e916a582e4 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Wed, 11 Feb 2026 13:32:59 +0300 Subject: [PATCH 012/666] fix(api): fix import order and context-aware error messages - Fix goimports order: duration before validation/field - Show rootHost in error messages only for Tenant kind where it actually affects the length calculation Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- pkg/registry/apps/application/rest.go | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/pkg/registry/apps/application/rest.go b/pkg/registry/apps/application/rest.go index 26e8429f..fcbcfcef 100644 --- a/pkg/registry/apps/application/rest.go +++ b/pkg/registry/apps/application/rest.go @@ -35,8 +35,8 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/selection" - "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/util/duration" + "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/watch" "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/registry/rest" @@ -1075,17 +1075,25 @@ func (r *REST) validateNameLength(name string) field.ErrorList { } } + isTenantWithHost := r.kindName == "Tenant" && r.rootHost != "" + if maxLen <= 0 { - allErrs = append(allErrs, field.Invalid(fldPath, name, - fmt.Sprintf("configuration error: no valid name length possible (release prefix %q, root host %q)", - r.releaseConfig.Prefix, r.rootHost))) + msg := fmt.Sprintf("configuration error: no valid name length possible (release prefix %q)", r.releaseConfig.Prefix) + if isTenantWithHost { + msg = fmt.Sprintf("configuration error: no valid name length possible (release prefix %q, root host %q)", + r.releaseConfig.Prefix, r.rootHost) + } + allErrs = append(allErrs, field.Invalid(fldPath, name, msg)) return allErrs } if len(name) > maxLen { - allErrs = append(allErrs, field.Invalid(fldPath, name, - fmt.Sprintf("must be no more than %d characters (release prefix %q, root host %q)", - maxLen, r.releaseConfig.Prefix, r.rootHost))) + msg := fmt.Sprintf("must be no more than %d characters (release prefix %q)", maxLen, r.releaseConfig.Prefix) + if isTenantWithHost { + msg = fmt.Sprintf("must be no more than %d characters (release prefix %q, root host %q)", + maxLen, r.releaseConfig.Prefix, r.rootHost) + } + allErrs = append(allErrs, field.Invalid(fldPath, name, msg)) } return allErrs } From 5bf481ae4dc27c051512c99fa9926e8eb834f3dc Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Wed, 11 Feb 2026 13:35:42 +0300 Subject: [PATCH 013/666] chore: update copyright year in start_test.go Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- pkg/cmd/server/start_test.go | 2 +- pkg/registry/apps/application/rest_validation_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/server/start_test.go b/pkg/cmd/server/start_test.go index 3d4d7856..3a43df72 100644 --- a/pkg/cmd/server/start_test.go +++ b/pkg/cmd/server/start_test.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Cozystack Authors. +Copyright 2026 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. diff --git a/pkg/registry/apps/application/rest_validation_test.go b/pkg/registry/apps/application/rest_validation_test.go index ee6d66da..07b78e1a 100644 --- a/pkg/registry/apps/application/rest_validation_test.go +++ b/pkg/registry/apps/application/rest_validation_test.go @@ -1,5 +1,5 @@ /* -Copyright 2025 The Cozystack Authors. +Copyright 2026 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. From f61c8f9859381bcf41466c8fffa7c9c82576af7c Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Wed, 11 Feb 2026 22:39:35 +0100 Subject: [PATCH 014/666] [platform] Clean up Helm secrets for removed -rd releases in migration 23 Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- packages/core/platform/images/migrations/migrations/23 | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/core/platform/images/migrations/migrations/23 b/packages/core/platform/images/migrations/migrations/23 index 4e8c8e18..7cb9c85e 100755 --- a/packages/core/platform/images/migrations/migrations/23 +++ b/packages/core/platform/images/migrations/migrations/23 @@ -6,6 +6,13 @@ set -euo pipefail # Remove old cozystack-resource-definition-crd HelmRelease (renamed to application-definition-crd) kubectl delete hr -n cozy-system cozystack-resource-definition-crd --ignore-not-found +kubectl delete secrets -n cozy-system $( + kubectl get secrets -n cozy-system \ + -l owner=helm \ + -o jsonpath='{range .items[*]}{.metadata.labels.name}{" "}{.metadata.name}{"\n"}{end}' \ + | awk '$1 ~ /-rd$/ {print $2}' +) --ignore-not-found + # Stamp version kubectl create configmap -n cozy-system cozystack-version \ --from-literal=version=24 --dry-run=client -o yaml | kubectl apply -f- From b8ccdedbf8929ca1a9f38ee5178c23563d08b129 Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Thu, 12 Feb 2026 00:14:37 +0100 Subject: [PATCH 015/666] [monitoring] Fix YAML parse error in monitoring-agents vmagent template Replace unrendered Helm template expressions in values.yaml with global.target parameter. Pass tenant-root from platform bundle, default to cozy-monitoring. Use tpl in vmagent template to render URLs. Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- .../core/platform/templates/bundles/system.yaml | 3 ++- .../monitoring-agents/templates/vmagent.yaml | 4 ++-- packages/system/monitoring-agents/values.yaml | 16 +++++++++------- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/core/platform/templates/bundles/system.yaml b/packages/core/platform/templates/bundles/system.yaml index 2d8b7e33..c24726ef 100644 --- a/packages/core/platform/templates/bundles/system.yaml +++ b/packages/core/platform/templates/bundles/system.yaml @@ -136,7 +136,8 @@ {{include "cozystack.platform.package.optional.default" (list "cozystack.velero" $) }} {{include "cozystack.platform.package.default" (list "cozystack.vertical-pod-autoscaler" $) }} {{include "cozystack.platform.package.default" (list "cozystack.metrics-server" $) }} -{{include "cozystack.platform.package.default" (list "cozystack.monitoring-agents" $) }} +{{- $monitoringAgentsComponents := dict "monitoring-agents" (dict "values" (dict "global" (dict "target" "tenant-root"))) -}} +{{include "cozystack.platform.package" (list "cozystack.monitoring-agents" "default" $ $monitoringAgentsComponents) }} {{include "cozystack.platform.package.default" (list "cozystack.goldpinger" $) }} {{include "cozystack.platform.package.default" (list "cozystack.prometheus-operator-crds" $) }} {{include "cozystack.platform.package.default" (list "cozystack.grafana-operator" $) }} diff --git a/packages/system/monitoring-agents/templates/vmagent.yaml b/packages/system/monitoring-agents/templates/vmagent.yaml index 56b756e7..00ab63c1 100644 --- a/packages/system/monitoring-agents/templates/vmagent.yaml +++ b/packages/system/monitoring-agents/templates/vmagent.yaml @@ -10,12 +10,12 @@ spec: shardCount: 1 externalLabels: cluster: {{ .Values.vmagent.externalLabels.cluster }} - tenant: {{ .Values.vmagent.externalLabels.tenant }} + tenant: {{ .Values.global.target }} extraArgs: {{- toYaml (deepCopy .Values.vmagent.extraArgs | mergeOverwrite (fromYaml (include "monitoring-agents.vmagent.defaultExtraArgs" .))) | nindent 4 }} remoteWrite: {{- range .Values.vmagent.remoteWrite.urls }} - - url: {{ . | quote }} + - url: {{ tpl . $ | quote }} {{- end }} scrapeInterval: 30s diff --git a/packages/system/monitoring-agents/values.yaml b/packages/system/monitoring-agents/values.yaml index 3b79de95..a4d4b025 100644 --- a/packages/system/monitoring-agents/values.yaml +++ b/packages/system/monitoring-agents/values.yaml @@ -1,3 +1,6 @@ +global: + target: cozy-monitoring + kube-state-metrics: extraArgs: - --metric-labels-allowlist=pods=[*],deployments=[*] @@ -275,11 +278,10 @@ kube-state-metrics: vmagent: externalLabels: cluster: cozystack - tenant: '{{ .Release.Namespace }}' remoteWrite: urls: - - 'http://vminsert-shortterm.{{ .Release.Namespace }}.svc:8480/insert/0/prometheus' - - 'http://vminsert-longterm.{{ .Release.Namespace }}.svc:8480/insert/0/prometheus' + - http://vminsert-shortterm.{{ .Values.global.target }}.svc:8480/insert/0/prometheus + - http://vminsert-longterm.{{ .Values.global.target }}.svc:8480/insert/0/prometheus extraArgs: {} fluent-bit: @@ -342,7 +344,7 @@ fluent-bit: [OUTPUT] Name http Match kube.* - Host vlogs-generic.{{ .Release.Namespace }}.svc + Host vlogs-generic.{{ .Values.global.target }}.svc port 9428 compress gzip uri /insert/jsonline?_stream_fields=log_source,stream,kubernetes_pod_name,kubernetes_container_name,kubernetes_namespace_name&_msg_field=log&_time_field=date @@ -353,7 +355,7 @@ fluent-bit: [OUTPUT] Name http Match events.* - Host vlogs-generic.{{ .Release.Namespace }}.svc + Host vlogs-generic.{{ .Values.global.target }}.svc port 9428 compress gzip uri /insert/jsonline?_stream_fields=log_source,reason,meatdata_namespace,metadata_name&_msg_field=message&_time_field=date @@ -364,7 +366,7 @@ fluent-bit: [OUTPUT] Name http Match audit.* - Host vlogs-generic.{{ .Release.Namespace }}.svc + Host vlogs-generic.{{ .Values.global.target }}.svc port 9428 compress gzip uri /insert/jsonline?_stream_fields=log_source,stage,user_username,verb,requestUri&_msg_field=requestURI&_time_field=date @@ -416,7 +418,7 @@ fluent-bit: [FILTER] Name modify Match * - Add tenant {{ .Release.Namespace }} + Add tenant {{ .Values.global.target }} [FILTER] Name modify Match * From fb8157ef9bb07e957ab8660c78cfcf0dc857b9b7 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Thu, 12 Feb 2026 13:52:37 +0300 Subject: [PATCH 016/666] refactor(api): remove rootHost-based name length validation Root-host validation for Tenant names is no longer needed here. The underlying issue (namespace.cozystack.io/host label exceeding 63-char limit) will be addressed in #2002 by moving the label to an annotation. Name length validation now only checks the Helm release name limit (53 - prefix length), which applies uniformly to all application types. Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- pkg/apis/apps/validation/validation.go | 2 +- pkg/apiserver/apiserver.go | 2 +- pkg/cmd/server/start.go | 41 -------- pkg/cmd/server/start_test.go | 97 ------------------- pkg/config/config.go | 1 - pkg/registry/apps/application/rest.go | 42 ++------ .../apps/application/rest_validation_test.go | 91 +---------------- 7 files changed, 14 insertions(+), 262 deletions(-) diff --git a/pkg/apis/apps/validation/validation.go b/pkg/apis/apps/validation/validation.go index 28810b72..273873a4 100644 --- a/pkg/apis/apps/validation/validation.go +++ b/pkg/apis/apps/validation/validation.go @@ -25,7 +25,7 @@ import ( // This is required because Application names are used to create Kubernetes resources // (Services, Namespaces, etc.) that must have DNS-1035 compliant names. // Note: length validation is handled separately by validateNameLength in the REST -// handler, which computes dynamic limits based on Helm release prefix and root-host. +// handler, which computes dynamic limits based on Helm release prefix. func ValidateApplicationName(name string, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index aba9a345..bc2b4857 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -208,7 +208,7 @@ func (c completedConfig) New() (*CozyServer, error) { // --- dynamically-configured, per-tenant resources --- appsV1alpha1Storage := map[string]rest.Storage{} for _, resConfig := range c.ResourceConfig.Resources { - storage := applicationstorage.NewREST(cli, watchCli, &resConfig, c.ResourceConfig.RootHost) + storage := applicationstorage.NewREST(cli, watchCli, &resConfig) appsV1alpha1Storage[resConfig.Application.Plural] = cozyregistry.RESTInPeace(storage) } appsApiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(apps.GroupName, Scheme, metav1.ParameterCodec, Codecs) diff --git a/pkg/cmd/server/start.go b/pkg/cmd/server/start.go index 09c9485c..9a688700 100644 --- a/pkg/cmd/server/start.go +++ b/pkg/cmd/server/start.go @@ -42,11 +42,9 @@ import ( utilfeature "k8s.io/apiserver/pkg/util/feature" basecompatibility "k8s.io/component-base/compatibility" baseversion "k8s.io/component-base/version" - "k8s.io/klog/v2" netutils "k8s.io/utils/net" "sigs.k8s.io/controller-runtime/pkg/client" k8sconfig "sigs.k8s.io/controller-runtime/pkg/client/config" - "sigs.k8s.io/yaml" ) // CozyServerOptions holds the state for the Cozy API server @@ -185,48 +183,9 @@ func (o *CozyServerOptions) Complete() error { o.ResourceConfig.Resources = append(o.ResourceConfig.Resources, resource) } - // Read root-host from cozystack-values secret (best-effort, non-fatal) - secret := &corev1.Secret{} - err = o.Client.Get(context.Background(), client.ObjectKey{ - Namespace: "cozy-system", - Name: "cozystack-values", - }, secret) - if err != nil { - klog.Warningf("failed to read cozystack-values secret: %v", err) - } else { - o.ResourceConfig.RootHost = parseRootHostFromSecret(secret) - if o.ResourceConfig.RootHost != "" { - klog.Infof("Loaded root-host: %s", o.ResourceConfig.RootHost) - } - } - return nil } -// parseRootHostFromSecret extracts _cluster.root-host from the cozystack-values secret. -func parseRootHostFromSecret(secret *corev1.Secret) string { - if secret == nil || secret.Data == nil { - return "" - } - - valuesYAML, ok := secret.Data["values.yaml"] - if !ok || len(valuesYAML) == 0 { - return "" - } - - var values struct { - Cluster struct { - RootHost string `json:"root-host"` - } `json:"_cluster"` - } - if err := yaml.Unmarshal(valuesYAML, &values); err != nil { - klog.Warningf("failed to parse values.yaml from cozystack-values secret: %v", err) - return "" - } - - return values.Cluster.RootHost -} - // Validate checks the correctness of the options func (o CozyServerOptions) Validate(args []string) error { var allErrors []error diff --git a/pkg/cmd/server/start_test.go b/pkg/cmd/server/start_test.go index 3a43df72..9dc2000c 100644 --- a/pkg/cmd/server/start_test.go +++ b/pkg/cmd/server/start_test.go @@ -15,100 +15,3 @@ limitations under the License. */ package server - -import ( - "testing" - - corev1 "k8s.io/api/core/v1" -) - -func TestParseRootHostFromSecret(t *testing.T) { - tests := []struct { - name string - secret *corev1.Secret - expected string - }{ - { - name: "valid secret with root-host", - secret: &corev1.Secret{ - Data: map[string][]byte{ - "values.yaml": []byte("_cluster:\n root-host: \"example.com\"\n bundle-name: \"paas-full\"\n"), - }, - }, - expected: "example.com", - }, - { - name: "valid secret with unquoted root-host", - secret: &corev1.Secret{ - Data: map[string][]byte{ - "values.yaml": []byte("_cluster:\n root-host: my.domain.org\n"), - }, - }, - expected: "my.domain.org", - }, - { - name: "missing values.yaml key", - secret: &corev1.Secret{ - Data: map[string][]byte{ - "other-key": []byte("some data"), - }, - }, - expected: "", - }, - { - name: "malformed YAML", - secret: &corev1.Secret{ - Data: map[string][]byte{ - "values.yaml": []byte("not: valid: yaml: [[["), - }, - }, - expected: "", - }, - { - name: "empty root-host", - secret: &corev1.Secret{ - Data: map[string][]byte{ - "values.yaml": []byte("_cluster:\n root-host: \"\"\n"), - }, - }, - expected: "", - }, - { - name: "no _cluster key", - secret: &corev1.Secret{ - Data: map[string][]byte{ - "values.yaml": []byte("other:\n key: value\n"), - }, - }, - expected: "", - }, - { - name: "_cluster without root-host", - secret: &corev1.Secret{ - Data: map[string][]byte{ - "values.yaml": []byte("_cluster:\n bundle-name: \"paas-full\"\n"), - }, - }, - expected: "", - }, - { - name: "nil data", - secret: &corev1.Secret{}, - expected: "", - }, - { - name: "nil secret", - secret: nil, - expected: "", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := parseRootHostFromSecret(tt.secret) - if result != tt.expected { - t.Errorf("parseRootHostFromSecret() = %q, want %q", result, tt.expected) - } - }) - } -} diff --git a/pkg/config/config.go b/pkg/config/config.go index 260d149a..1e123e2c 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -19,7 +19,6 @@ package config // ResourceConfig represents the structure of the configuration file. type ResourceConfig struct { Resources []Resource `yaml:"resources"` - RootHost string // cluster root host from cozystack-values secret } // Resource describes an individual resource. diff --git a/pkg/registry/apps/application/rest.go b/pkg/registry/apps/application/rest.go index fcbcfcef..7871bbe7 100644 --- a/pkg/registry/apps/application/rest.go +++ b/pkg/registry/apps/application/rest.go @@ -91,11 +91,10 @@ type REST struct { singularName string releaseConfig config.ReleaseConfig specSchema *structuralschema.Structural - rootHost string } // NewREST creates a new REST storage for Application with specific configuration -func NewREST(c client.Client, w client.WithWatch, config *config.Resource, rootHost string) *REST { +func NewREST(c client.Client, w client.WithWatch, config *config.Resource) *REST { var specSchema *structuralschema.Structural if raw := strings.TrimSpace(config.Application.OpenAPISchema); raw != "" { @@ -134,7 +133,6 @@ func NewREST(c client.Client, w client.WithWatch, config *config.Resource, rootH singularName: config.Application.Singular, releaseConfig: config.Release, specSchema: specSchema, - rootHost: rootHost, } } @@ -1051,49 +1049,23 @@ func validateNoInternalKeys(values *apiextv1.JSON) error { // chart-generated resource suffixes within the 63-char DNS-1035 label limit. const maxHelmReleaseName = 53 -// maxK8sLabelValue is the maximum length of a Kubernetes label value. -const maxK8sLabelValue = 63 - // validateNameLength checks that the application name won't exceed Kubernetes limits. -// For all apps: prefix + name must fit within the Helm release name limit (53 chars). -// For Tenants: additionally checks that the host label (name + "." + rootHost) fits -// within the Kubernetes label value limit (63 chars). -// -// Note: rootHost is read once at API server startup from the cozystack-values Secret. -// Changing root-host requires an API server restart for validation to reflect the new value. +// prefix + name must fit within the Helm release name limit (53 chars). func (r *REST) validateNameLength(name string) field.ErrorList { fldPath := field.NewPath("metadata").Child("name") allErrs := field.ErrorList{} - helmMax := maxHelmReleaseName - len(r.releaseConfig.Prefix) - maxLen := helmMax - - if r.kindName == "Tenant" && r.rootHost != "" { - hostLabelMax := maxK8sLabelValue - len(r.rootHost) - 1 // -1 for the dot separator - if hostLabelMax < maxLen { - maxLen = hostLabelMax - } - } - - isTenantWithHost := r.kindName == "Tenant" && r.rootHost != "" + maxLen := maxHelmReleaseName - len(r.releaseConfig.Prefix) if maxLen <= 0 { - msg := fmt.Sprintf("configuration error: no valid name length possible (release prefix %q)", r.releaseConfig.Prefix) - if isTenantWithHost { - msg = fmt.Sprintf("configuration error: no valid name length possible (release prefix %q, root host %q)", - r.releaseConfig.Prefix, r.rootHost) - } - allErrs = append(allErrs, field.Invalid(fldPath, name, msg)) + allErrs = append(allErrs, field.Invalid(fldPath, name, + fmt.Sprintf("configuration error: no valid name length possible (release prefix %q)", r.releaseConfig.Prefix))) return allErrs } if len(name) > maxLen { - msg := fmt.Sprintf("must be no more than %d characters (release prefix %q)", maxLen, r.releaseConfig.Prefix) - if isTenantWithHost { - msg = fmt.Sprintf("must be no more than %d characters (release prefix %q, root host %q)", - maxLen, r.releaseConfig.Prefix, r.rootHost) - } - allErrs = append(allErrs, field.Invalid(fldPath, name, msg)) + allErrs = append(allErrs, field.Invalid(fldPath, name, + fmt.Sprintf("must be no more than %d characters (release prefix %q)", maxLen, r.releaseConfig.Prefix))) } return allErrs } diff --git a/pkg/registry/apps/application/rest_validation_test.go b/pkg/registry/apps/application/rest_validation_test.go index 07b78e1a..c5d331b2 100644 --- a/pkg/registry/apps/application/rest_validation_test.go +++ b/pkg/registry/apps/application/rest_validation_test.go @@ -28,111 +28,48 @@ func TestValidateNameLength(t *testing.T) { name string kindName string prefix string - rootHost string appName string wantError bool }{ { - name: "non-tenant short name passes", + name: "short name passes", kindName: "MySQL", prefix: "mysql-", - rootHost: "example.com", appName: "mydb", wantError: false, }, { - name: "non-tenant at helm boundary passes", + name: "at helm boundary passes", kindName: "MySQL", prefix: "mysql-", - rootHost: "example.com", appName: strings.Repeat("a", 53-len("mysql-")), // exactly 47 chars wantError: false, }, { - name: "non-tenant exceeding helm limit fails", + name: "exceeding helm limit fails", kindName: "MySQL", prefix: "mysql-", - rootHost: "example.com", appName: strings.Repeat("a", 53-len("mysql-")+1), // 48 chars wantError: true, }, { - name: "tenant no rootHost within helm limit passes", + name: "tenant within helm limit passes", kindName: "Tenant", prefix: "tenant-", - rootHost: "", appName: strings.Repeat("a", 53-len("tenant-")), // 46 chars wantError: false, }, { - name: "tenant no rootHost exceeding helm limit fails", + name: "tenant exceeding helm limit fails", kindName: "Tenant", prefix: "tenant-", - rootHost: "", appName: strings.Repeat("a", 53-len("tenant-")+1), // 47 chars wantError: true, }, - { - name: "tenant with rootHost within both limits passes", - kindName: "Tenant", - prefix: "tenant-", - rootHost: "example.com", // 11 chars → host label max = 63-11-1 = 51 - appName: "short", - wantError: false, - }, - { - name: "tenant with short rootHost helm limit is still stricter", - kindName: "Tenant", - prefix: "tenant-", - rootHost: "example.com", // 11 chars → host label max = 51, helm max = 46 - appName: strings.Repeat("a", 53-len("tenant-")), // 46 chars — at Helm boundary - wantError: false, - }, - { - name: "tenant with long rootHost at host label boundary passes", - kindName: "Tenant", - prefix: "tenant-", - rootHost: "long-subdomain.hosting.example.com", // 34 chars → host label max = 63-34-1 = 28 - appName: strings.Repeat("a", 63-len("long-subdomain.hosting.example.com")-1), // exactly 28 chars - wantError: false, - }, - { - name: "tenant with long rootHost exceeding host label limit fails", - kindName: "Tenant", - prefix: "tenant-", - rootHost: "long-subdomain.hosting.example.com", // 34 chars → host label max = 28 - appName: strings.Repeat("a", 63-len("long-subdomain.hosting.example.com")-1+1), // 29 chars - wantError: true, - }, - { - name: "tenant host label limit stricter than helm limit", - kindName: "Tenant", - prefix: "tenant-", - rootHost: "very-long-subdomain.hosting.example.com", // 39 chars → host label max = 63-39-1 = 23 - appName: strings.Repeat("a", 24), // 24 > 23 (host limit) but < 46 (helm limit) - wantError: true, - }, - { - name: "tenant short rootHost where helm limit is stricter", - kindName: "Tenant", - prefix: "tenant-", - rootHost: "x.co", // 4 chars → host label max = 63-4-1 = 58, helm max = 46 - appName: strings.Repeat("a", 53-len("tenant-")+1), // 47 > 46 (helm limit) but < 58 (host limit) - wantError: true, - }, - { - name: "non-tenant ignores rootHost for limit calculation", - kindName: "MySQL", - prefix: "mysql-", - rootHost: "very-long-subdomain.hosting.example.com", - appName: strings.Repeat("a", 53-len("mysql-")), // at helm boundary - wantError: false, - }, { name: "prefix consuming all helm capacity returns config error", kindName: "MySQL", prefix: strings.Repeat("x", 53), // prefix == maxHelmReleaseName → helmMax = 0 - rootHost: "", appName: "a", wantError: true, }, @@ -140,23 +77,6 @@ func TestValidateNameLength(t *testing.T) { name: "prefix exceeding helm capacity returns config error", kindName: "MySQL", prefix: strings.Repeat("x", 60), // prefix > maxHelmReleaseName → helmMax < 0 - rootHost: "", - appName: "a", - wantError: true, - }, - { - name: "tenant with rootHost consuming all label capacity returns config error", - kindName: "Tenant", - prefix: "t-", - rootHost: strings.Repeat("h", 62), // 62 chars → hostLabelMax = 63-62-1 = 0, helmMax = 51 - appName: "a", - wantError: true, - }, - { - name: "tenant with rootHost exceeding label capacity returns config error", - kindName: "Tenant", - prefix: "t-", - rootHost: strings.Repeat("h", 70), // 70 chars → hostLabelMax = 63-70-1 = -8, helmMax = 51 appName: "a", wantError: true, }, @@ -169,7 +89,6 @@ func TestValidateNameLength(t *testing.T) { releaseConfig: config.ReleaseConfig{ Prefix: tt.prefix, }, - rootHost: tt.rootHost, } errs := r.validateNameLength(tt.appName) From bce530011699e03eaa08fefd3f0945caed55c9c3 Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Tue, 10 Feb 2026 19:11:22 +0100 Subject: [PATCH 017/666] refactor: rename mysql application to mariadb The mysql chart actually deploys MariaDB via mariadb-operator, but was incorrectly named "mysql". Rename all references to use the correct "mariadb" name across the codebase. Changes: - Rename packages/apps/mysql -> packages/apps/mariadb - Rename packages/system/mysql-rd -> packages/system/mariadb-rd - Rename platform source and bundle references - Update CRD kind from MySQL to MariaDB - Update RBAC, e2e tests, backup controller tests - Keep real MySQL CLI/config tool names unchanged (mysqldump, [mysqld], etc.) Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- Makefile | 2 +- api/backups/v1alpha1/DESIGN.md | 2 +- api/backups/v1alpha1/backupclass_types.go | 2 +- docs/agents/contributing.md | 2 +- hack/e2e-apps/mariadb.bats | 46 +++++++++++++++++++ hack/e2e-apps/mysql.bats | 46 ------------------- .../backupclass_resolver_test.go | 22 ++++----- .../factory/backupjob_test.go | 4 +- packages/apps/{mysql => mariadb}/.helmignore | 0 packages/apps/{mysql => mariadb}/Chart.yaml | 2 +- packages/apps/{mysql => mariadb}/Makefile | 0 packages/apps/{mysql => mariadb}/README.md | 2 +- .../apps/{mysql => mariadb}/charts/cozy-lib | 0 .../{mysql => mariadb}/files/versions.yaml | 0 .../hack/update-versions.sh | 6 +-- .../images/mariadb-backup.tag | 0 .../images/mariadb-backup/Dockerfile | 0 .../apps/{mysql => mariadb}/logos/mariadb.svg | 0 .../{mysql => mariadb}/templates/.gitkeep | 0 .../templates/_resources.tpl | 0 .../templates/_versions.tpl | 2 +- .../templates/backup-cronjob.yaml | 0 .../templates/backup-script.yaml | 0 .../templates/backup-secret.yaml | 0 .../{mysql => mariadb}/templates/config.yaml | 0 .../templates/dashboard-resourcemap.yaml | 0 .../apps/{mysql => mariadb}/templates/db.yaml | 0 .../templates/hooks/cleanup-pvc.yaml | 0 .../{mysql => mariadb}/templates/mariadb.yaml | 2 +- .../templates/regsecret.yaml | 0 .../{mysql => mariadb}/templates/secret.yaml | 0 .../{mysql => mariadb}/templates/user.yaml | 0 .../templates/workloadmonitor.yaml | 4 +- .../{mysql => mariadb}/values.schema.json | 2 +- packages/apps/{mysql => mariadb}/values.yaml | 2 +- ...lication.yaml => mariadb-application.yaml} | 12 ++--- .../core/platform/templates/bundles/paas.yaml | 2 +- .../templates/clusterroles.yaml | 2 +- .../{mysql-rd => mariadb-rd}/Chart.yaml | 2 +- .../system/{mysql-rd => mariadb-rd}/Makefile | 2 +- .../cozyrds/mariadb.yaml} | 24 +++++----- .../templates/cozyrd.yaml | 0 .../{mysql-rd => mariadb-rd}/values.yaml | 0 43 files changed, 96 insertions(+), 96 deletions(-) create mode 100644 hack/e2e-apps/mariadb.bats delete mode 100644 hack/e2e-apps/mysql.bats rename packages/apps/{mysql => mariadb}/.helmignore (100%) rename packages/apps/{mysql => mariadb}/Chart.yaml (93%) rename packages/apps/{mysql => mariadb}/Makefile (100%) rename packages/apps/{mysql => mariadb}/README.md (98%) rename packages/apps/{mysql => mariadb}/charts/cozy-lib (100%) rename packages/apps/{mysql => mariadb}/files/versions.yaml (100%) rename packages/apps/{mysql => mariadb}/hack/update-versions.sh (97%) rename packages/apps/{mysql => mariadb}/images/mariadb-backup.tag (100%) rename packages/apps/{mysql => mariadb}/images/mariadb-backup/Dockerfile (100%) rename packages/apps/{mysql => mariadb}/logos/mariadb.svg (100%) rename packages/apps/{mysql => mariadb}/templates/.gitkeep (100%) rename packages/apps/{mysql => mariadb}/templates/_resources.tpl (100%) rename packages/apps/{mysql => mariadb}/templates/_versions.tpl (89%) rename packages/apps/{mysql => mariadb}/templates/backup-cronjob.yaml (100%) rename packages/apps/{mysql => mariadb}/templates/backup-script.yaml (100%) rename packages/apps/{mysql => mariadb}/templates/backup-secret.yaml (100%) rename packages/apps/{mysql => mariadb}/templates/config.yaml (100%) rename packages/apps/{mysql => mariadb}/templates/dashboard-resourcemap.yaml (100%) rename packages/apps/{mysql => mariadb}/templates/db.yaml (100%) rename packages/apps/{mysql => mariadb}/templates/hooks/cleanup-pvc.yaml (100%) rename packages/apps/{mysql => mariadb}/templates/mariadb.yaml (97%) rename packages/apps/{mysql => mariadb}/templates/regsecret.yaml (100%) rename packages/apps/{mysql => mariadb}/templates/secret.yaml (100%) rename packages/apps/{mysql => mariadb}/templates/user.yaml (100%) rename packages/apps/{mysql => mariadb}/templates/workloadmonitor.yaml (88%) rename packages/apps/{mysql => mariadb}/values.schema.json (99%) rename packages/apps/{mysql => mariadb}/values.yaml (98%) rename packages/core/platform/sources/{mysql-application.yaml => mariadb-application.yaml} (71%) rename packages/system/{mysql-rd => mariadb-rd}/Chart.yaml (87%) rename packages/system/{mysql-rd => mariadb-rd}/Makefile (73%) rename packages/system/{mysql-rd/cozyrds/mysql.yaml => mariadb-rd/cozyrds/mariadb.yaml} (69%) rename packages/system/{mysql-rd => mariadb-rd}/templates/cozyrd.yaml (100%) rename packages/system/{mysql-rd => mariadb-rd}/values.yaml (100%) diff --git a/Makefile b/Makefile index 9b3b217c..e6c50458 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ build-deps: build: build-deps make -C packages/apps/http-cache image - make -C packages/apps/mysql image + make -C packages/apps/mariadb image make -C packages/apps/clickhouse image make -C packages/apps/kubernetes image make -C packages/system/monitoring image diff --git a/api/backups/v1alpha1/DESIGN.md b/api/backups/v1alpha1/DESIGN.md index bab753b2..7ba06fae 100644 --- a/api/backups/v1alpha1/DESIGN.md +++ b/api/backups/v1alpha1/DESIGN.md @@ -196,7 +196,7 @@ type ApplicationSelector struct { // +optional APIGroup *string `json:"apiGroup,omitempty"` - // Kind is the kind of the application (e.g., VirtualMachine, MySQL). + // Kind is the kind of the application (e.g., VirtualMachine, MariaDB). Kind string `json:"kind"` } ``` diff --git a/api/backups/v1alpha1/backupclass_types.go b/api/backups/v1alpha1/backupclass_types.go index 19bcfd52..2b2dc7ff 100644 --- a/api/backups/v1alpha1/backupclass_types.go +++ b/api/backups/v1alpha1/backupclass_types.go @@ -73,7 +73,7 @@ type ApplicationSelector struct { // +optional APIGroup *string `json:"apiGroup,omitempty"` - // Kind is the kind of the application (e.g., VirtualMachine, MySQL). + // Kind is the kind of the application (e.g., VirtualMachine, MariaDB). Kind string `json:"kind"` } diff --git a/docs/agents/contributing.md b/docs/agents/contributing.md index 4a2a7ef9..d5a4366e 100644 --- a/docs/agents/contributing.md +++ b/docs/agents/contributing.md @@ -27,7 +27,7 @@ git commit --signoff -m "[component] Brief description of changes" **Component prefixes:** - System: `[dashboard]`, `[platform]`, `[cilium]`, `[kube-ovn]`, `[linstor]`, `[fluxcd]`, `[cluster-api]` -- Apps: `[postgres]`, `[mysql]`, `[redis]`, `[kafka]`, `[clickhouse]`, `[virtual-machine]`, `[kubernetes]` +- Apps: `[postgres]`, `[mariadb]`, `[redis]`, `[kafka]`, `[clickhouse]`, `[virtual-machine]`, `[kubernetes]` - Other: `[tests]`, `[ci]`, `[docs]`, `[maintenance]` **Examples:** diff --git a/hack/e2e-apps/mariadb.bats b/hack/e2e-apps/mariadb.bats new file mode 100644 index 00000000..50e608ac --- /dev/null +++ b/hack/e2e-apps/mariadb.bats @@ -0,0 +1,46 @@ +#!/usr/bin/env bats + +@test "Create DB MariaDB" { + name='test' + kubectl apply -f- <` | diff --git a/packages/apps/mysql/charts/cozy-lib b/packages/apps/mariadb/charts/cozy-lib similarity index 100% rename from packages/apps/mysql/charts/cozy-lib rename to packages/apps/mariadb/charts/cozy-lib diff --git a/packages/apps/mysql/files/versions.yaml b/packages/apps/mariadb/files/versions.yaml similarity index 100% rename from packages/apps/mysql/files/versions.yaml rename to packages/apps/mariadb/files/versions.yaml diff --git a/packages/apps/mysql/hack/update-versions.sh b/packages/apps/mariadb/hack/update-versions.sh similarity index 97% rename from packages/apps/mysql/hack/update-versions.sh rename to packages/apps/mariadb/hack/update-versions.sh index 1b58591a..775c0327 100755 --- a/packages/apps/mysql/hack/update-versions.sh +++ b/packages/apps/mariadb/hack/update-versions.sh @@ -5,9 +5,9 @@ set -o nounset set -o pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -MYSQL_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" -VALUES_FILE="${MYSQL_DIR}/values.yaml" -VERSIONS_FILE="${MYSQL_DIR}/files/versions.yaml" +MARIADB_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +VALUES_FILE="${MARIADB_DIR}/values.yaml" +VERSIONS_FILE="${MARIADB_DIR}/files/versions.yaml" MARIADB_API_URL="https://downloads.mariadb.org/rest-api/mariadb/" # Check if jq is installed diff --git a/packages/apps/mysql/images/mariadb-backup.tag b/packages/apps/mariadb/images/mariadb-backup.tag similarity index 100% rename from packages/apps/mysql/images/mariadb-backup.tag rename to packages/apps/mariadb/images/mariadb-backup.tag diff --git a/packages/apps/mysql/images/mariadb-backup/Dockerfile b/packages/apps/mariadb/images/mariadb-backup/Dockerfile similarity index 100% rename from packages/apps/mysql/images/mariadb-backup/Dockerfile rename to packages/apps/mariadb/images/mariadb-backup/Dockerfile diff --git a/packages/apps/mysql/logos/mariadb.svg b/packages/apps/mariadb/logos/mariadb.svg similarity index 100% rename from packages/apps/mysql/logos/mariadb.svg rename to packages/apps/mariadb/logos/mariadb.svg diff --git a/packages/apps/mysql/templates/.gitkeep b/packages/apps/mariadb/templates/.gitkeep similarity index 100% rename from packages/apps/mysql/templates/.gitkeep rename to packages/apps/mariadb/templates/.gitkeep diff --git a/packages/apps/mysql/templates/_resources.tpl b/packages/apps/mariadb/templates/_resources.tpl similarity index 100% rename from packages/apps/mysql/templates/_resources.tpl rename to packages/apps/mariadb/templates/_resources.tpl diff --git a/packages/apps/mysql/templates/_versions.tpl b/packages/apps/mariadb/templates/_versions.tpl similarity index 89% rename from packages/apps/mysql/templates/_versions.tpl rename to packages/apps/mariadb/templates/_versions.tpl index cd4ee0e1..a896ea5f 100644 --- a/packages/apps/mysql/templates/_versions.tpl +++ b/packages/apps/mariadb/templates/_versions.tpl @@ -1,4 +1,4 @@ -{{- define "mysql.versionMap" }} +{{- define "mariadb.versionMap" }} {{- $versionMap := .Files.Get "files/versions.yaml" | fromYaml }} {{- if not (hasKey $versionMap .Values.version) }} {{- printf `MariaDB version %s is not supported, allowed versions are %s` $.Values.version (keys $versionMap) | fail }} diff --git a/packages/apps/mysql/templates/backup-cronjob.yaml b/packages/apps/mariadb/templates/backup-cronjob.yaml similarity index 100% rename from packages/apps/mysql/templates/backup-cronjob.yaml rename to packages/apps/mariadb/templates/backup-cronjob.yaml diff --git a/packages/apps/mysql/templates/backup-script.yaml b/packages/apps/mariadb/templates/backup-script.yaml similarity index 100% rename from packages/apps/mysql/templates/backup-script.yaml rename to packages/apps/mariadb/templates/backup-script.yaml diff --git a/packages/apps/mysql/templates/backup-secret.yaml b/packages/apps/mariadb/templates/backup-secret.yaml similarity index 100% rename from packages/apps/mysql/templates/backup-secret.yaml rename to packages/apps/mariadb/templates/backup-secret.yaml diff --git a/packages/apps/mysql/templates/config.yaml b/packages/apps/mariadb/templates/config.yaml similarity index 100% rename from packages/apps/mysql/templates/config.yaml rename to packages/apps/mariadb/templates/config.yaml diff --git a/packages/apps/mysql/templates/dashboard-resourcemap.yaml b/packages/apps/mariadb/templates/dashboard-resourcemap.yaml similarity index 100% rename from packages/apps/mysql/templates/dashboard-resourcemap.yaml rename to packages/apps/mariadb/templates/dashboard-resourcemap.yaml diff --git a/packages/apps/mysql/templates/db.yaml b/packages/apps/mariadb/templates/db.yaml similarity index 100% rename from packages/apps/mysql/templates/db.yaml rename to packages/apps/mariadb/templates/db.yaml diff --git a/packages/apps/mysql/templates/hooks/cleanup-pvc.yaml b/packages/apps/mariadb/templates/hooks/cleanup-pvc.yaml similarity index 100% rename from packages/apps/mysql/templates/hooks/cleanup-pvc.yaml rename to packages/apps/mariadb/templates/hooks/cleanup-pvc.yaml diff --git a/packages/apps/mysql/templates/mariadb.yaml b/packages/apps/mariadb/templates/mariadb.yaml similarity index 97% rename from packages/apps/mysql/templates/mariadb.yaml rename to packages/apps/mariadb/templates/mariadb.yaml index b4a888d8..8a530134 100644 --- a/packages/apps/mysql/templates/mariadb.yaml +++ b/packages/apps/mariadb/templates/mariadb.yaml @@ -8,7 +8,7 @@ spec: name: {{ .Release.Name }}-credentials key: root - image: "mariadb:{{ include "mysql.versionMap" $ }}" + image: "mariadb:{{ include "mariadb.versionMap" $ }}" port: 3306 diff --git a/packages/apps/mysql/templates/regsecret.yaml b/packages/apps/mariadb/templates/regsecret.yaml similarity index 100% rename from packages/apps/mysql/templates/regsecret.yaml rename to packages/apps/mariadb/templates/regsecret.yaml diff --git a/packages/apps/mysql/templates/secret.yaml b/packages/apps/mariadb/templates/secret.yaml similarity index 100% rename from packages/apps/mysql/templates/secret.yaml rename to packages/apps/mariadb/templates/secret.yaml diff --git a/packages/apps/mysql/templates/user.yaml b/packages/apps/mariadb/templates/user.yaml similarity index 100% rename from packages/apps/mysql/templates/user.yaml rename to packages/apps/mariadb/templates/user.yaml diff --git a/packages/apps/mysql/templates/workloadmonitor.yaml b/packages/apps/mariadb/templates/workloadmonitor.yaml similarity index 88% rename from packages/apps/mysql/templates/workloadmonitor.yaml rename to packages/apps/mariadb/templates/workloadmonitor.yaml index 9fc6d144..b69139bf 100644 --- a/packages/apps/mysql/templates/workloadmonitor.yaml +++ b/packages/apps/mariadb/templates/workloadmonitor.yaml @@ -6,8 +6,8 @@ metadata: spec: replicas: {{ .Values.replicas }} minReplicas: 1 - kind: mysql - type: mysql + kind: mariadb + type: mariadb selector: app.kubernetes.io/instance: {{ $.Release.Name }} version: {{ $.Chart.Version }} diff --git a/packages/apps/mysql/values.schema.json b/packages/apps/mariadb/values.schema.json similarity index 99% rename from packages/apps/mysql/values.schema.json rename to packages/apps/mariadb/values.schema.json index a36754b2..31c56378 100644 --- a/packages/apps/mysql/values.schema.json +++ b/packages/apps/mariadb/values.schema.json @@ -40,7 +40,7 @@ "s3Bucket": { "description": "S3 bucket used for storing backups.", "type": "string", - "default": "s3.example.org/mysql-backups" + "default": "s3.example.org/mariadb-backups" }, "s3Region": { "description": "AWS S3 region where backups are stored.", diff --git a/packages/apps/mysql/values.yaml b/packages/apps/mariadb/values.yaml similarity index 98% rename from packages/apps/mysql/values.yaml rename to packages/apps/mariadb/values.yaml index c7726b7a..6d629a83 100644 --- a/packages/apps/mysql/values.yaml +++ b/packages/apps/mariadb/values.yaml @@ -98,7 +98,7 @@ databases: {} backup: enabled: false s3Region: us-east-1 - s3Bucket: "s3.example.org/mysql-backups" + s3Bucket: "s3.example.org/mariadb-backups" schedule: "0 2 * * *" cleanupStrategy: "--keep-last=3 --keep-daily=3 --keep-within-weekly=1m" s3AccessKey: "" diff --git a/packages/core/platform/sources/mysql-application.yaml b/packages/core/platform/sources/mariadb-application.yaml similarity index 71% rename from packages/core/platform/sources/mysql-application.yaml rename to packages/core/platform/sources/mariadb-application.yaml index 4b22e36e..34ff901b 100644 --- a/packages/core/platform/sources/mysql-application.yaml +++ b/packages/core/platform/sources/mariadb-application.yaml @@ -2,7 +2,7 @@ apiVersion: cozystack.io/v1alpha1 kind: PackageSource metadata: - name: cozystack.mysql-application + name: cozystack.mariadb-application spec: sourceRef: kind: OCIRepository @@ -18,11 +18,11 @@ spec: - name: cozy-lib path: library/cozy-lib components: - - name: mysql - path: apps/mysql + - name: mariadb + path: apps/mariadb libraries: ["cozy-lib"] - - name: mysql-rd - path: system/mysql-rd + - name: mariadb-rd + path: system/mariadb-rd install: namespace: cozy-system - releaseName: mysql-rd + releaseName: mariadb-rd diff --git a/packages/core/platform/templates/bundles/paas.yaml b/packages/core/platform/templates/bundles/paas.yaml index 1b76a74d..5a478f9d 100644 --- a/packages/core/platform/templates/bundles/paas.yaml +++ b/packages/core/platform/templates/bundles/paas.yaml @@ -12,7 +12,7 @@ {{include "cozystack.platform.package.default" (list "cozystack.clickhouse-application" $) }} {{include "cozystack.platform.package.default" (list "cozystack.foundationdb-application" $) }} {{include "cozystack.platform.package.default" (list "cozystack.kafka-application" $) }} -{{include "cozystack.platform.package.default" (list "cozystack.mysql-application" $) }} +{{include "cozystack.platform.package.default" (list "cozystack.mariadb-application" $) }} {{include "cozystack.platform.package.default" (list "cozystack.mongodb-application" $) }} {{include "cozystack.platform.package.default" (list "cozystack.nats-application" $) }} {{include "cozystack.platform.package.default" (list "cozystack.postgres-application" $) }} diff --git a/packages/system/cozystack-basics/templates/clusterroles.yaml b/packages/system/cozystack-basics/templates/clusterroles.yaml index 2b3303ed..29395dfe 100644 --- a/packages/system/cozystack-basics/templates/clusterroles.yaml +++ b/packages/system/cozystack-basics/templates/clusterroles.yaml @@ -198,7 +198,7 @@ rules: - httpcaches - kafkas - kuberneteses - - mysqls + - mariadbs - natses - postgreses - rabbitmqs diff --git a/packages/system/mysql-rd/Chart.yaml b/packages/system/mariadb-rd/Chart.yaml similarity index 87% rename from packages/system/mysql-rd/Chart.yaml rename to packages/system/mariadb-rd/Chart.yaml index d3c6b081..091d535c 100644 --- a/packages/system/mysql-rd/Chart.yaml +++ b/packages/system/mariadb-rd/Chart.yaml @@ -1,3 +1,3 @@ apiVersion: v2 -name: mysql-rd +name: mariadb-rd version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process diff --git a/packages/system/mysql-rd/Makefile b/packages/system/mariadb-rd/Makefile similarity index 73% rename from packages/system/mysql-rd/Makefile rename to packages/system/mariadb-rd/Makefile index 2a8af787..def401d8 100644 --- a/packages/system/mysql-rd/Makefile +++ b/packages/system/mariadb-rd/Makefile @@ -1,4 +1,4 @@ -export NAME=mysql-rd +export NAME=mariadb-rd export NAMESPACE=cozy-system include ../../../hack/package.mk diff --git a/packages/system/mysql-rd/cozyrds/mysql.yaml b/packages/system/mariadb-rd/cozyrds/mariadb.yaml similarity index 69% rename from packages/system/mysql-rd/cozyrds/mysql.yaml rename to packages/system/mariadb-rd/cozyrds/mariadb.yaml index 9c2d6d32..5e8088c8 100644 --- a/packages/system/mysql-rd/cozyrds/mysql.yaml +++ b/packages/system/mariadb-rd/cozyrds/mariadb.yaml @@ -1,26 +1,26 @@ apiVersion: cozystack.io/v1alpha1 kind: ApplicationDefinition metadata: - name: mysql + name: mariadb spec: application: - kind: MySQL - plural: mysqls - singular: mysql + kind: MariaDB + plural: mariadbs + singular: mariadb openAPISchema: |- - {"title":"Chart Values","type":"object","properties":{"backup":{"description":"Backup configuration.","type":"object","default":{},"required":["cleanupStrategy","enabled","resticPassword","s3AccessKey","s3Bucket","s3Region","s3SecretKey","schedule"],"properties":{"cleanupStrategy":{"description":"Retention strategy for cleaning up old backups.","type":"string","default":"--keep-last=3 --keep-daily=3 --keep-within-weekly=1m"},"enabled":{"description":"Enable regular backups (default: false).","type":"boolean","default":false},"resticPassword":{"description":"Password for Restic backup encryption.","type":"string","default":""},"s3AccessKey":{"description":"Access key for S3 authentication.","type":"string","default":""},"s3Bucket":{"description":"S3 bucket used for storing backups.","type":"string","default":"s3.example.org/mysql-backups"},"s3Region":{"description":"AWS S3 region where backups are stored.","type":"string","default":"us-east-1"},"s3SecretKey":{"description":"Secret key for S3 authentication.","type":"string","default":""},"schedule":{"description":"Cron schedule for automated backups.","type":"string","default":"0 2 * * *"}}},"databases":{"description":"Databases configuration map.","type":"object","default":{},"additionalProperties":{"type":"object","properties":{"roles":{"description":"Roles assigned to users.","type":"object","properties":{"admin":{"description":"List of users with admin privileges.","type":"array","items":{"type":"string"}},"readonly":{"description":"List of users with read-only privileges.","type":"array","items":{"type":"string"}}}}}}},"external":{"description":"Enable external access from outside the cluster.","type":"boolean","default":false},"replicas":{"description":"Number of MariaDB replicas.","type":"integer","default":2},"resources":{"description":"Explicit CPU and memory configuration for each MariaDB replica. When omitted, 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.","type":"string","default":"nano","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]},"size":{"description":"Persistent Volume Claim size available for application data.","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","default":""},"users":{"description":"Users configuration map.","type":"object","default":{},"additionalProperties":{"type":"object","required":["maxUserConnections","password"],"properties":{"maxUserConnections":{"description":"Maximum number of connections.","type":"integer"},"password":{"description":"Password for the user.","type":"string"}}}},"version":{"description":"MariaDB major.minor version to deploy","type":"string","default":"v11.8","enum":["v11.8","v11.4","v10.11","v10.6"]}}} + {"title":"Chart Values","type":"object","properties":{"backup":{"description":"Backup configuration.","type":"object","default":{},"required":["cleanupStrategy","enabled","resticPassword","s3AccessKey","s3Bucket","s3Region","s3SecretKey","schedule"],"properties":{"cleanupStrategy":{"description":"Retention strategy for cleaning up old backups.","type":"string","default":"--keep-last=3 --keep-daily=3 --keep-within-weekly=1m"},"enabled":{"description":"Enable regular backups (default: false).","type":"boolean","default":false},"resticPassword":{"description":"Password for Restic backup encryption.","type":"string","default":""},"s3AccessKey":{"description":"Access key for S3 authentication.","type":"string","default":""},"s3Bucket":{"description":"S3 bucket used for storing backups.","type":"string","default":"s3.example.org/mariadb-backups"},"s3Region":{"description":"AWS S3 region where backups are stored.","type":"string","default":"us-east-1"},"s3SecretKey":{"description":"Secret key for S3 authentication.","type":"string","default":""},"schedule":{"description":"Cron schedule for automated backups.","type":"string","default":"0 2 * * *"}}},"databases":{"description":"Databases configuration map.","type":"object","default":{},"additionalProperties":{"type":"object","properties":{"roles":{"description":"Roles assigned to users.","type":"object","properties":{"admin":{"description":"List of users with admin privileges.","type":"array","items":{"type":"string"}},"readonly":{"description":"List of users with read-only privileges.","type":"array","items":{"type":"string"}}}}}}},"external":{"description":"Enable external access from outside the cluster.","type":"boolean","default":false},"replicas":{"description":"Number of MariaDB replicas.","type":"integer","default":2},"resources":{"description":"Explicit CPU and memory configuration for each MariaDB replica. When omitted, 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.","type":"string","default":"nano","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]},"size":{"description":"Persistent Volume Claim size available for application data.","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","default":""},"users":{"description":"Users configuration map.","type":"object","default":{},"additionalProperties":{"type":"object","required":["maxUserConnections","password"],"properties":{"maxUserConnections":{"description":"Maximum number of connections.","type":"integer"},"password":{"description":"Password for the user.","type":"string"}}}},"version":{"description":"MariaDB major.minor version to deploy","type":"string","default":"v11.8","enum":["v11.8","v11.4","v10.11","v10.6"]}}} release: - prefix: mysql- + prefix: mariadb- labels: sharding.fluxcd.io/key: tenants chartRef: kind: ExternalArtifact - name: cozystack-mysql-application-default-mysql + name: cozystack-mariadb-application-default-mariadb namespace: cozy-system dashboard: category: PaaS - singular: MySQL - plural: MySQL + singular: MariaDB + plural: MariaDB description: Managed MariaDB service tags: - database @@ -30,10 +30,10 @@ spec: exclude: [] include: - resourceNames: - - mysql-{{ .name }}-credentials + - mariadb-{{ .name }}-credentials services: exclude: [] include: - resourceNames: - - mysql-{{ .name }}-primary - - mysql-{{ .name }}-secondary + - mariadb-{{ .name }}-primary + - mariadb-{{ .name }}-secondary diff --git a/packages/system/mysql-rd/templates/cozyrd.yaml b/packages/system/mariadb-rd/templates/cozyrd.yaml similarity index 100% rename from packages/system/mysql-rd/templates/cozyrd.yaml rename to packages/system/mariadb-rd/templates/cozyrd.yaml diff --git a/packages/system/mysql-rd/values.yaml b/packages/system/mariadb-rd/values.yaml similarity index 100% rename from packages/system/mysql-rd/values.yaml rename to packages/system/mariadb-rd/values.yaml From 740eb7028b7cce4060f11307650caac8f7052d87 Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Tue, 10 Feb 2026 19:11:31 +0100 Subject: [PATCH 018/666] feat(platform): add migration 27 to rename mysql resources to mariadb Add platform migration that auto-discovers all deployed MySQL HelmReleases and renames their resources to use the mariadb prefix. The migration handles PVC data preservation via PV claimRef rebinding, temporarily disables protection-webhook for protected resource deletion, and scales mariadb-operator once for all instances. Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- packages/apps/mariadb/README.md | 2 +- .../platform/images/migrations/migrations/28 | 616 ++++++++++++++++++ packages/core/platform/values.yaml | 4 +- 3 files changed, 619 insertions(+), 3 deletions(-) create mode 100755 packages/core/platform/images/migrations/migrations/28 diff --git a/packages/apps/mariadb/README.md b/packages/apps/mariadb/README.md index 6b3b8199..bb3ea8ac 100644 --- a/packages/apps/mariadb/README.md +++ b/packages/apps/mariadb/README.md @@ -102,7 +102,7 @@ more details: | `backup` | Backup configuration. | `object` | `{}` | | `backup.enabled` | Enable regular backups (default: false). | `bool` | `false` | | `backup.s3Region` | AWS S3 region where backups are stored. | `string` | `us-east-1` | -| `backup.s3Bucket` | S3 bucket used for storing backups. | `string` | `s3.example.org/mariadb-backups` | +| `backup.s3Bucket` | S3 bucket used for storing backups. | `string` | `s3.example.org/mariadb-backups` | | `backup.schedule` | Cron schedule for automated backups. | `string` | `0 2 * * *` | | `backup.cleanupStrategy` | Retention strategy for cleaning up old backups. | `string` | `--keep-last=3 --keep-daily=3 --keep-within-weekly=1m` | | `backup.s3AccessKey` | Access key for S3 authentication. | `string` | `` | diff --git a/packages/core/platform/images/migrations/migrations/28 b/packages/core/platform/images/migrations/migrations/28 new file mode 100755 index 00000000..259d5fd8 --- /dev/null +++ b/packages/core/platform/images/migrations/migrations/28 @@ -0,0 +1,616 @@ +#!/bin/bash +# Migration 28 --> 29 +# Rename all mysql-prefixed resources to mariadb- across the cluster. +# Discovers all MySQL HelmReleases by label, migrates each instance. +# Idempotent: safe to re-run. + +set -euo pipefail + +OLD_PREFIX="mysql" +NEW_PREFIX="mariadb" +MARIADB_OPERATOR_NS="cozy-mariadb-operator" +MARIADB_WEBHOOK_NAME="mariadb-operator-webhook" +PROTECTION_WEBHOOK_NAME="protection-webhook" +PROTECTION_WEBHOOK_NS="protection-webhook" +declare -A OPERATOR_REPLICAS=() +declare -A ORIGINAL_PV_POLICIES=() + +# ============================================================ +# Helper functions +# ============================================================ + +resource_exists() { + local ns="$1" type="$2" name="$3" + kubectl -n "$ns" get "$type" "$name" --no-headers 2>/dev/null | grep -q . +} + +delete_resource() { + local ns="$1" type="$2" name="$3" + if resource_exists "$ns" "$type" "$name"; then + echo " [DELETE] ${type}/${name}" + kubectl -n "$ns" delete "$type" "$name" --wait=false + else + echo " [SKIP] ${type}/${name} already gone" + fi +} + +clone_resource() { + local ns="$1" type="$2" old_name="$3" new_name="$4" old_instance="$5" new_instance="$6" + + if resource_exists "$ns" "$type" "$new_name"; then + echo " [SKIP] ${type}/${new_name} already exists" + return 0 + fi + if ! resource_exists "$ns" "$type" "$old_name"; then + echo " [SKIP] ${type}/${old_name} not found" + return 0 + fi + + echo " [CREATE] ${type}/${new_name} from ${old_name}" + kubectl -n "$ns" get "$type" "$old_name" -o json | \ + jq --arg new_name "$new_name" --arg old "$old_instance" --arg new "$new_instance" ' + .metadata.name = $new_name | + del(.metadata.uid, .metadata.resourceVersion, .metadata.creationTimestamp, + .metadata.generation, .metadata.managedFields, .metadata.selfLink, + .metadata.ownerReferences) | + del(.status) | + .metadata.labels = ((.metadata.labels // {}) | with_entries( + if (.value | type) == "string" then .value |= gsub($old; $new) else . end)) | + .metadata.annotations = ((.metadata.annotations // {}) | with_entries( + select(.key != "kubectl.kubernetes.io/last-applied-configuration") | + if (.value | type) == "string" then .value |= gsub($old; $new) else . end)) + ' | kubectl -n "$ns" apply -f - +} + +patch_webhook_policy() { + local name="$1" policy="$2" + echo " [PATCH] Set failurePolicy=${policy} on ValidatingWebhookConfiguration/${name}" + kubectl get validatingwebhookconfiguration "$name" -o json | \ + jq --arg p "$policy" '.webhooks[].failurePolicy = $p' | \ + kubectl apply -f - +} + +# ============================================================ +# STEP 1: Discover all MySQL instances +# ============================================================ +echo "=== Discovering MySQL HelmReleases ===" +INSTANCES=() +while IFS=/ read -r ns name; do + [ -z "$ns" ] && continue + instance="${name#${OLD_PREFIX}-}" + INSTANCES+=("${ns}/${instance}") + echo " Found: ${ns}/${name} (instance=${instance})" +done < <(kubectl get hr -A -l "apps.cozystack.io/application.kind=MySQL" \ + -o jsonpath='{range .items[*]}{.metadata.namespace}/{.metadata.name}{"\n"}{end}' 2>/dev/null) + +if [ ${#INSTANCES[@]} -eq 0 ]; then + echo " No MySQL HelmReleases found. Nothing to migrate." + + # Recover operator if a prior run left it scaled down + for deploy in mariadb-operator mariadb-operator-cert-controller mariadb-operator-webhook; do + current=$(kubectl -n "$MARIADB_OPERATOR_NS" get deploy "$deploy" \ + -o jsonpath='{.spec.replicas}' 2>/dev/null || echo "1") + if [ "$current" = "0" ]; then + echo " [RECOVER] Scaling ${deploy} -> 1" + kubectl -n "$MARIADB_OPERATOR_NS" scale deploy "$deploy" --replicas=1 + fi + done + + # Restore webhook if left at Ignore + current_policy=$(kubectl get validatingwebhookconfiguration "$MARIADB_WEBHOOK_NAME" \ + -o jsonpath='{.webhooks[0].failurePolicy}' 2>/dev/null || echo "unknown") + if [ "$current_policy" = "Ignore" ]; then + patch_webhook_policy "$MARIADB_WEBHOOK_NAME" "Fail" + fi + + # Unsuspend any new HelmReleases left suspended from a prior partial run + while IFS=/ read -r ns name; do + [ -z "$ns" ] && continue + suspended=$(kubectl -n "$ns" get hr "$name" -o jsonpath='{.spec.suspend}' 2>/dev/null || echo "false") + if [ "$suspended" = "true" ]; then + echo " [UNSUSPEND] ${ns}/hr/${name}" + kubectl -n "$ns" patch hr "$name" --type=merge -p '{"spec":{"suspend":false}}' + fi + done < <(kubectl get hr -A -l "apps.cozystack.io/application.kind=MariaDB" \ + -o jsonpath='{range .items[*]}{.metadata.namespace}/{.metadata.name}{"\n"}{end}' 2>/dev/null) + + kubectl create configmap -n cozy-system cozystack-version \ + --from-literal=version=29 --dry-run=client -o yaml | kubectl apply -f- + exit 0 +fi +echo " Total: ${#INSTANCES[@]} instance(s)" + +# ============================================================ +# STEP 2: Scale down mariadb-operator and disable its webhook +# ============================================================ +echo "" +echo "--- Step 2: Scale down mariadb-operator ---" +for deploy in mariadb-operator mariadb-operator-cert-controller mariadb-operator-webhook; do + current=$(kubectl -n "$MARIADB_OPERATOR_NS" get deploy "$deploy" -o jsonpath='{.spec.replicas}' 2>/dev/null || echo "0") + OPERATOR_REPLICAS["$deploy"]="$current" + if [ "$current" != "0" ]; then + echo " [SCALE] ${deploy} -> 0 (was ${current})" + kubectl -n "$MARIADB_OPERATOR_NS" scale deploy "$deploy" --replicas=0 + else + echo " [SKIP] ${deploy} already scaled to 0" + fi +done + +echo " Waiting for operator pods to terminate..." +kubectl -n "$MARIADB_OPERATOR_NS" wait --for=delete pod \ + -l app.kubernetes.io/instance=mariadb-operator --timeout=60s 2>/dev/null || true + +current_policy=$(kubectl get validatingwebhookconfiguration "$MARIADB_WEBHOOK_NAME" \ + -o jsonpath='{.webhooks[0].failurePolicy}' 2>/dev/null || echo "unknown") +if [ "$current_policy" = "Fail" ]; then + patch_webhook_policy "$MARIADB_WEBHOOK_NAME" "Ignore" +else + echo " [SKIP] Webhook ${MARIADB_WEBHOOK_NAME} already failurePolicy=${current_policy}" +fi + +# ============================================================ +# STEP 3: Migrate each instance +# ============================================================ +ALL_PV_NAMES=() +ALL_PROTECTED_RESOURCES=() + +for entry in "${INSTANCES[@]}"; do + NAMESPACE="${entry%%/*}" + INSTANCE="${entry#*/}" + OLD_NAME="${OLD_PREFIX}-${INSTANCE}" + NEW_NAME="${NEW_PREFIX}-${INSTANCE}" + + echo "" + echo "======================================================================" + echo "=== Migrating: ${OLD_NAME} -> ${NEW_NAME} in ${NAMESPACE}" + echo "======================================================================" + + # --- 3a: Suspend old HelmRelease --- + echo " --- Suspend HelmRelease ---" + if resource_exists "$NAMESPACE" "hr" "$OLD_NAME"; then + suspended=$(kubectl -n "$NAMESPACE" get hr "$OLD_NAME" -o jsonpath='{.spec.suspend}' 2>/dev/null || echo "false") + if [ "$suspended" != "true" ]; then + echo " [SUSPEND] hr/${OLD_NAME}" + kubectl -n "$NAMESPACE" patch hr "$OLD_NAME" --type=merge -p '{"spec":{"suspend":true}}' + else + echo " [SKIP] hr/${OLD_NAME} already suspended" + fi + else + echo " [SKIP] hr/${OLD_NAME} not found" + fi + + # --- 3b: Switch PV reclaim policy to Retain --- + echo " --- Switch PV reclaim to Retain ---" + PV_NAMES=() + replicas=$(kubectl -n "$NAMESPACE" get mariadb.k8s.mariadb.com "$OLD_NAME" \ + -o jsonpath='{.spec.replicas}' 2>/dev/null || true) + if [ -z "$replicas" ]; then + # Fallback: count existing PVCs matching the old or new name pattern + replicas=$(kubectl -n "$NAMESPACE" get pvc --no-headers -o name 2>/dev/null \ + | grep -cE "storage-${OLD_NAME}-[0-9]+$|storage-${NEW_NAME}-[0-9]+$" || echo "0") + echo " [INFO] MariaDB CR not found, derived replicas=${replicas} from existing PVCs" + fi + for i in $(seq 0 $((replicas - 1))); do + pvc_name="storage-${OLD_NAME}-${i}" + if resource_exists "$NAMESPACE" "pvc" "$pvc_name"; then + pv_name=$(kubectl -n "$NAMESPACE" get pvc "$pvc_name" -o jsonpath='{.spec.volumeName}') + PV_NAMES+=("$pv_name") + ALL_PV_NAMES+=("$pv_name") + current_policy=$(kubectl get pv "$pv_name" -o jsonpath='{.spec.persistentVolumeReclaimPolicy}') + if [ "$current_policy" != "Retain" ]; then + echo " [PATCH] PV ${pv_name}: ${current_policy} -> Retain" + ORIGINAL_PV_POLICIES["$pv_name"]="$current_policy" + kubectl patch pv "$pv_name" -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}' + else + echo " [SKIP] PV ${pv_name} already Retain" + fi + else + new_pvc_name="storage-${NEW_NAME}-${i}" + if resource_exists "$NAMESPACE" "pvc" "$new_pvc_name"; then + pv_name=$(kubectl -n "$NAMESPACE" get pvc "$new_pvc_name" -o jsonpath='{.spec.volumeName}') + PV_NAMES+=("$pv_name") + ALL_PV_NAMES+=("$pv_name") + echo " [SKIP] New PVC ${new_pvc_name} already exists, PV=${pv_name}" + else + echo " [WARN] Neither old nor new PVC found for index ${i}" + PV_NAMES+=("") + fi + fi + done + + # --- 3c: Delete old StatefulSet and Deployment --- + echo " --- Delete old StatefulSet/Deployment ---" + if resource_exists "$NAMESPACE" "statefulset" "$OLD_NAME"; then + echo " [DELETE] statefulset/${OLD_NAME} (cascade=orphan)" + kubectl -n "$NAMESPACE" delete statefulset "$OLD_NAME" --cascade=orphan + else + echo " [SKIP] statefulset/${OLD_NAME} already gone" + fi + if resource_exists "$NAMESPACE" "deployment" "${OLD_NAME}-metrics"; then + echo " [DELETE] deployment/${OLD_NAME}-metrics" + kubectl -n "$NAMESPACE" delete deployment "${OLD_NAME}-metrics" + else + echo " [SKIP] deployment/${OLD_NAME}-metrics already gone" + fi + + # --- 3d: Delete old pods --- + echo " --- Delete old pods ---" + for i in $(seq 0 $((replicas - 1))); do + pod_name="${OLD_NAME}-${i}" + if resource_exists "$NAMESPACE" "pod" "$pod_name"; then + echo " [DELETE] pod/${pod_name}" + kubectl -n "$NAMESPACE" delete pod "$pod_name" --grace-period=30 + else + echo " [SKIP] pod/${pod_name} already gone" + fi + done + for pod in $(kubectl -n "$NAMESPACE" get pods \ + -l app.kubernetes.io/name=mysqld-exporter,app.kubernetes.io/instance="${OLD_NAME}" \ + -o name 2>/dev/null); do + echo " [DELETE] ${pod}" + kubectl -n "$NAMESPACE" delete "$pod" --grace-period=30 + done + + # --- 3e: Migrate PVCs --- + echo " --- Migrate PVCs ---" + for i in $(seq 0 $((replicas - 1))); do + old_pvc="storage-${OLD_NAME}-${i}" + new_pvc="storage-${NEW_NAME}-${i}" + pv_name="${PV_NAMES[$i]:-}" + + if [ -z "$pv_name" ]; then + echo " [WARN] No PV found for index ${i}, skipping" + continue + fi + + if resource_exists "$NAMESPACE" "pvc" "$new_pvc"; then + new_phase=$(kubectl -n "$NAMESPACE" get pvc "$new_pvc" \ + -o jsonpath='{.status.phase}' 2>/dev/null || echo "unknown") + if [ "$new_phase" = "Bound" ]; then + echo " [SKIP] PVC ${new_pvc} already Bound" + continue + fi + fi + + if resource_exists "$NAMESPACE" "pvc" "$old_pvc"; then + storage_size=$(kubectl -n "$NAMESPACE" get pvc "$old_pvc" \ + -o jsonpath='{.spec.resources.requests.storage}') + storage_class=$(kubectl -n "$NAMESPACE" get pvc "$old_pvc" \ + -o jsonpath='{.spec.storageClassName}') + old_labels=$(kubectl -n "$NAMESPACE" get pvc "$old_pvc" -o json | \ + jq --arg old "$OLD_NAME" --arg new "$NEW_NAME" \ + '(.metadata.labels // {}) | with_entries( + if (.value | type) == "string" and .value == $old then .value = $new else . end)') + + if ! resource_exists "$NAMESPACE" "pvc" "$new_pvc"; then + echo " [CREATE] PVC ${new_pvc} -> PV ${pv_name}" + cat < ${new_pvc}" + kubectl patch pv "$pv_name" --type=merge -p "{ + \"spec\":{\"claimRef\":{\"apiVersion\":\"v1\",\"kind\":\"PersistentVolumeClaim\", + \"name\":\"${new_pvc}\",\"namespace\":\"${NAMESPACE}\",\"uid\":\"${new_pvc_uid}\"}} + }" + + echo " Waiting for PVC ${new_pvc} to bind..." + kubectl -n "$NAMESPACE" wait pvc "$new_pvc" \ + --for=jsonpath='{.status.phase}'=Bound --timeout=60s + echo " [OK] PVC ${new_pvc} is Bound" + fi + done + + # --- 3f: Clone Services --- + echo " --- Clone Services ---" + for suffix in "" "-internal" "-primary" "-secondary" "-metrics"; do + old_svc="${OLD_NAME}${suffix}" + new_svc="${NEW_NAME}${suffix}" + if resource_exists "$NAMESPACE" "svc" "$old_svc"; then + if resource_exists "$NAMESPACE" "svc" "$new_svc"; then + echo " [SKIP] svc/${new_svc} already exists" + else + echo " [CREATE] svc/${new_svc} from ${old_svc}" + kubectl -n "$NAMESPACE" get svc "$old_svc" -o json | \ + jq --arg new_svc "$new_svc" --arg old "$OLD_NAME" --arg new "$NEW_NAME" ' + .metadata.name = $new_svc | + del(.metadata.uid, .metadata.resourceVersion, .metadata.creationTimestamp, + .metadata.generation, .metadata.managedFields, .metadata.selfLink, + .metadata.ownerReferences) | + del(.status) | + del(.spec.clusterIP, .spec.clusterIPs) | + .metadata.labels = ((.metadata.labels // {}) | with_entries( + if (.value | type) == "string" then .value |= gsub($old; $new) else . end)) | + .metadata.annotations = ((.metadata.annotations // {}) | with_entries( + select(.key != "kubectl.kubernetes.io/last-applied-configuration") | + if (.value | type) == "string" then .value |= gsub($old; $new) else . end)) | + .spec.selector = ((.spec.selector // {}) | with_entries( + if (.value | type) == "string" then .value |= gsub($old; $new) else . end)) + ' | kubectl -n "$NAMESPACE" apply -f - + fi + fi + done + + # --- 3g: Clone Secrets --- + echo " --- Clone Secrets ---" + for secret in $(kubectl -n "$NAMESPACE" get secret -o name 2>/dev/null \ + | grep "secret/${OLD_NAME}" | grep -v "sh.helm.release"); do + old_secret_name="${secret#secret/}" + new_secret_name="${NEW_NAME}${old_secret_name#${OLD_NAME}}" + clone_resource "$NAMESPACE" "secret" "$old_secret_name" "$new_secret_name" "$OLD_NAME" "$NEW_NAME" + done + + # --- 3h: Clone ConfigMaps --- + echo " --- Clone ConfigMaps ---" + for cm in $(kubectl -n "$NAMESPACE" get configmap -o name 2>/dev/null \ + | grep "configmap/${OLD_NAME}"); do + old_cm_name="${cm#configmap/}" + new_cm_name="${NEW_NAME}${old_cm_name#${OLD_NAME}}" + clone_resource "$NAMESPACE" "configmap" "$old_cm_name" "$new_cm_name" "$OLD_NAME" "$NEW_NAME" + done + + # --- 3i: Clone MariaDB CR --- + echo " --- Clone MariaDB CR ---" + if resource_exists "$NAMESPACE" "mariadb.k8s.mariadb.com" "$OLD_NAME"; then + if resource_exists "$NAMESPACE" "mariadb.k8s.mariadb.com" "$NEW_NAME"; then + echo " [SKIP] mariadb.k8s.mariadb.com/${NEW_NAME} already exists" + else + echo " [CREATE] mariadb.k8s.mariadb.com/${NEW_NAME}" + kubectl -n "$NAMESPACE" get mariadb.k8s.mariadb.com "$OLD_NAME" -o json | \ + jq --arg old "$OLD_NAME" --arg new "$NEW_NAME" ' + .metadata.name = $new | + del(.metadata.uid, .metadata.resourceVersion, .metadata.creationTimestamp, + .metadata.generation, .metadata.managedFields, .metadata.selfLink, + .metadata.finalizers, .metadata.ownerReferences) | + del(.status) | + .metadata.labels = ((.metadata.labels // {}) | with_entries( + if (.value | type) == "string" then .value |= gsub($old; $new) else . end)) | + .metadata.annotations = ((.metadata.annotations // {}) | with_entries( + select(.key != "kubectl.kubernetes.io/last-applied-configuration") | + if (.value | type) == "string" then .value |= gsub($old; $new) else . end)) | + .spec = (.spec | tostring | gsub($old; $new) | fromjson) + ' | kubectl -n "$NAMESPACE" apply -f - + fi + fi + + # --- 3j: Clone Users, Databases, Grants --- + echo " --- Clone Users, Databases, Grants ---" + for kind in "user.k8s.mariadb.com" "database.k8s.mariadb.com" "grant.k8s.mariadb.com"; do + for resource in $(kubectl -n "$NAMESPACE" get "$kind" -o name 2>/dev/null | grep "${OLD_NAME}"); do + old_res_name="${resource##*/}" + new_res_name="${NEW_NAME}${old_res_name#${OLD_NAME}}" + if resource_exists "$NAMESPACE" "$kind" "$new_res_name"; then + echo " [SKIP] ${kind}/${new_res_name} already exists" + else + echo " [CREATE] ${kind}/${new_res_name}" + kubectl -n "$NAMESPACE" get "$kind" "$old_res_name" -o json | \ + jq --arg old "$OLD_NAME" --arg new "$NEW_NAME" --arg new_name "$new_res_name" ' + .metadata.name = $new_name | + del(.metadata.uid, .metadata.resourceVersion, .metadata.creationTimestamp, + .metadata.generation, .metadata.managedFields, .metadata.selfLink, + .metadata.finalizers, .metadata.ownerReferences) | + del(.status) | + .metadata.labels = ((.metadata.labels // {}) | with_entries( + if (.value | type) == "string" then .value |= gsub($old; $new) else . end)) | + .metadata.annotations = ((.metadata.annotations // {}) | with_entries( + select(.key != "kubectl.kubernetes.io/last-applied-configuration") | + if (.value | type) == "string" then .value |= gsub($old; $new) else . end)) | + .spec = (.spec | tostring | gsub($old; $new) | fromjson) + ' | kubectl -n "$NAMESPACE" apply -f - + fi + done + done + + # --- 3k: Clone WorkloadMonitor --- + echo " --- Clone WorkloadMonitor ---" + clone_resource "$NAMESPACE" "workloadmonitor.cozystack.io" "$OLD_NAME" "$NEW_NAME" "$OLD_NAME" "$NEW_NAME" + + # --- 3l: Create new HelmRelease (suspended) --- + echo " --- Create new HelmRelease (suspended) ---" + if resource_exists "$NAMESPACE" "hr" "$NEW_NAME"; then + echo " [SKIP] hr/${NEW_NAME} already exists" + elif resource_exists "$NAMESPACE" "hr" "$OLD_NAME"; then + echo " [CREATE] hr/${NEW_NAME} from ${OLD_NAME} (suspended)" + kubectl -n "$NAMESPACE" get hr "$OLD_NAME" -o json | \ + jq --arg old "$OLD_NAME" --arg new "$NEW_NAME" \ + --arg old_prefix "$OLD_PREFIX" --arg new_prefix "$NEW_PREFIX" ' + .metadata.name = $new | + del(.metadata.uid, .metadata.resourceVersion, .metadata.creationTimestamp, + .metadata.generation, .metadata.managedFields, .metadata.selfLink, + .metadata.finalizers, .metadata.ownerReferences) | + del(.status) | + .metadata.labels = ((.metadata.labels // {}) | with_entries( + if (.value | type) == "string" then .value |= gsub($old; $new) else . end) | + if .["apps.cozystack.io/application.kind"] == "MySQL" + then .["apps.cozystack.io/application.kind"] = "MariaDB" + else . end) | + .metadata.annotations = ((.metadata.annotations // {}) | with_entries( + select(.key != "kubectl.kubernetes.io/last-applied-configuration") | + if (.value | type) == "string" then .value |= gsub($old; $new) else . end)) | + (if .spec.chart.spec.chart == $old_prefix + then .spec.chart.spec.chart = $new_prefix else . end) | + .spec.suspend = true + ' | kubectl -n "$NAMESPACE" apply -f - + else + echo " [WARN] hr/${OLD_NAME} not found, cannot clone" + fi + + # --- 3m: Delete old unprotected resources --- + echo " --- Delete old resources ---" + for kind in "grant.k8s.mariadb.com" "database.k8s.mariadb.com" "user.k8s.mariadb.com"; do + for resource in $(kubectl -n "$NAMESPACE" get "$kind" -o name 2>/dev/null | grep "${OLD_NAME}"); do + old_res_name="${resource##*/}" + kubectl -n "$NAMESPACE" patch "$kind" "$old_res_name" --type=json \ + -p='[{"op":"remove","path":"/metadata/finalizers"}]' 2>/dev/null || true + echo " [DELETE] ${kind}/${old_res_name}" + kubectl -n "$NAMESPACE" delete "$kind" "$old_res_name" --wait=false 2>/dev/null || true + done + done + + if resource_exists "$NAMESPACE" "mariadb.k8s.mariadb.com" "$OLD_NAME"; then + kubectl -n "$NAMESPACE" patch mariadb.k8s.mariadb.com "$OLD_NAME" --type=json \ + -p='[{"op":"remove","path":"/metadata/finalizers"}]' 2>/dev/null || true + delete_resource "$NAMESPACE" "mariadb.k8s.mariadb.com" "$OLD_NAME" + fi + + for secret in $(kubectl -n "$NAMESPACE" get secret -o name 2>/dev/null \ + | grep "secret/${OLD_NAME}" | grep -v "sh.helm.release"); do + old_secret_name="${secret#secret/}" + delete_resource "$NAMESPACE" "secret" "$old_secret_name" + done + + for cm in $(kubectl -n "$NAMESPACE" get configmap -o name 2>/dev/null \ + | grep "configmap/${OLD_NAME}"); do + old_cm_name="${cm#configmap/}" + delete_resource "$NAMESPACE" "configmap" "$old_cm_name" + done + + delete_resource "$NAMESPACE" "workloadmonitor.cozystack.io" "$OLD_NAME" + + if resource_exists "$NAMESPACE" "hr" "$OLD_NAME"; then + kubectl -n "$NAMESPACE" patch hr "$OLD_NAME" --type=json \ + -p='[{"op":"remove","path":"/metadata/finalizers"}]' 2>/dev/null || true + delete_resource "$NAMESPACE" "hr" "$OLD_NAME" + fi + + echo " [DELETE] secrets with label owner=helm,name=${OLD_NAME}" + kubectl -n "$NAMESPACE" delete secret -l "owner=helm,name=${OLD_NAME}" 2>/dev/null || true + + # Collect protected resources for batch deletion + for suffix in "" "-internal" "-primary" "-secondary" "-metrics"; do + svc_name="${OLD_NAME}${suffix}" + if resource_exists "$NAMESPACE" "svc" "$svc_name"; then + ALL_PROTECTED_RESOURCES+=("${NAMESPACE}:svc/${svc_name}") + fi + done + for i in $(seq 0 $((replicas - 1))); do + old_pvc="storage-${OLD_NAME}-${i}" + if resource_exists "$NAMESPACE" "pvc" "$old_pvc"; then + ALL_PROTECTED_RESOURCES+=("${NAMESPACE}:pvc/${old_pvc}") + fi + done +done + +# ============================================================ +# STEP 4: Delete protected resources (PVCs, Services) +# ============================================================ +echo "" +echo "--- Step 4: Delete protected resources ---" + +if [ ${#ALL_PROTECTED_RESOURCES[@]} -gt 0 ]; then + echo " --- Temporarily disabling protection-webhook ---" + + WEBHOOK_REPLICAS=$(kubectl -n "$PROTECTION_WEBHOOK_NS" get deploy "$PROTECTION_WEBHOOK_NAME" \ + -o jsonpath='{.spec.replicas}' 2>/dev/null || echo "1") + + echo " [SCALE] ${PROTECTION_WEBHOOK_NAME} -> 0 (was ${WEBHOOK_REPLICAS})" + kubectl -n "$PROTECTION_WEBHOOK_NS" scale deploy "$PROTECTION_WEBHOOK_NAME" --replicas=0 + + patch_webhook_policy "$PROTECTION_WEBHOOK_NAME" "Ignore" + + echo " Waiting for webhook pods to terminate..." + kubectl -n "$PROTECTION_WEBHOOK_NS" wait --for=delete pod \ + -l app.kubernetes.io/name=protection-webhook --timeout=60s 2>/dev/null || true + sleep 3 + + for entry in "${ALL_PROTECTED_RESOURCES[@]}"; do + ns="${entry%%:*}" + res="${entry#*:}" + echo " [DELETE] ${ns}/${res}" + kubectl -n "$ns" delete "$res" --wait=false 2>/dev/null || true + done + + patch_webhook_policy "$PROTECTION_WEBHOOK_NAME" "Fail" + + echo " [SCALE] ${PROTECTION_WEBHOOK_NAME} -> ${WEBHOOK_REPLICAS}" + kubectl -n "$PROTECTION_WEBHOOK_NS" scale deploy "$PROTECTION_WEBHOOK_NAME" \ + --replicas="$WEBHOOK_REPLICAS" + echo " --- protection-webhook restored ---" +else + echo " [SKIP] No protected resources to delete" +fi + +# ============================================================ +# STEP 5: Restore PV reclaim policies +# ============================================================ +echo "" +echo "--- Step 5: Restore PV reclaim policies ---" +for pv_name in "${ALL_PV_NAMES[@]}"; do + if [ -n "$pv_name" ]; then + original="${ORIGINAL_PV_POLICIES[$pv_name]:-}" + if [ -n "$original" ]; then + echo " [PATCH] PV ${pv_name}: Retain -> ${original}" + kubectl patch pv "$pv_name" -p "{\"spec\":{\"persistentVolumeReclaimPolicy\":\"${original}\"}}" + else + echo " [SKIP] PV ${pv_name} was already Retain, leaving as-is" + fi + fi +done + +# ============================================================ +# STEP 6: Restore mariadb-operator +# ============================================================ +echo "" +echo "--- Step 6: Restore mariadb-operator ---" + +current_policy=$(kubectl get validatingwebhookconfiguration "$MARIADB_WEBHOOK_NAME" \ + -o jsonpath='{.webhooks[0].failurePolicy}' 2>/dev/null || echo "unknown") +if [ "$current_policy" = "Ignore" ]; then + patch_webhook_policy "$MARIADB_WEBHOOK_NAME" "Fail" +else + echo " [SKIP] Webhook ${MARIADB_WEBHOOK_NAME} already failurePolicy=${current_policy}" +fi + +for deploy in mariadb-operator mariadb-operator-cert-controller mariadb-operator-webhook; do + current=$(kubectl -n "$MARIADB_OPERATOR_NS" get deploy "$deploy" \ + -o jsonpath='{.spec.replicas}' 2>/dev/null || echo "1") + if [ "$current" = "0" ]; then + target="${OPERATOR_REPLICAS[$deploy]:-1}" + echo " [SCALE] ${deploy} -> ${target}" + kubectl -n "$MARIADB_OPERATOR_NS" scale deploy "$deploy" --replicas="$target" + else + echo " [SKIP] ${deploy} already running (replicas=${current})" + fi +done + +# ============================================================ +# STEP 7: Unsuspend all new HelmReleases +# ============================================================ +echo "" +echo "--- Step 7: Unsuspend new HelmReleases ---" +for entry in "${INSTANCES[@]}"; do + ns="${entry%%/*}" + instance="${entry#*/}" + new_name="${NEW_PREFIX}-${instance}" + if resource_exists "$ns" "hr" "$new_name"; then + suspended=$(kubectl -n "$ns" get hr "$new_name" \ + -o jsonpath='{.spec.suspend}' 2>/dev/null || echo "false") + if [ "$suspended" = "true" ]; then + echo " [UNSUSPEND] ${ns}/hr/${new_name}" + kubectl -n "$ns" patch hr "$new_name" --type=merge -p '{"spec":{"suspend":false}}' + else + echo " [SKIP] ${ns}/hr/${new_name} already not suspended" + fi + fi +done + +echo "" +echo "=== Migration complete (${#INSTANCES[@]} instance(s)) ===" + +# Stamp version +kubectl create configmap -n cozy-system cozystack-version \ + --from-literal=version=29 --dry-run=client -o yaml | kubectl apply -f- diff --git a/packages/core/platform/values.yaml b/packages/core/platform/values.yaml index f923c22d..9bb8e2b1 100644 --- a/packages/core/platform/values.yaml +++ b/packages/core/platform/values.yaml @@ -5,8 +5,8 @@ sourceRef: path: / migrations: enabled: false - image: ghcr.io/cozystack/cozystack/platform-migrations:v1.0.0-beta.3@sha256:1ab28def39c9cb0233a03708ce00c9694edc9728789eef59abdc1d72bbe9b10a - targetVersion: 28 + image: ghcr.io/cozystack/cozystack/platform-migrations:latest + targetVersion: 29 # Bundle deployment configuration bundles: system: From 9a86551e40e2f168ce935416bb02010476795a0d Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Thu, 12 Feb 2026 15:18:06 +0100 Subject: [PATCH 019/666] fix(e2e): correct s3Bucket reference in mariadb test Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- hack/e2e-apps/mariadb.bats | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hack/e2e-apps/mariadb.bats b/hack/e2e-apps/mariadb.bats index 50e608ac..5b11c4c0 100644 --- a/hack/e2e-apps/mariadb.bats +++ b/hack/e2e-apps/mariadb.bats @@ -25,7 +25,7 @@ spec: backup: enabled: false s3Region: us-east-1 - s3Bucket: s3.example.org/postgres-backups + s3Bucket: s3.example.org/mariadb-backups schedule: "0 2 * * *" cleanupStrategy: "--keep-last=3 --keep-daily=3 --keep-within-weekly=1m" s3AccessKey: oobaiRus9pah8PhohL1ThaeTa4UVa7gu From 04218411784894dc88627a8630586d277d6805bd Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Thu, 12 Feb 2026 00:33:02 +0100 Subject: [PATCH 020/666] feat(nfs): add managed NFS application with kubernetes tenant integration Add managed NFS application and integrate it with tenant Kubernetes clusters: - New `nfs` app deploying NFS server as a HelmRelease with standalone RWX PVC storage - `nfs-rd` resource definition package for the NFS app - `system/nfs` package with Cilium egress policy for NFS traffic - `extraStorageClasses` support in `nfs-driver` system chart - NFS addon in `kubernetes` app with PVC-based volume lookup and top-level `nfs.instances` configuration - `nfs-application` PackageSource for platform artifact delivery - E2E test with NFS mount verification - Helm-unittest tests for new templates Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- hack/e2e-apps/run-kubernetes.sh | 88 +++++++++++- packages/apps/kubernetes/Makefile | 3 + packages/apps/kubernetes/README.md | 11 ++ .../templates/helmreleases/nfs-driver.yaml | 67 +++++++++ .../kubernetes/tests/nfs-driver_test.yaml | 45 ++++++ packages/apps/kubernetes/values.schema.json | 41 ++++++ packages/apps/kubernetes/values.yaml | 24 ++++ packages/apps/nfs/.helmignore | 2 + packages/apps/nfs/Chart.yaml | 6 + packages/apps/nfs/Makefile | 6 + packages/apps/nfs/README.md | 20 +++ packages/apps/nfs/charts/cozy-lib | 1 + packages/apps/nfs/logos/nfs.svg | 13 ++ packages/apps/nfs/templates/_helpers.tpl | 49 +++++++ .../nfs/templates/dashboard-resourcemap.yaml | 23 +++ packages/apps/nfs/templates/helmrelease.yaml | 22 +++ packages/apps/nfs/templates/pvc.yaml | 15 ++ .../apps/nfs/templates/workloadmonitor.yaml | 12 ++ packages/apps/nfs/values.schema.json | 25 ++++ packages/apps/nfs/values.yaml | 9 ++ .../platform/images/migrations/migrations/25 | 3 + .../sources/kubernetes-application.yaml | 2 + .../platform/sources/nfs-application.yaml | 29 ++++ .../kubernetes-rd/cozyrds/kubernetes.yaml | 4 +- packages/system/nfs-driver/Makefile | 3 + .../templates/extra-storageclasses.yaml | 18 +++ .../tests/extra-storageclasses_test.yaml | 133 ++++++++++++++++++ packages/system/nfs-driver/values.yaml | 2 + packages/system/nfs-rd/Chart.yaml | 3 + packages/system/nfs-rd/Makefile | 4 + packages/system/nfs-rd/cozyrds/nfs.yaml | 21 +++ packages/system/nfs-rd/templates/cozyrd.yaml | 4 + packages/system/nfs-rd/values.yaml | 1 + packages/system/nfs/.helmignore | 2 + packages/system/nfs/Chart.yaml | 3 + packages/system/nfs/Makefile | 3 + .../system/nfs/templates/cilium-policy.yaml | 20 +++ packages/system/nfs/values.yaml | 1 + 38 files changed, 734 insertions(+), 4 deletions(-) create mode 100644 packages/apps/kubernetes/templates/helmreleases/nfs-driver.yaml create mode 100644 packages/apps/kubernetes/tests/nfs-driver_test.yaml create mode 100644 packages/apps/nfs/.helmignore create mode 100644 packages/apps/nfs/Chart.yaml create mode 100644 packages/apps/nfs/Makefile create mode 100644 packages/apps/nfs/README.md create mode 120000 packages/apps/nfs/charts/cozy-lib create mode 100644 packages/apps/nfs/logos/nfs.svg create mode 100644 packages/apps/nfs/templates/_helpers.tpl create mode 100644 packages/apps/nfs/templates/dashboard-resourcemap.yaml create mode 100644 packages/apps/nfs/templates/helmrelease.yaml create mode 100644 packages/apps/nfs/templates/pvc.yaml create mode 100644 packages/apps/nfs/templates/workloadmonitor.yaml create mode 100644 packages/apps/nfs/values.schema.json create mode 100644 packages/apps/nfs/values.yaml create mode 100644 packages/core/platform/sources/nfs-application.yaml create mode 100644 packages/system/nfs-driver/templates/extra-storageclasses.yaml create mode 100644 packages/system/nfs-driver/tests/extra-storageclasses_test.yaml create mode 100644 packages/system/nfs-rd/Chart.yaml create mode 100644 packages/system/nfs-rd/Makefile create mode 100644 packages/system/nfs-rd/cozyrds/nfs.yaml create mode 100644 packages/system/nfs-rd/templates/cozyrd.yaml create mode 100644 packages/system/nfs-rd/values.yaml create mode 100644 packages/system/nfs/.helmignore create mode 100644 packages/system/nfs/Chart.yaml create mode 100644 packages/system/nfs/Makefile create mode 100644 packages/system/nfs/templates/cilium-policy.yaml create mode 100644 packages/system/nfs/values.yaml diff --git a/hack/e2e-apps/run-kubernetes.sh b/hack/e2e-apps/run-kubernetes.sh index 3e2e20f9..51dc837c 100644 --- a/hack/e2e-apps/run-kubernetes.sh +++ b/hack/e2e-apps/run-kubernetes.sh @@ -3,6 +3,25 @@ run_kubernetes_test() { local test_name="$2" local port="$3" local k8s_version=$(yq "$version_expr" packages/apps/kubernetes/files/versions.yaml) + local nfs_name="${test_name}" + + # Create NFS volume for testing NFS mount + kubectl apply -f - </dev/null; do sleep 5; done' + + # Create PVC with NFS StorageClass + kubectl --kubeconfig tenantkubeconfig-${test_name} apply -f - < /data/test.txt && cat /data/test.txt"] + volumeMounts: + - name: nfs-vol + mountPath: /data + volumes: + - name: nfs-vol + persistentVolumeClaim: + claimName: nfs-test-pvc + restartPolicy: Never +EOF + + # Wait for Pod to complete successfully + kubectl --kubeconfig tenantkubeconfig-${test_name} wait pod nfs-test-pod -n tenant-test --timeout=2m --for=jsonpath='{.status.phase}'=Succeeded + + # Verify NFS data integrity + nfs_result=$(kubectl --kubeconfig tenantkubeconfig-${test_name} logs nfs-test-pod -n tenant-test) + if [ "$nfs_result" != "nfs-mount-ok" ]; then + echo "NFS mount test failed: expected 'nfs-mount-ok', got '$nfs_result'" >&2 + exit 1 + fi + + # Cleanup NFS test resources in tenant cluster + kubectl --kubeconfig tenantkubeconfig-${test_name} delete pod nfs-test-pod -n tenant-test + kubectl --kubeconfig tenantkubeconfig-${test_name} delete pvc nfs-test-pvc -n tenant-test + # Wait for all machine deployment replicas to be ready (timeout after 10 minutes) kubectl wait machinedeployment kubernetes-${test_name}-md0 -n tenant-test --timeout=10m --for=jsonpath='{.status.v1beta2.readyReplicas}'=2 - for component in cilium coredns csi vsnap-crd; do + for component in cilium coredns csi nfs-driver vsnap-crd; do kubectl wait hr kubernetes-${test_name}-${component} -n tenant-test --timeout=1m --for=condition=ready done kubectl wait hr kubernetes-${test_name}-ingress-nginx -n tenant-test --timeout=5m --for=condition=ready @@ -229,4 +310,7 @@ fi # Clean up by deleting the Kubernetes resource kubectl -n tenant-test delete kuberneteses.apps.cozystack.io $test_name + # Clean up NFS volume + kubectl -n tenant-test delete nfs.apps.cozystack.io ${nfs_name} + } diff --git a/packages/apps/kubernetes/Makefile b/packages/apps/kubernetes/Makefile index e6ed7c6f..9b4dadf0 100644 --- a/packages/apps/kubernetes/Makefile +++ b/packages/apps/kubernetes/Makefile @@ -4,6 +4,9 @@ KUBERNETES_PKG_TAG = $(shell awk '$$1 == "version:" {print $$2}' Chart.yaml) include ../../../hack/common-envs.mk include ../../../hack/package.mk +test: + helm unittest . + generate: cozyvalues-gen -v values.yaml -s values.schema.json -r README.md ../../../hack/update-crd.sh diff --git a/packages/apps/kubernetes/README.md b/packages/apps/kubernetes/README.md index b2fe102f..66e72a6d 100644 --- a/packages/apps/kubernetes/README.md +++ b/packages/apps/kubernetes/README.md @@ -108,6 +108,15 @@ See the reference for components utilized in this service: | `host` | External hostname for Kubernetes cluster. Defaults to `.` if empty. | `string` | `""` | +### NFS + +| Name | Description | Type | Value | +| ----------------------- | -------------------------------------------------------------------------------------------------------------- | ---------- | ----- | +| `nfs` | NFS configuration. | `object` | `{}` | +| `nfs.instances` | List of NFS volume instances to connect. Each creates a StorageClass named `nfs-` in the tenant cluster. | `[]object` | `[]` | +| `nfs.instances[i].name` | Name of the NFS volume instance in the same namespace. | `string` | `""` | + + ### Cluster Addons | Name | Description | Type | Value | @@ -141,6 +150,8 @@ See the reference for components utilized in this service: | `addons.velero.valuesOverride` | Custom Helm values overrides. | `object` | `{}` | | `addons.coredns` | CoreDNS addon. | `object` | `{}` | | `addons.coredns.valuesOverride` | Custom Helm values overrides. | `object` | `{}` | +| `addons.nfs` | NFS CSI driver addon. | `object` | `{}` | +| `addons.nfs.valuesOverride` | Custom Helm values overrides. | `object` | `{}` | ### Kubernetes Control Plane Configuration diff --git a/packages/apps/kubernetes/templates/helmreleases/nfs-driver.yaml b/packages/apps/kubernetes/templates/helmreleases/nfs-driver.yaml new file mode 100644 index 00000000..c00ea763 --- /dev/null +++ b/packages/apps/kubernetes/templates/helmreleases/nfs-driver.yaml @@ -0,0 +1,67 @@ +{{- if .Values.nfs.instances }} +{{- $extraStorageClasses := list }} +{{- range .Values.nfs.instances }} +{{- $pvcName := printf "nfs-%s" .name }} +{{- $pvc := lookup "v1" "PersistentVolumeClaim" $.Release.Namespace $pvcName }} +{{- if not $pvc }} +{{- fail (printf "NFS PVC not found in namespace %s: %s" $.Release.Namespace $pvcName) }} +{{- end }} +{{- if not $pvc.spec.volumeName }} +{{- fail (printf "NFS PVC %s is not yet bound" $pvcName) }} +{{- end }} +{{- $pv := lookup "v1" "PersistentVolume" "" $pvc.spec.volumeName }} +{{- if not $pv }} +{{- fail (printf "PersistentVolume %s not found for PVC %s" $pvc.spec.volumeName $pvcName) }} +{{- end }} +{{- $nfsExport := index $pv.spec.csi.volumeAttributes "linstor.csi.linbit.com/nfs-export" }} +{{- if not $nfsExport }} +{{- fail (printf "PV %s has no linstor.csi.linbit.com/nfs-export attribute" $pv.metadata.name) }} +{{- end }} +{{- $parsed := urlParse $nfsExport }} +{{- $hostParts := splitList ":" $parsed.host }} +{{- $server := first $hostParts }} +{{- $port := last $hostParts }} +{{- $sc := dict "name" (printf "nfs-%s" .name) "server" $server "share" $parsed.path "mountOptions" (list (printf "nfsvers=4.2") (printf "port=%s" $port)) }} +{{- $extraStorageClasses = append $extraStorageClasses $sc }} +{{- end }} +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: {{ .Release.Name }}-nfs-driver + labels: + cozystack.io/repository: system + cozystack.io/target-cluster-name: {{ .Release.Name }} + sharding.fluxcd.io/key: tenants +spec: + releaseName: nfs-driver + chartRef: + kind: ExternalArtifact + name: cozystack-kubernetes-application-kubevirt-kubernetes-nfs-driver + namespace: cozy-system + kubeConfig: + secretRef: + name: {{ .Release.Name }}-admin-kubeconfig + key: super-admin.svc + targetNamespace: cozy-nfs-driver + storageNamespace: cozy-nfs-driver + interval: 5m + timeout: 10m + install: + createNamespace: true + remediation: + retries: -1 + upgrade: + force: true + remediation: + retries: -1 + values: + {{- $defaultValues := dict "csi-driver-nfs" (dict "storageClass" (dict "create" false)) "extraStorageClasses" $extraStorageClasses }} + {{- toYaml (deepCopy .Values.addons.nfs.valuesOverride | mergeOverwrite $defaultValues) | nindent 4 }} + dependsOn: + {{- if lookup "helm.toolkit.fluxcd.io/v2" "HelmRelease" .Release.Namespace .Release.Name }} + - name: {{ .Release.Name }} + namespace: {{ .Release.Namespace }} + {{- end }} + - name: {{ .Release.Name }}-cilium + namespace: {{ .Release.Namespace }} +{{- end }} diff --git a/packages/apps/kubernetes/tests/nfs-driver_test.yaml b/packages/apps/kubernetes/tests/nfs-driver_test.yaml new file mode 100644 index 00000000..902d3e69 --- /dev/null +++ b/packages/apps/kubernetes/tests/nfs-driver_test.yaml @@ -0,0 +1,45 @@ +suite: NFS driver HelmRelease tests + +templates: + - templates/helmreleases/nfs-driver.yaml + +tests: + - it: renders nothing when instances is empty + release: + name: kubernetes-test + namespace: tenant-root + asserts: + - hasDocuments: + count: 0 + + - it: renders nothing when instances is not set + release: + name: kubernetes-test + namespace: tenant-root + set: + nfs.instances: [] + asserts: + - hasDocuments: + count: 0 + + - it: fails when NFS PVC is not found + release: + name: kubernetes-test + namespace: tenant-root + set: + nfs.instances: + - name: my-data + asserts: + - failedTemplate: + errorMessage: "NFS PVC not found in namespace tenant-root: nfs-my-data" + + - it: constructs correct PVC name for lookup + release: + name: kubernetes-test + namespace: custom-ns + set: + nfs.instances: + - name: backup + asserts: + - failedTemplate: + errorMessage: "NFS PVC not found in namespace custom-ns: nfs-backup" diff --git a/packages/apps/kubernetes/values.schema.json b/packages/apps/kubernetes/values.schema.json index 1f6c9361..caf741b5 100644 --- a/packages/apps/kubernetes/values.schema.json +++ b/packages/apps/kubernetes/values.schema.json @@ -15,6 +15,7 @@ "gpuOperator", "ingressNginx", "monitoringAgents", + "nfs", "velero", "verticalPodAutoscaler" ], @@ -194,6 +195,22 @@ } } }, + "nfs": { + "description": "NFS CSI driver addon.", + "type": "object", + "default": {}, + "required": [ + "valuesOverride" + ], + "properties": { + "valuesOverride": { + "description": "Custom Helm values overrides.", + "type": "object", + "default": {}, + "x-kubernetes-preserve-unknown-fields": true + } + } + }, "velero": { "description": "Velero backup/restore addon.", "type": "object", @@ -500,6 +517,30 @@ "type": "string", "default": "" }, + "nfs": { + "description": "NFS configuration.", + "type": "object", + "default": {}, + "properties": { + "instances": { + "description": "List of NFS volume instances to connect. Each creates a StorageClass named `nfs-\u003cname\u003e` in the tenant cluster.", + "type": "array", + "default": [], + "items": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "description": "Name of the NFS volume instance in the same namespace.", + "type": "string" + } + } + } + } + } + }, "nodeGroups": { "description": "Worker nodes configuration map.", "type": "object", diff --git a/packages/apps/kubernetes/values.yaml b/packages/apps/kubernetes/values.yaml index 7f15752f..d4e2a1c2 100644 --- a/packages/apps/kubernetes/values.yaml +++ b/packages/apps/kubernetes/values.yaml @@ -62,6 +62,24 @@ version: "v1.33" ## @param {string} host - External hostname for Kubernetes cluster. Defaults to `.` if empty. host: "" +## +## @section NFS +## + +## @typedef {struct} NFSInstance - Reference to an NFS volume instance. +## @field {string} name - Name of the NFS volume instance in the same namespace. + +## @typedef {struct} NFS - NFS configuration. +## @field {[]NFSInstance} instances - List of NFS volume instances to connect. Each creates a StorageClass named `nfs-` in the tenant cluster. + +## @param {NFS} nfs - NFS configuration. +nfs: + instances: [] + ## Example: + ## instances: + ## - name: my-data + ## - name: my-backup + ## ## @section Cluster Addons ## @@ -108,6 +126,9 @@ host: "" ## @typedef {struct} CoreDNSAddon - CoreDNS addon. ## @field {object} valuesOverride - Custom Helm values overrides. +## @typedef {struct} NFSAddon - NFS CSI driver addon. +## @field {object} valuesOverride - Custom Helm values overrides. + ## @typedef {struct} Addons - Cluster addons configuration. ## @field {CertManagerAddon} certManager - Cert-manager addon. ## @field {CiliumAddon} cilium - Cilium CNI plugin. @@ -119,6 +140,7 @@ host: "" ## @field {VerticalPodAutoscalerAddon} verticalPodAutoscaler - Vertical Pod Autoscaler. ## @field {VeleroAddon} velero - Velero backup/restore addon. ## @field {CoreDNSAddon} coredns - CoreDNS addon. +## @field {NFSAddon} nfs - NFS CSI driver addon. ## @param {Addons} addons - Cluster addons configuration. addons: @@ -150,6 +172,8 @@ addons: valuesOverride: {} coredns: valuesOverride: {} + nfs: + valuesOverride: {} ## ## @section Kubernetes Control Plane Configuration diff --git a/packages/apps/nfs/.helmignore b/packages/apps/nfs/.helmignore new file mode 100644 index 00000000..481ff39c --- /dev/null +++ b/packages/apps/nfs/.helmignore @@ -0,0 +1,2 @@ +logos/ +hack/ diff --git a/packages/apps/nfs/Chart.yaml b/packages/apps/nfs/Chart.yaml new file mode 100644 index 00000000..3426d019 --- /dev/null +++ b/packages/apps/nfs/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: nfs +description: Managed NFS storage +icon: /logos/nfs.svg +type: application +version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process diff --git a/packages/apps/nfs/Makefile b/packages/apps/nfs/Makefile new file mode 100644 index 00000000..3df774a0 --- /dev/null +++ b/packages/apps/nfs/Makefile @@ -0,0 +1,6 @@ +include ../../../hack/common-envs.mk +include ../../../hack/package.mk + +generate: + cozyvalues-gen -v values.yaml -s values.schema.json -r README.md + ../../../hack/update-crd.sh diff --git a/packages/apps/nfs/README.md b/packages/apps/nfs/README.md new file mode 100644 index 00000000..07e46408 --- /dev/null +++ b/packages/apps/nfs/README.md @@ -0,0 +1,20 @@ +# Managed File Share Service + +NFS Ganesha based managed file sharing service that provides NFSv4 network storage accessible from multiple clients simultaneously. + +## Deployment Details + +This service deploys NFS Ganesha as a StatefulSet with persistent volume storage, exposing NFSv4 on port 2049. + +- Docs: +- GitHub: + +## Parameters + +### Common parameters + +| Name | Description | Type | Value | +| -------------- | ------------------------------------ | ---------- | ------------ | +| `storageClass` | StorageClass used to store the data. | `string` | `replicated` | +| `size` | Volume size for NFS storage. | `quantity` | `10Gi` | + diff --git a/packages/apps/nfs/charts/cozy-lib b/packages/apps/nfs/charts/cozy-lib new file mode 120000 index 00000000..e1813509 --- /dev/null +++ b/packages/apps/nfs/charts/cozy-lib @@ -0,0 +1 @@ +../../../library/cozy-lib \ No newline at end of file diff --git a/packages/apps/nfs/logos/nfs.svg b/packages/apps/nfs/logos/nfs.svg new file mode 100644 index 00000000..3d57d868 --- /dev/null +++ b/packages/apps/nfs/logos/nfs.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/packages/apps/nfs/templates/_helpers.tpl b/packages/apps/nfs/templates/_helpers.tpl new file mode 100644 index 00000000..63d66461 --- /dev/null +++ b/packages/apps/nfs/templates/_helpers.tpl @@ -0,0 +1,49 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "nfs.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +*/}} +{{- define "nfs.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "nfs.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "nfs.labels" -}} +helm.sh/chart: {{ include "nfs.chart" . }} +{{ include "nfs.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "nfs.selectorLabels" -}} +app.kubernetes.io/name: {{ include "nfs.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} diff --git a/packages/apps/nfs/templates/dashboard-resourcemap.yaml b/packages/apps/nfs/templates/dashboard-resourcemap.yaml new file mode 100644 index 00000000..106af486 --- /dev/null +++ b/packages/apps/nfs/templates/dashboard-resourcemap.yaml @@ -0,0 +1,23 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ .Release.Name }}-dashboard-resources +rules: +- apiGroups: + - "" + resources: + - persistentvolumeclaims + resourceNames: + - {{ .Release.Name }} + verbs: ["get", "list", "watch"] +--- +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: {{ .Release.Name }}-dashboard-resources +subjects: +{{ include "cozy-lib.rbac.subjectsForTenantAndAccessLevel" (list "use" .Release.Namespace) }} +roleRef: + kind: Role + name: {{ .Release.Name }}-dashboard-resources + apiGroup: rbac.authorization.k8s.io diff --git a/packages/apps/nfs/templates/helmrelease.yaml b/packages/apps/nfs/templates/helmrelease.yaml new file mode 100644 index 00000000..598f065f --- /dev/null +++ b/packages/apps/nfs/templates/helmrelease.yaml @@ -0,0 +1,22 @@ +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: {{ .Release.Name }}-system + labels: + sharding.fluxcd.io/key: tenants +spec: + chartRef: + kind: ExternalArtifact + name: cozystack-nfs-application-default-nfs-system + namespace: cozy-system + interval: 5m + timeout: 10m + install: + remediation: + retries: -1 + upgrade: + force: true + remediation: + retries: -1 + values: + pvcName: {{ .Release.Name }} diff --git a/packages/apps/nfs/templates/pvc.yaml b/packages/apps/nfs/templates/pvc.yaml new file mode 100644 index 00000000..edfa64dc --- /dev/null +++ b/packages/apps/nfs/templates/pvc.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ .Release.Name }} + labels: + {{- include "nfs.labels" . | nindent 4 }} +spec: + accessModes: + - ReadWriteMany + {{- with .Values.storageClass }} + storageClassName: "{{ . }}" + {{- end }} + resources: + requests: + storage: {{ .Values.size }} diff --git a/packages/apps/nfs/templates/workloadmonitor.yaml b/packages/apps/nfs/templates/workloadmonitor.yaml new file mode 100644 index 00000000..2125a1b3 --- /dev/null +++ b/packages/apps/nfs/templates/workloadmonitor.yaml @@ -0,0 +1,12 @@ +apiVersion: cozystack.io/v1alpha1 +kind: WorkloadMonitor +metadata: + name: {{ $.Release.Name }} +spec: + replicas: 1 + minReplicas: 1 + kind: nfs + type: nfs + selector: + app.kubernetes.io/instance: {{ .Release.Name }} + version: {{ $.Chart.Version }} diff --git a/packages/apps/nfs/values.schema.json b/packages/apps/nfs/values.schema.json new file mode 100644 index 00000000..012171ef --- /dev/null +++ b/packages/apps/nfs/values.schema.json @@ -0,0 +1,25 @@ +{ + "title": "Chart Values", + "type": "object", + "properties": { + "size": { + "description": "Volume size for NFS storage.", + "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", + "default": "replicated" + } + } +} \ No newline at end of file diff --git a/packages/apps/nfs/values.yaml b/packages/apps/nfs/values.yaml new file mode 100644 index 00000000..d08d9331 --- /dev/null +++ b/packages/apps/nfs/values.yaml @@ -0,0 +1,9 @@ +## +## @section Common parameters +## + +## @param {string} storageClass - StorageClass used to store the data. +storageClass: "replicated" + +## @param {quantity} size - Volume size for NFS storage. +size: 10Gi diff --git a/packages/core/platform/images/migrations/migrations/25 b/packages/core/platform/images/migrations/migrations/25 index 655cce23..5085699f 100755 --- a/packages/core/platform/images/migrations/migrations/25 +++ b/packages/core/platform/images/migrations/migrations/25 @@ -12,6 +12,9 @@ kubectl delete deploy cozystack -n cozy-system --ignore-not-found # Delete old cozystack-assets statefulset kubectl delete sts cozystack-assets -n cozy-system --ignore-not-found +kubectl delete helmrepositories.source.toolkit.fluxcd.io -n cozy-public cozystack-apps cozystack-extra --ignore-not-found --wait=false +kubectl delete helmrepositories.source.toolkit.fluxcd.io -n cozy-system cozystack-system --ignore-not-found --wait=false + # Delete old bootbox HelmReleases (will be recreated by new platform chart if enabled) kubectl delete hr bootbox -n cozy-system --ignore-not-found kubectl delete hr bootbox-rd -n cozy-system --ignore-not-found diff --git a/packages/core/platform/sources/kubernetes-application.yaml b/packages/core/platform/sources/kubernetes-application.yaml index 9787cb19..a329f002 100644 --- a/packages/core/platform/sources/kubernetes-application.yaml +++ b/packages/core/platform/sources/kubernetes-application.yaml @@ -58,6 +58,8 @@ spec: path: system/prometheus-operator-crds - name: kubernetes-metrics-server path: system/metrics-server + - name: kubernetes-nfs-driver + path: system/nfs-driver - name: kubernetes path: apps/kubernetes libraries: ["cozy-lib"] diff --git a/packages/core/platform/sources/nfs-application.yaml b/packages/core/platform/sources/nfs-application.yaml new file mode 100644 index 00000000..c9dc8575 --- /dev/null +++ b/packages/core/platform/sources/nfs-application.yaml @@ -0,0 +1,29 @@ +--- +apiVersion: cozystack.io/v1alpha1 +kind: PackageSource +metadata: + name: cozystack.nfs-application +spec: + sourceRef: + kind: OCIRepository + name: cozystack-packages + namespace: cozy-system + path: / + variants: + - name: default + dependsOn: + - cozystack.networking + libraries: + - name: cozy-lib + path: library/cozy-lib + components: + - name: nfs-system + path: system/nfs + - name: nfs + path: apps/nfs + libraries: ["cozy-lib"] + - name: nfs-rd + path: system/nfs-rd + install: + namespace: cozy-system + releaseName: nfs-rd diff --git a/packages/system/kubernetes-rd/cozyrds/kubernetes.yaml b/packages/system/kubernetes-rd/cozyrds/kubernetes.yaml index 1b4c7361..7ac884ea 100644 --- a/packages/system/kubernetes-rd/cozyrds/kubernetes.yaml +++ b/packages/system/kubernetes-rd/cozyrds/kubernetes.yaml @@ -8,7 +8,7 @@ spec: singular: kubernetes plural: kuberneteses openAPISchema: |- - {"title":"Chart Values","type":"object","properties":{"addons":{"description":"Cluster addons configuration.","type":"object","default":{},"required":["certManager","cilium","coredns","fluxcd","gatewayAPI","gpuOperator","ingressNginx","monitoringAgents","velero","verticalPodAutoscaler"],"properties":{"certManager":{"description":"Cert-manager addon.","type":"object","default":{},"required":["enabled","valuesOverride"],"properties":{"enabled":{"description":"Enable cert-manager.","type":"boolean","default":false},"valuesOverride":{"description":"Custom Helm values overrides.","type":"object","default":{},"x-kubernetes-preserve-unknown-fields":true}}},"cilium":{"description":"Cilium CNI plugin.","type":"object","default":{},"required":["valuesOverride"],"properties":{"valuesOverride":{"description":"Custom Helm values overrides.","type":"object","default":{},"x-kubernetes-preserve-unknown-fields":true}}},"coredns":{"description":"CoreDNS addon.","type":"object","default":{},"required":["valuesOverride"],"properties":{"valuesOverride":{"description":"Custom Helm values overrides.","type":"object","default":{},"x-kubernetes-preserve-unknown-fields":true}}},"fluxcd":{"description":"FluxCD GitOps operator.","type":"object","default":{},"required":["enabled","valuesOverride"],"properties":{"enabled":{"description":"Enable FluxCD.","type":"boolean","default":false},"valuesOverride":{"description":"Custom Helm values overrides.","type":"object","default":{},"x-kubernetes-preserve-unknown-fields":true}}},"gatewayAPI":{"description":"Gateway API addon.","type":"object","default":{},"required":["enabled"],"properties":{"enabled":{"description":"Enable Gateway API.","type":"boolean","default":false}}},"gpuOperator":{"description":"NVIDIA GPU Operator.","type":"object","default":{},"required":["enabled","valuesOverride"],"properties":{"enabled":{"description":"Enable GPU Operator.","type":"boolean","default":false},"valuesOverride":{"description":"Custom Helm values overrides.","type":"object","default":{},"x-kubernetes-preserve-unknown-fields":true}}},"ingressNginx":{"description":"Ingress-NGINX controller.","type":"object","default":{},"required":["enabled","exposeMethod","valuesOverride"],"properties":{"enabled":{"description":"Enable the controller (requires nodes labeled `ingress-nginx`).","type":"boolean","default":false},"exposeMethod":{"description":"Method to expose the controller. Allowed values: `Proxied`, `LoadBalancer`.","type":"string","default":"Proxied","enum":["Proxied","LoadBalancer"]},"hosts":{"description":"Domains routed to this tenant cluster when `exposeMethod` is `Proxied`.","type":"array","default":[],"items":{"type":"string"}},"valuesOverride":{"description":"Custom Helm values overrides.","type":"object","default":{},"x-kubernetes-preserve-unknown-fields":true}}},"monitoringAgents":{"description":"Monitoring agents.","type":"object","default":{},"required":["enabled","valuesOverride"],"properties":{"enabled":{"description":"Enable monitoring agents.","type":"boolean","default":false},"valuesOverride":{"description":"Custom Helm values overrides.","type":"object","default":{},"x-kubernetes-preserve-unknown-fields":true}}},"velero":{"description":"Velero backup/restore addon.","type":"object","default":{},"required":["enabled","valuesOverride"],"properties":{"enabled":{"description":"Enable Velero.","type":"boolean","default":false},"valuesOverride":{"description":"Custom Helm values overrides.","type":"object","default":{},"x-kubernetes-preserve-unknown-fields":true}}},"verticalPodAutoscaler":{"description":"Vertical Pod Autoscaler.","type":"object","default":{},"required":["valuesOverride"],"properties":{"valuesOverride":{"description":"Custom Helm values overrides.","type":"object","default":{},"x-kubernetes-preserve-unknown-fields":true}}}}},"controlPlane":{"description":"Kubernetes control-plane configuration.","type":"object","default":{},"required":["apiServer","controllerManager","konnectivity","replicas","scheduler"],"properties":{"apiServer":{"description":"API Server configuration.","type":"object","default":{},"required":["resources","resourcesPreset"],"properties":{"resources":{"description":"CPU and memory resources for API Server.","type":"object","default":{},"properties":{"cpu":{"description":"CPU available.","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.","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":"Preset if `resources` omitted.","type":"string","default":"large","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]}}},"controllerManager":{"description":"Controller Manager configuration.","type":"object","default":{},"required":["resources","resourcesPreset"],"properties":{"resources":{"description":"CPU and memory resources for Controller Manager.","type":"object","default":{},"properties":{"cpu":{"description":"CPU available.","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.","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":"Preset if `resources` omitted.","type":"string","default":"micro","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]}}},"konnectivity":{"description":"Konnectivity configuration.","type":"object","default":{},"required":["server"],"properties":{"server":{"description":"Konnectivity Server configuration.","type":"object","default":{},"required":["resources","resourcesPreset"],"properties":{"resources":{"description":"CPU and memory resources for Konnectivity.","type":"object","default":{},"properties":{"cpu":{"description":"CPU available.","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.","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":"Preset if `resources` omitted.","type":"string","default":"micro","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]}}}}},"replicas":{"description":"Number of control-plane replicas.","type":"integer","default":2},"scheduler":{"description":"Scheduler configuration.","type":"object","default":{},"required":["resources","resourcesPreset"],"properties":{"resources":{"description":"CPU and memory resources for Scheduler.","type":"object","default":{},"properties":{"cpu":{"description":"CPU available.","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.","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":"Preset if `resources` omitted.","type":"string","default":"micro","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]}}}}},"host":{"description":"External hostname for Kubernetes cluster. Defaults to `.` if empty.","type":"string","default":""},"nodeGroups":{"description":"Worker nodes configuration map.","type":"object","default":{"md0":{"ephemeralStorage":"20Gi","gpus":[],"instanceType":"u1.medium","maxReplicas":10,"minReplicas":0,"resources":{},"roles":["ingress-nginx"]}},"additionalProperties":{"type":"object","required":["ephemeralStorage","instanceType","maxReplicas","minReplicas","resources"],"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":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"gpus":{"description":"List of GPUs to attach (NVIDIA driver requires at least 4 GiB RAM).","type":"array","items":{"type":"object","required":["name"],"properties":{"name":{"description":"Name of GPU, such as \"nvidia.com/AD102GL_L40S\".","type":"string"}}}},"instanceType":{"description":"Virtual machine instance type.","type":"string","default":"u1.medium"},"maxReplicas":{"description":"Maximum number of replicas.","type":"integer","default":10},"minReplicas":{"description":"Minimum number of replicas.","type":"integer","default":0},"resources":{"description":"CPU and memory resources for each worker node.","type":"object","properties":{"cpu":{"description":"CPU available.","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.","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}}},"roles":{"description":"List of node roles.","type":"array","items":{"type":"string"}}}}},"storageClass":{"description":"StorageClass used to store the data.","type":"string","default":"replicated"},"version":{"description":"Kubernetes major.minor version to deploy","type":"string","default":"v1.33","enum":["v1.33","v1.32","v1.31","v1.30","v1.29","v1.28"]}}} + {"title":"Chart Values","type":"object","properties":{"addons":{"description":"Cluster addons configuration.","type":"object","default":{},"required":["certManager","cilium","coredns","fluxcd","gatewayAPI","gpuOperator","ingressNginx","monitoringAgents","nfs","velero","verticalPodAutoscaler"],"properties":{"certManager":{"description":"Cert-manager addon.","type":"object","default":{},"required":["enabled","valuesOverride"],"properties":{"enabled":{"description":"Enable cert-manager.","type":"boolean","default":false},"valuesOverride":{"description":"Custom Helm values overrides.","type":"object","default":{},"x-kubernetes-preserve-unknown-fields":true}}},"cilium":{"description":"Cilium CNI plugin.","type":"object","default":{},"required":["valuesOverride"],"properties":{"valuesOverride":{"description":"Custom Helm values overrides.","type":"object","default":{},"x-kubernetes-preserve-unknown-fields":true}}},"coredns":{"description":"CoreDNS addon.","type":"object","default":{},"required":["valuesOverride"],"properties":{"valuesOverride":{"description":"Custom Helm values overrides.","type":"object","default":{},"x-kubernetes-preserve-unknown-fields":true}}},"fluxcd":{"description":"FluxCD GitOps operator.","type":"object","default":{},"required":["enabled","valuesOverride"],"properties":{"enabled":{"description":"Enable FluxCD.","type":"boolean","default":false},"valuesOverride":{"description":"Custom Helm values overrides.","type":"object","default":{},"x-kubernetes-preserve-unknown-fields":true}}},"gatewayAPI":{"description":"Gateway API addon.","type":"object","default":{},"required":["enabled"],"properties":{"enabled":{"description":"Enable Gateway API.","type":"boolean","default":false}}},"gpuOperator":{"description":"NVIDIA GPU Operator.","type":"object","default":{},"required":["enabled","valuesOverride"],"properties":{"enabled":{"description":"Enable GPU Operator.","type":"boolean","default":false},"valuesOverride":{"description":"Custom Helm values overrides.","type":"object","default":{},"x-kubernetes-preserve-unknown-fields":true}}},"ingressNginx":{"description":"Ingress-NGINX controller.","type":"object","default":{},"required":["enabled","exposeMethod","valuesOverride"],"properties":{"enabled":{"description":"Enable the controller (requires nodes labeled `ingress-nginx`).","type":"boolean","default":false},"exposeMethod":{"description":"Method to expose the controller. Allowed values: `Proxied`, `LoadBalancer`.","type":"string","default":"Proxied","enum":["Proxied","LoadBalancer"]},"hosts":{"description":"Domains routed to this tenant cluster when `exposeMethod` is `Proxied`.","type":"array","default":[],"items":{"type":"string"}},"valuesOverride":{"description":"Custom Helm values overrides.","type":"object","default":{},"x-kubernetes-preserve-unknown-fields":true}}},"monitoringAgents":{"description":"Monitoring agents.","type":"object","default":{},"required":["enabled","valuesOverride"],"properties":{"enabled":{"description":"Enable monitoring agents.","type":"boolean","default":false},"valuesOverride":{"description":"Custom Helm values overrides.","type":"object","default":{},"x-kubernetes-preserve-unknown-fields":true}}},"nfs":{"description":"NFS CSI driver addon.","type":"object","default":{},"required":["valuesOverride"],"properties":{"valuesOverride":{"description":"Custom Helm values overrides.","type":"object","default":{},"x-kubernetes-preserve-unknown-fields":true}}},"velero":{"description":"Velero backup/restore addon.","type":"object","default":{},"required":["enabled","valuesOverride"],"properties":{"enabled":{"description":"Enable Velero.","type":"boolean","default":false},"valuesOverride":{"description":"Custom Helm values overrides.","type":"object","default":{},"x-kubernetes-preserve-unknown-fields":true}}},"verticalPodAutoscaler":{"description":"Vertical Pod Autoscaler.","type":"object","default":{},"required":["valuesOverride"],"properties":{"valuesOverride":{"description":"Custom Helm values overrides.","type":"object","default":{},"x-kubernetes-preserve-unknown-fields":true}}}}},"controlPlane":{"description":"Kubernetes control-plane configuration.","type":"object","default":{},"required":["apiServer","controllerManager","konnectivity","replicas","scheduler"],"properties":{"apiServer":{"description":"API Server configuration.","type":"object","default":{},"required":["resources","resourcesPreset"],"properties":{"resources":{"description":"CPU and memory resources for API Server.","type":"object","default":{},"properties":{"cpu":{"description":"CPU available.","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.","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":"Preset if `resources` omitted.","type":"string","default":"large","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]}}},"controllerManager":{"description":"Controller Manager configuration.","type":"object","default":{},"required":["resources","resourcesPreset"],"properties":{"resources":{"description":"CPU and memory resources for Controller Manager.","type":"object","default":{},"properties":{"cpu":{"description":"CPU available.","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.","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":"Preset if `resources` omitted.","type":"string","default":"micro","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]}}},"konnectivity":{"description":"Konnectivity configuration.","type":"object","default":{},"required":["server"],"properties":{"server":{"description":"Konnectivity Server configuration.","type":"object","default":{},"required":["resources","resourcesPreset"],"properties":{"resources":{"description":"CPU and memory resources for Konnectivity.","type":"object","default":{},"properties":{"cpu":{"description":"CPU available.","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.","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":"Preset if `resources` omitted.","type":"string","default":"micro","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]}}}}},"replicas":{"description":"Number of control-plane replicas.","type":"integer","default":2},"scheduler":{"description":"Scheduler configuration.","type":"object","default":{},"required":["resources","resourcesPreset"],"properties":{"resources":{"description":"CPU and memory resources for Scheduler.","type":"object","default":{},"properties":{"cpu":{"description":"CPU available.","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.","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":"Preset if `resources` omitted.","type":"string","default":"micro","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]}}}}},"host":{"description":"External hostname for Kubernetes cluster. Defaults to `.` if empty.","type":"string","default":""},"nfs":{"description":"NFS configuration.","type":"object","default":{},"properties":{"instances":{"description":"List of NFS volume instances to connect. Each creates a StorageClass named `nfs-` in the tenant cluster.","type":"array","default":[],"items":{"type":"object","required":["name"],"properties":{"name":{"description":"Name of the NFS volume instance in the same namespace.","type":"string"}}}}}},"nodeGroups":{"description":"Worker nodes configuration map.","type":"object","default":{"md0":{"ephemeralStorage":"20Gi","gpus":[],"instanceType":"u1.medium","maxReplicas":10,"minReplicas":0,"resources":{},"roles":["ingress-nginx"]}},"additionalProperties":{"type":"object","required":["ephemeralStorage","instanceType","maxReplicas","minReplicas","resources"],"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":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"gpus":{"description":"List of GPUs to attach (NVIDIA driver requires at least 4 GiB RAM).","type":"array","items":{"type":"object","required":["name"],"properties":{"name":{"description":"Name of GPU, such as \"nvidia.com/AD102GL_L40S\".","type":"string"}}}},"instanceType":{"description":"Virtual machine instance type.","type":"string","default":"u1.medium"},"maxReplicas":{"description":"Maximum number of replicas.","type":"integer","default":10},"minReplicas":{"description":"Minimum number of replicas.","type":"integer","default":0},"resources":{"description":"CPU and memory resources for each worker node.","type":"object","properties":{"cpu":{"description":"CPU available.","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.","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}}},"roles":{"description":"List of node roles.","type":"array","items":{"type":"string"}}}}},"storageClass":{"description":"StorageClass used to store the data.","type":"string","default":"replicated"},"version":{"description":"Kubernetes major.minor version to deploy","type":"string","default":"v1.33","enum":["v1.33","v1.32","v1.31","v1.30","v1.29","v1.28"]}}} release: prefix: kubernetes- labels: @@ -26,7 +26,7 @@ spec: tags: - compute icon: PHN2ZyB3aWR0aD0iMTQ0IiBoZWlnaHQ9IjE0NCIgdmlld0JveD0iMCAwIDE0NCAxNDQiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIxNDQiIGhlaWdodD0iMTQ0IiByeD0iMjQiIGZpbGw9InVybCgjcGFpbnQwX2xpbmVhcl82ODFfMjg0NSkiLz4KPHBhdGggZD0iTTcxLjk5NjggMTlDNzAuMzAzOSAxOS4wMDAyIDY4LjkzMTIgMjAuNTMyMiA2OC45MzE0IDIyLjQyMjFDNjguOTMxNCAyMi40NTExIDY4LjkzNzMgMjIuNDc4OCA2OC45Mzc5IDIyLjUwNzZDNjguOTM1NCAyMi43NjQ0IDY4LjkyMzEgMjMuMDczNyA2OC45MzE0IDIzLjI5NzNDNjguOTcxNyAyNC4zODczIDY5LjIwODIgMjUuMjIxNiA2OS4zNTA2IDI2LjIyNThDNjkuNjA4NCAyOC4zNzUyIDY5LjgyNDUgMzAuMTU2OSA2OS42OTEyIDMxLjgxM0M2OS41NjE1IDMyLjQzNzUgNjkuMTAzNyAzMy4wMDg2IDY4LjY5NTYgMzMuNDA1Nkw2OC42MjM1IDM0LjcwODZDNjYuNzgzOSAzNC44NjE3IDY0LjkzMTkgMzUuMTQyMSA2My4wODIxIDM1LjU2NDFDNTUuMTIyNiAzNy4zNzk4IDQ4LjI2OTUgNDEuNDk5MSA0My4wNTIgNDcuMDYwOUM0Mi43MTM0IDQ2LjgyODggNDIuMTIxMSA0Ni40MDE5IDQxLjk0NSA0Ni4yNzEyQzQxLjM5NzcgNDYuMzQ1NCA0MC44NDQ1IDQ2LjUxNTEgNDAuMTI0MSA0Ni4wOTM1QzM4Ljc1MjIgNDUuMTY1NyAzNy41MDI4IDQzLjg4NTEgMzUuOTkxIDQyLjM0MjRDMzUuMjk4MiA0MS42MDQ0IDM0Ljc5NjYgNDAuOTAxOCAzMy45NzM1IDQwLjE5MDRDMzMuNzg2NiA0MC4wMjg5IDMzLjUwMTQgMzkuODEwNCAzMy4yOTIzIDM5LjY0NDJDMzIuNjQ4OSAzOS4xMjg4IDMxLjg5IDM4Ljg2IDMxLjE1NyAzOC44MzQ4QzMwLjIxNDcgMzguODAyNCAyOS4zMDc1IDM5LjE3MjUgMjguNzEzOCAzOS45MjA2QzI3LjY1ODQgNDEuMjUwNiAyNy45OTYzIDQzLjI4MzMgMjkuNDY3MSA0NC40NjE0QzI5LjQ4MiA0NC40NzM0IDI5LjQ5NzkgNDQuNDgyNyAyOS41MTI5IDQ0LjQ5NDNDMjkuNzE1IDQ0LjY1ODkgMjkuOTYyNSA0NC44Njk4IDMwLjE0ODMgNDUuMDA3NkMzMS4wMjE3IDQ1LjY1NTUgMzEuODE5NSA0NS45ODcyIDMyLjY4OTcgNDYuNTAxNUMzNC41MjMxIDQ3LjYzOTEgMzYuMDQzIDQ4LjU4MjMgMzcuMjQ4NiA0OS43MTk2QzM3LjcxOTQgNTAuMjIzNyAzNy44MDE2IDUxLjExMjIgMzcuODY0MyA1MS40OTY0TDM4Ljg0NjggNTIuMzc4MkMzMy41ODcyIDYwLjMzMDggMzEuMTUzIDcwLjE1MzkgMzIuNTkxNSA4MC4xNjI3TDMxLjMwNzcgODAuNTM3OEMzMC45NjkzIDgwLjk3NjggMzAuNDkxMiA4MS42Njc2IDI5Ljk5MTEgODEuODczOEMyOC40MTM4IDgyLjM3MjkgMjYuNjM4NyA4Mi41NTYyIDI0LjQ5NTYgODIuNzgxOUMyMy40ODk0IDgyLjg2NiAyMi42MjEzIDgyLjgxNTggMjEuNTU0NiA4My4wMTg4QzIxLjMxOTggODMuMDYzNSAyMC45OTI3IDgzLjE0OTEgMjAuNzM1OCA4My4yMDk3QzIwLjcyNjkgODMuMjExNiAyMC43MTg2IDgzLjIxNDIgMjAuNzA5NiA4My4yMTYyQzIwLjY5NTYgODMuMjE5NSAyMC42NzcyIDgzLjIyNjMgMjAuNjYzOCA4My4yMjk0QzE4Ljg1NyA4My42NjggMTcuNjk2MyA4NS4zMzY1IDE4LjA2OTkgODYuOTgwNUMxOC40NDM3IDg4LjYyNDggMjAuMjA4NiA4OS42MjQ4IDIyLjAyNjIgODkuMjMxMkMyMi4wMzkzIDg5LjIyODIgMjIuMDU4NCA4OS4yMjc3IDIyLjA3MiA4OS4yMjQ2QzIyLjA5MjYgODkuMjE5OSAyMi4xMTA2IDg5LjIwOTkgMjIuMTMxIDg5LjIwNDlDMjIuMzg0NCA4OS4xNDkgMjIuNzAxOSA4OS4wODY4IDIyLjkyMzYgODkuMDI3MkMyMy45NzIzIDg4Ljc0NTEgMjQuNzMxOCA4OC4zMzA2IDI1LjY3NDYgODcuOTY3N0MyNy43MDI5IDg3LjIzNjggMjkuMzgyOCA4Ni42MjYyIDMxLjAxOTUgODYuMzg4M0MzMS43MDMgODYuMzM0NSAzMi40MjMyIDg2LjgxMiAzMi43ODE0IDg3LjAxMzRMMzQuMTE3NyA4Ni43ODMxQzM3LjE5MjYgOTYuMzYxMyA0My42MzY2IDEwNC4xMDMgNTEuNzk2MyAxMDguOTYxTDUxLjIzOTYgMTEwLjMwM0M1MS40NDAzIDExMC44MjQgNTEuNjYxNiAxMTEuNTMgNTEuNTEyMSAxMTIuMDQ1QzUwLjkxNzEgMTEzLjU5NSA0OS44OTggMTE1LjIzMSA0OC43Mzc0IDExNy4wNTVDNDguMTc1NSAxMTcuODk4IDQ3LjYwMDQgMTE4LjU1MiA0Ny4wOTM0IDExOS41MTZDNDYuOTcyIDExOS43NDcgNDYuODE3NSAxMjAuMTAyIDQ2LjcwMDQgMTIwLjM0NkM0NS45MTI1IDEyMi4wMzkgNDYuNDkwNCAxMjMuOTkgNDguMDAzOCAxMjQuNzIyQzQ5LjUyNjggMTI1LjQ1OSA1MS40MTcxIDEyNC42ODIgNTIuMjM1MiAxMjIuOTg1QzUyLjIzNjQgMTIyLjk4MiA1Mi4yNDA2IDEyMi45OCA1Mi4yNDE3IDEyMi45NzhDNTIuMjQyNiAxMjIuOTc2IDUyLjI0MDkgMTIyLjk3MyA1Mi4yNDE3IDEyMi45NzFDNTIuMzU4MiAxMjIuNzMxIDUyLjUyMzMgMTIyLjQxNSA1Mi42MjE2IDEyMi4xODhDNTMuMDU2IDEyMS4xODkgNTMuMjAwNSAxMjAuMzMyIDUzLjUwNTkgMTE5LjM2NUM1NC4zMTcgMTE3LjMxOCA1NC43NjI2IDExNS4xNyA1NS44NzkxIDExMy44MzJDNTYuMTg0OSAxMTMuNDY2IDU2LjY4MzMgMTEzLjMyNSA1Ny4yMDAxIDExMy4xODZMNTcuODk0NCAxMTEuOTIyQzY1LjAwOCAxMTQuNjY1IDcyLjk3MDUgMTE1LjQwMiA4MC45MjQ1IDExMy41ODdDODIuNzM5MSAxMTMuMTczIDg0LjQ5MDggMTEyLjYzNyA4Ni4xODQzIDExMS45OTRDODYuMzc5NCAxMTIuMzQyIDg2Ljc0MiAxMTMuMDExIDg2LjgzOTMgMTEzLjE3OUM4Ny4zNjQ0IDExMy4zNTEgODcuOTM3NyAxMTMuNDM5IDg4LjQwNDcgMTE0LjEzM0M4OS4yNDAxIDExNS41NjcgODkuODExNCAxMTcuMjYzIDkwLjUwNzMgMTE5LjMxMkM5MC44MTI4IDEyMC4yNzkgOTAuOTYzOCAxMjEuMTM2IDkxLjM5ODEgMTIyLjEzNkM5MS40OTcxIDEyMi4zNjMgOTEuNjYxNCAxMjIuNjg0IDkxLjc3OCAxMjIuOTI1QzkyLjU5NDQgMTI0LjYyOCA5NC40OTA3IDEyNS40MDcgOTYuMDE1OSAxMjQuNjY5Qzk3LjUyOTIgMTIzLjkzNyA5OC4xMDc3IDEyMS45ODYgOTcuMzE5NCAxMjAuMjkzQzk3LjIwMjMgMTIwLjA0OSA5Ny4wNDEyIDExOS42OTUgOTYuOTE5OCAxMTkuNDY0Qzk2LjQxMjcgMTE4LjQ5OSA5NS44Mzc3IDExNy44NTIgOTUuMjc1OCAxMTcuMDA5Qzk0LjExNTIgMTE1LjE4NSA5My4xNTI2IDExMy42NyA5Mi41NTc1IDExMi4xMkM5Mi4zMDg3IDExMS4zMiA5Mi41OTk1IDExMC44MjMgOTIuNzkzMyAxMTAuMzAzQzkyLjY3NzIgMTEwLjE3IDkyLjQyODggMTA5LjQxNCA5Mi4yODI0IDEwOS4wNTlDMTAwLjc2MiAxMDQuMDI5IDEwNy4wMTcgOTUuOTk4NSAxMDkuOTU1IDg2LjcyMzlDMTEwLjM1MSA4Ni43ODY1IDExMS4wNDEgODYuOTA5MSAxMTEuMjY1IDg2Ljk1NDJDMTExLjcyNiA4Ni42NDg3IDExMi4xNDkgODYuMjUwMSAxMTIuOTgxIDg2LjMxNTlDMTE0LjYxNyA4Ni41NTM3IDExNi4yOTcgODcuMTY0NSAxMTguMzI2IDg3Ljg5NTNDMTE5LjI2OCA4OC4yNTgxIDEyMC4wMjggODguNjc5MyAxMjEuMDc3IDg4Ljk2MTRDMTIxLjI5OCA4OS4wMjEgMTIxLjYxNiA4OS4wNzY2IDEyMS44NjkgODkuMTMyNUMxMjEuODg5IDg5LjEzNzUgMTIxLjkwOCA4OS4xNDc1IDEyMS45MjggODkuMTUyMkMxMjEuOTQyIDg5LjE1NTMgMTIxLjk2MSA4OS4xNTU4IDEyMS45NzQgODkuMTU4OEMxMjMuNzkyIDg5LjU1MiAxMjUuNTU3IDg4LjU1MjYgMTI1LjkzIDg2LjkwODFDMTI2LjMwMyA4NS4yNjQxIDEyNS4xNDMgODMuNTk1MiAxMjMuMzM2IDgzLjE1N0MxMjMuMDc0IDgzLjA5NyAxMjIuNzAxIDgyLjk5NSAxMjIuNDQ2IDgyLjk0NjVDMTIxLjM3OSA4Mi43NDM1IDEyMC41MTEgODIuNzkzNSAxMTkuNTA1IDgyLjcwOTVDMTE3LjM2MSA4Mi40ODM5IDExNS41ODYgODIuMzAwNCAxMTQuMDA5IDgxLjgwMTRDMTEzLjM2NiA4MS41NTA3IDExMi45MDggODAuNzgxOSAxMTIuNjg2IDgwLjQ2NTVMMTExLjQ0OCA4MC4xMDM1QzExMi4wOSA3NS40MzggMTExLjkxNyA3MC41ODI1IDExMC44MDYgNjUuNzI0M0MxMDkuNjg1IDYwLjgyMDggMTA3LjcwNCA1Ni4zMzYxIDEwNS4wNjIgNTIuMzg0OEMxMDUuMzc5IDUyLjA5NDggMTA1Ljk3OSA1MS41NjEyIDEwNi4xNDkgNTEuNDA0M0MxMDYuMTk5IDUwLjg1MTcgMTA2LjE1NiA1MC4yNzIyIDEwNi43MjUgNDkuNjYwM0MxMDcuOTMxIDQ4LjUyMyAxMDkuNDUxIDQ3LjU3OTkgMTExLjI4NCA0Ni40NDIzQzExMi4xNTQgNDUuOTI3OSAxMTIuOTU5IDQ1LjU5NjQgMTEzLjgzMiA0NC45NDg0QzExNC4wMyA0NC44MDE5IDExNC4yOTkgNDQuNTY5OSAxMTQuNTA3IDQ0LjQwMjJDMTE1Ljk3NyA0My4yMjM3IDExNi4zMTYgNDEuMTkxMSAxMTUuMjYgMzkuODYxNEMxMTQuMjA0IDM4LjUzMTcgMTEyLjE1OSAzOC40MDY1IDExMC42ODggMzkuNTg1QzExMC40NzkgMzkuNzUxNiAxMTAuMTk1IDM5Ljk2ODggMTEwLjAwNyA0MC4xMzEyQzEwOS4xODQgNDAuODQyNiAxMDguNjc2IDQxLjU0NTIgMTA3Ljk4MyA0Mi4yODMyQzEwNi40NzEgNDMuODI1OSAxMDUuMjIyIDQ1LjExMyAxMDMuODUgNDYuMDQwOUMxMDMuMjU1IDQ2LjM4ODUgMTAyLjM4NSA0Ni4yNjgyIDEwMS45OSA0Ni4yNDQ5TDEwMC44MjQgNDcuMDgwNkM5NC4xNzUzIDQwLjA3NjMgODUuMTIzNSAzNS41OTgyIDc1LjM3NjYgMzQuNzI4M0M3NS4zNDk0IDM0LjMxNzkgNzUuMzEzNyAzMy41NzYxIDc1LjMwNDYgMzMuMzUyOUM3NC45MDU2IDMyLjk2OTMgNzQuNDIzNSAzMi42NDE4IDc0LjMwMjQgMzEuODEzQzc0LjE2OTEgMzAuMTU2OSA3NC4zOTE3IDI4LjM3NTIgNzQuNjQ5NiAyNi4yMjU4Qzc0Ljc5MTkgMjUuMjIxNiA3NS4wMjg0IDI0LjM4NzMgNzUuMDY4OCAyMy4yOTczQzc1LjA3OCAyMy4wNDk1IDc1LjA2MzIgMjIuNjkgNzUuMDYyMiAyMi40MjIxQzc1LjA2MiAyMC41MzIyIDczLjY4OTggMTguOTk5OCA3MS45OTY4IDE5Wk02OC4xNTg1IDQyLjg4ODZMNjcuMjQ4IDU5LjA0NDdMNjcuMTgyNSA1OS4wNzc2QzY3LjEyMTQgNjAuNTIyOSA2NS45Mzc1IDYxLjY3NyA2NC40ODM5IDYxLjY3N0M2My44ODg0IDYxLjY3NyA2My4zMzg4IDYxLjQ4NDkgNjIuODkyMiA2MS4xNTcxTDYyLjg2NiA2MS4xNzAzTDQ5LjY4MDcgNTEuNzc5NEM1My43MzMxIDQ3Ljc3NTkgNTguOTE2NCA0NC44MTcyIDY0Ljg5IDQzLjQ1NDZDNjUuOTgxMiA0My4yMDU2IDY3LjA3MTkgNDMuMDIwOSA2OC4xNTg1IDQyLjg4ODZaTTc1Ljg0MTcgNDIuODg4NkM4Mi44MTU5IDQzLjc1MDQgODkuMjY1NyA0Ni45MjMyIDk0LjIwODEgNTEuNzg2TDgxLjEwOCA2MS4xMTc2TDgxLjA2MjEgNjEuMDk3OUM3OS44OTk0IDYxLjk1MTIgNzguMjYxMSA2MS43Mzk0IDc3LjM1NDggNjAuNTk3OEM3Ni45ODM1IDYwLjEzMDEgNzYuNzg4NyA1OS41ODAxIDc2Ljc2NTMgNTkuMDI0OUw3Ni43NTIyIDU5LjAxODRMNzUuODQxNyA0Mi44ODg2Wk00NC44OTkxIDU3LjgxNEw1Ni45MzgyIDY4LjYzM0w1Ni45MjUxIDY4LjY5ODhDNTguMDExNyA2OS42NDc5IDU4LjE3MiA3MS4yOTQ5IDU3LjI2NTcgNzIuNDM2OEM1Ni44OTQ0IDcyLjkwNDUgNTYuMzk3NSA3My4yMTgyIDU1Ljg2MzkgNzMuMzY0N0w1NS44NTA4IDczLjQxNzNMNDAuNDE4OCA3Ny44OTIzQzM5LjYzMzQgNzAuNjc2NSA0MS4zMjYxIDYzLjY2MjEgNDQuODk5MSA1Ny44MTRaTTk5LjAwOTQgNTcuODIwNkMxMDAuNzk4IDYwLjczMzYgMTAyLjE1MyA2My45ODcxIDEwMi45NTkgNjcuNTE0M0MxMDMuNzU2IDcwLjk5OTEgMTAzLjk1NiA3NC40Nzc4IDEwMy42MjcgNzcuODM5N0w4OC4xMTY2IDczLjM1MTVMODguMTAzNSA3My4yODU3Qzg2LjcxNDUgNzIuOTA0MyA4NS44NjA5IDcxLjQ4NDggODYuMTg0MyA3MC4wNjExQzg2LjMxNjggNjkuNDc3OCA4Ni42MjQ5IDY4Ljk4NDQgODcuMDQyMyA2OC42MTk4TDg3LjAzNTggNjguNTg2OUw5OS4wMDk0IDU3LjgyMDZaTTY5LjUyNzQgNjkuNDY4OEg3NC40NTk2TDc3LjUyNTEgNzMuMzE4Nkw3Ni40MjQ3IDc4LjEyMjZMNzEuOTk2OCA4MC4yNjE0TDY3LjU1NTggNzguMTE2MUw2Ni40NTU0IDczLjMxMkw2OS41Mjc0IDY5LjQ2ODhaTTg1LjMzOTMgODIuNjQzN0M4NS41NDg5IDgyLjYzMzEgODUuNzU3NiA4Mi42NTIgODUuOTYxNiA4Mi42ODk4TDg1Ljk4NzggODIuNjU2OUwxMDEuOTUgODUuMzY4MkM5OS42MTQyIDkxLjk2MjQgOTUuMTQ0IDk3LjY3NSA4OS4xNzExIDEwMS40OThMODIuOTc0NyA4Ni40NjA2TDgyLjk5NDQgODYuNDM0M0M4Mi40MjUyIDg1LjEwNTUgODIuOTk0OCA4My41NDcyIDg0LjMwNDQgODIuOTEzNUM4NC42Mzk3IDgyLjc1MTMgODQuOTkgODIuNjYxNCA4NS4zMzkzIDgyLjY0MzdaTTU4LjUyOTggODIuNzA5NUM1OS43NDggODIuNzI2NyA2MC44NDA2IDgzLjU3NjEgNjEuMTIzNyA4NC44MjJDNjEuMjU2MiA4NS40MDUyIDYxLjE5MTcgODUuOTgzMSA2MC45NzMgODYuNDkzNUw2MS4wMTg5IDg2LjU1MjdMNTQuODg4IDEwMS40MzlDNDkuMTU1OSA5Ny43NDMyIDQ0LjU5MDQgOTIuMjA5OSA0Mi4xNDgxIDg1LjQyMDhMNTcuOTczMSA4Mi43MjI3TDU3Ljk5OTMgODIuNzU1NkM1OC4xNzYzIDgyLjcyMjkgNTguMzU1OCA4Mi43MDcxIDU4LjUyOTggODIuNzA5NVpNNzEuODk4NiA4OS4yMzEyQzcyLjMyMjkgODkuMjE1NSA3Mi43NTM0IDg5LjMwMyA3My4xNjI3IDg5LjUwMUM3My42OTkyIDg5Ljc2MDYgNzQuMTEzNiA5MC4xNjkyIDc0LjM3NDUgOTAuNjU5Mkg3NC40MzM0TDgyLjIzNDYgMTA0LjgyMUM4MS4yMjIxIDEwNS4xNjIgODAuMTgxMyAxMDUuNDU0IDc5LjExNjcgMTA1LjY5N0M3My4xNTA1IDEwNy4wNTggNjcuMjAzMiAxMDYuNjQ1IDYxLjgxOCAxMDQuODAyTDY5LjU5OTUgOTAuNjY1OEg2OS42MTI2QzcwLjA3OTUgODkuNzg4OCA3MC45NjUgODkuMjY1NiA3MS44OTg2IDg5LjIzMTJaIiBmaWxsPSJ3aGl0ZSIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIwLjI1Ii8+CjxkZWZzPgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50MF9saW5lYXJfNjgxXzI4NDUiIHgxPSIxMCIgeTE9IjE1LjUiIHgyPSIxNDQiIHkyPSIxMzEuNSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBzdG9wLWNvbG9yPSIjNEQ4N0ZGIi8+CjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzA1NDdEMCIvPgo8L2xpbmVhckdyYWRpZW50Pgo8L2RlZnM+Cjwvc3ZnPgo= - keysOrder: [["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata", "name"], ["spec", "storageClass"], ["spec", "nodeGroups"], ["spec", "nodeGroups", "md0"], ["spec", "nodeGroups", "md0", "minReplicas"], ["spec", "nodeGroups", "md0", "maxReplicas"], ["spec", "nodeGroups", "md0", "instanceType"], ["spec", "nodeGroups", "md0", "ephemeralStorage"], ["spec", "nodeGroups", "md0", "roles"], ["spec", "nodeGroups", "md0", "resources"], ["spec", "nodeGroups", "md0", "gpus"], ["spec", "version"], ["spec", "host"], ["spec", "addons"], ["spec", "addons", "certManager"], ["spec", "addons", "certManager", "enabled"], ["spec", "addons", "certManager", "valuesOverride"], ["spec", "addons", "cilium"], ["spec", "addons", "cilium", "valuesOverride"], ["spec", "addons", "gatewayAPI"], ["spec", "addons", "gatewayAPI", "enabled"], ["spec", "addons", "ingressNginx"], ["spec", "addons", "ingressNginx", "enabled"], ["spec", "addons", "ingressNginx", "exposeMethod"], ["spec", "addons", "ingressNginx", "hosts"], ["spec", "addons", "ingressNginx", "valuesOverride"], ["spec", "addons", "gpuOperator"], ["spec", "addons", "gpuOperator", "enabled"], ["spec", "addons", "gpuOperator", "valuesOverride"], ["spec", "addons", "fluxcd"], ["spec", "addons", "fluxcd", "enabled"], ["spec", "addons", "fluxcd", "valuesOverride"], ["spec", "addons", "monitoringAgents"], ["spec", "addons", "monitoringAgents", "enabled"], ["spec", "addons", "monitoringAgents", "valuesOverride"], ["spec", "addons", "verticalPodAutoscaler"], ["spec", "addons", "verticalPodAutoscaler", "valuesOverride"], ["spec", "addons", "velero"], ["spec", "addons", "velero", "enabled"], ["spec", "addons", "velero", "valuesOverride"], ["spec", "addons", "coredns"], ["spec", "addons", "coredns", "valuesOverride"], ["spec", "controlPlane"], ["spec", "controlPlane", "replicas"], ["spec", "controlPlane", "apiServer"], ["spec", "controlPlane", "apiServer", "resources"], ["spec", "controlPlane", "apiServer", "resourcesPreset"], ["spec", "controlPlane", "controllerManager"], ["spec", "controlPlane", "controllerManager", "resources"], ["spec", "controlPlane", "controllerManager", "resourcesPreset"], ["spec", "controlPlane", "scheduler"], ["spec", "controlPlane", "scheduler", "resources"], ["spec", "controlPlane", "scheduler", "resourcesPreset"], ["spec", "controlPlane", "konnectivity"], ["spec", "controlPlane", "konnectivity", "server"], ["spec", "controlPlane", "konnectivity", "server", "resources"], ["spec", "controlPlane", "konnectivity", "server", "resourcesPreset"]] + keysOrder: [["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata", "name"], ["spec", "storageClass"], ["spec", "nodeGroups"], ["spec", "nodeGroups", "md0"], ["spec", "nodeGroups", "md0", "minReplicas"], ["spec", "nodeGroups", "md0", "maxReplicas"], ["spec", "nodeGroups", "md0", "instanceType"], ["spec", "nodeGroups", "md0", "ephemeralStorage"], ["spec", "nodeGroups", "md0", "roles"], ["spec", "nodeGroups", "md0", "resources"], ["spec", "nodeGroups", "md0", "gpus"], ["spec", "version"], ["spec", "host"], ["spec", "nfs"], ["spec", "nfs", "instances"], ["spec", "addons"], ["spec", "addons", "certManager"], ["spec", "addons", "certManager", "enabled"], ["spec", "addons", "certManager", "valuesOverride"], ["spec", "addons", "cilium"], ["spec", "addons", "cilium", "valuesOverride"], ["spec", "addons", "gatewayAPI"], ["spec", "addons", "gatewayAPI", "enabled"], ["spec", "addons", "ingressNginx"], ["spec", "addons", "ingressNginx", "enabled"], ["spec", "addons", "ingressNginx", "exposeMethod"], ["spec", "addons", "ingressNginx", "hosts"], ["spec", "addons", "ingressNginx", "valuesOverride"], ["spec", "addons", "gpuOperator"], ["spec", "addons", "gpuOperator", "enabled"], ["spec", "addons", "gpuOperator", "valuesOverride"], ["spec", "addons", "fluxcd"], ["spec", "addons", "fluxcd", "enabled"], ["spec", "addons", "fluxcd", "valuesOverride"], ["spec", "addons", "monitoringAgents"], ["spec", "addons", "monitoringAgents", "enabled"], ["spec", "addons", "monitoringAgents", "valuesOverride"], ["spec", "addons", "verticalPodAutoscaler"], ["spec", "addons", "verticalPodAutoscaler", "valuesOverride"], ["spec", "addons", "velero"], ["spec", "addons", "velero", "enabled"], ["spec", "addons", "velero", "valuesOverride"], ["spec", "addons", "coredns"], ["spec", "addons", "coredns", "valuesOverride"], ["spec", "addons", "nfs"], ["spec", "addons", "nfs", "valuesOverride"], ["spec", "controlPlane"], ["spec", "controlPlane", "replicas"], ["spec", "controlPlane", "apiServer"], ["spec", "controlPlane", "apiServer", "resources"], ["spec", "controlPlane", "apiServer", "resourcesPreset"], ["spec", "controlPlane", "controllerManager"], ["spec", "controlPlane", "controllerManager", "resources"], ["spec", "controlPlane", "controllerManager", "resourcesPreset"], ["spec", "controlPlane", "scheduler"], ["spec", "controlPlane", "scheduler", "resources"], ["spec", "controlPlane", "scheduler", "resourcesPreset"], ["spec", "controlPlane", "konnectivity"], ["spec", "controlPlane", "konnectivity", "server"], ["spec", "controlPlane", "konnectivity", "server", "resources"], ["spec", "controlPlane", "konnectivity", "server", "resourcesPreset"]] secrets: exclude: [] include: diff --git a/packages/system/nfs-driver/Makefile b/packages/system/nfs-driver/Makefile index 2f14aa28..6cf799f5 100644 --- a/packages/system/nfs-driver/Makefile +++ b/packages/system/nfs-driver/Makefile @@ -4,6 +4,9 @@ export NAMESPACE=cozy-$(NAME) include ../../../hack/common-envs.mk include ../../../hack/package.mk +test: + helm unittest . + update: rm -rf charts helm repo add csi-driver-nfs https://raw.githubusercontent.com/kubernetes-csi/csi-driver-nfs/master/charts diff --git a/packages/system/nfs-driver/templates/extra-storageclasses.yaml b/packages/system/nfs-driver/templates/extra-storageclasses.yaml new file mode 100644 index 00000000..f627a474 --- /dev/null +++ b/packages/system/nfs-driver/templates/extra-storageclasses.yaml @@ -0,0 +1,18 @@ +{{- range .Values.extraStorageClasses }} +--- +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: {{ .name }} +provisioner: nfs.csi.k8s.io +parameters: + server: {{ .server | quote }} + share: {{ .share | quote }} +reclaimPolicy: Delete +volumeBindingMode: Immediate +allowVolumeExpansion: true +{{- with .mountOptions }} +mountOptions: +{{- toYaml . | nindent 2 }} +{{- end }} +{{- end }} diff --git a/packages/system/nfs-driver/tests/extra-storageclasses_test.yaml b/packages/system/nfs-driver/tests/extra-storageclasses_test.yaml new file mode 100644 index 00000000..6009c761 --- /dev/null +++ b/packages/system/nfs-driver/tests/extra-storageclasses_test.yaml @@ -0,0 +1,133 @@ +suite: Extra StorageClasses tests + +templates: + - templates/extra-storageclasses.yaml + +tests: + - it: renders nothing when extraStorageClasses is empty + asserts: + - hasDocuments: + count: 0 + + - it: creates one StorageClass per entry + set: + extraStorageClasses: + - name: nfs-data + server: "10.96.1.1" + share: "/" + mountOptions: + - nfsvers=4.1 + - name: nfs-backup + server: "10.96.2.2" + share: "/" + mountOptions: + - nfsvers=4.1 + asserts: + - hasDocuments: + count: 2 + + - it: sets correct provisioner + set: + extraStorageClasses: + - name: nfs-test + server: "10.96.1.1" + share: "/" + mountOptions: + - nfsvers=4.1 + asserts: + - equal: + path: provisioner + value: nfs.csi.k8s.io + + - it: sets server and share parameters + set: + extraStorageClasses: + - name: nfs-mydata + server: "10.96.50.100" + share: "/exports" + mountOptions: + - nfsvers=4.1 + asserts: + - equal: + path: metadata.name + value: nfs-mydata + - equal: + path: parameters.server + value: "10.96.50.100" + - equal: + path: parameters.share + value: "/exports" + + - it: sets mount options + set: + extraStorageClasses: + - name: nfs-test + server: "10.96.1.1" + share: "/" + mountOptions: + - nfsvers=4.1 + asserts: + - equal: + path: mountOptions[0] + value: nfsvers=4.1 + + - it: allows volume expansion + set: + extraStorageClasses: + - name: nfs-test + server: "10.96.1.1" + share: "/" + mountOptions: + - nfsvers=4.1 + asserts: + - equal: + path: allowVolumeExpansion + value: true + + - it: uses Delete reclaim policy + set: + extraStorageClasses: + - name: nfs-test + server: "10.96.1.1" + share: "/" + mountOptions: + - nfsvers=4.1 + asserts: + - equal: + path: reclaimPolicy + value: Delete + + - it: creates distinct StorageClasses for multiple shares + set: + extraStorageClasses: + - name: nfs-alpha + server: "10.96.10.1" + share: "/" + mountOptions: + - nfsvers=4.1 + - name: nfs-beta + server: "10.96.20.2" + share: "/data" + mountOptions: + - nfsvers=4.1 + asserts: + - equal: + path: metadata.name + value: nfs-alpha + documentIndex: 0 + - equal: + path: parameters.server + value: "10.96.10.1" + documentIndex: 0 + - equal: + path: metadata.name + value: nfs-beta + documentIndex: 1 + - equal: + path: parameters.server + value: "10.96.20.2" + documentIndex: 1 + - equal: + path: parameters.share + value: "/data" + documentIndex: 1 diff --git a/packages/system/nfs-driver/values.yaml b/packages/system/nfs-driver/values.yaml index 856e67be..eb1f99f0 100644 --- a/packages/system/nfs-driver/values.yaml +++ b/packages/system/nfs-driver/values.yaml @@ -1 +1,3 @@ csi-driver-nfs: {} + +extraStorageClasses: [] diff --git a/packages/system/nfs-rd/Chart.yaml b/packages/system/nfs-rd/Chart.yaml new file mode 100644 index 00000000..20c4e272 --- /dev/null +++ b/packages/system/nfs-rd/Chart.yaml @@ -0,0 +1,3 @@ +apiVersion: v2 +name: nfs-rd +version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process diff --git a/packages/system/nfs-rd/Makefile b/packages/system/nfs-rd/Makefile new file mode 100644 index 00000000..058bec71 --- /dev/null +++ b/packages/system/nfs-rd/Makefile @@ -0,0 +1,4 @@ +export NAME=nfs-rd +export NAMESPACE=cozy-system + +include ../../../hack/package.mk diff --git a/packages/system/nfs-rd/cozyrds/nfs.yaml b/packages/system/nfs-rd/cozyrds/nfs.yaml new file mode 100644 index 00000000..125401e6 --- /dev/null +++ b/packages/system/nfs-rd/cozyrds/nfs.yaml @@ -0,0 +1,21 @@ +apiVersion: cozystack.io/v1alpha1 +kind: ApplicationDefinition +metadata: + name: nfs +spec: + application: + kind: NFS + plural: nfs + openAPISchema: |- + {"title":"Chart Values","type":"object","properties":{"size":{"description":"Volume size for NFS storage.","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","default":"replicated"}}} + release: + prefix: nfs- + chartRef: + kind: ExternalArtifact + name: cozystack-nfs-application-default-nfs + namespace: cozy-system + dashboard: + description: Managed NFS storage + plural: NFS + icon: PHN2ZyB3aWR0aD0iMTQ0IiBoZWlnaHQ9IjE0NCIgdmlld0JveD0iMCAwIDE0NCAxNDQiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIxNDQiIGhlaWdodD0iMTQ0IiByeD0iMjQiIGZpbGw9InVybCgjcGFpbnQwX2xpbmVhcikiLz4KPHBhdGggZD0iTTM2IDU4VjEwNEMzNiAxMDcuMzE0IDM4LjY4NjMgMTEwIDQyIDExMEgxMDJDMTA1LjMxNCAxMTAgMTA4IDEwNy4zMTQgMTA4IDEwNFY1MkMxMDggNDguNjg2MyAxMDUuMzE0IDQ2IDEwMiA0Nkg3MEw2MiAzNEg0MkMzOC42ODYzIDM0IDM2IDM2LjY4NjMgMzYgNDBWNThaIiBmaWxsPSIjMTU2NUMwIi8+CjxwYXRoIGQ9Ik0zMCA2MkMzMCA1OC42ODYzIDMyLjY4NjMgNTYgMzYgNTZIMTA4QzExMS4zMTQgNTYgMTE0IDU4LjY4NjMgMTE0IDYyVjEwNEMxMTQgMTA3LjMxNCAxMTEuMzE0IDExMCAxMDggMTEwSDM2QzMyLjY4NjMgMTEwIDMwIDEwNy4zMTQgMzAgMTA0VjYyWiIgZmlsbD0iIzQyQTVGNSIvPgo8cGF0aCBkPSJNNjMgNzZWOTRNNjMgNzZMNTQgODVNNjMgNzZMNzIgODUiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iNCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+CjxwYXRoIGQ9Ik04MSA5NFY3Nk04MSA5NEw5MCA4NU04MSA5NEw3MiA4NSIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPGRlZnM+CjxsaW5lYXJHcmFkaWVudCBpZD0icGFpbnQwX2xpbmVhciIgeDE9IjAiIHkxPSIwIiB4Mj0iMTQ0IiB5Mj0iMTQ0IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wIHN0b3AtY29sb3I9IiNFM0YyRkQiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjNjRCNUY2Ii8+CjwvbGluZWFyR3JhZGllbnQ+CjwvZGVmcz4KPC9zdmc+Cg== + keysOrder: [["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata", "name"], ["spec", "storageClass"], ["spec", "size"]] diff --git a/packages/system/nfs-rd/templates/cozyrd.yaml b/packages/system/nfs-rd/templates/cozyrd.yaml new file mode 100644 index 00000000..e079ddcf --- /dev/null +++ b/packages/system/nfs-rd/templates/cozyrd.yaml @@ -0,0 +1,4 @@ +{{- range $path, $_ := .Files.Glob "cozyrds/*" }} +--- +{{ $.Files.Get $path }} +{{- end }} diff --git a/packages/system/nfs-rd/values.yaml b/packages/system/nfs-rd/values.yaml new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/packages/system/nfs-rd/values.yaml @@ -0,0 +1 @@ +{} diff --git a/packages/system/nfs/.helmignore b/packages/system/nfs/.helmignore new file mode 100644 index 00000000..481ff39c --- /dev/null +++ b/packages/system/nfs/.helmignore @@ -0,0 +1,2 @@ +logos/ +hack/ diff --git a/packages/system/nfs/Chart.yaml b/packages/system/nfs/Chart.yaml new file mode 100644 index 00000000..fed4e8ae --- /dev/null +++ b/packages/system/nfs/Chart.yaml @@ -0,0 +1,3 @@ +apiVersion: v2 +name: cozy-nfs +version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process diff --git a/packages/system/nfs/Makefile b/packages/system/nfs/Makefile new file mode 100644 index 00000000..194a6fa4 --- /dev/null +++ b/packages/system/nfs/Makefile @@ -0,0 +1,3 @@ +export NAME=nfs-system + +include ../../../hack/package.mk diff --git a/packages/system/nfs/templates/cilium-policy.yaml b/packages/system/nfs/templates/cilium-policy.yaml new file mode 100644 index 00000000..93008d07 --- /dev/null +++ b/packages/system/nfs/templates/cilium-policy.yaml @@ -0,0 +1,20 @@ +{{- $pvc := lookup "v1" "PersistentVolumeClaim" .Release.Namespace .Values.pvcName }} +{{- $pv := lookup "v1" "PersistentVolume" "" $pvc.spec.volumeName }} +{{- $nfsExport := index $pv.spec.csi.volumeAttributes "linstor.csi.linbit.com/nfs-export" }} +{{- $parsed := urlParse $nfsExport }} +{{- $port := last (splitList ":" $parsed.host) }} +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: allow-nfs-{{ .Values.pvcName }} +spec: + endpointSelector: {} + egress: + - toEndpoints: + - matchLabels: + k8s:app.kubernetes.io/component: linstor-csi-nfs-server + k8s:io.kubernetes.pod.namespace: cozy-linstor + toPorts: + - ports: + - port: "{{ $port }}" + protocol: TCP diff --git a/packages/system/nfs/values.yaml b/packages/system/nfs/values.yaml new file mode 100644 index 00000000..b405be18 --- /dev/null +++ b/packages/system/nfs/values.yaml @@ -0,0 +1 @@ +pvcName: "" From 053481f248b443e0791489558056a957581f1c1f Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Thu, 12 Feb 2026 15:55:24 +0100 Subject: [PATCH 021/666] fix(nfs): add required fields to ApplicationDefinition and include in IaaS bundle Add missing required fields (singular, category) to NFS ApplicationDefinition and add nfs-application to the IaaS bundle. Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- packages/core/platform/templates/bundles/iaas.yaml | 1 + packages/system/nfs-rd/cozyrds/nfs.yaml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/packages/core/platform/templates/bundles/iaas.yaml b/packages/core/platform/templates/bundles/iaas.yaml index 7c856b8c..8ee5d101 100644 --- a/packages/core/platform/templates/bundles/iaas.yaml +++ b/packages/core/platform/templates/bundles/iaas.yaml @@ -12,6 +12,7 @@ {{include "cozystack.platform.package.default" (list "cozystack.capi-provider-cp-kamaji" $) }} {{include "cozystack.platform.package.default" (list "cozystack.capi-provider-infra-kubevirt" $) }} {{include "cozystack.platform.package.default" (list "cozystack.bucket-application" $) }} +{{include "cozystack.platform.package.default" (list "cozystack.nfs-application" $) }} {{include "cozystack.platform.package" (list "cozystack.kubernetes-application" "kubevirt" $) }} {{include "cozystack.platform.package" (list "cozystack.virtual-machine-application" "kubevirt" $) }} {{include "cozystack.platform.package" (list "cozystack.virtualprivatecloud-application" "kubevirt" $) }} diff --git a/packages/system/nfs-rd/cozyrds/nfs.yaml b/packages/system/nfs-rd/cozyrds/nfs.yaml index 125401e6..9a51f6af 100644 --- a/packages/system/nfs-rd/cozyrds/nfs.yaml +++ b/packages/system/nfs-rd/cozyrds/nfs.yaml @@ -5,6 +5,7 @@ metadata: spec: application: kind: NFS + singular: nfs plural: nfs openAPISchema: |- {"title":"Chart Values","type":"object","properties":{"size":{"description":"Volume size for NFS storage.","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","default":"replicated"}}} @@ -15,6 +16,8 @@ spec: name: cozystack-nfs-application-default-nfs namespace: cozy-system dashboard: + category: IaaS + singular: NFS description: Managed NFS storage plural: NFS icon: PHN2ZyB3aWR0aD0iMTQ0IiBoZWlnaHQ9IjE0NCIgdmlld0JveD0iMCAwIDE0NCAxNDQiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIxNDQiIGhlaWdodD0iMTQ0IiByeD0iMjQiIGZpbGw9InVybCgjcGFpbnQwX2xpbmVhcikiLz4KPHBhdGggZD0iTTM2IDU4VjEwNEMzNiAxMDcuMzE0IDM4LjY4NjMgMTEwIDQyIDExMEgxMDJDMTA1LjMxNCAxMTAgMTA4IDEwNy4zMTQgMTA4IDEwNFY1MkMxMDggNDguNjg2MyAxMDUuMzE0IDQ2IDEwMiA0Nkg3MEw2MiAzNEg0MkMzOC42ODYzIDM0IDM2IDM2LjY4NjMgMzYgNDBWNThaIiBmaWxsPSIjMTU2NUMwIi8+CjxwYXRoIGQ9Ik0zMCA2MkMzMCA1OC42ODYzIDMyLjY4NjMgNTYgMzYgNTZIMTA4QzExMS4zMTQgNTYgMTE0IDU4LjY4NjMgMTE0IDYyVjEwNEMxMTQgMTA3LjMxNCAxMTEuMzE0IDExMCAxMDggMTEwSDM2QzMyLjY4NjMgMTEwIDMwIDEwNy4zMTQgMzAgMTA0VjYyWiIgZmlsbD0iIzQyQTVGNSIvPgo8cGF0aCBkPSJNNjMgNzZWOTRNNjMgNzZMNTQgODVNNjMgNzZMNzIgODUiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iNCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+CjxwYXRoIGQ9Ik04MSA5NFY3Nk04MSA5NEw5MCA4NU04MSA5NEw3MiA4NSIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPGRlZnM+CjxsaW5lYXJHcmFkaWVudCBpZD0icGFpbnQwX2xpbmVhciIgeDE9IjAiIHkxPSIwIiB4Mj0iMTQ0IiB5Mj0iMTQ0IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wIHN0b3AtY29sb3I9IiNFM0YyRkQiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjNjRCNUY2Ii8+CjwvbGluZWFyR3JhZGllbnQ+CjwvZGVmcz4KPC9zdmc+Cg== From a628adeb35826ff292ecd0eeefbf8f0c0b16e493 Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Thu, 12 Feb 2026 17:18:14 +0100 Subject: [PATCH 022/666] [platform] Switch cozystack-api from DaemonSet to Deployment with PreferClose Replace DaemonSet with direct host API access in favor of a regular Deployment using Service trafficDistribution: PreferClose. This provides prefer-local routing to the nearest cozystack-api pod with fallback to remote pods when local one is unavailable. - Replace DaemonSet/Deployment toggle with always-Deployment - Replace internalTrafficPolicy: Local with trafficDistribution: PreferClose - Remove KUBERNETES_SERVICE_HOST/PORT override (use default kubernetes service) - Replace hard nodeSelector with soft nodeAffinity for control-plane nodes - Simplify migration hook to always clean up DaemonSet if present Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- .../cozystack-api/templates/deployment.yaml | 30 +++++-------------- .../system/cozystack-api/templates/hook.yaml | 17 ++--------- .../cozystack-api/templates/service.yaml | 4 +-- packages/system/cozystack-api/values.yaml | 7 ----- 4 files changed, 12 insertions(+), 46 deletions(-) diff --git a/packages/system/cozystack-api/templates/deployment.yaml b/packages/system/cozystack-api/templates/deployment.yaml index aa877266..9febfe1e 100644 --- a/packages/system/cozystack-api/templates/deployment.yaml +++ b/packages/system/cozystack-api/templates/deployment.yaml @@ -1,18 +1,12 @@ apiVersion: apps/v1 -{{- if .Values.cozystackAPI.localK8sAPIEndpoint.enabled }} -kind: DaemonSet -{{- else }} kind: Deployment -{{- end }} metadata: name: cozystack-api namespace: cozy-system labels: app: cozystack-api spec: - {{- if not .Values.cozystackAPI.localK8sAPIEndpoint.enabled }} replicas: {{ .Values.cozystackAPI.replicas }} - {{- end }} selector: matchLabels: app: cozystack-api @@ -24,27 +18,19 @@ spec: tolerations: - operator: Exists serviceAccountName: cozystack-api - {{- if .Values.cozystackAPI.localK8sAPIEndpoint.enabled }} - {{- with .Values.cozystackAPI.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- end }} + affinity: + nodeAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + preference: + matchExpressions: + - key: node-role.kubernetes.io/control-plane + operator: Exists containers: - name: cozystack-api args: - --tls-cert-file=/tmp/cozystack-api-certs/tls.crt - --tls-private-key-file=/tmp/cozystack-api-certs/tls.key - {{- if .Values.cozystackAPI.localK8sAPIEndpoint.enabled }} - env: - - name: KUBERNETES_SERVICE_HOST - valueFrom: - fieldRef: - apiVersion: v1 - fieldPath: status.hostIP - - name: KUBERNETES_SERVICE_PORT - value: "6443" - {{- end }} image: "{{ .Values.cozystackAPI.image }}" ports: - containerPort: 443 diff --git a/packages/system/cozystack-api/templates/hook.yaml b/packages/system/cozystack-api/templates/hook.yaml index 3c6b3080..852b2db0 100644 --- a/packages/system/cozystack-api/templates/hook.yaml +++ b/packages/system/cozystack-api/templates/hook.yaml @@ -1,16 +1,5 @@ -{{- $shouldRunUpdateHook := false }} -{{- $previousKind := "Deployment" }} -{{- $previousKindPlural := "deployments" }} -{{- if not .Values.cozystackAPI.localK8sAPIEndpoint.enabled }} - {{- $previousKind = "DaemonSet" }} - {{- $previousKindPlural = "daemonsets" }} -{{- end }} -{{- $previous := lookup "apps/v1" $previousKind .Release.Namespace "cozystack-api" }} +{{- $previous := lookup "apps/v1" "DaemonSet" .Release.Namespace "cozystack-api" }} {{- if $previous }} - {{- $shouldRunUpdateHook = true }} -{{- end }} - -{{- if $shouldRunUpdateHook }} --- apiVersion: batch/v1 kind: Job @@ -36,7 +25,7 @@ spec: - -exc - |- kubectl --namespace={{ .Release.Namespace }} delete --ignore-not-found \ - {{ $previousKindPlural }}.apps cozystack-api + daemonsets.apps cozystack-api restartPolicy: Never --- apiVersion: rbac.authorization.k8s.io/v1 @@ -51,7 +40,7 @@ rules: - apiGroups: - "apps" resources: - - "{{ $previousKindPlural }}" + - "daemonsets" verbs: - get - delete diff --git a/packages/system/cozystack-api/templates/service.yaml b/packages/system/cozystack-api/templates/service.yaml index abe67abc..64ba149d 100644 --- a/packages/system/cozystack-api/templates/service.yaml +++ b/packages/system/cozystack-api/templates/service.yaml @@ -4,9 +4,7 @@ metadata: name: cozystack-api namespace: cozy-system spec: - {{- if .Values.cozystackAPI.localK8sAPIEndpoint.enabled }} - internalTrafficPolicy: Local - {{- end }} + trafficDistribution: PreferClose ports: - port: 443 protocol: TCP diff --git a/packages/system/cozystack-api/values.yaml b/packages/system/cozystack-api/values.yaml index d9631b94..769cd4b6 100644 --- a/packages/system/cozystack-api/values.yaml +++ b/packages/system/cozystack-api/values.yaml @@ -1,10 +1,3 @@ cozystackAPI: image: ghcr.io/cozystack/cozystack/cozystack-api:v1.0.0-beta.3@sha256:a263702859c36f85d097c300c1be462aaa45a3955d69e9ae72a37cabf6dadaa9 - localK8sAPIEndpoint: - enabled: true replicas: 2 - # nodeSelector for DaemonSet mode (localK8sAPIEndpoint.enabled: true) - # Talos uses empty value: "node-role.kubernetes.io/control-plane": "" - # Generic k8s (k3s, kubeadm) uses: "node-role.kubernetes.io/control-plane": "true" - nodeSelector: - node-role.kubernetes.io/control-plane: "" From fe6d8bd8d173c38bb59606fedc9bb271692c08bd Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Thu, 12 Feb 2026 18:13:32 +0100 Subject: [PATCH 023/666] refactor(nfs): replace system/nfs HelmRelease with setup Job Replace the system/nfs chart (which only created CiliumNetworkPolicy via Helm lookup) with a Helm hook Job in apps/nfs that: - Mounts the PVC to trigger WaitForFirstConsumer binding - Reads NFS export info from PV's CSI volume attributes - Creates CiliumNetworkPolicy for NFS server egress - Creates credentials Secret with host, port, path, endpoint - Sets ownerReferences to PVC for automatic cleanup Remove system/nfs chart entirely and its PackageSource component. Update dashboard-resourcemap to include credentials Secret. Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- .../nfs/templates/dashboard-resourcemap.yaml | 7 + packages/apps/nfs/templates/helmrelease.yaml | 22 --- .../apps/nfs/templates/nfs-setup-hook.yaml | 168 ++++++++++++++++++ .../platform/sources/nfs-application.yaml | 2 - packages/system/nfs/.helmignore | 2 - packages/system/nfs/Chart.yaml | 3 - packages/system/nfs/Makefile | 3 - .../system/nfs/templates/cilium-policy.yaml | 20 --- packages/system/nfs/values.yaml | 1 - 9 files changed, 175 insertions(+), 53 deletions(-) delete mode 100644 packages/apps/nfs/templates/helmrelease.yaml create mode 100644 packages/apps/nfs/templates/nfs-setup-hook.yaml delete mode 100644 packages/system/nfs/.helmignore delete mode 100644 packages/system/nfs/Chart.yaml delete mode 100644 packages/system/nfs/Makefile delete mode 100644 packages/system/nfs/templates/cilium-policy.yaml delete mode 100644 packages/system/nfs/values.yaml diff --git a/packages/apps/nfs/templates/dashboard-resourcemap.yaml b/packages/apps/nfs/templates/dashboard-resourcemap.yaml index 106af486..e5f7f063 100644 --- a/packages/apps/nfs/templates/dashboard-resourcemap.yaml +++ b/packages/apps/nfs/templates/dashboard-resourcemap.yaml @@ -10,6 +10,13 @@ rules: resourceNames: - {{ .Release.Name }} verbs: ["get", "list", "watch"] +- apiGroups: + - "" + resources: + - secrets + resourceNames: + - {{ .Release.Name }}-credentials + verbs: ["get", "list", "watch"] --- kind: RoleBinding apiVersion: rbac.authorization.k8s.io/v1 diff --git a/packages/apps/nfs/templates/helmrelease.yaml b/packages/apps/nfs/templates/helmrelease.yaml deleted file mode 100644 index 598f065f..00000000 --- a/packages/apps/nfs/templates/helmrelease.yaml +++ /dev/null @@ -1,22 +0,0 @@ -apiVersion: helm.toolkit.fluxcd.io/v2 -kind: HelmRelease -metadata: - name: {{ .Release.Name }}-system - labels: - sharding.fluxcd.io/key: tenants -spec: - chartRef: - kind: ExternalArtifact - name: cozystack-nfs-application-default-nfs-system - namespace: cozy-system - interval: 5m - timeout: 10m - install: - remediation: - retries: -1 - upgrade: - force: true - remediation: - retries: -1 - values: - pvcName: {{ .Release.Name }} diff --git a/packages/apps/nfs/templates/nfs-setup-hook.yaml b/packages/apps/nfs/templates/nfs-setup-hook.yaml new file mode 100644 index 00000000..759bfd21 --- /dev/null +++ b/packages/apps/nfs/templates/nfs-setup-hook.yaml @@ -0,0 +1,168 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ .Release.Name }}-nfs-setup + annotations: + helm.sh/hook: post-install,post-upgrade + helm.sh/hook-weight: "-5" + helm.sh/hook-delete-policy: before-hook-creation +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ .Release.Name }}-nfs-setup + annotations: + helm.sh/hook: post-install,post-upgrade + helm.sh/hook-weight: "-5" + helm.sh/hook-delete-policy: before-hook-creation +rules: + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: ["get"] + - apiGroups: [""] + resources: ["secrets"] + verbs: ["create", "get", "patch", "update"] + - apiGroups: ["cilium.io"] + resources: ["ciliumnetworkpolicies"] + verbs: ["create", "get", "patch", "update"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ .Release.Name }}-nfs-setup + annotations: + helm.sh/hook: post-install,post-upgrade + helm.sh/hook-weight: "-5" + helm.sh/hook-delete-policy: before-hook-creation +subjects: + - kind: ServiceAccount + name: {{ .Release.Name }}-nfs-setup +roleRef: + kind: Role + name: {{ .Release.Name }}-nfs-setup + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ .Release.Namespace }}-{{ .Release.Name }}-nfs-setup + annotations: + helm.sh/hook: post-install,post-upgrade + helm.sh/hook-weight: "-5" + helm.sh/hook-delete-policy: before-hook-creation +rules: + - apiGroups: [""] + resources: ["persistentvolumes"] + verbs: ["get"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ .Release.Namespace }}-{{ .Release.Name }}-nfs-setup + annotations: + helm.sh/hook: post-install,post-upgrade + helm.sh/hook-weight: "-5" + helm.sh/hook-delete-policy: before-hook-creation +subjects: + - kind: ServiceAccount + name: {{ .Release.Name }}-nfs-setup + namespace: {{ .Release.Namespace }} +roleRef: + kind: ClusterRole + name: {{ .Release.Namespace }}-{{ .Release.Name }}-nfs-setup + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ .Release.Name }}-nfs-setup + annotations: + helm.sh/hook: post-install,post-upgrade + helm.sh/hook-weight: "0" + helm.sh/hook-delete-policy: hook-succeeded,before-hook-creation +spec: + backoffLimit: 3 + template: + metadata: + labels: + policy.cozystack.io/allow-to-apiserver: "true" + spec: + serviceAccountName: {{ .Release.Name }}-nfs-setup + restartPolicy: Never + containers: + - name: setup + image: docker.io/alpine/k8s:1.33.4 + command: ["sh", "-xec"] + args: + - | + PVC_NAME="{{ .Release.Name }}" + NAMESPACE="{{ .Release.Namespace }}" + + # Get PV name from PVC (PVC is bound since we mount it) + PV_NAME=$(kubectl get pvc "$PVC_NAME" -n "$NAMESPACE" -o jsonpath='{.spec.volumeName}') + + # Get NFS export URL from PV + NFS_EXPORT=$(kubectl get pv "$PV_NAME" -o 'jsonpath={.spec.csi.volumeAttributes.linstor\.csi\.linbit\.com/nfs-export}') + + # Parse port from URL (format: nfs://host:port/path) + PORT=$(echo "$NFS_EXPORT" | sed -E 's|.*://[^:]+:([0-9]+)/.*|\1|') + + # Get PVC UID for owner reference + PVC_UID=$(kubectl get pvc "$PVC_NAME" -n "$NAMESPACE" -o jsonpath='{.metadata.uid}') + + # Parse host and path from URL + HOST=$(echo "$NFS_EXPORT" | sed -E 's|.*://([^:]+):.*|\1|') + PATH_EXPORT=$(echo "$NFS_EXPORT" | sed -E 's|.*://[^/]+(/.*)|\1|') + + # Create credentials secret with NFS access info + cat < Date: Thu, 12 Feb 2026 18:13:40 +0100 Subject: [PATCH 024/666] refactor(nfs-driver): change extraStorageClasses from list to map Convert extraStorageClasses from list to map format to support Flux HelmRelease valuesFrom with targetPath deep-merge semantics. Update template to iterate over map entries and construct mountOptions from port parameter. Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- .../templates/extra-storageclasses.yaml | 13 +++-- .../tests/extra-storageclasses_test.yaml | 52 ++++++++----------- packages/system/nfs-driver/values.yaml | 2 +- 3 files changed, 30 insertions(+), 37 deletions(-) diff --git a/packages/system/nfs-driver/templates/extra-storageclasses.yaml b/packages/system/nfs-driver/templates/extra-storageclasses.yaml index f627a474..c86e1355 100644 --- a/packages/system/nfs-driver/templates/extra-storageclasses.yaml +++ b/packages/system/nfs-driver/templates/extra-storageclasses.yaml @@ -1,18 +1,17 @@ -{{- range .Values.extraStorageClasses }} +{{- range $name, $config := .Values.extraStorageClasses }} --- apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: - name: {{ .name }} + name: {{ $name }} provisioner: nfs.csi.k8s.io parameters: - server: {{ .server | quote }} - share: {{ .share | quote }} + server: {{ $config.server | quote }} + share: {{ $config.share | quote }} reclaimPolicy: Delete volumeBindingMode: Immediate allowVolumeExpansion: true -{{- with .mountOptions }} mountOptions: -{{- toYaml . | nindent 2 }} -{{- end }} + - nfsvers=4.2 + - port={{ $config.port }} {{- end }} diff --git a/packages/system/nfs-driver/tests/extra-storageclasses_test.yaml b/packages/system/nfs-driver/tests/extra-storageclasses_test.yaml index 6009c761..da3bd34d 100644 --- a/packages/system/nfs-driver/tests/extra-storageclasses_test.yaml +++ b/packages/system/nfs-driver/tests/extra-storageclasses_test.yaml @@ -12,16 +12,14 @@ tests: - it: creates one StorageClass per entry set: extraStorageClasses: - - name: nfs-data + nfs-data: server: "10.96.1.1" share: "/" - mountOptions: - - nfsvers=4.1 - - name: nfs-backup + port: "2049" + nfs-backup: server: "10.96.2.2" share: "/" - mountOptions: - - nfsvers=4.1 + port: "2050" asserts: - hasDocuments: count: 2 @@ -29,11 +27,10 @@ tests: - it: sets correct provisioner set: extraStorageClasses: - - name: nfs-test + nfs-test: server: "10.96.1.1" share: "/" - mountOptions: - - nfsvers=4.1 + port: "2049" asserts: - equal: path: provisioner @@ -42,11 +39,10 @@ tests: - it: sets server and share parameters set: extraStorageClasses: - - name: nfs-mydata + nfs-mydata: server: "10.96.50.100" share: "/exports" - mountOptions: - - nfsvers=4.1 + port: "2049" asserts: - equal: path: metadata.name @@ -58,27 +54,28 @@ tests: path: parameters.share value: "/exports" - - it: sets mount options + - it: sets mount options with port set: extraStorageClasses: - - name: nfs-test + nfs-test: server: "10.96.1.1" share: "/" - mountOptions: - - nfsvers=4.1 + port: "2049" asserts: - equal: path: mountOptions[0] - value: nfsvers=4.1 + value: nfsvers=4.2 + - equal: + path: mountOptions[1] + value: port=2049 - it: allows volume expansion set: extraStorageClasses: - - name: nfs-test + nfs-test: server: "10.96.1.1" share: "/" - mountOptions: - - nfsvers=4.1 + port: "2049" asserts: - equal: path: allowVolumeExpansion @@ -87,11 +84,10 @@ tests: - it: uses Delete reclaim policy set: extraStorageClasses: - - name: nfs-test + nfs-test: server: "10.96.1.1" share: "/" - mountOptions: - - nfsvers=4.1 + port: "2049" asserts: - equal: path: reclaimPolicy @@ -100,16 +96,14 @@ tests: - it: creates distinct StorageClasses for multiple shares set: extraStorageClasses: - - name: nfs-alpha + nfs-alpha: server: "10.96.10.1" share: "/" - mountOptions: - - nfsvers=4.1 - - name: nfs-beta + port: "2049" + nfs-beta: server: "10.96.20.2" share: "/data" - mountOptions: - - nfsvers=4.1 + port: "2050" asserts: - equal: path: metadata.name diff --git a/packages/system/nfs-driver/values.yaml b/packages/system/nfs-driver/values.yaml index eb1f99f0..a5676ee9 100644 --- a/packages/system/nfs-driver/values.yaml +++ b/packages/system/nfs-driver/values.yaml @@ -1,3 +1,3 @@ csi-driver-nfs: {} -extraStorageClasses: [] +extraStorageClasses: {} From b7df10de6c14bf674fb730200c0bfcca4685c396 Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Thu, 12 Feb 2026 18:13:46 +0100 Subject: [PATCH 025/666] refactor(kubernetes): use valuesFrom for NFS driver credentials Replace PVC/PV lookup logic with valuesFrom references to NFS credentials secrets, passing host, port, and path via targetPath to the nfs-driver extraStorageClasses map. Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- .../templates/helmreleases/nfs-driver.yaml | 49 +++++++++---------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/packages/apps/kubernetes/templates/helmreleases/nfs-driver.yaml b/packages/apps/kubernetes/templates/helmreleases/nfs-driver.yaml index c00ea763..590f3c4b 100644 --- a/packages/apps/kubernetes/templates/helmreleases/nfs-driver.yaml +++ b/packages/apps/kubernetes/templates/helmreleases/nfs-driver.yaml @@ -1,29 +1,4 @@ {{- if .Values.nfs.instances }} -{{- $extraStorageClasses := list }} -{{- range .Values.nfs.instances }} -{{- $pvcName := printf "nfs-%s" .name }} -{{- $pvc := lookup "v1" "PersistentVolumeClaim" $.Release.Namespace $pvcName }} -{{- if not $pvc }} -{{- fail (printf "NFS PVC not found in namespace %s: %s" $.Release.Namespace $pvcName) }} -{{- end }} -{{- if not $pvc.spec.volumeName }} -{{- fail (printf "NFS PVC %s is not yet bound" $pvcName) }} -{{- end }} -{{- $pv := lookup "v1" "PersistentVolume" "" $pvc.spec.volumeName }} -{{- if not $pv }} -{{- fail (printf "PersistentVolume %s not found for PVC %s" $pvc.spec.volumeName $pvcName) }} -{{- end }} -{{- $nfsExport := index $pv.spec.csi.volumeAttributes "linstor.csi.linbit.com/nfs-export" }} -{{- if not $nfsExport }} -{{- fail (printf "PV %s has no linstor.csi.linbit.com/nfs-export attribute" $pv.metadata.name) }} -{{- end }} -{{- $parsed := urlParse $nfsExport }} -{{- $hostParts := splitList ":" $parsed.host }} -{{- $server := first $hostParts }} -{{- $port := last $hostParts }} -{{- $sc := dict "name" (printf "nfs-%s" .name) "server" $server "share" $parsed.path "mountOptions" (list (printf "nfsvers=4.2") (printf "port=%s" $port)) }} -{{- $extraStorageClasses = append $extraStorageClasses $sc }} -{{- end }} apiVersion: helm.toolkit.fluxcd.io/v2 kind: HelmRelease metadata: @@ -54,9 +29,29 @@ spec: force: true remediation: retries: -1 + valuesFrom: + {{- range .Values.nfs.instances }} + {{- $secretName := printf "nfs-%s-credentials" .name }} + - kind: Secret + name: {{ $secretName }} + valuesKey: host + targetPath: extraStorageClasses.nfs-{{ .name }}.server + - kind: Secret + name: {{ $secretName }} + valuesKey: port + targetPath: extraStorageClasses.nfs-{{ .name }}.port + - kind: Secret + name: {{ $secretName }} + valuesKey: path + targetPath: extraStorageClasses.nfs-{{ .name }}.share + {{- end }} values: - {{- $defaultValues := dict "csi-driver-nfs" (dict "storageClass" (dict "create" false)) "extraStorageClasses" $extraStorageClasses }} - {{- toYaml (deepCopy .Values.addons.nfs.valuesOverride | mergeOverwrite $defaultValues) | nindent 4 }} + csi-driver-nfs: + storageClass: + create: false + {{- with .Values.addons.nfs.valuesOverride }} + {{- toYaml . | nindent 4 }} + {{- end }} dependsOn: {{- if lookup "helm.toolkit.fluxcd.io/v2" "HelmRelease" .Release.Namespace .Release.Name }} - name: {{ .Release.Name }} From a05fead357c0f8b4b20f2c4b322ea45064f85073 Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Thu, 12 Feb 2026 18:13:52 +0100 Subject: [PATCH 026/666] fix(nfs): add secrets section to ApplicationDefinition Add secrets include/exclude configuration so the lineage webhook sets internal.cozystack.io/tenantresource label on NFS credentials secrets, making them visible via the tenantsecrets API. Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- packages/system/nfs-rd/cozyrds/nfs.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/system/nfs-rd/cozyrds/nfs.yaml b/packages/system/nfs-rd/cozyrds/nfs.yaml index 9a51f6af..9e720e1f 100644 --- a/packages/system/nfs-rd/cozyrds/nfs.yaml +++ b/packages/system/nfs-rd/cozyrds/nfs.yaml @@ -22,3 +22,8 @@ spec: plural: NFS icon: PHN2ZyB3aWR0aD0iMTQ0IiBoZWlnaHQ9IjE0NCIgdmlld0JveD0iMCAwIDE0NCAxNDQiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIxNDQiIGhlaWdodD0iMTQ0IiByeD0iMjQiIGZpbGw9InVybCgjcGFpbnQwX2xpbmVhcikiLz4KPHBhdGggZD0iTTM2IDU4VjEwNEMzNiAxMDcuMzE0IDM4LjY4NjMgMTEwIDQyIDExMEgxMDJDMTA1LjMxNCAxMTAgMTA4IDEwNy4zMTQgMTA4IDEwNFY1MkMxMDggNDguNjg2MyAxMDUuMzE0IDQ2IDEwMiA0Nkg3MEw2MiAzNEg0MkMzOC42ODYzIDM0IDM2IDM2LjY4NjMgMzYgNDBWNThaIiBmaWxsPSIjMTU2NUMwIi8+CjxwYXRoIGQ9Ik0zMCA2MkMzMCA1OC42ODYzIDMyLjY4NjMgNTYgMzYgNTZIMTA4QzExMS4zMTQgNTYgMTE0IDU4LjY4NjMgMTE0IDYyVjEwNEMxMTQgMTA3LjMxNCAxMTEuMzE0IDExMCAxMDggMTEwSDM2QzMyLjY4NjMgMTEwIDMwIDEwNy4zMTQgMzAgMTA0VjYyWiIgZmlsbD0iIzQyQTVGNSIvPgo8cGF0aCBkPSJNNjMgNzZWOTRNNjMgNzZMNTQgODVNNjMgNzZMNzIgODUiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iNCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+CjxwYXRoIGQ9Ik04MSA5NFY3Nk04MSA5NEw5MCA4NU04MSA5NEw3MiA4NSIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPGRlZnM+CjxsaW5lYXJHcmFkaWVudCBpZD0icGFpbnQwX2xpbmVhciIgeDE9IjAiIHkxPSIwIiB4Mj0iMTQ0IiB5Mj0iMTQ0IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wIHN0b3AtY29sb3I9IiNFM0YyRkQiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjNjRCNUY2Ii8+CjwvbGluZWFyR3JhZGllbnQ+CjwvZGVmcz4KPC9zdmc+Cg== keysOrder: [["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata", "name"], ["spec", "storageClass"], ["spec", "size"]] + secrets: + exclude: [] + include: + - resourceNames: + - nfs-{{ .name }}-credentials From 1af999a5000dfe4475d3a8753cd1c76e378352f0 Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Thu, 12 Feb 2026 23:24:43 +0100 Subject: [PATCH 027/666] feat(csi): add RWX Filesystem (NFS) support to kubevirt-csi-driver wrapper Implement a wrapper around upstream kubevirt-csi-driver that adds RWX Filesystem volume support via LINSTOR NFS exports: - CreateVolume: intercepts RWX+Filesystem requests, creates DataVolume with explicit AccessMode=RWX and VolumeMode=Filesystem - ControllerPublishVolume: waits for PVC bound, extracts NFS export URL from infra PV, creates CiliumNetworkPolicy with VMI ownerReferences - ControllerUnpublishVolume: manages CNP ownerReferences per-VMI - ControllerExpandVolume: delegates to upstream, disables node expansion for NFS volumes - NodeStageVolume/NodePublishVolume: mounts NFS at target path - NodeExpandVolume: no-op for NFS (LINSTOR handles resize) Also includes: - RBAC for CiliumNetworkPolicy management in infra cluster - Explicit --run-node-service=false for controller deployment - Explicit --run-controller-service=false for node DaemonSet - nfs-utils in container image for NFS mount support Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- .../kubernetes/images/kubevirt-csi-driver.tag | 2 +- .../images/kubevirt-csi-driver/Dockerfile | 28 +- .../images/kubevirt-csi-driver/controller.go | 525 +++++++++++++++++ .../images/kubevirt-csi-driver/go.mod | 101 ++++ .../images/kubevirt-csi-driver/go.sum | 543 ++++++++++++++++++ .../images/kubevirt-csi-driver/main.go | 219 +++++++ .../images/kubevirt-csi-driver/node.go | 106 ++++ .../apps/kubernetes/templates/csi/deploy.yaml | 1 + .../csi/infra-cluster-service-account.yaml | 3 + .../kubevirt-csi-node/templates/deploy.yaml | 1 + packages/system/kubevirt-csi-node/values.yaml | 2 +- 11 files changed, 1511 insertions(+), 20 deletions(-) create mode 100644 packages/apps/kubernetes/images/kubevirt-csi-driver/controller.go create mode 100644 packages/apps/kubernetes/images/kubevirt-csi-driver/go.mod create mode 100644 packages/apps/kubernetes/images/kubevirt-csi-driver/go.sum create mode 100644 packages/apps/kubernetes/images/kubevirt-csi-driver/main.go create mode 100644 packages/apps/kubernetes/images/kubevirt-csi-driver/node.go diff --git a/packages/apps/kubernetes/images/kubevirt-csi-driver.tag b/packages/apps/kubernetes/images/kubevirt-csi-driver.tag index 421a4084..ca67ba13 100644 --- a/packages/apps/kubernetes/images/kubevirt-csi-driver.tag +++ b/packages/apps/kubernetes/images/kubevirt-csi-driver.tag @@ -1 +1 @@ -ghcr.io/cozystack/cozystack/kubevirt-csi-driver:0.0.0@sha256:8f1ab4c3b2bed3a0adc40fcc823b040fa04b4722bec7735c030e79a3a2fd6c85 +ghcr.io/cozystack/cozystack/kubevirt-csi-driver:latest@sha256:cd58760c97ba50ef74ff940cdcda4b9a6d8554eec8305fc96c2e8bbea72b75a9 diff --git a/packages/apps/kubernetes/images/kubevirt-csi-driver/Dockerfile b/packages/apps/kubernetes/images/kubevirt-csi-driver/Dockerfile index 5ae5817f..be8125d7 100644 --- a/packages/apps/kubernetes/images/kubevirt-csi-driver/Dockerfile +++ b/packages/apps/kubernetes/images/kubevirt-csi-driver/Dockerfile @@ -1,31 +1,23 @@ -# Source: https://github.com/kubevirt/csi-driver/blob/main/Dockerfile ARG builder_image=docker.io/library/golang:1.22.5 FROM ${builder_image} AS builder -RUN git clone https://github.com/kubevirt/csi-driver /src/kubevirt-csi-driver \ - && cd /src/kubevirt-csi-driver \ - && git checkout a8d6605bc9997bcfda3fb9f1f82ba6445b4984cc ARG TARGETOS ARG TARGETARCH -ENV GOOS=$TARGETOS -ENV GOARCH=$TARGETARCH -WORKDIR /src/kubevirt-csi-driver +WORKDIR /src -RUN make build +COPY go.mod go.sum ./ +RUN go mod download + +COPY *.go ./ +RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build \ + -ldflags "-X kubevirt.io/csi-driver/pkg/service.VendorVersion=0.2.0" \ + -o kubevirt-csi-driver . FROM quay.io/centos/centos:stream9 -ARG git_url=https://github.com/kubevirt/csi-driver.git - -LABEL maintainers="The KubeVirt Project " \ - description="KubeVirt CSI Driver" \ - multi.GIT_URL=${git_url} ENTRYPOINT ["./kubevirt-csi-driver"] -RUN dnf install -y e2fsprogs xfsprogs && dnf clean all +RUN dnf install -y e2fsprogs xfsprogs nfs-utils && dnf clean all -ARG git_sha=NONE -LABEL multi.GIT_SHA=${git_sha} - -COPY --from=builder /src/kubevirt-csi-driver/kubevirt-csi-driver . +COPY --from=builder /src/kubevirt-csi-driver . diff --git a/packages/apps/kubernetes/images/kubevirt-csi-driver/controller.go b/packages/apps/kubernetes/images/kubevirt-csi-driver/controller.go new file mode 100644 index 00000000..524b45ea --- /dev/null +++ b/packages/apps/kubernetes/images/kubevirt-csi-driver/controller.go @@ -0,0 +1,525 @@ +package main + +import ( + "context" + "fmt" + "net/url" + "time" + + csi "github.com/container-storage-interface/spec/lib/go/csi" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/cache" + "k8s.io/klog/v2" + cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" + + kubevirtclient "kubevirt.io/csi-driver/pkg/kubevirt" + "kubevirt.io/csi-driver/pkg/service" + "kubevirt.io/csi-driver/pkg/util" +) + +const ( + nfsVolumeKey = "nfsVolume" + nfsExportKey = "nfsExport" + busParameter = "bus" + serialParameter = "serial" +) + +var ciliumNetworkPolicyGVR = schema.GroupVersionResource{ + Group: "cilium.io", + Version: "v2", + Resource: "ciliumnetworkpolicies", +} + +var _ csi.ControllerServer = &WrappedControllerService{} + +// WrappedControllerService embeds the upstream ControllerService and adds RWX Filesystem (NFS) support. +type WrappedControllerService struct { + *service.ControllerService + infraClient kubernetes.Interface + dynamicClient dynamic.Interface + virtClient kubevirtclient.Client + infraNamespace string + infraClusterLabels map[string]string + storageClassEnforcement util.StorageClassEnforcement +} + +// isRWXFilesystem checks if the volume capabilities request RWX access with filesystem mode. +func isRWXFilesystem(caps []*csi.VolumeCapability) bool { + hasRWX := false + hasMount := false + for _, cap := range caps { + if cap == nil { + continue + } + if cap.GetMount() != nil { + hasMount = true + } + if am := cap.GetAccessMode(); am != nil && am.Mode == csi.VolumeCapability_AccessMode_MULTI_NODE_MULTI_WRITER { + hasRWX = true + } + } + return hasRWX && hasMount +} + +// CreateVolume intercepts RWX Filesystem requests and creates a DataVolume in the infra +// cluster with AccessMode=RWX and VolumeMode=Filesystem. Upstream rejects RWX+Filesystem, +// so we handle DataVolume creation ourselves. Using DataVolume (not bare PVC) preserves +// compatibility with upstream snapshot and clone operations. +// For all other requests, delegates to upstream. +func (w *WrappedControllerService) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest) (*csi.CreateVolumeResponse, error) { + if !isRWXFilesystem(req.GetVolumeCapabilities()) { + return w.ControllerService.CreateVolume(ctx, req) + } + + if req == nil { + return nil, status.Error(codes.InvalidArgument, "missing request") + } + if len(req.GetName()) == 0 { + return nil, status.Error(codes.InvalidArgument, "name missing in request") + } + + // Storage class enforcement + storageClassName := req.Parameters[kubevirtclient.InfraStorageClassNameParameter] + if !w.storageClassEnforcement.AllowAll { + if storageClassName == "" { + if !w.storageClassEnforcement.AllowDefault { + return nil, status.Error(codes.InvalidArgument, "infraStorageclass is not in the allowed list") + } + } else if !util.Contains(w.storageClassEnforcement.AllowList, storageClassName) { + return nil, status.Error(codes.InvalidArgument, "infraStorageclass is not in the allowed list") + } + } + + storageSize := req.GetCapacityRange().GetRequiredBytes() + dvName := req.Name + + // Determine DataVolume source (blank, snapshot, or clone) + source, err := w.determineDvSource(ctx, req) + if err != nil { + return nil, err + } + + // Handle CSI clone: CDI doesn't allow cloning PVCs in use by a pod, + // so use DataSourceRef instead (same approach as upstream) + sourcePVCName := "" + if source.PVC != nil { + sourcePVCName = source.PVC.Name + source = nil + } + + volumeMode := corev1.PersistentVolumeFilesystem + dv := &cdiv1.DataVolume{ + TypeMeta: metav1.TypeMeta{ + Kind: "DataVolume", + APIVersion: cdiv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: dvName, + Namespace: w.infraNamespace, + Labels: w.infraClusterLabels, + Annotations: map[string]string{ + "cdi.kubevirt.io/storage.deleteAfterCompletion": "false", + "cdi.kubevirt.io/storage.bind.immediate.requested": "true", + }, + }, + Spec: cdiv1.DataVolumeSpec{ + Storage: &cdiv1.StorageSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteMany}, + VolumeMode: &volumeMode, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: *resource.NewScaledQuantity(storageSize, 0), + }, + }, + }, + Source: source, + }, + } + + if sourcePVCName != "" { + dv.Spec.Storage.DataSourceRef = &corev1.TypedObjectReference{ + Kind: "PersistentVolumeClaim", + Name: sourcePVCName, + } + } + + if storageClassName != "" { + dv.Spec.Storage.StorageClassName = &storageClassName + } + + // Idempotency: check if DataVolume already exists + if existingDv, err := w.virtClient.GetDataVolume(ctx, w.infraNamespace, dvName); errors.IsNotFound(err) { + klog.Infof("Creating NFS DataVolume %s/%s", w.infraNamespace, dvName) + dv, err = w.virtClient.CreateDataVolume(ctx, w.infraNamespace, dv) + if err != nil { + klog.Errorf("Failed creating NFS DataVolume %s: %v", dvName, err) + return nil, err + } + } else if err != nil { + return nil, err + } else { + if existingDv != nil && existingDv.Spec.Storage != nil { + existingRequest := existingDv.Spec.Storage.Resources.Requests[corev1.ResourceStorage] + newRequest := dv.Spec.Storage.Resources.Requests[corev1.ResourceStorage] + if newRequest.Cmp(existingRequest) != 0 { + return nil, status.Error(codes.AlreadyExists, "requested storage size does not match existing size") + } + dv = existingDv + } + } + + serial := string(dv.GetUID()) + + return &csi.CreateVolumeResponse{ + Volume: &csi.Volume{ + CapacityBytes: storageSize, + VolumeId: dvName, + VolumeContext: map[string]string{ + busParameter: "scsi", + serialParameter: serial, + nfsVolumeKey: "true", + }, + ContentSource: req.GetVolumeContentSource(), + }, + }, nil +} + +// determineDvSource determines the DataVolume source from the CSI request content source. +// Mirrors upstream logic for blank, snapshot, and clone sources. +func (w *WrappedControllerService) determineDvSource(ctx context.Context, req *csi.CreateVolumeRequest) (*cdiv1.DataVolumeSource, error) { + res := &cdiv1.DataVolumeSource{} + if req.GetVolumeContentSource() != nil { + source := req.GetVolumeContentSource() + switch source.Type.(type) { + case *csi.VolumeContentSource_Snapshot: + snapshot, err := w.virtClient.GetVolumeSnapshot(ctx, w.infraNamespace, source.GetSnapshot().GetSnapshotId()) + if errors.IsNotFound(err) { + return nil, status.Errorf(codes.NotFound, "source snapshot %s not found", source.GetSnapshot().GetSnapshotId()) + } else if err != nil { + return nil, err + } + if snapshot != nil { + res.Snapshot = &cdiv1.DataVolumeSourceSnapshot{ + Name: snapshot.Name, + Namespace: w.infraNamespace, + } + } + case *csi.VolumeContentSource_Volume: + volume, err := w.virtClient.GetDataVolume(ctx, w.infraNamespace, source.GetVolume().GetVolumeId()) + if errors.IsNotFound(err) { + return nil, status.Errorf(codes.NotFound, "source volume %s not found", source.GetVolume().GetVolumeId()) + } else if err != nil { + return nil, err + } + if volume != nil { + res.PVC = &cdiv1.DataVolumeSourcePVC{ + Name: volume.Name, + Namespace: w.infraNamespace, + } + } + default: + return nil, status.Error(codes.InvalidArgument, "unknown content type") + } + } else { + res.Blank = &cdiv1.DataVolumeBlankImage{} + } + return res, nil +} + +// ControllerPublishVolume for NFS volumes: annotates infra PVC for WFFC binding, +// waits for PVC bound, extracts NFS export from PV, and creates CiliumNetworkPolicy. +// For RWO volumes, delegates to upstream (hotplug SCSI). +func (w *WrappedControllerService) ControllerPublishVolume(ctx context.Context, req *csi.ControllerPublishVolumeRequest) (*csi.ControllerPublishVolumeResponse, error) { + if req.GetVolumeContext()[nfsVolumeKey] != "true" { + return w.ControllerService.ControllerPublishVolume(ctx, req) + } + + if len(req.GetVolumeId()) == 0 { + return nil, status.Error(codes.InvalidArgument, "volume id missing in request") + } + if len(req.GetNodeId()) == 0 { + return nil, status.Error(codes.InvalidArgument, "node id missing in request") + } + + dvName := req.GetVolumeId() + vmNamespace, vmName, err := cache.SplitMetaNamespaceKey(req.GetNodeId()) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to parse node ID %q: %v", req.GetNodeId(), err) + } + + klog.V(3).Infof("Publishing NFS volume %s to node %s/%s", dvName, vmNamespace, vmName) + + // Get VMI for CiliumNetworkPolicy ownerReference + vmi, err := w.virtClient.GetVirtualMachine(ctx, w.infraNamespace, vmName) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get VMI %s: %v", vmName, err) + } + + // Wait for PVC to be bound (CDI handles immediate binding via annotation) + klog.V(3).Infof("Waiting for PVC %s to be bound", dvName) + if err := wait.PollUntilContextTimeout(ctx, time.Second, 2*time.Minute, true, func(ctx context.Context) (bool, error) { + p, err := w.infraClient.CoreV1().PersistentVolumeClaims(w.infraNamespace).Get(ctx, dvName, metav1.GetOptions{}) + if err != nil { + return false, err + } + return p.Status.Phase == corev1.ClaimBound, nil + }); err != nil { + return nil, status.Errorf(codes.Internal, "timed out waiting for PVC %s to be bound: %v", dvName, err) + } + + // Read PV to get NFS export + pvc, err := w.infraClient.CoreV1().PersistentVolumeClaims(w.infraNamespace).Get(ctx, dvName, metav1.GetOptions{}) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to re-read PVC %s: %v", dvName, err) + } + pv, err := w.infraClient.CoreV1().PersistentVolumes().Get(ctx, pvc.Spec.VolumeName, metav1.GetOptions{}) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get PV %s: %v", pvc.Spec.VolumeName, err) + } + nfsExport, err := getNFSExport(pv) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to extract NFS export from PV %s: %v", pv.Name, err) + } + klog.V(3).Infof("NFS export for volume %s: %s", dvName, nfsExport) + + // Parse NFS URL for CiliumNetworkPolicy port + _, port, _, err := parseNFSExport(nfsExport) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to parse NFS export URL: %v", err) + } + + // Create or update CiliumNetworkPolicy allowing egress to NFS server + cnpName := fmt.Sprintf("csi-nfs-%s", dvName) + vmiOwnerRef := map[string]interface{}{ + "apiVersion": "kubevirt.io/v1", + "kind": "VirtualMachineInstance", + "name": vmName, + "uid": string(vmi.UID), + } + cnp := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "cilium.io/v2", + "kind": "CiliumNetworkPolicy", + "metadata": map[string]interface{}{ + "name": cnpName, + "namespace": vmNamespace, + "ownerReferences": []interface{}{vmiOwnerRef}, + }, + "spec": map[string]interface{}{ + "endpointSelector": map[string]interface{}{}, + "egress": []interface{}{ + map[string]interface{}{ + "toEndpoints": []interface{}{ + map[string]interface{}{ + "matchLabels": map[string]interface{}{ + "k8s:app.kubernetes.io/component": "linstor-csi-nfs-server", + "k8s:io.kubernetes.pod.namespace": "cozy-linstor", + }, + }, + }, + "toPorts": []interface{}{ + map[string]interface{}{ + "ports": []interface{}{ + map[string]interface{}{ + "port": port, + "protocol": "TCP", + }, + }, + }, + }, + }, + }, + }, + }, + } + + if _, err := w.dynamicClient.Resource(ciliumNetworkPolicyGVR).Namespace(vmNamespace).Create(ctx, cnp, metav1.CreateOptions{}); err != nil { + if !errors.IsAlreadyExists(err) { + return nil, status.Errorf(codes.Internal, "failed to create CiliumNetworkPolicy %s: %v", cnpName, err) + } + // CNP exists — add ownerReference for this VMI + if err := w.addCNPOwnerReference(ctx, vmNamespace, cnpName, vmiOwnerRef); err != nil { + return nil, err + } + } + + klog.V(3).Infof("Successfully published NFS volume %s", dvName) + return &csi.ControllerPublishVolumeResponse{ + PublishContext: map[string]string{ + nfsExportKey: nfsExport, + }, + }, nil +} + +// ControllerUnpublishVolume for NFS volumes: deletes CiliumNetworkPolicy. +// For RWO volumes, delegates to upstream (hotplug removal). +func (w *WrappedControllerService) ControllerUnpublishVolume(ctx context.Context, req *csi.ControllerUnpublishVolumeRequest) (*csi.ControllerUnpublishVolumeResponse, error) { + dvName := req.GetVolumeId() + + // Determine if NFS by checking infra PVC access modes + pvc, err := w.infraClient.CoreV1().PersistentVolumeClaims(w.infraNamespace).Get(ctx, dvName, metav1.GetOptions{}) + if err != nil { + if errors.IsNotFound(err) { + return &csi.ControllerUnpublishVolumeResponse{}, nil + } + return nil, err + } + + if !hasRWXAccessMode(pvc) { + return w.ControllerService.ControllerUnpublishVolume(ctx, req) + } + + // NFS volume: remove VMI ownerReference from CiliumNetworkPolicy + vmNamespace, vmName, err := cache.SplitMetaNamespaceKey(req.GetNodeId()) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to parse node ID %q: %v", req.GetNodeId(), err) + } + + cnpName := fmt.Sprintf("csi-nfs-%s", dvName) + klog.V(3).Infof("Removing VMI %s ownerReference from CiliumNetworkPolicy %s/%s", vmName, vmNamespace, cnpName) + if err := w.removeCNPOwnerReference(ctx, vmNamespace, cnpName, vmName); err != nil { + return nil, err + } + + klog.V(3).Infof("Successfully unpublished NFS volume %s", dvName) + return &csi.ControllerUnpublishVolumeResponse{}, nil +} + +// ControllerExpandVolume delegates to upstream for the actual DataVolume/PVC resize. +// For NFS volumes, LINSTOR handles NFS server resize automatically, so no node expansion is needed. +func (w *WrappedControllerService) ControllerExpandVolume(ctx context.Context, req *csi.ControllerExpandVolumeRequest) (*csi.ControllerExpandVolumeResponse, error) { + resp, err := w.ControllerService.ControllerExpandVolume(ctx, req) + if err != nil { + return nil, err + } + + // For NFS volumes, no node-side expansion is needed + pvc, err := w.infraClient.CoreV1().PersistentVolumeClaims(w.infraNamespace).Get(ctx, req.GetVolumeId(), metav1.GetOptions{}) + if err == nil && hasRWXAccessMode(pvc) { + resp.NodeExpansionRequired = false + } + + return resp, nil +} + +// addCNPOwnerReference adds a VMI ownerReference to an existing CiliumNetworkPolicy. +func (w *WrappedControllerService) addCNPOwnerReference(ctx context.Context, namespace, cnpName string, ownerRef map[string]interface{}) error { + existing, err := w.dynamicClient.Resource(ciliumNetworkPolicyGVR).Namespace(namespace).Get(ctx, cnpName, metav1.GetOptions{}) + if err != nil { + return status.Errorf(codes.Internal, "failed to get CiliumNetworkPolicy %s: %v", cnpName, err) + } + + ownerRefs, _, _ := unstructured.NestedSlice(existing.Object, "metadata", "ownerReferences") + uid, _, _ := unstructured.NestedString(ownerRef, "uid") + for _, ref := range ownerRefs { + if refMap, ok := ref.(map[string]interface{}); ok { + if refMap["uid"] == uid { + return nil // already present + } + } + } + + ownerRefs = append(ownerRefs, ownerRef) + if err := unstructured.SetNestedSlice(existing.Object, ownerRefs, "metadata", "ownerReferences"); err != nil { + return status.Errorf(codes.Internal, "failed to set ownerReferences: %v", err) + } + if _, err := w.dynamicClient.Resource(ciliumNetworkPolicyGVR).Namespace(namespace).Update(ctx, existing, metav1.UpdateOptions{}); err != nil { + return status.Errorf(codes.Internal, "failed to update CiliumNetworkPolicy %s: %v", cnpName, err) + } + klog.V(3).Infof("Added ownerReference to CiliumNetworkPolicy %s", cnpName) + return nil +} + +// removeCNPOwnerReference removes a VMI ownerReference from a CiliumNetworkPolicy. +// Deletes the CNP if no ownerReferences remain. +func (w *WrappedControllerService) removeCNPOwnerReference(ctx context.Context, namespace, cnpName, vmName string) error { + existing, err := w.dynamicClient.Resource(ciliumNetworkPolicyGVR).Namespace(namespace).Get(ctx, cnpName, metav1.GetOptions{}) + if err != nil { + if errors.IsNotFound(err) { + return nil + } + return status.Errorf(codes.Internal, "failed to get CiliumNetworkPolicy %s: %v", cnpName, err) + } + + ownerRefs, _, _ := unstructured.NestedSlice(existing.Object, "metadata", "ownerReferences") + var remaining []interface{} + for _, ref := range ownerRefs { + if refMap, ok := ref.(map[string]interface{}); ok { + if refMap["name"] == vmName { + continue + } + } + remaining = append(remaining, ref) + } + + if len(remaining) == 0 { + // Last owner — delete CNP + if err := w.dynamicClient.Resource(ciliumNetworkPolicyGVR).Namespace(namespace).Delete(ctx, cnpName, metav1.DeleteOptions{}); err != nil { + if !errors.IsNotFound(err) { + return status.Errorf(codes.Internal, "failed to delete CiliumNetworkPolicy %s: %v", cnpName, err) + } + } + klog.V(3).Infof("Deleted CiliumNetworkPolicy %s (no more owners)", cnpName) + return nil + } + + if err := unstructured.SetNestedSlice(existing.Object, remaining, "metadata", "ownerReferences"); err != nil { + return status.Errorf(codes.Internal, "failed to set ownerReferences: %v", err) + } + if _, err := w.dynamicClient.Resource(ciliumNetworkPolicyGVR).Namespace(namespace).Update(ctx, existing, metav1.UpdateOptions{}); err != nil { + return status.Errorf(codes.Internal, "failed to update CiliumNetworkPolicy %s: %v", cnpName, err) + } + klog.V(3).Infof("Removed VMI %s ownerReference from CiliumNetworkPolicy %s", vmName, cnpName) + return nil +} + +func hasRWXAccessMode(pvc *corev1.PersistentVolumeClaim) bool { + for _, mode := range pvc.Spec.AccessModes { + if mode == corev1.ReadWriteMany { + return true + } + } + return false +} + +// getNFSExport extracts the NFS export URL from a PersistentVolume. +// Supports both native NFS PVs and CSI PVs with nfs-export volume attribute. +func getNFSExport(pv *corev1.PersistentVolume) (string, error) { + if pv.Spec.NFS != nil { + return fmt.Sprintf("nfs://%s:2049%s", pv.Spec.NFS.Server, pv.Spec.NFS.Path), nil + } + if pv.Spec.CSI != nil && pv.Spec.CSI.VolumeAttributes != nil { + if export, ok := pv.Spec.CSI.VolumeAttributes["linstor.csi.linbit.com/nfs-export"]; ok { + return export, nil + } + } + return "", fmt.Errorf("no NFS export info found in PV %s", pv.Name) +} + +// parseNFSExport parses an NFS URL of the form nfs://host:port/path. +func parseNFSExport(nfsURL string) (host, port, path string, err error) { + u, err := url.Parse(nfsURL) + if err != nil { + return "", "", "", fmt.Errorf("failed to parse NFS URL %q: %w", nfsURL, err) + } + host = u.Hostname() + port = u.Port() + if port == "" { + port = "2049" + } + path = u.Path + if path == "" { + path = "/" + } + return host, port, path, nil +} diff --git a/packages/apps/kubernetes/images/kubevirt-csi-driver/go.mod b/packages/apps/kubernetes/images/kubevirt-csi-driver/go.mod new file mode 100644 index 00000000..96a96107 --- /dev/null +++ b/packages/apps/kubernetes/images/kubevirt-csi-driver/go.mod @@ -0,0 +1,101 @@ +module cozystack.io/kubevirt-csi-driver + +go 1.22.0 + +toolchain go1.22.5 + +require ( + github.com/container-storage-interface/spec v1.10.0 + google.golang.org/grpc v1.65.0 + gopkg.in/yaml.v2 v2.4.0 + k8s.io/api v0.31.4 + k8s.io/apimachinery v0.31.4 + k8s.io/client-go v12.0.0+incompatible + k8s.io/klog/v2 v2.130.1 + k8s.io/mount-utils v0.33.1 + kubevirt.io/containerized-data-importer-api v1.59.0 + kubevirt.io/csi-driver v0.0.0-20250702202414-a8d6605bc999 +) + +require ( + github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.22.4 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/mock v1.6.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/imdario/mergo v0.3.15 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/kubernetes-csi/csi-lib-utils v0.18.1 // indirect + github.com/kubernetes-csi/external-snapshotter/client/v6 v6.3.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/moby/sys/mountinfo v0.7.1 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/opencontainers/runc v1.1.13 // indirect + github.com/opencontainers/runtime-spec v1.0.3-0.20220909204839-494a5a6aca78 // indirect + github.com/openshift/api v0.0.0-20230503133300-8bbcb7ca7183 // indirect + github.com/openshift/custom-resource-status v1.1.2 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/x448/float16 v0.8.4 // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/oauth2 v0.21.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/term v0.27.0 // indirect + golang.org/x/text v0.21.0 // indirect + golang.org/x/time v0.3.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/apiextensions-apiserver v0.26.4 // indirect + k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect + k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect + kubevirt.io/api v1.2.2 // indirect + kubevirt.io/controller-lifecycle-operator-sdk/api v0.0.0-20220329064328-f3cc58c6ed90 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) + +replace ( + k8s.io/api => k8s.io/api v0.31.4 + k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.31.4 + k8s.io/apimachinery => k8s.io/apimachinery v0.31.4 + k8s.io/apiserver => k8s.io/apiserver v0.31.4 + k8s.io/cli-runtime => k8s.io/cli-runtime v0.31.4 + k8s.io/client-go => k8s.io/client-go v0.31.4 + k8s.io/cloud-provider => k8s.io/cloud-provider v0.31.4 + k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.31.4 + k8s.io/code-generator => k8s.io/code-generator v0.31.4 + k8s.io/component-base => k8s.io/component-base v0.31.4 + k8s.io/component-helpers => k8s.io/component-helpers v0.31.4 + k8s.io/controller-manager => k8s.io/controller-manager v0.31.4 + k8s.io/cri-api => k8s.io/cri-api v0.31.4 + k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.31.4 + k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.31.4 + k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.31.4 + k8s.io/kube-proxy => k8s.io/kube-proxy v0.31.4 + k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.31.4 + k8s.io/kubectl => k8s.io/kubectl v0.31.4 + k8s.io/kubelet => k8s.io/kubelet v0.31.4 + k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.31.4 + k8s.io/metrics => k8s.io/metrics v0.31.4 + k8s.io/mount-utils => k8s.io/mount-utils v0.31.4 + k8s.io/pod-security-admission => k8s.io/pod-security-admission v0.31.4 + k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.31.4 + kubevirt.io/csi-driver => github.com/kubevirt/csi-driver v0.0.0-20250702202414-a8d6605bc999 +) diff --git a/packages/apps/kubernetes/images/kubevirt-csi-driver/go.sum b/packages/apps/kubernetes/images/kubevirt-csi-driver/go.sum new file mode 100644 index 00000000..a0c4ac81 --- /dev/null +++ b/packages/apps/kubernetes/images/kubevirt-csi-driver/go.sum @@ -0,0 +1,543 @@ +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= +github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs= +github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= +github.com/container-storage-interface/spec v1.10.0 h1:YkzWPV39x+ZMTa6Ax2czJLLwpryrQ+dPesB34mrRMXA= +github.com/container-storage-interface/spec v1.10.0/go.mod h1:DtUvaQszPml1YJfIK7c00mlv6/g4wNMLanLgiUbKFRI= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.15.0+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/jsonreference v0.20.1/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= +github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af h1:kmjWCqn2qkEml422C2Rrd27c3VGxi6a/6HNq8QmHRKM= +github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20240312041847-bd984b5ce465/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= +github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= +github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kubernetes-csi/csi-lib-utils v0.18.1 h1:vpg1kbQ6lFVCz7mY71zcqVE7W0GAQXXBoFfHvbW3gdw= +github.com/kubernetes-csi/csi-lib-utils v0.18.1/go.mod h1:PIcn27zmbY0KBue4JDdZVfDF56tjcS3jKroZPi+pMoY= +github.com/kubernetes-csi/external-snapshotter/client/v6 v6.3.0 h1:qS4r4ljINLWKJ9m9Ge3Q3sGZ/eIoDVDT2RhAdQFHb1k= +github.com/kubernetes-csi/external-snapshotter/client/v6 v6.3.0/go.mod h1:oGXx2XTEzs9ikW2V6IC1dD8trgjRsS/Mvc2JRiC618Y= +github.com/kubevirt/csi-driver v0.0.0-20250702202414-a8d6605bc999 h1:nq11swNPIRarVZm/YTOWh9rbuEqivgpA1c4RUEFSL90= +github.com/kubevirt/csi-driver v0.0.0-20250702202414-a8d6605bc999/go.mod h1:Pzl14YlqPpoK/ZdS79tUSuHbC2L/NPqOIjFTN5kTmUg= +github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/moby/spdystream v0.4.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= +github.com/moby/sys/mountinfo v0.7.1 h1:/tTvQaSJRr2FshkhXiIpux6fQ2Zvc4j7tAhMTStAG2g= +github.com/moby/sys/mountinfo v0.7.1/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU= +github.com/onsi/ginkgo/v2 v2.1.6/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= +github.com/onsi/ginkgo/v2 v2.3.0/go.mod h1:Eew0uilEqZmIEZr8JrvYlvOM7Rr6xzTmMV8AyFNU9d0= +github.com/onsi/ginkgo/v2 v2.4.0/go.mod h1:iHkDK1fKGcBoEHT5W7YBq4RFWaQulw+caOMkAt4OrFo= +github.com/onsi/ginkgo/v2 v2.5.0/go.mod h1:Luc4sArBICYCS8THh8v3i3i5CuSZO+RaQRaJoeNwomw= +github.com/onsi/ginkgo/v2 v2.7.0/go.mod h1:yjiuMwPokqY1XauOgju45q3sJt6VzQ/Fict1LFVcsAo= +github.com/onsi/ginkgo/v2 v2.8.1/go.mod h1:N1/NbDngAFcSLdyZ+/aYTYGSlq9qMCS/cNKGJjy+csc= +github.com/onsi/ginkgo/v2 v2.9.0/go.mod h1:4xkjoL/tZv4SMWeww56BU5kAt19mVB47gTWxmrTcxyk= +github.com/onsi/ginkgo/v2 v2.9.1/go.mod h1:FEcmzVcCHl+4o9bQZVab+4dC9+j+91t2FHSzmGAPfuo= +github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts= +github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= +github.com/onsi/ginkgo/v2 v2.9.7/go.mod h1:cxrmXWykAwTwhQsJOPfdIDiJ+l2RYq7U8hFU+M/1uw0= +github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM= +github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= +github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs= +github.com/onsi/ginkgo/v2 v2.17.2/go.mod h1:nP2DPOQoNsQmsVyv5rDA8JkXQoCs6goXIvr/PRJ1eCc= +github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= +github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo= +github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc= +github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM= +github.com/onsi/gomega v1.24.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg= +github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM= +github.com/onsi/gomega v1.26.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= +github.com/onsi/gomega v1.27.1/go.mod h1:aHX5xOykVYzWOV4WqQy0sy8BQptgukenXpCXfadcIAw= +github.com/onsi/gomega v1.27.3/go.mod h1:5vG284IBtfDAmDyrK+eGyZmUgUlmi+Wngqo557cZ6Gw= +github.com/onsi/gomega v1.27.4/go.mod h1:riYq/GJKh8hhoM01HN6Vmuy93AarCXCBGpvFDK3q3fQ= +github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= +github.com/onsi/gomega v1.27.7/go.mod h1:1p8OOlwo2iUUDsHnOrjE5UKYJ+e3W8eQ3qSlRahPmr4= +github.com/onsi/gomega v1.27.8/go.mod h1:2J8vzI/s+2shY9XHRApDkdgPo1TKT7P2u6fXeJKFnNQ= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= +github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/onsi/gomega v1.33.0/go.mod h1:+925n5YtiFsLzzafLUHzVMBpvvRAzrydIBiSIxjX3wY= +github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= +github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= +github.com/opencontainers/runc v1.1.13 h1:98S2srgG9vw0zWcDpFMn5TRrh8kLxa/5OFUstuUhmRs= +github.com/opencontainers/runc v1.1.13/go.mod h1:R016aXacfp/gwQBYw2FDGa9m+n6atbLWrYY8hNMT/sA= +github.com/opencontainers/runtime-spec v1.0.3-0.20220909204839-494a5a6aca78 h1:R5M2qXZiK/mWPMT4VldCOiSL9HIAMuxQZWdG0CSM5+4= +github.com/opencontainers/runtime-spec v1.0.3-0.20220909204839-494a5a6aca78/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/openshift/api v0.0.0-20230503133300-8bbcb7ca7183 h1:t/CahSnpqY46sQR01SoS+Jt0jtjgmhgE6lFmRnO4q70= +github.com/openshift/api v0.0.0-20230503133300-8bbcb7ca7183/go.mod h1:4VWG+W22wrB4HfBL88P40DxLEpSOaiBVxUnfalfJo9k= +github.com/openshift/custom-resource-status v1.1.2 h1:C3DL44LEbvlbItfd8mT5jWrqPfHnSOQoQf/sypqA6A4= +github.com/openshift/custom-resource-status v1.1.2/go.mod h1:DB/Mf2oTeiAmVVX1gN+NEqweonAPY0TKUwADizj8+ZA= +github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU= +golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= +golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808/go.mod h1:KG1lNk5ZFNssSZLrpVb4sMXKMpGwGXOxSG3rnu2gZQQ= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= +golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= +golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= +golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= +golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= +golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= +golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= +golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.31.4 h1:I2QNzitPVsPeLQvexMEsj945QumYraqv9m74isPDKhM= +k8s.io/api v0.31.4/go.mod h1:d+7vgXLvmcdT1BCo79VEgJxHHryww3V5np2OYTr6jdw= +k8s.io/apiextensions-apiserver v0.31.4 h1:FxbqzSvy92Ca9DIs5jqot883G0Ln/PGXfm/07t39LS0= +k8s.io/apiextensions-apiserver v0.31.4/go.mod h1:hIW9YU8UsqZqIWGG99/gsdIU0Ar45Qd3A12QOe/rvpg= +k8s.io/apimachinery v0.31.4 h1:8xjE2C4CzhYVm9DGf60yohpNUh5AEBnPxCryPBECmlM= +k8s.io/apimachinery v0.31.4/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/client-go v0.31.4 h1:t4QEXt4jgHIkKKlx06+W3+1JOwAFU/2OPiOo7H92eRQ= +k8s.io/client-go v0.31.4/go.mod h1:kvuMro4sFYIa8sulL5Gi5GFqUPvfH2O/dXuKstbaaeg= +k8s.io/code-generator v0.31.4/go.mod h1:yMDt13Kn7m4MMZ4LxB1KBzdZjEyxzdT4b4qXq+lnI90= +k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/gengo v0.0.0-20211129171323-c02415ce4185/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/gengo/v2 v2.0.0-20240228010128-51d4e06bde70/go.mod h1:VH3AT8AaQOqiGjMF9p0/IM1Dj+82ZwjfxUP1IxaHE+8= +k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= +k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= +k8s.io/klog/v2 v2.40.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20220124234850-424119656bbf/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= +k8s.io/mount-utils v0.31.4 h1:9aWJ5BpJvs6fdIo36wWIuCC6ZMNllUT0JSFsVNJloFI= +k8s.io/mount-utils v0.31.4/go.mod h1:HV/VYBUGqYUj4vt82YltzpWvgv8FPg0G9ItyInT3NPU= +k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +kubevirt.io/api v1.2.2 h1:PeA937vsZawmKAsiiDQZJ/BbGH4OhEWsIzWrCNfmYXk= +kubevirt.io/api v1.2.2/go.mod h1:SbeR9ma4EwnaOZEUkh/lNz0kzYm5LPpEDE30vKXC5Zg= +kubevirt.io/containerized-data-importer-api v1.59.0 h1:GdDt9BlR0qHejpMaPfASbsG8JWDmBf1s7xZBj5W9qn0= +kubevirt.io/containerized-data-importer-api v1.59.0/go.mod h1:4yOGtCE7HvgKp7wftZZ3TBvDJ0x9d6N6KaRjRYcUFpE= +kubevirt.io/controller-lifecycle-operator-sdk/api v0.0.0-20220329064328-f3cc58c6ed90 h1:QMrd0nKP0BGbnxTqakhDZAUhGKxPiPiN5gSDqKUmGGc= +kubevirt.io/controller-lifecycle-operator-sdk/api v0.0.0-20220329064328-f3cc58c6ed90/go.mod h1:018lASpFYBsYN6XwmA2TIrPCx6e0gviTd/ZNtSitKgc= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/packages/apps/kubernetes/images/kubevirt-csi-driver/main.go b/packages/apps/kubernetes/images/kubevirt-csi-driver/main.go new file mode 100644 index 00000000..3d4d8e83 --- /dev/null +++ b/packages/apps/kubernetes/images/kubevirt-csi-driver/main.go @@ -0,0 +1,219 @@ +package main + +import ( + "context" + "flag" + "fmt" + "os" + "strings" + + csi "github.com/container-storage-interface/spec/lib/go/csi" + "gopkg.in/yaml.v2" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + klog "k8s.io/klog/v2" + mount "k8s.io/mount-utils" + + snapcli "kubevirt.io/csi-driver/pkg/generated/external-snapshotter/client-go/clientset/versioned" + "kubevirt.io/csi-driver/pkg/kubevirt" + "kubevirt.io/csi-driver/pkg/service" + "kubevirt.io/csi-driver/pkg/util" +) + +var ( + endpoint = flag.String("endpoint", "unix:/csi/csi.sock", "CSI endpoint") + nodeName = flag.String("node-name", "", "The node name - the node this pods runs on") + infraClusterNamespace = flag.String("infra-cluster-namespace", "", "The infra-cluster namespace") + infraClusterKubeconfig = flag.String("infra-cluster-kubeconfig", "", "the infra-cluster kubeconfig file. If not set, defaults to in cluster config.") + infraClusterLabels = flag.String("infra-cluster-labels", "", "The infra-cluster labels to use when creating resources in infra cluster. 'name=value' fields separated by a comma") + volumePrefix = flag.String("volume-prefix", "pvc", "The prefix expected for persistent volumes") + + infraStorageClassEnforcement = os.Getenv("INFRA_STORAGE_CLASS_ENFORCEMENT") + + tenantClusterKubeconfig = flag.String("tenant-cluster-kubeconfig", "", "the tenant cluster kubeconfig file. If not set, defaults to in cluster config.") + + runNodeService = flag.Bool("run-node-service", true, "Specifies rather or not to run the node service, the default is true") + runControllerService = flag.Bool("run-controller-service", true, "Specifies rather or not to run the controller service, the default is true") +) + +func init() { + klog.InitFlags(nil) +} + +func main() { + flag.Parse() + handle() + os.Exit(0) +} + +func handle() { + var tenantRestConfig *rest.Config + var infraRestConfig *rest.Config + var identityClientset *kubernetes.Clientset + + if service.VendorVersion == "" { + klog.Fatal("VendorVersion must be set at compile time") + } + klog.V(2).Infof("Driver vendor %v %v", service.VendorName, service.VendorVersion) + + if (infraClusterLabels == nil || *infraClusterLabels == "") && !*runNodeService { + klog.Fatal("infra-cluster-labels must be set") + } + if volumePrefix == nil || *volumePrefix == "" { + klog.Fatal("volume-prefix must be set") + } + + inClusterConfig, err := rest.InClusterConfig() + if err != nil { + klog.Fatalf("Failed to build in cluster config: %v", err) + } + + if *tenantClusterKubeconfig != "" { + tenantRestConfig, err = clientcmd.BuildConfigFromFlags("", *tenantClusterKubeconfig) + if err != nil { + klog.Fatalf("failed to build tenant cluster config: %v", err) + } + } else { + tenantRestConfig = inClusterConfig + } + + if *infraClusterKubeconfig != "" { + infraRestConfig, err = clientcmd.BuildConfigFromFlags("", *infraClusterKubeconfig) + if err != nil { + klog.Fatalf("failed to build infra cluster config: %v", err) + } + } else { + infraRestConfig = inClusterConfig + } + + tenantClientSet, err := kubernetes.NewForConfig(tenantRestConfig) + if err != nil { + klog.Fatalf("Failed to build tenant client set: %v", err) + } + tenantSnapshotClientSet, err := snapcli.NewForConfig(tenantRestConfig) + if err != nil { + klog.Fatalf("Failed to build tenant snapshot client set: %v", err) + } + + infraClusterLabelsMap := parseLabels() + klog.V(5).Infof("Storage class enforcement string: \n%s", infraStorageClassEnforcement) + storageClassEnforcement := configureStorageClassEnforcement(infraStorageClassEnforcement) + + virtClient, err := kubevirt.NewClient(infraRestConfig, infraClusterLabelsMap, tenantClientSet, tenantSnapshotClientSet, storageClassEnforcement, *volumePrefix) + if err != nil { + klog.Fatal(err) + } + + var nodeID string + if *nodeName != "" { + node, err := tenantClientSet.CoreV1().Nodes().Get(context.TODO(), *nodeName, v1.GetOptions{}) + if err != nil { + klog.Fatal(fmt.Errorf("failed to find node by name %v: %v", nodeName, err)) + } + if node.Spec.ProviderID == "" { + klog.Fatal("provider name missing from node, something's not right") + } + vmName := strings.TrimPrefix(node.Spec.ProviderID, `kubevirt://`) + vmNamespace, ok := node.Annotations["cluster.x-k8s.io/cluster-namespace"] + if !ok { + klog.Fatal("cannot infer infra vm namespace") + } + nodeID = fmt.Sprintf("%s/%s", vmNamespace, vmName) + klog.Infof("Node name: %v, Node ID: %s", *nodeName, nodeID) + } + + identityClientset = tenantClientSet + if *runControllerService { + identityClientset, err = kubernetes.NewForConfig(infraRestConfig) + if err != nil { + klog.Fatalf("Failed to build infra client set: %v", err) + } + } + + // Create upstream driver (provides Identity, Controller, Node services) + upstreamDriver := service.NewKubevirtCSIDriver(virtClient, + identityClientset, + *infraClusterNamespace, + infraClusterLabelsMap, + storageClassEnforcement, + nodeID, + *runNodeService, + *runControllerService) + + // Wrap controller and node services with NFS/RWX support + var cs csi.ControllerServer + if *runControllerService { + infraKubernetesClient, err := kubernetes.NewForConfig(infraRestConfig) + if err != nil { + klog.Fatalf("Failed to build infra kubernetes client: %v", err) + } + infraDynamicClient, err := dynamic.NewForConfig(infraRestConfig) + if err != nil { + klog.Fatalf("Failed to build infra dynamic client: %v", err) + } + cs = &WrappedControllerService{ + ControllerService: upstreamDriver.ControllerService, + infraClient: infraKubernetesClient, + dynamicClient: infraDynamicClient, + virtClient: virtClient, + infraNamespace: *infraClusterNamespace, + infraClusterLabels: infraClusterLabelsMap, + storageClassEnforcement: storageClassEnforcement, + } + } + + var ns csi.NodeServer + if *runNodeService { + ns = &WrappedNodeService{ + NodeService: upstreamDriver.NodeService, + mounter: mount.New(""), + } + } + + // Run gRPC server with upstream Identity + wrapped Controller/Node + s := service.NewNonBlockingGRPCServer() + s.Start(*endpoint, upstreamDriver.IdentityService, cs, ns) + s.Wait() +} + +func configureStorageClassEnforcement(infraStorageClassEnforcement string) util.StorageClassEnforcement { + var storageClassEnforcement util.StorageClassEnforcement + + if infraStorageClassEnforcement == "" { + storageClassEnforcement = util.StorageClassEnforcement{ + AllowAll: true, + AllowDefault: true, + } + } else { + err := yaml.Unmarshal([]byte(infraStorageClassEnforcement), &storageClassEnforcement) + if err != nil { + klog.Fatalf("Failed to parse infra-storage-class-enforcement %v", err) + } + } + return storageClassEnforcement +} + +func parseLabels() map[string]string { + infraClusterLabelsMap := map[string]string{} + + if *infraClusterLabels == "" { + return infraClusterLabelsMap + } + + labelStrings := strings.Split(*infraClusterLabels, ",") + + for _, label := range labelStrings { + labelPair := strings.SplitN(label, "=", 2) + + if len(labelPair) != 2 { + panic("Bad labels format. Should be 'key=value,key=value,...'") + } + + infraClusterLabelsMap[labelPair[0]] = labelPair[1] + } + + return infraClusterLabelsMap +} diff --git a/packages/apps/kubernetes/images/kubevirt-csi-driver/node.go b/packages/apps/kubernetes/images/kubevirt-csi-driver/node.go new file mode 100644 index 00000000..2260900b --- /dev/null +++ b/packages/apps/kubernetes/images/kubevirt-csi-driver/node.go @@ -0,0 +1,106 @@ +package main + +import ( + "context" + "fmt" + "os" + + csi "github.com/container-storage-interface/spec/lib/go/csi" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "k8s.io/klog/v2" + mount "k8s.io/mount-utils" + + "kubevirt.io/csi-driver/pkg/service" +) + +var _ csi.NodeServer = &WrappedNodeService{} + +// WrappedNodeService embeds the upstream NodeService and adds NFS mount support. +type WrappedNodeService struct { + *service.NodeService + mounter mount.Interface +} + +// NodeStageVolume for NFS volumes is a no-op (NFS doesn't need staging). +// For RWO volumes, delegates to upstream (lsblk + mkfs). +func (w *WrappedNodeService) NodeStageVolume(ctx context.Context, req *csi.NodeStageVolumeRequest) (*csi.NodeStageVolumeResponse, error) { + if req.GetPublishContext()[nfsExportKey] != "" { + klog.V(3).Infof("NFS volume %s: skipping stage", req.GetVolumeId()) + return &csi.NodeStageVolumeResponse{}, nil + } + return w.NodeService.NodeStageVolume(ctx, req) +} + +// NodePublishVolume for NFS volumes: mounts NFS at the target path. +// For RWO volumes, delegates to upstream (mount block device). +func (w *WrappedNodeService) NodePublishVolume(ctx context.Context, req *csi.NodePublishVolumeRequest) (*csi.NodePublishVolumeResponse, error) { + nfsExport := req.GetPublishContext()[nfsExportKey] + if nfsExport == "" { + return w.NodeService.NodePublishVolume(ctx, req) + } + + klog.V(3).Infof("Publishing NFS volume %s at %s", req.GetVolumeId(), req.GetTargetPath()) + + host, port, path, err := parseNFSExport(nfsExport) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to parse NFS export: %v", err) + } + + targetPath := req.GetTargetPath() + + // Check if already mounted + notMnt, err := w.mounter.IsLikelyNotMountPoint(targetPath) + if err != nil { + if !os.IsNotExist(err) { + return nil, status.Errorf(codes.Internal, "failed to check mount point %s: %v", targetPath, err) + } + if err := os.MkdirAll(targetPath, 0750); err != nil { + return nil, status.Errorf(codes.Internal, "failed to create target path %s: %v", targetPath, err) + } + notMnt = true + } + + if !notMnt { + klog.V(3).Infof("NFS volume %s already mounted at %s", req.GetVolumeId(), targetPath) + return &csi.NodePublishVolumeResponse{}, nil + } + + source := fmt.Sprintf("%s:%s", host, path) + mountOptions := []string{ + "nfsvers=4.2", + fmt.Sprintf("port=%s", port), + } + + klog.V(3).Infof("Mounting NFS %s at %s with options %v", source, targetPath, mountOptions) + if err := w.mounter.Mount(source, targetPath, "nfs", mountOptions); err != nil { + return nil, status.Errorf(codes.Internal, "NFS mount of %s at %s failed: %v", source, targetPath, err) + } + + return &csi.NodePublishVolumeResponse{}, nil +} + +// NodeExpandVolume for NFS volumes is a no-op (LINSTOR handles NFS resize automatically). +// This should not normally be called for NFS since ControllerExpandVolume returns +// NodeExpansionRequired=false, but we handle it gracefully as a safety net. +func (w *WrappedNodeService) NodeExpandVolume(ctx context.Context, req *csi.NodeExpandVolumeRequest) (*csi.NodeExpandVolumeResponse, error) { + if isNFSMount(req.GetVolumePath(), w.mounter) { + klog.V(3).Infof("NFS volume %s: skipping node expansion", req.GetVolumeId()) + return &csi.NodeExpandVolumeResponse{}, nil + } + return w.NodeService.NodeExpandVolume(ctx, req) +} + +// isNFSMount checks if the given path is an NFS mount point. +func isNFSMount(path string, m mount.Interface) bool { + mountPoints, err := m.List() + if err != nil { + return false + } + for _, mp := range mountPoints { + if mp.Path == path && (mp.Type == "nfs" || mp.Type == "nfs4") { + return true + } + } + return false +} diff --git a/packages/apps/kubernetes/templates/csi/deploy.yaml b/packages/apps/kubernetes/templates/csi/deploy.yaml index b79a6d64..938b6d67 100644 --- a/packages/apps/kubernetes/templates/csi/deploy.yaml +++ b/packages/apps/kubernetes/templates/csi/deploy.yaml @@ -32,6 +32,7 @@ spec: - "--endpoint=$(CSI_ENDPOINT)" - "--infra-cluster-namespace=$(INFRACLUSTER_NAMESPACE)" - "--infra-cluster-labels=$(INFRACLUSTER_LABELS)" + - "--run-node-service=false" - "--v=5" ports: - name: healthz diff --git a/packages/apps/kubernetes/templates/csi/infra-cluster-service-account.yaml b/packages/apps/kubernetes/templates/csi/infra-cluster-service-account.yaml index 793871d2..fb11ab25 100644 --- a/packages/apps/kubernetes/templates/csi/infra-cluster-service-account.yaml +++ b/packages/apps/kubernetes/templates/csi/infra-cluster-service-account.yaml @@ -24,6 +24,9 @@ rules: - apiGroups: [""] resources: ["persistentvolumeclaims"] verbs: ["get", "patch"] +- apiGroups: ["cilium.io"] + resources: ["ciliumnetworkpolicies"] + verbs: ["get", "create", "update", "delete"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding diff --git a/packages/system/kubevirt-csi-node/templates/deploy.yaml b/packages/system/kubevirt-csi-node/templates/deploy.yaml index ed3b43fb..4e95bf77 100644 --- a/packages/system/kubevirt-csi-node/templates/deploy.yaml +++ b/packages/system/kubevirt-csi-node/templates/deploy.yaml @@ -167,6 +167,7 @@ spec: args: - "--endpoint=unix:/csi/csi.sock" - "--node-name=$(KUBE_NODE_NAME)" + - "--run-controller-service=false" - "--v=5" env: - name: KUBE_NODE_NAME diff --git a/packages/system/kubevirt-csi-node/values.yaml b/packages/system/kubevirt-csi-node/values.yaml index cf8b4fa8..f7fd69a1 100644 --- a/packages/system/kubevirt-csi-node/values.yaml +++ b/packages/system/kubevirt-csi-node/values.yaml @@ -1,3 +1,3 @@ storageClass: replicated csiDriver: - image: ghcr.io/cozystack/cozystack/kubevirt-csi-driver:0.0.0@sha256:8f1ab4c3b2bed3a0adc40fcc823b040fa04b4722bec7735c030e79a3a2fd6c85 + image: ghcr.io/cozystack/cozystack/kubevirt-csi-driver:latest@sha256:cd58760c97ba50ef74ff940cdcda4b9a6d8554eec8305fc96c2e8bbea72b75a9 From 46103400f2ccec25a848878d7c875c7d531e5956 Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Thu, 12 Feb 2026 23:27:44 +0100 Subject: [PATCH 028/666] test(e2e): adapt kubernetes NFS test for native RWX CSI support Remove separate NFS Application dependency from e2e test. The kubevirt CSI driver wrapper now handles RWX Filesystem volumes natively - PVCs with ReadWriteMany accessMode use the standard kubevirt StorageClass. Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- hack/e2e-apps/run-kubernetes.sh | 55 ++++++++++++++++++++++++++++ packages/apps/kubernetes/.helmignore | 1 + 2 files changed, 56 insertions(+) diff --git a/hack/e2e-apps/run-kubernetes.sh b/hack/e2e-apps/run-kubernetes.sh index 3e2e20f9..c62ae131 100644 --- a/hack/e2e-apps/run-kubernetes.sh +++ b/hack/e2e-apps/run-kubernetes.sh @@ -218,6 +218,61 @@ fi kubectl delete deployment --kubeconfig tenantkubeconfig-${test_name} "${test_name}-backend" -n tenant-test kubectl delete service --kubeconfig tenantkubeconfig-${test_name} "${test_name}-backend" -n tenant-test + # Test RWX NFS mount in tenant cluster (uses kubevirt CSI driver with RWX support) + kubectl --kubeconfig tenantkubeconfig-${test_name} apply -f - < /data/test.txt && cat /data/test.txt"] + volumeMounts: + - name: nfs-vol + mountPath: /data + volumes: + - name: nfs-vol + persistentVolumeClaim: + claimName: nfs-test-pvc + restartPolicy: Never +EOF + + # Wait for Pod to complete successfully + kubectl --kubeconfig tenantkubeconfig-${test_name} wait pod nfs-test-pod -n tenant-test --timeout=2m --for=jsonpath='{.status.phase}'=Succeeded + + # Verify NFS data integrity + nfs_result=$(kubectl --kubeconfig tenantkubeconfig-${test_name} logs nfs-test-pod -n tenant-test) + if [ "$nfs_result" != "nfs-mount-ok" ]; then + echo "NFS mount test failed: expected 'nfs-mount-ok', got '$nfs_result'" >&2 + exit 1 + fi + + # Cleanup NFS test resources in tenant cluster + kubectl --kubeconfig tenantkubeconfig-${test_name} delete pod nfs-test-pod -n tenant-test + kubectl --kubeconfig tenantkubeconfig-${test_name} delete pvc nfs-test-pvc -n tenant-test + # Wait for all machine deployment replicas to be ready (timeout after 10 minutes) kubectl wait machinedeployment kubernetes-${test_name}-md0 -n tenant-test --timeout=10m --for=jsonpath='{.status.v1beta2.readyReplicas}'=2 diff --git a/packages/apps/kubernetes/.helmignore b/packages/apps/kubernetes/.helmignore index 3de7d4a5..cdada667 100644 --- a/packages/apps/kubernetes/.helmignore +++ b/packages/apps/kubernetes/.helmignore @@ -2,3 +2,4 @@ /logos /Makefile /hack +/images/*/* From 168f6f2445a629e24a9e326c86063bda84e567df Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Fri, 13 Feb 2026 09:04:33 +0100 Subject: [PATCH 029/666] fix(csi): address review feedback for kubevirt-csi-driver RWX support - Move nil check before req dereference in CreateVolume - Scope CiliumNetworkPolicy endpointSelector to specific VMI - Use vmNamespace from NodeId for VMI lookup instead of infraNamespace - Log PVC lookup errors in ControllerExpandVolume - Wrap CNP ownerReference updates in retry.RetryOnConflict - Fix infraClusterLabels validation to check runControllerService flag - Dereference nodeName pointer in error message - Replace panic with klog.Fatal for consistent error handling - Honor CSI readonly flag in NFS NodePublishVolume - Log mount list errors in isNFSMount - Reorder Dockerfile ENTRYPOINT after COPY for better layer caching - Add cleanup on e2e test failure and --wait on pod deletion Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- hack/e2e-apps/run-kubernetes.sh | 4 +- .../images/kubevirt-csi-driver/Dockerfile | 4 +- .../images/kubevirt-csi-driver/controller.go | 129 ++++++++++-------- .../images/kubevirt-csi-driver/main.go | 6 +- .../images/kubevirt-csi-driver/node.go | 4 + 5 files changed, 82 insertions(+), 65 deletions(-) diff --git a/hack/e2e-apps/run-kubernetes.sh b/hack/e2e-apps/run-kubernetes.sh index c62ae131..5b8b1b4b 100644 --- a/hack/e2e-apps/run-kubernetes.sh +++ b/hack/e2e-apps/run-kubernetes.sh @@ -266,11 +266,13 @@ EOF nfs_result=$(kubectl --kubeconfig tenantkubeconfig-${test_name} logs nfs-test-pod -n tenant-test) if [ "$nfs_result" != "nfs-mount-ok" ]; then echo "NFS mount test failed: expected 'nfs-mount-ok', got '$nfs_result'" >&2 + kubectl --kubeconfig tenantkubeconfig-${test_name} delete pod nfs-test-pod -n tenant-test --wait=false 2>/dev/null || true + kubectl --kubeconfig tenantkubeconfig-${test_name} delete pvc nfs-test-pvc -n tenant-test --wait=false 2>/dev/null || true exit 1 fi # Cleanup NFS test resources in tenant cluster - kubectl --kubeconfig tenantkubeconfig-${test_name} delete pod nfs-test-pod -n tenant-test + kubectl --kubeconfig tenantkubeconfig-${test_name} delete pod nfs-test-pod -n tenant-test --wait kubectl --kubeconfig tenantkubeconfig-${test_name} delete pvc nfs-test-pvc -n tenant-test # Wait for all machine deployment replicas to be ready (timeout after 10 minutes) diff --git a/packages/apps/kubernetes/images/kubevirt-csi-driver/Dockerfile b/packages/apps/kubernetes/images/kubevirt-csi-driver/Dockerfile index be8125d7..b156d08e 100644 --- a/packages/apps/kubernetes/images/kubevirt-csi-driver/Dockerfile +++ b/packages/apps/kubernetes/images/kubevirt-csi-driver/Dockerfile @@ -16,8 +16,8 @@ RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build \ FROM quay.io/centos/centos:stream9 -ENTRYPOINT ["./kubevirt-csi-driver"] - RUN dnf install -y e2fsprogs xfsprogs nfs-utils && dnf clean all COPY --from=builder /src/kubevirt-csi-driver . + +ENTRYPOINT ["./kubevirt-csi-driver"] diff --git a/packages/apps/kubernetes/images/kubevirt-csi-driver/controller.go b/packages/apps/kubernetes/images/kubevirt-csi-driver/controller.go index 524b45ea..7192b95b 100644 --- a/packages/apps/kubernetes/images/kubevirt-csi-driver/controller.go +++ b/packages/apps/kubernetes/images/kubevirt-csi-driver/controller.go @@ -19,6 +19,7 @@ import ( "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/cache" + "k8s.io/client-go/util/retry" "k8s.io/klog/v2" cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" @@ -77,13 +78,13 @@ func isRWXFilesystem(caps []*csi.VolumeCapability) bool { // compatibility with upstream snapshot and clone operations. // For all other requests, delegates to upstream. func (w *WrappedControllerService) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest) (*csi.CreateVolumeResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "missing request") + } if !isRWXFilesystem(req.GetVolumeCapabilities()) { return w.ControllerService.CreateVolume(ctx, req) } - if req == nil { - return nil, status.Error(codes.InvalidArgument, "missing request") - } if len(req.GetName()) == 0 { return nil, status.Error(codes.InvalidArgument, "name missing in request") } @@ -260,9 +261,9 @@ func (w *WrappedControllerService) ControllerPublishVolume(ctx context.Context, klog.V(3).Infof("Publishing NFS volume %s to node %s/%s", dvName, vmNamespace, vmName) // Get VMI for CiliumNetworkPolicy ownerReference - vmi, err := w.virtClient.GetVirtualMachine(ctx, w.infraNamespace, vmName) + vmi, err := w.virtClient.GetVirtualMachine(ctx, vmNamespace, vmName) if err != nil { - return nil, status.Errorf(codes.Internal, "failed to get VMI %s: %v", vmName, err) + return nil, status.Errorf(codes.Internal, "failed to get VMI %s/%s: %v", vmNamespace, vmName, err) } // Wait for PVC to be bound (CDI handles immediate binding via annotation) @@ -316,7 +317,11 @@ func (w *WrappedControllerService) ControllerPublishVolume(ctx context.Context, "ownerReferences": []interface{}{vmiOwnerRef}, }, "spec": map[string]interface{}{ - "endpointSelector": map[string]interface{}{}, + "endpointSelector": map[string]interface{}{ + "matchLabels": map[string]interface{}{ + "kubevirt.io/vm": vmName, + }, + }, "egress": []interface{}{ map[string]interface{}{ "toEndpoints": []interface{}{ @@ -405,7 +410,9 @@ func (w *WrappedControllerService) ControllerExpandVolume(ctx context.Context, r // For NFS volumes, no node-side expansion is needed pvc, err := w.infraClient.CoreV1().PersistentVolumeClaims(w.infraNamespace).Get(ctx, req.GetVolumeId(), metav1.GetOptions{}) - if err == nil && hasRWXAccessMode(pvc) { + if err != nil { + klog.Warningf("Failed to check PVC access mode for %s/%s: %v", w.infraNamespace, req.GetVolumeId(), err) + } else if hasRWXAccessMode(pvc) { resp.NodeExpansionRequired = false } @@ -414,73 +421,77 @@ func (w *WrappedControllerService) ControllerExpandVolume(ctx context.Context, r // addCNPOwnerReference adds a VMI ownerReference to an existing CiliumNetworkPolicy. func (w *WrappedControllerService) addCNPOwnerReference(ctx context.Context, namespace, cnpName string, ownerRef map[string]interface{}) error { - existing, err := w.dynamicClient.Resource(ciliumNetworkPolicyGVR).Namespace(namespace).Get(ctx, cnpName, metav1.GetOptions{}) - if err != nil { - return status.Errorf(codes.Internal, "failed to get CiliumNetworkPolicy %s: %v", cnpName, err) - } + return retry.RetryOnConflict(retry.DefaultRetry, func() error { + existing, err := w.dynamicClient.Resource(ciliumNetworkPolicyGVR).Namespace(namespace).Get(ctx, cnpName, metav1.GetOptions{}) + if err != nil { + return status.Errorf(codes.Internal, "failed to get CiliumNetworkPolicy %s: %v", cnpName, err) + } - ownerRefs, _, _ := unstructured.NestedSlice(existing.Object, "metadata", "ownerReferences") - uid, _, _ := unstructured.NestedString(ownerRef, "uid") - for _, ref := range ownerRefs { - if refMap, ok := ref.(map[string]interface{}); ok { - if refMap["uid"] == uid { - return nil // already present + ownerRefs, _, _ := unstructured.NestedSlice(existing.Object, "metadata", "ownerReferences") + uid, _, _ := unstructured.NestedString(ownerRef, "uid") + for _, ref := range ownerRefs { + if refMap, ok := ref.(map[string]interface{}); ok { + if refMap["uid"] == uid { + return nil // already present + } } } - } - ownerRefs = append(ownerRefs, ownerRef) - if err := unstructured.SetNestedSlice(existing.Object, ownerRefs, "metadata", "ownerReferences"); err != nil { - return status.Errorf(codes.Internal, "failed to set ownerReferences: %v", err) - } - if _, err := w.dynamicClient.Resource(ciliumNetworkPolicyGVR).Namespace(namespace).Update(ctx, existing, metav1.UpdateOptions{}); err != nil { - return status.Errorf(codes.Internal, "failed to update CiliumNetworkPolicy %s: %v", cnpName, err) - } - klog.V(3).Infof("Added ownerReference to CiliumNetworkPolicy %s", cnpName) - return nil + ownerRefs = append(ownerRefs, ownerRef) + if err := unstructured.SetNestedSlice(existing.Object, ownerRefs, "metadata", "ownerReferences"); err != nil { + return status.Errorf(codes.Internal, "failed to set ownerReferences: %v", err) + } + if _, err := w.dynamicClient.Resource(ciliumNetworkPolicyGVR).Namespace(namespace).Update(ctx, existing, metav1.UpdateOptions{}); err != nil { + return err + } + klog.V(3).Infof("Added ownerReference to CiliumNetworkPolicy %s", cnpName) + return nil + }) } // removeCNPOwnerReference removes a VMI ownerReference from a CiliumNetworkPolicy. // Deletes the CNP if no ownerReferences remain. func (w *WrappedControllerService) removeCNPOwnerReference(ctx context.Context, namespace, cnpName, vmName string) error { - existing, err := w.dynamicClient.Resource(ciliumNetworkPolicyGVR).Namespace(namespace).Get(ctx, cnpName, metav1.GetOptions{}) - if err != nil { - if errors.IsNotFound(err) { + return retry.RetryOnConflict(retry.DefaultRetry, func() error { + existing, err := w.dynamicClient.Resource(ciliumNetworkPolicyGVR).Namespace(namespace).Get(ctx, cnpName, metav1.GetOptions{}) + if err != nil { + if errors.IsNotFound(err) { + return nil + } + return status.Errorf(codes.Internal, "failed to get CiliumNetworkPolicy %s: %v", cnpName, err) + } + + ownerRefs, _, _ := unstructured.NestedSlice(existing.Object, "metadata", "ownerReferences") + var remaining []interface{} + for _, ref := range ownerRefs { + if refMap, ok := ref.(map[string]interface{}); ok { + if refMap["name"] == vmName { + continue + } + } + remaining = append(remaining, ref) + } + + if len(remaining) == 0 { + // Last owner — delete CNP + if err := w.dynamicClient.Resource(ciliumNetworkPolicyGVR).Namespace(namespace).Delete(ctx, cnpName, metav1.DeleteOptions{}); err != nil { + if !errors.IsNotFound(err) { + return status.Errorf(codes.Internal, "failed to delete CiliumNetworkPolicy %s: %v", cnpName, err) + } + } + klog.V(3).Infof("Deleted CiliumNetworkPolicy %s (no more owners)", cnpName) return nil } - return status.Errorf(codes.Internal, "failed to get CiliumNetworkPolicy %s: %v", cnpName, err) - } - ownerRefs, _, _ := unstructured.NestedSlice(existing.Object, "metadata", "ownerReferences") - var remaining []interface{} - for _, ref := range ownerRefs { - if refMap, ok := ref.(map[string]interface{}); ok { - if refMap["name"] == vmName { - continue - } + if err := unstructured.SetNestedSlice(existing.Object, remaining, "metadata", "ownerReferences"); err != nil { + return status.Errorf(codes.Internal, "failed to set ownerReferences: %v", err) } - remaining = append(remaining, ref) - } - - if len(remaining) == 0 { - // Last owner — delete CNP - if err := w.dynamicClient.Resource(ciliumNetworkPolicyGVR).Namespace(namespace).Delete(ctx, cnpName, metav1.DeleteOptions{}); err != nil { - if !errors.IsNotFound(err) { - return status.Errorf(codes.Internal, "failed to delete CiliumNetworkPolicy %s: %v", cnpName, err) - } + if _, err := w.dynamicClient.Resource(ciliumNetworkPolicyGVR).Namespace(namespace).Update(ctx, existing, metav1.UpdateOptions{}); err != nil { + return err } - klog.V(3).Infof("Deleted CiliumNetworkPolicy %s (no more owners)", cnpName) + klog.V(3).Infof("Removed VMI %s ownerReference from CiliumNetworkPolicy %s", vmName, cnpName) return nil - } - - if err := unstructured.SetNestedSlice(existing.Object, remaining, "metadata", "ownerReferences"); err != nil { - return status.Errorf(codes.Internal, "failed to set ownerReferences: %v", err) - } - if _, err := w.dynamicClient.Resource(ciliumNetworkPolicyGVR).Namespace(namespace).Update(ctx, existing, metav1.UpdateOptions{}); err != nil { - return status.Errorf(codes.Internal, "failed to update CiliumNetworkPolicy %s: %v", cnpName, err) - } - klog.V(3).Infof("Removed VMI %s ownerReference from CiliumNetworkPolicy %s", vmName, cnpName) - return nil + }) } func hasRWXAccessMode(pvc *corev1.PersistentVolumeClaim) bool { diff --git a/packages/apps/kubernetes/images/kubevirt-csi-driver/main.go b/packages/apps/kubernetes/images/kubevirt-csi-driver/main.go index 3d4d8e83..396cee42 100644 --- a/packages/apps/kubernetes/images/kubevirt-csi-driver/main.go +++ b/packages/apps/kubernetes/images/kubevirt-csi-driver/main.go @@ -59,7 +59,7 @@ func handle() { } klog.V(2).Infof("Driver vendor %v %v", service.VendorName, service.VendorVersion) - if (infraClusterLabels == nil || *infraClusterLabels == "") && !*runNodeService { + if (infraClusterLabels == nil || *infraClusterLabels == "") && *runControllerService { klog.Fatal("infra-cluster-labels must be set") } if volumePrefix == nil || *volumePrefix == "" { @@ -111,7 +111,7 @@ func handle() { if *nodeName != "" { node, err := tenantClientSet.CoreV1().Nodes().Get(context.TODO(), *nodeName, v1.GetOptions{}) if err != nil { - klog.Fatal(fmt.Errorf("failed to find node by name %v: %v", nodeName, err)) + klog.Fatal(fmt.Errorf("failed to find node by name %v: %v", *nodeName, err)) } if node.Spec.ProviderID == "" { klog.Fatal("provider name missing from node, something's not right") @@ -209,7 +209,7 @@ func parseLabels() map[string]string { labelPair := strings.SplitN(label, "=", 2) if len(labelPair) != 2 { - panic("Bad labels format. Should be 'key=value,key=value,...'") + klog.Fatal("Bad labels format. Should be 'key=value,key=value,...'") } infraClusterLabelsMap[labelPair[0]] = labelPair[1] diff --git a/packages/apps/kubernetes/images/kubevirt-csi-driver/node.go b/packages/apps/kubernetes/images/kubevirt-csi-driver/node.go index 2260900b..5636a1d5 100644 --- a/packages/apps/kubernetes/images/kubevirt-csi-driver/node.go +++ b/packages/apps/kubernetes/images/kubevirt-csi-driver/node.go @@ -71,6 +71,9 @@ func (w *WrappedNodeService) NodePublishVolume(ctx context.Context, req *csi.Nod "nfsvers=4.2", fmt.Sprintf("port=%s", port), } + if req.GetReadonly() { + mountOptions = append(mountOptions, "ro") + } klog.V(3).Infof("Mounting NFS %s at %s with options %v", source, targetPath, mountOptions) if err := w.mounter.Mount(source, targetPath, "nfs", mountOptions); err != nil { @@ -95,6 +98,7 @@ func (w *WrappedNodeService) NodeExpandVolume(ctx context.Context, req *csi.Node func isNFSMount(path string, m mount.Interface) bool { mountPoints, err := m.List() if err != nil { + klog.Warningf("Failed to list mount points: %v", err) return false } for _, mp := range mountPoints { From 574c636761e8a990685d26c610985f17f34f57d5 Mon Sep 17 00:00:00 2001 From: Kirill Ilin Date: Fri, 13 Feb 2026 16:31:09 +0500 Subject: [PATCH 030/666] [cozystack-operator] Preserve existing suspend field in package reconciler Signed-off-by: Kirill Ilin --- internal/operator/package_reconciler.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/operator/package_reconciler.go b/internal/operator/package_reconciler.go index 79d78a87..e15f57ac 100644 --- a/internal/operator/package_reconciler.go +++ b/internal/operator/package_reconciler.go @@ -211,13 +211,13 @@ func (r *PackageReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct Namespace: "cozy-system", }, Install: &helmv2.Install{ - Timeout: &metav1.Duration{Duration: 10 * 60 * 1000000000}, // 10m + Timeout: &metav1.Duration{Duration: 10 * 60 * 1000000000}, // 10m Remediation: &helmv2.InstallRemediation{ Retries: -1, }, }, Upgrade: &helmv2.Upgrade{ - Timeout: &metav1.Duration{Duration: 10 * 60 * 1000000000}, // 10m + Timeout: &metav1.Duration{Duration: 10 * 60 * 1000000000}, // 10m Remediation: &helmv2.UpgradeRemediation{ Retries: -1, }, @@ -387,6 +387,7 @@ func (r *PackageReconciler) createOrUpdateHelmRelease(ctx context.Context, hr *h } hr.SetAnnotations(annotations) + hr.Spec.Suspend = existing.Spec.Suspend // Update Spec existing.Spec = hr.Spec existing.SetLabels(hr.GetLabels()) @@ -816,7 +817,7 @@ func (r *PackageReconciler) reconcileNamespaces(ctx context.Context, pkg *cozyv1 func (r *PackageReconciler) createOrUpdateNamespace(ctx context.Context, namespace *corev1.Namespace) error { // Ensure TypeMeta is set for server-side apply namespace.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Namespace")) - + // Use server-side apply with field manager // This is atomic and avoids race conditions from Get/Create/Update pattern // Labels and annotations will be merged automatically by the server From 371f67276a2989e385fd28e47c4de41e39ec3bf5 Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Fri, 13 Feb 2026 16:34:00 +0100 Subject: [PATCH 031/666] fix(operator): use per-Package SSA field owner for namespace reconciliation When multiple Packages share a namespace (e.g. cozystack.linstor and cozystack.linstor-scheduler both use cozy-linstor), the shared SSA field owner "cozystack-package-controller" caused a race condition: the last Package to reconcile would overwrite the namespace labels set by others. This meant that if cozystack.linstor set pod-security.kubernetes.io/enforce= privileged and then cozystack.linstor-scheduler reconciled without the privileged flag, SSA would remove the label. On Talos clusters (which default to "baseline" PodSecurity enforcement), this caused privileged satellite pods to be rejected with PodSecurity violations. Fix by using per-Package field owners (cozystack-package-{name}), so each Package independently manages its own namespace labels without interfering with other Packages sharing the same namespace. Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- internal/operator/package_reconciler.go | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/internal/operator/package_reconciler.go b/internal/operator/package_reconciler.go index e15f57ac..b69539e4 100644 --- a/internal/operator/package_reconciler.go +++ b/internal/operator/package_reconciler.go @@ -803,7 +803,7 @@ func (r *PackageReconciler) reconcileNamespaces(ctx context.Context, pkg *cozyv1 namespace.Labels["pod-security.kubernetes.io/enforce"] = "privileged" } - if err := r.createOrUpdateNamespace(ctx, namespace); err != nil { + if err := r.createOrUpdateNamespace(ctx, namespace, pkg.Name); err != nil { logger.Error(err, "failed to reconcile namespace", "name", nsName, "privileged", info.privileged) return fmt.Errorf("failed to reconcile namespace %s: %w", nsName, err) } @@ -813,16 +813,19 @@ func (r *PackageReconciler) reconcileNamespaces(ctx context.Context, pkg *cozyv1 return nil } -// createOrUpdateNamespace creates or updates a namespace using server-side apply -func (r *PackageReconciler) createOrUpdateNamespace(ctx context.Context, namespace *corev1.Namespace) error { +// createOrUpdateNamespace creates or updates a namespace using server-side apply. +// Each Package uses its own field owner to prevent different Packages sharing the same +// namespace from overwriting each other's labels. This ensures that if any Package sets +// the privileged PSA label, other Packages reconciling the same namespace won't remove it. +func (r *PackageReconciler) createOrUpdateNamespace(ctx context.Context, namespace *corev1.Namespace, packageName string) error { // Ensure TypeMeta is set for server-side apply namespace.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Namespace")) - // Use server-side apply with field manager - // This is atomic and avoids race conditions from Get/Create/Update pattern - // Labels and annotations will be merged automatically by the server - // Each label/annotation key is treated as a separate field, so existing ones are preserved - return r.Patch(ctx, namespace, client.Apply, client.FieldOwner("cozystack-package-controller")) + // Use per-Package field owner to avoid conflicts between Packages sharing a namespace. + // ForceOwnership is needed to take over fields from the previous shared field owner + // ("cozystack-package-controller") during the transition. + fieldOwner := fmt.Sprintf("cozystack-package-%s", packageName) + return r.Patch(ctx, namespace, client.Apply, client.FieldOwner(fieldOwner), client.ForceOwnership) } // cleanupOrphanedHelmReleases removes HelmReleases that are no longer needed From 6576b3bb8742a2c9ae8a238194d092ee0ddd5dd6 Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Fri, 13 Feb 2026 17:08:01 +0100 Subject: [PATCH 032/666] fix(operator): resolve namespace privileged flag by checking all Packages Instead of using per-Package SSA field owners (which is a workaround relying on SSA mechanics), properly resolve whether a namespace should be privileged by iterating all PackageSources and their active Packages. A namespace gets the privileged PodSecurity label if ANY Package has a component with privileged: true installed in it. This fixes the race condition where Packages sharing a namespace (e.g. linstor and linstor-scheduler in cozy-linstor) would overwrite each other's labels depending on reconciliation order. Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- internal/operator/package_reconciler.go | 126 +++++++++++++++--------- 1 file changed, 81 insertions(+), 45 deletions(-) diff --git a/internal/operator/package_reconciler.go b/internal/operator/package_reconciler.go index b69539e4..32e667e4 100644 --- a/internal/operator/package_reconciler.go +++ b/internal/operator/package_reconciler.go @@ -736,53 +736,39 @@ func (r *PackageReconciler) updateDependentPackagesDependencies(ctx context.Cont return nil } -// reconcileNamespaces creates or updates namespaces based on components in the variant +// reconcileNamespaces creates or updates namespaces based on components in the variant. +// For each namespace, it checks ALL Packages sharing that namespace to determine whether +// the namespace should be privileged — it is privileged if ANY Package has a privileged +// component installed in it. func (r *PackageReconciler) reconcileNamespaces(ctx context.Context, pkg *cozyv1alpha1.Package, variant *cozyv1alpha1.Variant) error { logger := log.FromContext(ctx) - // Collect namespaces from components - // Map: namespace -> {isPrivileged} - type namespaceInfo struct { - privileged bool - } - namespacesMap := make(map[string]namespaceInfo) - + // Collect namespaces from this Package's components + targetNamespaces := make(map[string]struct{}) for _, component := range variant.Components { - // Skip components without Install section if component.Install == nil { continue } - - // Check if component is disabled via Package spec if pkgComponent, ok := pkg.Spec.Components[component.Name]; ok { if pkgComponent.Enabled != nil && !*pkgComponent.Enabled { continue } } - - // Namespace must be set namespace := component.Install.Namespace if namespace == "" { return fmt.Errorf("component %s has empty namespace in Install section", component.Name) } + targetNamespaces[namespace] = struct{}{} + } - info, exists := namespacesMap[namespace] - if !exists { - info = namespaceInfo{ - privileged: false, - } - } - - // If component is privileged, mark namespace as privileged - if component.Install.Privileged { - info.privileged = true - } - - namespacesMap[namespace] = info + // Determine which namespaces should be privileged by checking ALL Packages + privileged, err := r.resolvePrivilegedNamespaces(ctx, targetNamespaces) + if err != nil { + return fmt.Errorf("failed to resolve privileged namespaces: %w", err) } // Create or update all namespaces - for nsName, info := range namespacesMap { + for nsName := range targetNamespaces { namespace := &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: nsName, @@ -793,39 +779,89 @@ func (r *PackageReconciler) reconcileNamespaces(ctx context.Context, pkg *cozyv1 }, } - // Add system label only for non-tenant namespaces if !strings.HasPrefix(nsName, "tenant-") { namespace.Labels["cozystack.io/system"] = "true" } - // Add privileged label if needed - if info.privileged { + if privileged[nsName] { namespace.Labels["pod-security.kubernetes.io/enforce"] = "privileged" } - if err := r.createOrUpdateNamespace(ctx, namespace, pkg.Name); err != nil { - logger.Error(err, "failed to reconcile namespace", "name", nsName, "privileged", info.privileged) + if err := r.createOrUpdateNamespace(ctx, namespace); err != nil { + logger.Error(err, "failed to reconcile namespace", "name", nsName, "privileged", privileged[nsName]) return fmt.Errorf("failed to reconcile namespace %s: %w", nsName, err) } - logger.Info("reconciled namespace", "name", nsName, "privileged", info.privileged) + logger.Info("reconciled namespace", "name", nsName, "privileged", privileged[nsName]) } return nil } -// createOrUpdateNamespace creates or updates a namespace using server-side apply. -// Each Package uses its own field owner to prevent different Packages sharing the same -// namespace from overwriting each other's labels. This ensures that if any Package sets -// the privileged PSA label, other Packages reconciling the same namespace won't remove it. -func (r *PackageReconciler) createOrUpdateNamespace(ctx context.Context, namespace *corev1.Namespace, packageName string) error { - // Ensure TypeMeta is set for server-side apply - namespace.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Namespace")) +// resolvePrivilegedNamespaces checks all PackageSources and their corresponding Packages +// to determine which of the given namespaces require the privileged PodSecurity level. +// A namespace is privileged if ANY active Package has a component with privileged: true in it. +func (r *PackageReconciler) resolvePrivilegedNamespaces(ctx context.Context, namespaces map[string]struct{}) (map[string]bool, error) { + result := make(map[string]bool) - // Use per-Package field owner to avoid conflicts between Packages sharing a namespace. - // ForceOwnership is needed to take over fields from the previous shared field owner - // ("cozystack-package-controller") during the transition. - fieldOwner := fmt.Sprintf("cozystack-package-%s", packageName) - return r.Patch(ctx, namespace, client.Apply, client.FieldOwner(fieldOwner), client.ForceOwnership) + packageSources := &cozyv1alpha1.PackageSourceList{} + if err := r.List(ctx, packageSources); err != nil { + return nil, fmt.Errorf("failed to list PackageSources: %w", err) + } + + for i := range packageSources.Items { + ps := &packageSources.Items[i] + + // Check if a Package exists for this PackageSource + pkg := &cozyv1alpha1.Package{} + if err := r.Get(ctx, types.NamespacedName{Name: ps.Name}, pkg); err != nil { + if apierrors.IsNotFound(err) { + continue + } + return nil, fmt.Errorf("failed to get Package %s: %w", ps.Name, err) + } + + // Resolve active variant + variantName := pkg.Spec.Variant + if variantName == "" { + variantName = "default" + } + + var variant *cozyv1alpha1.Variant + for j := range ps.Spec.Variants { + if ps.Spec.Variants[j].Name == variantName { + variant = &ps.Spec.Variants[j] + break + } + } + if variant == nil { + continue + } + + for _, component := range variant.Components { + if component.Install == nil { + continue + } + if pkgComponent, ok := pkg.Spec.Components[component.Name]; ok { + if pkgComponent.Enabled != nil && !*pkgComponent.Enabled { + continue + } + } + if _, relevant := namespaces[component.Install.Namespace]; !relevant { + continue + } + if component.Install.Privileged { + result[component.Install.Namespace] = true + } + } + } + + return result, nil +} + +// createOrUpdateNamespace creates or updates a namespace using server-side apply. +func (r *PackageReconciler) createOrUpdateNamespace(ctx context.Context, namespace *corev1.Namespace) error { + namespace.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Namespace")) + return r.Patch(ctx, namespace, client.Apply, client.FieldOwner("cozystack-package-controller"), client.ForceOwnership) } // cleanupOrphanedHelmReleases removes HelmReleases that are no longer needed From 62116030dc0bbd732e257879a2c284a49f2ed7db Mon Sep 17 00:00:00 2001 From: Kirill Ilin Date: Fri, 13 Feb 2026 23:03:48 +0500 Subject: [PATCH 033/666] [etcd-operator] fix dependencies: add vertical-pod-autoscaler Signed-off-by: Kirill Ilin --- packages/core/platform/sources/etcd-operator.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/platform/sources/etcd-operator.yaml b/packages/core/platform/sources/etcd-operator.yaml index 599ed2a3..5f42a4a0 100644 --- a/packages/core/platform/sources/etcd-operator.yaml +++ b/packages/core/platform/sources/etcd-operator.yaml @@ -14,6 +14,7 @@ spec: dependsOn: - cozystack.networking - cozystack.cert-manager + - cozystack.vertical-pod-autoscaler components: - name: etcd-operator path: system/etcd-operator From 88baceae2c3297ece860b47928814b5c567307e1 Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Sat, 14 Feb 2026 01:59:40 +0100 Subject: [PATCH 034/666] [platform] Make cozystack-api reconciliation always use Deployment Signed-off-by: Andrei Kvapil --- cmd/cozystack-controller/main.go | 12 ++------- .../applicationdefinition_controller.go | 26 +++++-------------- .../templates/deployment.yaml | 3 --- .../system/cozystack-controller/values.yaml | 1 - 4 files changed, 9 insertions(+), 33 deletions(-) diff --git a/cmd/cozystack-controller/main.go b/cmd/cozystack-controller/main.go index 82fc673c..77c4f9dc 100644 --- a/cmd/cozystack-controller/main.go +++ b/cmd/cozystack-controller/main.go @@ -68,7 +68,6 @@ func main() { var disableTelemetry bool var telemetryEndpoint string var telemetryInterval string - var reconcileDeployment bool 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,8 +85,6 @@ func main() { "Endpoint for sending telemetry data") flag.StringVar(&telemetryInterval, "telemetry-interval", "15m", "Interval between telemetry data collection (e.g. 15m, 1h)") - flag.BoolVar(&reconcileDeployment, "reconcile-deployment", false, - "If set, the Cozystack API server is assumed to run as a Deployment, else as a DaemonSet.") opts := zap.Options{ Development: false, } @@ -196,14 +193,9 @@ func main() { os.Exit(1) } - cozyAPIKind := "DaemonSet" - if reconcileDeployment { - cozyAPIKind = "Deployment" - } if err = (&controller.ApplicationDefinitionReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - CozystackAPIKind: cozyAPIKind, + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "ApplicationDefinitionReconciler") os.Exit(1) diff --git a/internal/controller/applicationdefinition_controller.go b/internal/controller/applicationdefinition_controller.go index e5920b4c..6ce25249 100644 --- a/internal/controller/applicationdefinition_controller.go +++ b/internal/controller/applicationdefinition_controller.go @@ -32,8 +32,6 @@ type ApplicationDefinitionReconciler struct { mu sync.Mutex lastEvent time.Time lastHandled time.Time - - CozystackAPIKind string } func (r *ApplicationDefinitionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { @@ -67,7 +65,7 @@ func (r *ApplicationDefinitionReconciler) SetupWithManager(mgr ctrl.Manager) err } type appDefHashView struct { - Name string `json:"name"` + Name string `json:"name"` Spec cozyv1alpha1.ApplicationDefinitionSpec `json:"spec"` } @@ -155,23 +153,13 @@ func (r *ApplicationDefinitionReconciler) getWorkload( ctx context.Context, key types.NamespacedName, ) (tpl *corev1.PodTemplateSpec, obj client.Object, patch client.Patch, err error) { - if r.CozystackAPIKind == "Deployment" { - dep := &appsv1.Deployment{} - if err := r.Get(ctx, key, dep); err != nil { - return nil, nil, nil, err - } - obj = dep - tpl = &dep.Spec.Template - patch = client.MergeFrom(dep.DeepCopy()) - } else { - ds := &appsv1.DaemonSet{} - if err := r.Get(ctx, key, ds); err != nil { - return nil, nil, nil, err - } - obj = ds - tpl = &ds.Spec.Template - patch = client.MergeFrom(ds.DeepCopy()) + dep := &appsv1.Deployment{} + if err := r.Get(ctx, key, dep); err != nil { + return nil, nil, nil, err } + obj = dep + tpl = &dep.Spec.Template + patch = client.MergeFrom(dep.DeepCopy()) if tpl.Annotations == nil { tpl.Annotations = make(map[string]string) } diff --git a/packages/system/cozystack-controller/templates/deployment.yaml b/packages/system/cozystack-controller/templates/deployment.yaml index 201aeb72..aab7bbe1 100644 --- a/packages/system/cozystack-controller/templates/deployment.yaml +++ b/packages/system/cozystack-controller/templates/deployment.yaml @@ -27,6 +27,3 @@ spec: {{- if .Values.cozystackController.disableTelemetry }} - --disable-telemetry {{- end }} - {{- if eq .Values.cozystackController.cozystackAPIKind "Deployment" }} - - --reconcile-deployment - {{- end }} diff --git a/packages/system/cozystack-controller/values.yaml b/packages/system/cozystack-controller/values.yaml index 2a1f9df4..908a955d 100644 --- a/packages/system/cozystack-controller/values.yaml +++ b/packages/system/cozystack-controller/values.yaml @@ -2,4 +2,3 @@ cozystackController: image: ghcr.io/cozystack/cozystack/cozystack-controller:v1.0.0-beta.3@sha256:00b0ba15fef7c3ce8c0801ecb11b1953665a511990e18f51c2ca3ca40127c7aa debug: false disableTelemetry: false - cozystackAPIKind: "DaemonSet" From b7e16aaa96a304bc683a083067ad72e8d1df5a4b Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Sat, 14 Feb 2026 02:49:47 +0100 Subject: [PATCH 035/666] Update kilo v0.7.1 Signed-off-by: Andrei Kvapil --- packages/system/kilo/Makefile | 2 +- packages/system/kilo/values.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/system/kilo/Makefile b/packages/system/kilo/Makefile index fcce8bd9..48dcb938 100644 --- a/packages/system/kilo/Makefile +++ b/packages/system/kilo/Makefile @@ -6,7 +6,7 @@ include ../../../hack/package.mk update: tag=$$(git ls-remote --tags --sort="v:refname" https://github.com/cozystack/kilo | awk -F'[/^]' 'END{print $$3}') && \ - digest=$$(skopeo inspect --override-os linux --format '{{.Digest}}' docker://ghcr.io/cozystack/cozystack/kilo:$${tag}) && \ + digest=$$(skopeo inspect --override-os linux --override-arch amd64 --format '{{.Digest}}' docker://ghcr.io/cozystack/cozystack/kilo:amd64-$${tag}) && \ REPOSITORY="ghcr.io/cozystack/cozystack/kilo" \ yq -i '.kilo.image.repository = strenv(REPOSITORY)' values.yaml && \ TAG="$${tag}@$${digest}" \ diff --git a/packages/system/kilo/values.yaml b/packages/system/kilo/values.yaml index 279f62b0..762f3c26 100644 --- a/packages/system/kilo/values.yaml +++ b/packages/system/kilo/values.yaml @@ -1,7 +1,7 @@ kilo: image: pullPolicy: IfNotPresent - tag: v0.7.0@sha256:69c14b8292c0dc9045fd646d332811619a2d93bcc1f9e19e11da9a9a172c988c + tag: v0.7.1@sha256:c2160097ef3aa4e9e460430665b743a14b8f19480211edab6f946264718340ff repository: ghcr.io/cozystack/cozystack/kilo podCIDR: 10.244.0.0/16 serviceCIDR: 10.96.0.0/16 From 5568c0be9f0f76b5297d0499276ef031e4df39d8 Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Thu, 12 Feb 2026 01:00:05 +0100 Subject: [PATCH 036/666] feat(vm-instance): port cpuModel, instancetype switching, and runStrategy changes Port three features from virtual-machine to vm-instance: - Add cpuModel field to specify CPU model without instanceType - Allow switching between instancetype and custom resources via update hook - Migrate from running boolean to runStrategy enum Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- packages/apps/vm-instance/README.md | 45 ++++++++--------- .../apps/vm-instance/templates/_helpers.tpl | 23 +++++++++ .../vm-instance/templates/vm-update-hook.yaml | 48 +++++++++++++++++-- packages/apps/vm-instance/templates/vm.yaml | 22 +++++---- packages/apps/vm-instance/values.schema.json | 20 ++++++-- packages/apps/vm-instance/values.yaml | 14 +++++- .../vm-instance-rd/cozyrds/vm-instance.yaml | 4 +- 7 files changed, 133 insertions(+), 43 deletions(-) diff --git a/packages/apps/vm-instance/README.md b/packages/apps/vm-instance/README.md index dc63508a..11d1441d 100644 --- a/packages/apps/vm-instance/README.md +++ b/packages/apps/vm-instance/README.md @@ -36,28 +36,29 @@ virtctl ssh @ ### Common parameters -| Name | Description | Type | Value | -| ------------------- | ------------------------------------------------------------------- | ---------- | ----------- | -| `external` | Enable external access from outside the cluster. | `bool` | `false` | -| `externalMethod` | Method to pass through traffic to the VM. | `string` | `PortList` | -| `externalPorts` | Ports to forward from outside the cluster. | `[]int` | `[22]` | -| `running` | Determines if the virtual machine should be running. | `bool` | `true` | -| `instanceType` | Virtual Machine instance type. | `string` | `u1.medium` | -| `instanceProfile` | Virtual Machine preferences profile. | `string` | `ubuntu` | -| `disks` | List of disks to attach. | `[]object` | `[]` | -| `disks[i].name` | Disk name. | `string` | `""` | -| `disks[i].bus` | Disk bus type (e.g. "sata"). | `string` | `""` | -| `subnets` | Additional subnets | `[]object` | `[]` | -| `subnets[i].name` | Subnet name | `string` | `""` | -| `gpus` | List of GPUs to attach (NVIDIA driver requires at least 4 GiB RAM). | `[]object` | `[]` | -| `gpus[i].name` | The name of the GPU resource to attach. | `string` | `""` | -| `resources` | Resource configuration for the virtual machine. | `object` | `{}` | -| `resources.cpu` | Number of CPU cores allocated. | `quantity` | `""` | -| `resources.memory` | Amount of memory allocated. | `quantity` | `""` | -| `resources.sockets` | Number of CPU sockets (vCPU topology). | `quantity` | `""` | -| `sshKeys` | List of SSH public keys for authentication. | `[]string` | `[]` | -| `cloudInit` | Cloud-init user data. | `string` | `""` | -| `cloudInitSeed` | Seed string to generate SMBIOS UUID for the VM. | `string` | `""` | +| Name | Description | Type | Value | +| ------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ---------- | ----------- | +| `external` | Enable external access from outside the cluster. | `bool` | `false` | +| `externalMethod` | Method to pass through traffic to the VM. | `string` | `PortList` | +| `externalPorts` | Ports to forward from outside the cluster. | `[]int` | `[22]` | +| `runStrategy` | Requested running state of the VirtualMachineInstance | `string` | `Always` | +| `instanceType` | Virtual Machine instance type. | `string` | `u1.medium` | +| `instanceProfile` | Virtual Machine preferences profile. | `string` | `ubuntu` | +| `disks` | List of disks to attach. | `[]object` | `[]` | +| `disks[i].name` | Disk name. | `string` | `""` | +| `disks[i].bus` | Disk bus type (e.g. "sata"). | `string` | `""` | +| `subnets` | Additional subnets | `[]object` | `[]` | +| `subnets[i].name` | Subnet name | `string` | `""` | +| `gpus` | List of GPUs to attach (NVIDIA driver requires at least 4 GiB RAM). | `[]object` | `[]` | +| `gpus[i].name` | The name of the GPU resource to attach. | `string` | `""` | +| `cpuModel` | Model specifies the CPU model inside the VMI. List of available models https://github.com/libvirt/libvirt/tree/master/src/cpu_map | `string` | `""` | +| `resources` | Resource configuration for the virtual machine. | `object` | `{}` | +| `resources.cpu` | Number of CPU cores allocated. | `quantity` | `""` | +| `resources.memory` | Amount of memory allocated. | `quantity` | `""` | +| `resources.sockets` | Number of CPU sockets (vCPU topology). | `quantity` | `""` | +| `sshKeys` | List of SSH public keys for authentication. | `[]string` | `[]` | +| `cloudInit` | Cloud-init user data. | `string` | `""` | +| `cloudInitSeed` | Seed string to generate SMBIOS UUID for the VM. | `string` | `""` | ## U Series diff --git a/packages/apps/vm-instance/templates/_helpers.tpl b/packages/apps/vm-instance/templates/_helpers.tpl index e65cad9d..ddea90fe 100644 --- a/packages/apps/vm-instance/templates/_helpers.tpl +++ b/packages/apps/vm-instance/templates/_helpers.tpl @@ -70,6 +70,29 @@ Generate a stable UUID for cloud-init re-initialization upon upgrade. {{- $uuid }} {{- end }} +{{/* +Domain resources (cpu, memory) as a JSON object. +Used in vm.yaml for rendering and in the update hook for merge patches. +*/}} +{{- define "virtual-machine.domainResources" -}} +{{- $result := dict -}} +{{- if or .Values.cpuModel (and .Values.resources .Values.resources.cpu .Values.resources.sockets) -}} + {{- $cpu := dict -}} + {{- if and .Values.resources .Values.resources.cpu .Values.resources.sockets -}} + {{- $_ := set $cpu "cores" (.Values.resources.cpu | int64) -}} + {{- $_ := set $cpu "sockets" (.Values.resources.sockets | int64) -}} + {{- end -}} + {{- if .Values.cpuModel -}} + {{- $_ := set $cpu "model" .Values.cpuModel -}} + {{- end -}} + {{- $_ := set $result "cpu" $cpu -}} +{{- end -}} +{{- if and .Values.resources .Values.resources.memory -}} + {{- $_ := set $result "resources" (dict "requests" (dict "memory" .Values.resources.memory)) -}} +{{- end -}} +{{- $result | toJson -}} +{{- end -}} + {{/* Node Affinity for Windows VMs */}} diff --git a/packages/apps/vm-instance/templates/vm-update-hook.yaml b/packages/apps/vm-instance/templates/vm-update-hook.yaml index a12e8583..995dafff 100644 --- a/packages/apps/vm-instance/templates/vm-update-hook.yaml +++ b/packages/apps/vm-instance/templates/vm-update-hook.yaml @@ -2,26 +2,43 @@ {{- $namespace := .Release.Namespace -}} {{- $existingVM := lookup "kubevirt.io/v1" "VirtualMachine" $namespace $vmName -}} +{{- $existingService := lookup "v1" "Service" $namespace $vmName -}} {{- $instanceType := .Values.instanceType | default "" -}} {{- $instanceProfile := .Values.instanceProfile | default "" -}} +{{- $desiredServiceType := ternary "LoadBalancer" "ClusterIP" .Values.external -}} {{- $needUpdateType := false -}} {{- $needUpdateProfile := false -}} +{{- $needRecreateService := false -}} +{{- $needRemoveInstanceType := false -}} +{{- $needRemoveCustomResources := false -}} -{{- if and $existingVM $instanceType -}} +{{- $existingHasInstanceType := and $existingVM $existingVM.spec.instancetype -}} +{{- if and $existingHasInstanceType (not $instanceType) -}} + {{- $needRemoveInstanceType = true -}} +{{- else if and $existingHasInstanceType $instanceType -}} {{- if not (eq $existingVM.spec.instancetype.name $instanceType) -}} {{- $needUpdateType = true -}} {{- end -}} +{{- else if and $existingVM (not $existingHasInstanceType) $instanceType -}} + {{- $needRemoveCustomResources = true -}} {{- end -}} -{{- if and $existingVM $instanceProfile -}} +{{- if and $existingVM $existingVM.spec.preference $instanceProfile -}} {{- if not (eq $existingVM.spec.preference.name $instanceProfile) -}} {{- $needUpdateProfile = true -}} {{- end -}} {{- end -}} -{{- if or $needUpdateType $needUpdateProfile }} +{{- if $existingService -}} + {{- $currentServiceType := $existingService.spec.type -}} + {{- if ne $currentServiceType $desiredServiceType -}} + {{- $needRecreateService = true -}} + {{- end -}} +{{- end -}} + +{{- if or $needUpdateType $needUpdateProfile $needRecreateService $needRemoveInstanceType $needRemoveCustomResources }} apiVersion: batch/v1 kind: Job metadata: @@ -58,13 +75,32 @@ spec: --type merge \ -p '{"spec":{"instancetype":{"name": "{{ $instanceType }}", "revisionName": null}}}' {{- end }} - + {{- if $needUpdateProfile }} echo "Patching VirtualMachine for preference update..." kubectl patch virtualmachines.kubevirt.io {{ $vmName }} -n {{ $namespace }} \ --type merge \ -p '{"spec":{"preference":{"name": "{{ $instanceProfile }}", "revisionName": null}}}' {{- end }} + + {{- if $needRemoveInstanceType }} + echo "Removing instancetype from VM (switching to custom resources)..." + kubectl patch virtualmachines.kubevirt.io {{ $vmName }} -n {{ $namespace }} \ + --type merge \ + -p '{"spec":{"instancetype":null{{- if not $instanceProfile }},"preference":null{{- end }},"template":{"spec":{"domain":{{ include "virtual-machine.domainResources" . }}}}}}' + {{- end }} + + {{- if $needRemoveCustomResources }} + echo "Removing custom CPU/memory from domain (switching to instancetype)..." + kubectl patch virtualmachines.kubevirt.io {{ $vmName }} -n {{ $namespace }} \ + --type merge \ + -p '{"spec":{"instancetype":{"name":"{{ $instanceType }}","revisionName":null},"template":{"spec":{"domain":{"cpu":null,"resources":null}}}}}' + {{- end }} + + {{- if $needRecreateService }} + echo "Removing Service..." + kubectl delete service --cascade=orphan -n {{ $namespace }} {{ $vmName }} + {{- end }} --- apiVersion: v1 kind: ServiceAccount @@ -87,6 +123,10 @@ rules: - apiGroups: ["kubevirt.io"] resources: ["virtualmachines"] verbs: ["patch", "get", "list", "watch"] + - apiGroups: [""] + resources: ["services"] + resourceNames: ["{{ $vmName }}"] + verbs: ["delete"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding diff --git a/packages/apps/vm-instance/templates/vm.yaml b/packages/apps/vm-instance/templates/vm.yaml index acad3475..226e5aa7 100644 --- a/packages/apps/vm-instance/templates/vm.yaml +++ b/packages/apps/vm-instance/templates/vm.yaml @@ -4,6 +4,9 @@ {{- if and .Values.instanceProfile (not (lookup "instancetype.kubevirt.io/v1beta1" "VirtualMachineClusterPreference" "" .Values.instanceProfile)) }} {{- fail (printf "Specified instanceProfile does not exist in the cluster: %s" .Values.instanceProfile) }} {{- end }} +{{- if and (not .Values.instanceType) (not (and .Values.resources .Values.resources.cpu .Values.resources.sockets .Values.resources.memory)) }} +{{- fail "Either instanceType or resources (cpu, sockets, memory) must be specified" }} +{{- end }} apiVersion: kubevirt.io/v1 kind: VirtualMachine @@ -12,7 +15,11 @@ metadata: labels: {{- include "virtual-machine.labels" . | nindent 4 }} spec: - running: {{ .Values.running }} + {{- if hasKey .Values "runStrategy" }} + runStrategy: {{ .Values.runStrategy }} + {{- else }} + runStrategy: {{ ternary "Always" "Halted" (.Values.running | default true) }} + {{- end }} {{- with .Values.instanceType }} instancetype: kind: VirtualMachineClusterInstancetype @@ -31,15 +38,12 @@ spec: {{- include "virtual-machine.labels" . | nindent 8 }} spec: domain: - {{- if and .Values.resources .Values.resources.cpu .Values.resources.sockets }} - cpu: - cores: {{ .Values.resources.cpu }} - sockets: {{ .Values.resources.sockets }} + {{- $domainRes := include "virtual-machine.domainResources" . | fromJson -}} + {{- with $domainRes.cpu }} + cpu: {{- . | toYaml | nindent 10 }} {{- end }} - {{- if and .Values.resources .Values.resources.memory }} - resources: - requests: - memory: {{ .Values.resources.memory | quote }} + {{- with $domainRes.resources }} + resources: {{- . | toYaml | nindent 10 }} {{- end }} firmware: uuid: {{ include "virtual-machine.stableUuid" . }} diff --git a/packages/apps/vm-instance/values.schema.json b/packages/apps/vm-instance/values.schema.json index ca6b34e7..0788c879 100644 --- a/packages/apps/vm-instance/values.schema.json +++ b/packages/apps/vm-instance/values.schema.json @@ -12,6 +12,11 @@ "type": "string", "default": "" }, + "cpuModel": { + "description": "Model specifies the CPU model inside the VMI. List of available models https://github.com/libvirt/libvirt/tree/master/src/cpu_map", + "type": "string", + "default": "" + }, "disks": { "description": "List of disks to attach.", "type": "array", @@ -175,10 +180,17 @@ } } }, - "running": { - "description": "Determines if the virtual machine should be running.", - "type": "boolean", - "default": true + "runStrategy": { + "description": "Requested running state of the VirtualMachineInstance", + "type": "string", + "default": "Always", + "enum": [ + "Always", + "Halted", + "Manual", + "RerunOnFailure", + "Once" + ] }, "sshKeys": { "description": "List of SSH public keys for authentication.", diff --git a/packages/apps/vm-instance/values.yaml b/packages/apps/vm-instance/values.yaml index 9d0a1522..d5ed5679 100644 --- a/packages/apps/vm-instance/values.yaml +++ b/packages/apps/vm-instance/values.yaml @@ -31,8 +31,15 @@ externalMethod: PortList externalPorts: - 22 -## @param {bool} running - Determines if the virtual machine should be running. -running: true +## @enum {string} RunStrategy - Requested running state of the VirtualMachineInstance +## @value Always - VMI should always be running +## @value Halted - VMI should never be running +## @value Manual - VMI can be started/stopped using API endpoints +## @value RerunOnFailure - VMI will initially be running and restarted if a failure occurs, but will not be restarted upon successful completion +## @value Once - VMI will run once and not be restarted upon completion regardless if the completion is of phase Failure or Success + +## @param {RunStrategy} runStrategy - Requested running state of the VirtualMachineInstance +runStrategy: Always ## @param {string} instanceType - Virtual Machine instance type. instanceType: "u1.medium" @@ -62,6 +69,9 @@ gpus: [] ## gpus: ## - name: nvidia.com/GA102GL_A10 +## @param {string} cpuModel - Model specifies the CPU model inside the VMI. List of available models https://github.com/libvirt/libvirt/tree/master/src/cpu_map +cpuModel: "" + ## @param {Resources} [resources] - Resource configuration for the virtual machine. resources: {} diff --git a/packages/system/vm-instance-rd/cozyrds/vm-instance.yaml b/packages/system/vm-instance-rd/cozyrds/vm-instance.yaml index b03d82a0..ff7c8cfe 100644 --- a/packages/system/vm-instance-rd/cozyrds/vm-instance.yaml +++ b/packages/system/vm-instance-rd/cozyrds/vm-instance.yaml @@ -8,7 +8,7 @@ spec: singular: vminstance plural: vminstances openAPISchema: |- - {"title":"Chart Values","type":"object","properties":{"cloudInit":{"description":"Cloud-init user data.","type":"string","default":""},"cloudInitSeed":{"description":"Seed string to generate SMBIOS UUID for the VM.","type":"string","default":""},"disks":{"description":"List of disks to attach.","type":"array","default":[],"items":{"type":"object","required":["name"],"properties":{"bus":{"description":"Disk bus type (e.g. \"sata\").","type":"string"},"name":{"description":"Disk name.","type":"string"}}}},"external":{"description":"Enable external access from outside the cluster.","type":"boolean","default":false},"externalMethod":{"description":"Method to pass through traffic to the VM.","type":"string","default":"PortList","enum":["PortList","WholeIP"]},"externalPorts":{"description":"Ports to forward from outside the cluster.","type":"array","default":[22],"items":{"type":"integer"}},"gpus":{"description":"List of GPUs to attach (NVIDIA driver requires at least 4 GiB RAM).","type":"array","default":[],"items":{"type":"object","required":["name"],"properties":{"name":{"description":"The name of the GPU resource to attach.","type":"string"}}}},"instanceProfile":{"description":"Virtual Machine preferences profile.","type":"string","default":"ubuntu","enum":["alpine","centos.7","centos.7.desktop","centos.stream10","centos.stream10.desktop","centos.stream8","centos.stream8.desktop","centos.stream8.dpdk","centos.stream9","centos.stream9.desktop","centos.stream9.dpdk","cirros","fedora","fedora.arm64","opensuse.leap","opensuse.tumbleweed","rhel.10","rhel.10.arm64","rhel.7","rhel.7.desktop","rhel.8","rhel.8.desktop","rhel.8.dpdk","rhel.9","rhel.9.arm64","rhel.9.desktop","rhel.9.dpdk","rhel.9.realtime","sles","ubuntu","windows.10","windows.10.virtio","windows.11","windows.11.virtio","windows.2k16","windows.2k16.virtio","windows.2k19","windows.2k19.virtio","windows.2k22","windows.2k22.virtio","windows.2k25","windows.2k25.virtio",""]},"instanceType":{"description":"Virtual Machine instance type.","type":"string","default":"u1.medium"},"resources":{"description":"Resource configuration for the virtual machine.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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},"sockets":{"description":"Number of CPU sockets (vCPU topology).","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}}},"running":{"description":"Determines if the virtual machine should be running.","type":"boolean","default":true},"sshKeys":{"description":"List of SSH public keys for authentication.","type":"array","default":[],"items":{"type":"string"}},"subnets":{"description":"Additional subnets","type":"array","default":[],"items":{"type":"object","properties":{"name":{"description":"Subnet name","type":"string"}}}}}} + {"title":"Chart Values","type":"object","properties":{"cloudInit":{"description":"Cloud-init user data.","type":"string","default":""},"cloudInitSeed":{"description":"Seed string to generate SMBIOS UUID for the VM.","type":"string","default":""},"cpuModel":{"description":"Model specifies the CPU model inside the VMI. List of available models https://github.com/libvirt/libvirt/tree/master/src/cpu_map","type":"string","default":""},"disks":{"description":"List of disks to attach.","type":"array","default":[],"items":{"type":"object","required":["name"],"properties":{"bus":{"description":"Disk bus type (e.g. \"sata\").","type":"string"},"name":{"description":"Disk name.","type":"string"}}}},"external":{"description":"Enable external access from outside the cluster.","type":"boolean","default":false},"externalMethod":{"description":"Method to pass through traffic to the VM.","type":"string","default":"PortList","enum":["PortList","WholeIP"]},"externalPorts":{"description":"Ports to forward from outside the cluster.","type":"array","default":[22],"items":{"type":"integer"}},"gpus":{"description":"List of GPUs to attach (NVIDIA driver requires at least 4 GiB RAM).","type":"array","default":[],"items":{"type":"object","required":["name"],"properties":{"name":{"description":"The name of the GPU resource to attach.","type":"string"}}}},"instanceProfile":{"description":"Virtual Machine preferences profile.","type":"string","default":"ubuntu","enum":["alpine","centos.7","centos.7.desktop","centos.stream10","centos.stream10.desktop","centos.stream8","centos.stream8.desktop","centos.stream8.dpdk","centos.stream9","centos.stream9.desktop","centos.stream9.dpdk","cirros","fedora","fedora.arm64","opensuse.leap","opensuse.tumbleweed","rhel.10","rhel.10.arm64","rhel.7","rhel.7.desktop","rhel.8","rhel.8.desktop","rhel.8.dpdk","rhel.9","rhel.9.arm64","rhel.9.desktop","rhel.9.dpdk","rhel.9.realtime","sles","ubuntu","windows.10","windows.10.virtio","windows.11","windows.11.virtio","windows.2k16","windows.2k16.virtio","windows.2k19","windows.2k19.virtio","windows.2k22","windows.2k22.virtio","windows.2k25","windows.2k25.virtio",""]},"instanceType":{"description":"Virtual Machine instance type.","type":"string","default":"u1.medium"},"resources":{"description":"Resource configuration for the virtual machine.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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},"sockets":{"description":"Number of CPU sockets (vCPU topology).","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}}},"runStrategy":{"description":"Requested running state of the VirtualMachineInstance","type":"string","default":"Always","enum":["Always","Halted","Manual","RerunOnFailure","Once"]},"sshKeys":{"description":"List of SSH public keys for authentication.","type":"array","default":[],"items":{"type":"string"}},"subnets":{"description":"Additional subnets","type":"array","default":[],"items":{"type":"object","properties":{"name":{"description":"Subnet name","type":"string"}}}}}} release: prefix: vm-instance- labels: @@ -26,7 +26,7 @@ spec: tags: - compute icon: PHN2ZyB3aWR0aD0iMTQ0IiBoZWlnaHQ9IjE0NCIgdmlld0JveD0iMCAwIDE0NCAxNDQiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIxNDQiIGhlaWdodD0iMTQ0IiByeD0iMjQiIGZpbGw9InVybCgjcGFpbnQwX2xpbmVhcl82ODdfMzQ1NCkiLz4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzY4N18zNDU0KSI+CjxwYXRoIGQ9Ik04OS41MDM5IDExMS43MDdINTQuNDk3QzU0LjE3MjcgMTExLjcwNyA1NC4wMTA4IDExMS4yMjEgNTQuMzM0OSAxMTEuMDU5TDU3LjI1MjIgMTA4Ljk1MkM2MC4zMzE0IDEwNi42ODMgNjEuOTUyMiAxMDIuNjMxIDYwLjk3OTcgOTguNzQxMkg4My4wMjFDODIuMDQ4NSAxMDIuNjMxIDgzLjY2OTMgMTA2LjY4MyA4Ni43NDg1IDEwOC45NTJMODkuNjY1OCAxMTEuMDU5Qzg5Ljk5IDExMS4yMjEgODkuODI3OSAxMTEuNzA3IDg5LjUwMzkgMTExLjcwN1oiIGZpbGw9IiNCMEI2QkIiLz4KPHBhdGggZD0iTTExMy4zMjggOTguNzQxSDMwLjY3MjVDMjcuNTkzMSA5OC43NDEgMjUgOTYuMTQ4IDI1IDkzLjA2ODdWMzMuMTAzMkMyNSAzMC4wMjM5IDI3LjU5MzEgMjcuNDMwNyAzMC42NzI1IDI3LjQzMDdIMTEzLjMyOEMxMTYuNDA3IDI3LjQzMDcgMTE5IDMwLjAyMzcgMTE5IDMzLjEwMzJWOTMuMDY4N0MxMTkgOTYuMTQ4IDExNi40MDcgOTguNzQxIDExMy4zMjggOTguNzQxWiIgZmlsbD0iI0U4RURFRSIvPgo8cGF0aCBkPSJNMTE5IDg0LjE1NDlIMjVWMzMuMTAzMkMyNSAzMC4wMjM5IDI3LjU5MzEgMjcuNDMwNyAzMC42NzI1IDI3LjQzMDdIMTEzLjMyOEMxMTYuNDA3IDI3LjQzMDcgMTE5IDMwLjAyMzcgMTE5IDMzLjEwMzJMMTE5IDg0LjE1NDlaIiBmaWxsPSIjMDBCM0ZGIi8+CjxwYXRoIGQ9Ik05MC42Mzc0IDExNi41NjlINTMuMzYxNkM1Mi4wNjUxIDExNi41NjkgNTAuOTMwNyAxMTUuNDM1IDUwLjkzMDcgMTE0LjEzOEM1MC45MzA3IDExMi44NDEgNTIuMDY1MSAxMTEuNzA3IDUzLjM2MTYgMTExLjcwN0g5MC42Mzc0QzkxLjkzMzkgMTExLjcwNyA5My4wNjg0IDExMi44NDEgOTMuMDY4NCAxMTQuMTM4QzkzLjA2ODQgMTE1LjQzNSA5MS45MzM5IDExNi41NjkgOTAuNjM3NCAxMTYuNTY5WiIgZmlsbD0iI0U4RURFRSIvPgo8L2c+CjxwYXRoIGQ9Ik03Mi41Mjc1IDUzLjgzNjdDNzIuNDQzMSA1My44MzUxIDcyLjM2MDUgNTMuODEyMiA3Mi4yODczIDUzLjc3MDFMNTYuNDY5OSA0NC43OTM0QzU2LjM5ODMgNDQuNzUxOSA1Ni4zMzg4IDQ0LjY5MjMgNTYuMjk3MyA0NC42MjA3QzU2LjI1NTkgNDQuNTQ5IDU2LjIzMzggNDQuNDY3OCA1Ni4yMzM0IDQ0LjM4NUM1Ni4yMzM0IDQ0LjIxNjkgNTYuMzI1OCA0NC4wNjE3IDU2LjQ2OTkgNDMuOTc4NUw3Mi4xOTEyIDM1LjA2MDlDNzIuMjYzNyAzNS4wMjEgNzIuMzQ1IDM1IDcyLjQyNzcgMzVDNzIuNTEwNSAzNSA3Mi41OTE4IDM1LjAyMSA3Mi42NjQzIDM1LjA2MDlMODguNDg3MiA0NC4wMzk1Qzg4LjU1OTEgNDQuMDgwMSA4OC42MTg4IDQ0LjEzOTIgODguNjYgNDQuMjEwN0M4OC43MDEzIDQ0LjI4MjIgODguNzIyNyA0NC4zNjM1IDg4LjcyMTkgNDQuNDQ2Qzg4LjcyMjUgNDQuNTI4NSA4OC43MDEgNDQuNjA5NyA4OC42NTk4IDQ0LjY4MTJDODguNjE4NSA0NC43NTI2IDg4LjU1ODkgNDQuODExOCA4OC40ODcyIDQ0Ljg1MjVMNzIuNzcxNCA1My43NjgzQzcyLjY5NzIgNTMuODExNCA3Mi42MTMzIDUzLjgzNDkgNzIuNTI3NSA1My44MzY3IiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBvcGFjaXR5PSIwLjciIGQ9Ik03MC4yNTUzIDc1LjY1MTdDNzAuMTcxIDc1LjY1MzUgNzAuMDg3OCA3NS42MzE3IDcwLjAxNTEgNzUuNTg4OEw1NC4yNDU4IDY2LjY0MTdDNTQuMTcxNSA2Ni42MDI0IDU0LjEwOTUgNjYuNTQzNiA1NC4wNjYxIDY2LjQ3MTZDNTQuMDIyOCA2Ni4zOTk3IDU0IDY2LjMxNzMgNTQgNjYuMjMzM1Y0OC4yNzhDNTQgNDguMTA4IDU0LjA5MjQgNDcuOTU0NiA1NC4yNDM5IDQ3Ljg2OTZDNTQuMzE3MiA0Ny44MjcxIDU0LjQwMDQgNDcuODA0NyA1NC40ODUxIDQ3LjgwNDdDNTQuNTY5NyA0Ny44MDQ3IDU0LjY1MjkgNDcuODI3MSA1NC43MjYyIDQ3Ljg2OTZMNzAuNDkzNyA1Ni44MTMxQzcwLjU2NDIgNTYuODU2NSA3MC42MjI1IDU2LjkxNyA3MC42NjMyIDU2Ljk4OTFDNzAuNzAzOSA1Ny4wNjEyIDcwLjcyNTcgNTcuMTQyNCA3MC43MjY1IDU3LjIyNTFWNzUuMTgwNUM3MC43MjU5IDc1LjI2MjggNzAuNzA0MiA3NS4zNDM2IDcwLjY2MzUgNzUuNDE1MUM3MC42MjI3IDc1LjQ4NjYgNzAuNTY0MiA3NS41NDY0IDcwLjQ5MzcgNzUuNTg4OEM3MC40MjA2IDc1LjYyOTEgNzAuMzM4NyA3NS42NTA3IDcwLjI1NTMgNzUuNjUxNyIgZmlsbD0id2hpdGUiLz4KPHBhdGggb3BhY2l0eT0iMC40IiBkPSJNNzQuNzE5OCA3NS42NTExQzc0LjYzMzMgNzUuNjUxMiA3NC41NDgyIDc1LjYyOTYgNzQuNDcyMiA3NS41ODgzQzc0LjQwMTYgNzUuNTQ2MSA3NC4zNDMyIDc1LjQ4NjIgNzQuMzAyNyA3NS40MTQ3Qzc0LjI2MjMgNzUuMzQzMSA3NC4yNDExIDc1LjI2MjIgNzQuMjQxMiA3NS4xOFY1Ny4zMzczQzc0LjI0MTIgNTcuMTcxIDc0LjMzMzYgNTcuMDE1OCA3NC40NzIyIDU2LjkyOUw5MC4yMzk3IDQ3Ljk4NTVDOTAuMzExOSA0Ny45NDM4IDkwLjM5MzggNDcuOTIxOSA5MC40NzcxIDQ3LjkyMTlDOTAuNTYwNSA0Ny45MjE5IDkwLjY0MjQgNDcuOTQzOCA5MC43MTQ2IDQ3Ljk4NTVDOTAuNzg3NiA0OC4wMjU1IDkwLjg0ODUgNDguMDg0MiA5MC44OTExIDQ4LjE1NTdDOTAuOTMzNyA0OC4yMjcyIDkwLjk1NjMgNDguMzA4OCA5MC45NTY2IDQ4LjM5MlY2Ni4yMzI4QzkwLjk1NyA2Ni4zMTY0IDkwLjkzNDcgNjYuMzk4NSA5MC44OTIxIDY2LjQ3MDRDOTAuODQ5NSA2Ni41NDI0IDkwLjc4ODEgNjYuNjAxNCA5MC43MTQ2IDY2LjY0MTFMNzQuOTUyNiA3NS41ODgzQzc0Ljg4MjUgNzUuNjMwNyA3NC44MDE4IDc1LjY1MjUgNzQuNzE5OCA3NS42NTExIiBmaWxsPSJ3aGl0ZSIvPgo8ZGVmcz4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDBfbGluZWFyXzY4N18zNDU0IiB4MT0iMTYxIiB5MT0iMTgwIiB4Mj0iMy41OTI4NGUtMDciIHkyPSI0Ljk5OTk4IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjNTk1NjU2Ii8+CjwvbGluZWFyR3JhZGllbnQ+CjxjbGlwUGF0aCBpZD0iY2xpcDBfNjg3XzM0NTQiPgo8cmVjdCB3aWR0aD0iOTQiIGhlaWdodD0iOTQiIGZpbGw9IndoaXRlIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyNSAyNSkiLz4KPC9jbGlwUGF0aD4KPC9kZWZzPgo8L3N2Zz4K - keysOrder: [["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata", "name"], ["spec", "external"], ["spec", "externalMethod"], ["spec", "externalPorts"], ["spec", "running"], ["spec", "instanceType"], ["spec", "instanceProfile"], ["spec", "disks"], ["spec", "subnets"], ["spec", "gpus"], ["spec", "resources"], ["spec", "sshKeys"], ["spec", "cloudInit"], ["spec", "cloudInitSeed"]] + keysOrder: [["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata", "name"], ["spec", "external"], ["spec", "externalMethod"], ["spec", "externalPorts"], ["spec", "runStrategy"], ["spec", "instanceType"], ["spec", "instanceProfile"], ["spec", "disks"], ["spec", "subnets"], ["spec", "gpus"], ["spec", "cpuModel"], ["spec", "resources"], ["spec", "sshKeys"], ["spec", "cloudInit"], ["spec", "cloudInitSeed"]] secrets: exclude: [] include: [] From ee54495dfb643432d70e6f3cf9157fa3ec709619 Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Thu, 12 Feb 2026 01:07:19 +0100 Subject: [PATCH 037/666] feat(platform): add migration 28 to convert virtual-machine to vm-disk + vm-instance Splits virtual-machine HelmReleases into separate vm-disk and vm-instance components. Handles PV data preservation, kube-ovn IP migration, and LoadBalancer IP pinning via metallb annotation for external VMs. Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- .../platform/images/migrations/migrations/29 | 691 ++++++++++++++++++ packages/core/platform/values.yaml | 2 +- 2 files changed, 692 insertions(+), 1 deletion(-) create mode 100755 packages/core/platform/images/migrations/migrations/29 diff --git a/packages/core/platform/images/migrations/migrations/29 b/packages/core/platform/images/migrations/migrations/29 new file mode 100755 index 00000000..557fad0e --- /dev/null +++ b/packages/core/platform/images/migrations/migrations/29 @@ -0,0 +1,691 @@ +#!/bin/bash +# Migration 29 --> 30 +# Convert virtual-machine HelmReleases to vm-disk + vm-instance. +# Discovers all VirtualMachine HelmReleases by label, migrates each instance. +# Idempotent: safe to re-run. + +set -euo pipefail + +OLD_PREFIX="virtual-machine" +NEW_DISK_PREFIX="vm-disk" +NEW_INSTANCE_PREFIX="vm-instance" +PROTECTION_WEBHOOK_NAME="protection-webhook" +PROTECTION_WEBHOOK_NS="protection-webhook" + +# ============================================================ +# Helper functions +# ============================================================ + +resource_exists() { + local ns="$1" type="$2" name="$3" + kubectl -n "$ns" get "$type" "$name" --no-headers 2>/dev/null | grep -q . +} + +delete_resource() { + local ns="$1" type="$2" name="$3" + if resource_exists "$ns" "$type" "$name"; then + echo " [DELETE] ${type}/${name}" + kubectl -n "$ns" delete "$type" "$name" --wait=false + else + echo " [SKIP] ${type}/${name} already gone" + fi +} + +clone_resource() { + local ns="$1" type="$2" old_name="$3" new_name="$4" old_instance="$5" new_instance="$6" + + if resource_exists "$ns" "$type" "$new_name"; then + echo " [SKIP] ${type}/${new_name} already exists" + return 0 + fi + if ! resource_exists "$ns" "$type" "$old_name"; then + echo " [SKIP] ${type}/${old_name} not found" + return 0 + fi + + echo " [CREATE] ${type}/${new_name} from ${old_name}" + kubectl -n "$ns" get "$type" "$old_name" -o json | \ + jq --arg new_name "$new_name" --arg old "$old_instance" --arg new "$new_instance" ' + .metadata.name = $new_name | + del(.metadata.uid, .metadata.resourceVersion, .metadata.creationTimestamp, + .metadata.generation, .metadata.managedFields, .metadata.selfLink, + .metadata.ownerReferences) | + del(.status) | + .metadata.labels = ((.metadata.labels // {}) | with_entries( + if (.value | type) == "string" then .value |= gsub($old; $new) else . end)) | + .metadata.annotations = ((.metadata.annotations // {}) | with_entries( + select(.key != "kubectl.kubernetes.io/last-applied-configuration") | + if (.value | type) == "string" then .value |= gsub($old; $new) else . end)) + ' | kubectl -n "$ns" apply -f - +} + +# ============================================================ +# STEP 1: Discover all VirtualMachine instances +# ============================================================ +echo "=== Discovering VirtualMachine HelmReleases ===" +INSTANCES=() +while IFS=/ read -r ns name; do + [ -z "$ns" ] && continue + instance="${name#${OLD_PREFIX}-}" + INSTANCES+=("${ns}/${instance}") + echo " Found: ${ns}/${name} (instance=${instance})" +done < <(kubectl get hr -A -l "apps.cozystack.io/application.kind=VirtualMachine" \ + -o jsonpath='{range .items[*]}{.metadata.namespace}/{.metadata.name}{"\n"}{end}' 2>/dev/null) + +if [ ${#INSTANCES[@]} -eq 0 ]; then + echo " No VirtualMachine HelmReleases found. Nothing to migrate." + kubectl create configmap -n cozy-system cozystack-version \ + --from-literal=version=30 --dry-run=client -o yaml | kubectl apply -f- + exit 0 +fi +echo " Total: ${#INSTANCES[@]} instance(s)" + +# ============================================================ +# STEP 2: Migrate each instance +# ============================================================ +ALL_PV_NAMES=() +ALL_PROTECTED_RESOURCES=() + +for entry in "${INSTANCES[@]}"; do + NAMESPACE="${entry%%/*}" + INSTANCE="${entry#*/}" + OLD_NAME="${OLD_PREFIX}-${INSTANCE}" + NEW_DISK_NAME="${NEW_DISK_PREFIX}-${INSTANCE}" + NEW_INSTANCE_NAME="${NEW_INSTANCE_PREFIX}-${INSTANCE}" + + echo "" + echo "======================================================================" + echo "=== Migrating: ${OLD_NAME} -> ${NEW_DISK_NAME} + ${NEW_INSTANCE_NAME} in ${NAMESPACE}" + echo "======================================================================" + + # --- 2a: Suspend old HelmRelease --- + echo " --- Suspend HelmRelease ---" + if resource_exists "$NAMESPACE" "hr" "$OLD_NAME"; then + suspended=$(kubectl -n "$NAMESPACE" get hr "$OLD_NAME" -o jsonpath='{.spec.suspend}' 2>/dev/null || echo "false") + if [ "$suspended" != "true" ]; then + echo " [SUSPEND] hr/${OLD_NAME}" + kubectl -n "$NAMESPACE" patch hr "$OLD_NAME" --type=merge -p '{"spec":{"suspend":true}}' + else + echo " [SKIP] hr/${OLD_NAME} already suspended" + fi + else + echo " [SKIP] hr/${OLD_NAME} not found" + fi + + # --- 2b: Extract values from HelmRelease --- + echo " --- Extract values ---" + VALUES_SECRET="" + VALUES_KEY="values.yaml" + if resource_exists "$NAMESPACE" "hr" "$OLD_NAME"; then + VALUES_SECRET=$(kubectl -n "$NAMESPACE" get hr "$OLD_NAME" -o json | \ + jq -r '.spec.valuesFrom // [] | map(select(.kind == "Secret" and (.name | test("cozystack-values") | not))) | .[0].name // ""') + if [ -n "$VALUES_SECRET" ]; then + VALUES_KEY=$(kubectl -n "$NAMESPACE" get hr "$OLD_NAME" -o json | \ + jq -r '.spec.valuesFrom // [] | map(select(.kind == "Secret" and (.name | test("cozystack-values") | not))) | .[0].valuesKey // "values.yaml"') + fi + fi + + # Extract values from secret + EXTERNAL="false" + EXTERNAL_METHOD="PortList" + EXTERNAL_PORTS='[22]' + INSTANCE_TYPE="u1.medium" + INSTANCE_PROFILE="ubuntu" + RUN_STRATEGY="Always" + SYSTEM_DISK_IMAGE="ubuntu" + SYSTEM_DISK_STORAGE="5Gi" + SYSTEM_DISK_STORAGE_CLASS="replicated" + SSH_KEYS="[]" + CLOUD_INIT="" + CLOUD_INIT_SEED="" + SUBNETS="[]" + GPUS="[]" + CPU_MODEL="" + RESOURCES="{}" + + if [ -n "$VALUES_SECRET" ] && resource_exists "$NAMESPACE" "secret" "$VALUES_SECRET"; then + echo " Reading values from secret: ${VALUES_SECRET}" + VALUES_YAML=$(kubectl -n "$NAMESPACE" get secret "$VALUES_SECRET" -o jsonpath="{.data.${VALUES_KEY}}" 2>/dev/null | base64 -d 2>/dev/null || echo "") + if [ -z "$VALUES_YAML" ]; then + VALUES_YAML=$(kubectl -n "$NAMESPACE" get secret "$VALUES_SECRET" -o jsonpath="{.stringData.${VALUES_KEY}}" 2>/dev/null || echo "") + fi + if [ -n "$VALUES_YAML" ]; then + EXTERNAL=$(echo "$VALUES_YAML" | yq -r '.external // false') + EXTERNAL_METHOD=$(echo "$VALUES_YAML" | yq -r '.externalMethod // "PortList"') + EXTERNAL_PORTS=$(echo "$VALUES_YAML" | yq -c '.externalPorts // [22]') + INSTANCE_TYPE=$(echo "$VALUES_YAML" | yq -r '.instanceType // "u1.medium"') + INSTANCE_PROFILE=$(echo "$VALUES_YAML" | yq -r '.instanceProfile // "ubuntu"') + RUN_STRATEGY=$(echo "$VALUES_YAML" | yq -r '.runStrategy // (if .running == false then "Halted" else "Always" end)') + SYSTEM_DISK_IMAGE=$(echo "$VALUES_YAML" | yq -r '.systemDisk.image // "ubuntu"') + SYSTEM_DISK_STORAGE=$(echo "$VALUES_YAML" | yq -r '.systemDisk.storage // "5Gi"') + SYSTEM_DISK_STORAGE_CLASS=$(echo "$VALUES_YAML" | yq -r '.systemDisk.storageClass // "replicated"') + SSH_KEYS=$(echo "$VALUES_YAML" | yq -c '.sshKeys // []') + CLOUD_INIT=$(echo "$VALUES_YAML" | yq -r '.cloudInit // ""') + CLOUD_INIT_SEED=$(echo "$VALUES_YAML" | yq -r '.cloudInitSeed // ""') + SUBNETS=$(echo "$VALUES_YAML" | yq -c '.subnets // []') + GPUS=$(echo "$VALUES_YAML" | yq -c '.gpus // []') + CPU_MODEL=$(echo "$VALUES_YAML" | yq -r '.cpuModel // ""') + RESOURCES=$(echo "$VALUES_YAML" | yq -c '.resources // {}') + fi + fi + + echo " external=${EXTERNAL}, instanceType=${INSTANCE_TYPE}, image=${SYSTEM_DISK_IMAGE}" + + # --- 2c: Save kube-ovn IP --- + echo " --- Save kube-ovn IP ---" + OVN_IP="" + OVN_MAC="" + OVN_SUBNET="ovn-default" + OVN_IP_NAME="${OLD_NAME}.${NAMESPACE}" + if kubectl get ip.kubeovn.io "$OVN_IP_NAME" --no-headers 2>/dev/null | grep -q .; then + OVN_IP=$(kubectl get ip.kubeovn.io "$OVN_IP_NAME" -o jsonpath='{.spec.ipAddress}') + OVN_MAC=$(kubectl get ip.kubeovn.io "$OVN_IP_NAME" -o jsonpath='{.spec.macAddress}') + OVN_SUBNET=$(kubectl get ip.kubeovn.io "$OVN_IP_NAME" -o jsonpath='{.spec.subnet}') + echo " kube-ovn IP: ${OVN_IP}, MAC: ${OVN_MAC}, subnet: ${OVN_SUBNET}" + else + echo " [SKIP] No kube-ovn IP resource found" + fi + + # --- 2d: Save LoadBalancer IP (if external=true) --- + echo " --- Save LoadBalancer IP ---" + LB_IP="" + if [ "$EXTERNAL" = "true" ] && resource_exists "$NAMESPACE" "svc" "$OLD_NAME"; then + LB_IP=$(kubectl -n "$NAMESPACE" get svc "$OLD_NAME" -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || echo "") + if [ -n "$LB_IP" ]; then + echo " LoadBalancer IP: ${LB_IP}" + else + echo " [WARN] external=true but no LoadBalancer IP assigned" + fi + else + echo " [SKIP] Not external or no service" + fi + + # --- 2e: Switch PV reclaim policy to Retain --- + echo " --- Switch PV reclaim to Retain ---" + PV_NAME="" + if resource_exists "$NAMESPACE" "pvc" "$OLD_NAME"; then + PV_NAME=$(kubectl -n "$NAMESPACE" get pvc "$OLD_NAME" -o jsonpath='{.spec.volumeName}') + if [ -n "$PV_NAME" ]; then + ALL_PV_NAMES+=("$PV_NAME") + current_policy=$(kubectl get pv "$PV_NAME" -o jsonpath='{.spec.persistentVolumeReclaimPolicy}') + if [ "$current_policy" != "Retain" ]; then + echo " [PATCH] PV ${PV_NAME}: ${current_policy} -> Retain" + kubectl patch pv "$PV_NAME" -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}' + else + echo " [SKIP] PV ${PV_NAME} already Retain" + fi + fi + elif resource_exists "$NAMESPACE" "pvc" "$NEW_DISK_NAME"; then + PV_NAME=$(kubectl -n "$NAMESPACE" get pvc "$NEW_DISK_NAME" -o jsonpath='{.spec.volumeName}') + ALL_PV_NAMES+=("$PV_NAME") + echo " [SKIP] New PVC ${NEW_DISK_NAME} already exists, PV=${PV_NAME}" + else + echo " [WARN] No PVC found for ${OLD_NAME} or ${NEW_DISK_NAME}" + fi + + # --- 2f: Stop and delete the VirtualMachine --- + echo " --- Stop VirtualMachine ---" + if resource_exists "$NAMESPACE" "vm" "$OLD_NAME"; then + echo " [PATCH] Stop VM ${OLD_NAME}" + kubectl -n "$NAMESPACE" patch vm "$OLD_NAME" --type merge -p '{"spec":{"runStrategy":"Halted"}}' 2>/dev/null || \ + kubectl -n "$NAMESPACE" patch vm "$OLD_NAME" --type merge -p '{"spec":{"running":false}}' 2>/dev/null || true + + echo " Waiting for VMI to terminate..." + kubectl -n "$NAMESPACE" wait --for=delete vmi "$OLD_NAME" --timeout=120s 2>/dev/null || true + fi + + # --- 2g: Delete VirtualMachine CR (cascades to DataVolume and PVC) --- + echo " --- Delete VirtualMachine CR ---" + if resource_exists "$NAMESPACE" "vm" "$OLD_NAME"; then + echo " [DELETE] vm/${OLD_NAME}" + kubectl -n "$NAMESPACE" delete vm "$OLD_NAME" --wait=true --timeout=60s 2>/dev/null || true + fi + + echo " Waiting for old PVC to be deleted..." + for i in $(seq 1 30); do + if ! resource_exists "$NAMESPACE" "pvc" "$OLD_NAME"; then + echo " [OK] PVC ${OLD_NAME} deleted" + break + fi + sleep 2 + done + + # If PVC still exists (orphaned), delete it manually + if resource_exists "$NAMESPACE" "pvc" "$OLD_NAME"; then + echo " [DELETE] Orphaned pvc/${OLD_NAME}" + kubectl -n "$NAMESPACE" delete pvc "$OLD_NAME" --wait=false 2>/dev/null || true + fi + + # Delete orphaned DataVolume if still present + if resource_exists "$NAMESPACE" "dv" "$OLD_NAME"; then + echo " [DELETE] Orphaned dv/${OLD_NAME}" + kubectl -n "$NAMESPACE" delete dv "$OLD_NAME" --wait=false 2>/dev/null || true + fi + + # --- 2h: Create new PVC for vm-disk --- + echo " --- Create new PVC for vm-disk ---" + if [ -n "$PV_NAME" ]; then + if resource_exists "$NAMESPACE" "pvc" "$NEW_DISK_NAME"; then + new_phase=$(kubectl -n "$NAMESPACE" get pvc "$NEW_DISK_NAME" \ + -o jsonpath='{.status.phase}' 2>/dev/null || echo "unknown") + if [ "$new_phase" = "Bound" ]; then + echo " [SKIP] PVC ${NEW_DISK_NAME} already Bound" + fi + else + echo " [CREATE] PVC ${NEW_DISK_NAME} -> PV ${PV_NAME}" + cat < ${NEW_DISK_NAME}" + kubectl patch pv "$PV_NAME" --type=merge -p "{ + \"spec\":{\"claimRef\":{\"apiVersion\":\"v1\",\"kind\":\"PersistentVolumeClaim\", + \"name\":\"${NEW_DISK_NAME}\",\"namespace\":\"${NAMESPACE}\",\"uid\":\"${new_pvc_uid}\"}} + }" + + echo " Waiting for PVC ${NEW_DISK_NAME} to bind..." + kubectl -n "$NAMESPACE" wait pvc "$NEW_DISK_NAME" \ + --for=jsonpath='{.status.phase}'=Bound --timeout=60s + echo " [OK] PVC ${NEW_DISK_NAME} is Bound" + fi + fi + + # --- 2i: Clone Secrets --- + echo " --- Clone Secrets ---" + for secret in $(kubectl -n "$NAMESPACE" get secret -o name 2>/dev/null \ + | grep "secret/${OLD_NAME}" | grep -v "sh.helm.release" | grep -v "values"); do + old_secret_name="${secret#secret/}" + suffix="${old_secret_name#${OLD_NAME}}" + new_secret_name="${NEW_INSTANCE_NAME}${suffix}" + clone_resource "$NAMESPACE" "secret" "$old_secret_name" "$new_secret_name" "$OLD_NAME" "$NEW_INSTANCE_NAME" + done + + # --- 2j: Clone WorkloadMonitor --- + echo " --- Clone WorkloadMonitor ---" + clone_resource "$NAMESPACE" "workloadmonitor.cozystack.io" "$OLD_NAME" "$NEW_INSTANCE_NAME" "$OLD_NAME" "$NEW_INSTANCE_NAME" + + # --- 2k: Migrate kube-ovn IP --- + echo " --- Migrate kube-ovn IP ---" + NEW_OVN_IP_NAME="${NEW_INSTANCE_NAME}.${NAMESPACE}" + if [ -n "$OVN_IP" ]; then + if kubectl get ip.kubeovn.io "$NEW_OVN_IP_NAME" --no-headers 2>/dev/null | grep -q .; then + echo " [SKIP] ip.kubeovn.io/${NEW_OVN_IP_NAME} already exists" + else + echo " [CREATE] ip.kubeovn.io/${NEW_OVN_IP_NAME}" + cat </dev/null | grep -q .; then + echo " [DELETE] ip.kubeovn.io/${OVN_IP_NAME}" + kubectl delete ip.kubeovn.io "$OVN_IP_NAME" 2>/dev/null || true + fi + fi + + # --- 2l: Create vm-disk values secret --- + echo " --- Create vm-disk values secret ---" + DISK_VALUES_SECRET="${NEW_DISK_NAME}-values" + if ! resource_exists "$NAMESPACE" "secret" "$DISK_VALUES_SECRET"; then + echo " [CREATE] secret/${DISK_VALUES_SECRET}" + cat </dev/null \ + | grep "secret/${OLD_NAME}" | grep -v "sh.helm.release" | grep -v "values"); do + old_secret_name="${secret#secret/}" + delete_resource "$NAMESPACE" "secret" "$old_secret_name" + done + + delete_resource "$NAMESPACE" "workloadmonitor.cozystack.io" "$OLD_NAME" + + if resource_exists "$NAMESPACE" "hr" "$OLD_NAME"; then + kubectl -n "$NAMESPACE" patch hr "$OLD_NAME" --type=json \ + -p='[{"op":"remove","path":"/metadata/finalizers"}]' 2>/dev/null || true + delete_resource "$NAMESPACE" "hr" "$OLD_NAME" + fi + + echo " [DELETE] secrets with label owner=helm,name=${OLD_NAME}" + kubectl -n "$NAMESPACE" delete secret -l "owner=helm,name=${OLD_NAME}" 2>/dev/null || true + + # Delete old values secret + if [ -n "$VALUES_SECRET" ]; then + delete_resource "$NAMESPACE" "secret" "$VALUES_SECRET" + fi + + # Collect protected resources for batch deletion + if resource_exists "$NAMESPACE" "svc" "$OLD_NAME"; then + ALL_PROTECTED_RESOURCES+=("${NAMESPACE}:svc/${OLD_NAME}") + fi +done + +# ============================================================ +# STEP 3: Delete protected resources (Services) +# ============================================================ +echo "" +echo "--- Step 3: Delete protected resources ---" + +if [ ${#ALL_PROTECTED_RESOURCES[@]} -gt 0 ]; then + echo " --- Temporarily disabling protection-webhook ---" + + WEBHOOK_REPLICAS=$(kubectl -n "$PROTECTION_WEBHOOK_NS" get deploy "$PROTECTION_WEBHOOK_NAME" \ + -o jsonpath='{.spec.replicas}' 2>/dev/null || echo "1") + + echo " [SCALE] ${PROTECTION_WEBHOOK_NAME} -> 0 (was ${WEBHOOK_REPLICAS})" + kubectl -n "$PROTECTION_WEBHOOK_NS" scale deploy "$PROTECTION_WEBHOOK_NAME" --replicas=0 + + echo " [PATCH] Set failurePolicy=Ignore on ValidatingWebhookConfiguration/${PROTECTION_WEBHOOK_NAME}" + kubectl get validatingwebhookconfiguration "$PROTECTION_WEBHOOK_NAME" -o json | \ + jq '.webhooks[].failurePolicy = "Ignore"' | \ + kubectl apply -f - + + echo " Waiting for webhook pods to terminate..." + kubectl -n "$PROTECTION_WEBHOOK_NS" wait --for=delete pod \ + -l app.kubernetes.io/name=protection-webhook --timeout=60s 2>/dev/null || true + sleep 3 + + for entry in "${ALL_PROTECTED_RESOURCES[@]}"; do + ns="${entry%%:*}" + res="${entry#*:}" + echo " [DELETE] ${ns}/${res}" + kubectl -n "$ns" delete "$res" --wait=false 2>/dev/null || true + done + + echo " [PATCH] Set failurePolicy=Fail on ValidatingWebhookConfiguration/${PROTECTION_WEBHOOK_NAME}" + kubectl get validatingwebhookconfiguration "$PROTECTION_WEBHOOK_NAME" -o json | \ + jq '.webhooks[].failurePolicy = "Fail"' | \ + kubectl apply -f - + + echo " [SCALE] ${PROTECTION_WEBHOOK_NAME} -> ${WEBHOOK_REPLICAS}" + kubectl -n "$PROTECTION_WEBHOOK_NS" scale deploy "$PROTECTION_WEBHOOK_NAME" \ + --replicas="$WEBHOOK_REPLICAS" + echo " --- protection-webhook restored ---" +else + echo " [SKIP] No protected resources to delete" +fi + +# ============================================================ +# STEP 4: Restore PV reclaim policies +# ============================================================ +echo "" +echo "--- Step 4: Restore PV reclaim policies ---" +for pv_name in "${ALL_PV_NAMES[@]}"; do + if [ -n "$pv_name" ]; then + current_policy=$(kubectl get pv "$pv_name" \ + -o jsonpath='{.spec.persistentVolumeReclaimPolicy}' 2>/dev/null || echo "unknown") + if [ "$current_policy" = "Retain" ]; then + echo " [PATCH] PV ${pv_name}: Retain -> Delete" + kubectl patch pv "$pv_name" -p '{"spec":{"persistentVolumeReclaimPolicy":"Delete"}}' + else + echo " [SKIP] PV ${pv_name} already ${current_policy}" + fi + fi +done + +# ============================================================ +# STEP 5: Unsuspend vm-disk HelmReleases first +# ============================================================ +echo "" +echo "--- Step 5: Unsuspend vm-disk HelmReleases ---" +for entry in "${INSTANCES[@]}"; do + ns="${entry%%/*}" + instance="${entry#*/}" + disk_name="${NEW_DISK_PREFIX}-${instance}" + if resource_exists "$ns" "hr" "$disk_name"; then + suspended=$(kubectl -n "$ns" get hr "$disk_name" \ + -o jsonpath='{.spec.suspend}' 2>/dev/null || echo "false") + if [ "$suspended" = "true" ]; then + echo " [UNSUSPEND] ${ns}/hr/${disk_name}" + kubectl -n "$ns" patch hr "$disk_name" --type=merge -p '{"spec":{"suspend":false}}' + else + echo " [SKIP] ${ns}/hr/${disk_name} already not suspended" + fi + fi +done + +# Wait for disk HelmReleases to become ready +echo " Waiting for vm-disk HelmReleases to reconcile..." +for entry in "${INSTANCES[@]}"; do + ns="${entry%%/*}" + instance="${entry#*/}" + disk_name="${NEW_DISK_PREFIX}-${instance}" + for i in $(seq 1 30); do + ready=$(kubectl -n "$ns" get hr "$disk_name" -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null || echo "") + if [ "$ready" = "True" ]; then + echo " [OK] ${ns}/hr/${disk_name} is Ready" + break + fi + sleep 5 + done +done + +# ============================================================ +# STEP 6: Unsuspend vm-instance HelmReleases +# ============================================================ +echo "" +echo "--- Step 6: Unsuspend vm-instance HelmReleases ---" +for entry in "${INSTANCES[@]}"; do + ns="${entry%%/*}" + instance="${entry#*/}" + instance_name="${NEW_INSTANCE_PREFIX}-${instance}" + if resource_exists "$ns" "hr" "$instance_name"; then + suspended=$(kubectl -n "$ns" get hr "$instance_name" \ + -o jsonpath='{.spec.suspend}' 2>/dev/null || echo "false") + if [ "$suspended" = "true" ]; then + echo " [UNSUSPEND] ${ns}/hr/${instance_name}" + kubectl -n "$ns" patch hr "$instance_name" --type=merge -p '{"spec":{"suspend":false}}' + else + echo " [SKIP] ${ns}/hr/${instance_name} already not suspended" + fi + fi +done + +echo "" +echo "=== Migration complete (${#INSTANCES[@]} instance(s)) ===" + +# Stamp version +kubectl create configmap -n cozy-system cozystack-version \ + --from-literal=version=30 --dry-run=client -o yaml | kubectl apply -f- diff --git a/packages/core/platform/values.yaml b/packages/core/platform/values.yaml index 9bb8e2b1..38452a17 100644 --- a/packages/core/platform/values.yaml +++ b/packages/core/platform/values.yaml @@ -6,7 +6,7 @@ sourceRef: migrations: enabled: false image: ghcr.io/cozystack/cozystack/platform-migrations:latest - targetVersion: 29 + targetVersion: 30 # Bundle deployment configuration bundles: system: From 783682f171ddccf6e493085c37d0859be2240050 Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Thu, 12 Feb 2026 02:07:13 +0100 Subject: [PATCH 038/666] fix(platform): fix migration 28 CDI webhook and cloud-init issues Disable both cdi-operator and cdi-apiserver during DataVolume creation to prevent CDI webhook from rejecting PVC adoption. Also delete mutating webhook configurations alongside validating ones. Fix cloud-init value serialization to avoid spurious newline. Add volumeMode detection from PV. Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- .../platform/images/migrations/migrations/29 | 140 ++++++++++++++---- 1 file changed, 112 insertions(+), 28 deletions(-) diff --git a/packages/core/platform/images/migrations/migrations/29 b/packages/core/platform/images/migrations/migrations/29 index 557fad0e..96ac66eb 100755 --- a/packages/core/platform/images/migrations/migrations/29 +++ b/packages/core/platform/images/migrations/migrations/29 @@ -11,6 +11,10 @@ NEW_DISK_PREFIX="vm-disk" NEW_INSTANCE_PREFIX="vm-instance" PROTECTION_WEBHOOK_NAME="protection-webhook" PROTECTION_WEBHOOK_NS="protection-webhook" +CDI_APISERVER_NS="cozy-kubevirt-cdi" +CDI_APISERVER_DEPLOY="cdi-apiserver" +CDI_VALIDATING_WEBHOOKS="cdi-api-datavolume-validate cdi-api-dataimportcron-validate cdi-api-populator-validate cdi-api-validate" +CDI_MUTATING_WEBHOOKS="cdi-api-datavolume-mutate" # ============================================================ # Helper functions @@ -272,7 +276,8 @@ for entry in "${INSTANCES[@]}"; do echo " [SKIP] PVC ${NEW_DISK_NAME} already Bound" fi else - echo " [CREATE] PVC ${NEW_DISK_NAME} -> PV ${PV_NAME}" + PV_VOLUME_MODE=$(kubectl get pv "$PV_NAME" -o jsonpath='{.spec.volumeMode}' 2>/dev/null || echo "Filesystem") + echo " [CREATE] PVC ${NEW_DISK_NAME} -> PV ${PV_NAME} (volumeMode=${PV_VOLUME_MODE})" cat </dev/null | grep -q .; then + WEBHOOK_EXISTS=true + fi - WEBHOOK_REPLICAS=$(kubectl -n "$PROTECTION_WEBHOOK_NS" get deploy "$PROTECTION_WEBHOOK_NAME" \ - -o jsonpath='{.spec.replicas}' 2>/dev/null || echo "1") + if [ "$WEBHOOK_EXISTS" = "true" ]; then + echo " --- Temporarily disabling protection-webhook ---" - echo " [SCALE] ${PROTECTION_WEBHOOK_NAME} -> 0 (was ${WEBHOOK_REPLICAS})" - kubectl -n "$PROTECTION_WEBHOOK_NS" scale deploy "$PROTECTION_WEBHOOK_NAME" --replicas=0 + WEBHOOK_REPLICAS=$(kubectl -n "$PROTECTION_WEBHOOK_NS" get deploy "$PROTECTION_WEBHOOK_NAME" \ + -o jsonpath='{.spec.replicas}' 2>/dev/null || echo "1") - echo " [PATCH] Set failurePolicy=Ignore on ValidatingWebhookConfiguration/${PROTECTION_WEBHOOK_NAME}" - kubectl get validatingwebhookconfiguration "$PROTECTION_WEBHOOK_NAME" -o json | \ - jq '.webhooks[].failurePolicy = "Ignore"' | \ - kubectl apply -f - + echo " [SCALE] ${PROTECTION_WEBHOOK_NAME} -> 0 (was ${WEBHOOK_REPLICAS})" + kubectl -n "$PROTECTION_WEBHOOK_NS" scale deploy "$PROTECTION_WEBHOOK_NAME" --replicas=0 - echo " Waiting for webhook pods to terminate..." - kubectl -n "$PROTECTION_WEBHOOK_NS" wait --for=delete pod \ - -l app.kubernetes.io/name=protection-webhook --timeout=60s 2>/dev/null || true - sleep 3 + echo " [PATCH] Set failurePolicy=Ignore on ValidatingWebhookConfiguration/${PROTECTION_WEBHOOK_NAME}" + kubectl get validatingwebhookconfiguration "$PROTECTION_WEBHOOK_NAME" -o json | \ + jq '.webhooks[].failurePolicy = "Ignore"' | \ + kubectl apply -f - 2>/dev/null || true + + echo " Waiting for webhook pods to terminate..." + kubectl -n "$PROTECTION_WEBHOOK_NS" wait --for=delete pod \ + -l app.kubernetes.io/name=protection-webhook --timeout=60s 2>/dev/null || true + sleep 3 + fi for entry in "${ALL_PROTECTED_RESOURCES[@]}"; do ns="${entry%%:*}" @@ -594,15 +607,17 @@ if [ ${#ALL_PROTECTED_RESOURCES[@]} -gt 0 ]; then kubectl -n "$ns" delete "$res" --wait=false 2>/dev/null || true done - echo " [PATCH] Set failurePolicy=Fail on ValidatingWebhookConfiguration/${PROTECTION_WEBHOOK_NAME}" - kubectl get validatingwebhookconfiguration "$PROTECTION_WEBHOOK_NAME" -o json | \ - jq '.webhooks[].failurePolicy = "Fail"' | \ - kubectl apply -f - + if [ "$WEBHOOK_EXISTS" = "true" ]; then + echo " [PATCH] Set failurePolicy=Fail on ValidatingWebhookConfiguration/${PROTECTION_WEBHOOK_NAME}" + kubectl get validatingwebhookconfiguration "$PROTECTION_WEBHOOK_NAME" -o json | \ + jq '.webhooks[].failurePolicy = "Fail"' | \ + kubectl apply -f - 2>/dev/null || true - echo " [SCALE] ${PROTECTION_WEBHOOK_NAME} -> ${WEBHOOK_REPLICAS}" - kubectl -n "$PROTECTION_WEBHOOK_NS" scale deploy "$PROTECTION_WEBHOOK_NAME" \ - --replicas="$WEBHOOK_REPLICAS" - echo " --- protection-webhook restored ---" + echo " [SCALE] ${PROTECTION_WEBHOOK_NAME} -> ${WEBHOOK_REPLICAS}" + kubectl -n "$PROTECTION_WEBHOOK_NS" scale deploy "$PROTECTION_WEBHOOK_NAME" \ + --replicas="$WEBHOOK_REPLICAS" + echo " --- protection-webhook restored ---" + fi else echo " [SKIP] No protected resources to delete" fi @@ -626,10 +641,52 @@ for pv_name in "${ALL_PV_NAMES[@]}"; do done # ============================================================ -# STEP 5: Unsuspend vm-disk HelmReleases first +# STEP 5: Temporarily disable CDI datavolume webhooks +# ============================================================ +# CDI's datavolume-validate webhook rejects DataVolume creation when a PVC +# with the same name already exists. We must disable it so that vm-disk +# HelmReleases can reconcile and adopt the pre-created PVCs. +# We scale down both cdi-operator (which recreates webhook configs) and +# cdi-apiserver (which serves the webhooks), then delete webhook configs. +# Both are restored after vm-disk HRs reconcile. +echo "" +echo "--- Step 5: Temporarily disable CDI webhooks ---" + +CDI_OPERATOR_REPLICAS=$(kubectl -n "$CDI_APISERVER_NS" get deploy cdi-operator \ + -o jsonpath='{.spec.replicas}' 2>/dev/null || echo "1") +CDI_APISERVER_REPLICAS=$(kubectl -n "$CDI_APISERVER_NS" get deploy "$CDI_APISERVER_DEPLOY" \ + -o jsonpath='{.spec.replicas}' 2>/dev/null || echo "1") + +echo " [SCALE] cdi-operator -> 0 (was ${CDI_OPERATOR_REPLICAS})" +kubectl -n "$CDI_APISERVER_NS" scale deploy cdi-operator --replicas=0 +echo " [SCALE] ${CDI_APISERVER_DEPLOY} -> 0 (was ${CDI_APISERVER_REPLICAS})" +kubectl -n "$CDI_APISERVER_NS" scale deploy "$CDI_APISERVER_DEPLOY" --replicas=0 + +echo " Waiting for cdi pods to terminate..." +kubectl -n "$CDI_APISERVER_NS" wait --for=delete pod \ + -l cdi.kubevirt.io=cdi-apiserver --timeout=60s 2>/dev/null || true +kubectl -n "$CDI_APISERVER_NS" wait --for=delete pod \ + -l name=cdi-operator --timeout=60s 2>/dev/null || true + +for wh in $CDI_VALIDATING_WEBHOOKS; do + if kubectl get validatingwebhookconfiguration "$wh" >/dev/null 2>&1; then + echo " [DELETE] validatingwebhookconfiguration/${wh}" + kubectl delete validatingwebhookconfiguration "$wh" 2>/dev/null || true + fi +done +for wh in $CDI_MUTATING_WEBHOOKS; do + if kubectl get mutatingwebhookconfiguration "$wh" >/dev/null 2>&1; then + echo " [DELETE] mutatingwebhookconfiguration/${wh}" + kubectl delete mutatingwebhookconfiguration "$wh" 2>/dev/null || true + fi +done +sleep 2 + +# ============================================================ +# STEP 6: Unsuspend vm-disk HelmReleases first # ============================================================ echo "" -echo "--- Step 5: Unsuspend vm-disk HelmReleases ---" +echo "--- Step 6: Unsuspend vm-disk HelmReleases ---" for entry in "${INSTANCES[@]}"; do ns="${entry%%/*}" instance="${entry#*/}" @@ -643,6 +700,10 @@ for entry in "${INSTANCES[@]}"; do else echo " [SKIP] ${ns}/hr/${disk_name} already not suspended" fi + # Force immediate reconciliation + echo " [TRIGGER] Reconcile ${ns}/hr/${disk_name}" + kubectl -n "$ns" annotate hr "$disk_name" --overwrite \ + "reconcile.fluxcd.io/requestedAt=$(date +%s)" 2>/dev/null || true fi done @@ -652,21 +713,44 @@ for entry in "${INSTANCES[@]}"; do ns="${entry%%/*}" instance="${entry#*/}" disk_name="${NEW_DISK_PREFIX}-${instance}" - for i in $(seq 1 30); do + for i in $(seq 1 60); do ready=$(kubectl -n "$ns" get hr "$disk_name" -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null || echo "") if [ "$ready" = "True" ]; then echo " [OK] ${ns}/hr/${disk_name} is Ready" break fi + if [ "$i" = "60" ]; then + echo " [WARN] ${ns}/hr/${disk_name} did not become Ready within timeout" + fi sleep 5 done done # ============================================================ -# STEP 6: Unsuspend vm-instance HelmReleases +# STEP 7: Restore CDI webhooks +# ============================================================ +# Scale cdi-operator and cdi-apiserver back up. +# cdi-apiserver will recreate webhook configurations automatically on start. +echo "" +echo "--- Step 7: Restore CDI webhooks ---" + +echo " [SCALE] cdi-operator -> ${CDI_OPERATOR_REPLICAS}" +kubectl -n "$CDI_APISERVER_NS" scale deploy cdi-operator \ + --replicas="$CDI_OPERATOR_REPLICAS" +echo " [SCALE] ${CDI_APISERVER_DEPLOY} -> ${CDI_APISERVER_REPLICAS}" +kubectl -n "$CDI_APISERVER_NS" scale deploy "$CDI_APISERVER_DEPLOY" \ + --replicas="$CDI_APISERVER_REPLICAS" + +echo " Waiting for CDI to be ready..." +kubectl -n "$CDI_APISERVER_NS" rollout status deploy cdi-operator --timeout=120s 2>/dev/null || true +kubectl -n "$CDI_APISERVER_NS" rollout status deploy "$CDI_APISERVER_DEPLOY" --timeout=120s 2>/dev/null || true +echo " --- CDI webhooks restored ---" + +# ============================================================ +# STEP 8: Unsuspend vm-instance HelmReleases # ============================================================ echo "" -echo "--- Step 6: Unsuspend vm-instance HelmReleases ---" +echo "--- Step 8: Unsuspend vm-instance HelmReleases ---" for entry in "${INSTANCES[@]}"; do ns="${entry%%/*}" instance="${entry#*/}" From 08a5f9890e71cfd5be937ec2fd5ea7ebec531afb Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Thu, 12 Feb 2026 08:41:33 +0100 Subject: [PATCH 039/666] feat(platform): remove virtual-machine application The virtual-machine application is replaced by separate vm-disk and vm-instance applications. Migration 28 handles the conversion of existing VirtualMachine HelmReleases. Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- packages/apps/virtual-machine/.helmignore | 3 - packages/apps/virtual-machine/Chart.yaml | 7 - packages/apps/virtual-machine/Makefile | 10 - packages/apps/virtual-machine/README.md | 278 ------------------ packages/apps/virtual-machine/charts/cozy-lib | 1 - .../hack/update-instance-types.sh | 1 - packages/apps/virtual-machine/logos/vm.svg | 21 -- .../virtual-machine/templates/_helpers.tpl | 126 -------- .../templates/dashboard-resourcemap.yaml | 43 --- .../templates/secret-network-config.yaml | 40 --- .../virtual-machine/templates/secret.yaml | 33 --- .../virtual-machine/templates/service.yaml | 38 --- .../templates/vm-update-hook.yaml | 170 ----------- .../apps/virtual-machine/templates/vm.yaml | 159 ---------- .../apps/virtual-machine/values.schema.json | 230 --------------- packages/apps/virtual-machine/values.yaml | 108 ------- .../sources/virtual-machine-application.yaml | 29 -- .../core/platform/templates/bundles/iaas.yaml | 1 - packages/system/virtual-machine-rd/Chart.yaml | 3 - packages/system/virtual-machine-rd/Makefile | 4 - .../cozyrds/virtual-machine.yaml | 37 --- .../templates/backupstrategy.yaml | 35 --- .../virtual-machine-rd/templates/cozyrd.yaml | 4 - .../system/virtual-machine-rd/values.yaml | 1 - 24 files changed, 1382 deletions(-) delete mode 100644 packages/apps/virtual-machine/.helmignore delete mode 100644 packages/apps/virtual-machine/Chart.yaml delete mode 100644 packages/apps/virtual-machine/Makefile delete mode 100644 packages/apps/virtual-machine/README.md delete mode 120000 packages/apps/virtual-machine/charts/cozy-lib delete mode 100755 packages/apps/virtual-machine/hack/update-instance-types.sh delete mode 100644 packages/apps/virtual-machine/logos/vm.svg delete mode 100644 packages/apps/virtual-machine/templates/_helpers.tpl delete mode 100644 packages/apps/virtual-machine/templates/dashboard-resourcemap.yaml delete mode 100644 packages/apps/virtual-machine/templates/secret-network-config.yaml delete mode 100644 packages/apps/virtual-machine/templates/secret.yaml delete mode 100644 packages/apps/virtual-machine/templates/service.yaml delete mode 100644 packages/apps/virtual-machine/templates/vm-update-hook.yaml delete mode 100644 packages/apps/virtual-machine/templates/vm.yaml delete mode 100644 packages/apps/virtual-machine/values.schema.json delete mode 100644 packages/apps/virtual-machine/values.yaml delete mode 100644 packages/core/platform/sources/virtual-machine-application.yaml delete mode 100644 packages/system/virtual-machine-rd/Chart.yaml delete mode 100644 packages/system/virtual-machine-rd/Makefile delete mode 100644 packages/system/virtual-machine-rd/cozyrds/virtual-machine.yaml delete mode 100644 packages/system/virtual-machine-rd/templates/backupstrategy.yaml delete mode 100644 packages/system/virtual-machine-rd/templates/cozyrd.yaml delete mode 100644 packages/system/virtual-machine-rd/values.yaml diff --git a/packages/apps/virtual-machine/.helmignore b/packages/apps/virtual-machine/.helmignore deleted file mode 100644 index 1ea0ae84..00000000 --- a/packages/apps/virtual-machine/.helmignore +++ /dev/null @@ -1,3 +0,0 @@ -.helmignore -/logos -/Makefile diff --git a/packages/apps/virtual-machine/Chart.yaml b/packages/apps/virtual-machine/Chart.yaml deleted file mode 100644 index 7067744b..00000000 --- a/packages/apps/virtual-machine/Chart.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: v2 -name: virtual-machine -description: Virtual Machine (simple) -icon: /logos/vm.svg -type: application -version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process -appVersion: 0.14.0 diff --git a/packages/apps/virtual-machine/Makefile b/packages/apps/virtual-machine/Makefile deleted file mode 100644 index 7c482228..00000000 --- a/packages/apps/virtual-machine/Makefile +++ /dev/null @@ -1,10 +0,0 @@ -include ../../../hack/package.mk - -generate: - cozyvalues-gen -v values.yaml -s values.schema.json -r README.md - yq -o json -i '.properties.gpus.items.type = "object" | .properties.gpus.default = []' values.schema.json - # INSTANCE_TYPES=$$(yq e '.metadata.name' -o=json -r ../../system/kubevirt-instancetypes/templates/instancetypes.yaml | yq 'split(" ") | . + [""]' -o json) \ - # && yq -i -o json ".properties.instanceType.enum = $${INSTANCE_TYPES}" values.schema.json - PREFERENCES=$$(yq e '.metadata.name' -o=json -r ../../system/kubevirt-instancetypes/templates/preferences.yaml | yq 'split(" ") | . + [""]' -o json) \ - && yq -i -o json ".properties.instanceProfile.enum = $${PREFERENCES}" values.schema.json - ../../../hack/update-crd.sh diff --git a/packages/apps/virtual-machine/README.md b/packages/apps/virtual-machine/README.md deleted file mode 100644 index c7cfe788..00000000 --- a/packages/apps/virtual-machine/README.md +++ /dev/null @@ -1,278 +0,0 @@ -# Virtual Machine (simple) - -A Virtual Machine (VM) simulates computer hardware, enabling various operating systems and applications to run in an isolated environment. - -## Deployment Details - -The virtual machine is managed and hosted through KubeVirt, allowing you to harness the benefits of virtualization within your Kubernetes ecosystem. - -- Docs: [KubeVirt User Guide](https://kubevirt.io/user-guide/) -- GitHub: [KubeVirt Repository](https://github.com/kubevirt/kubevirt) - -## Accessing virtual machine - -You can access the virtual machine using the virtctl tool: -- [KubeVirt User Guide - Virtctl Client Tool](https://kubevirt.io/user-guide/user_workloads/virtctl_client_tool/) - -To access the serial console: - -``` -virtctl console -``` - -To access the VM using VNC: - -``` -virtctl vnc -``` - -To SSH into the VM: - -``` -virtctl ssh @ -``` - -## Parameters - -### Common parameters - -| Name | Description | Type | Value | -| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ---------- | ------------ | -| `external` | Enable external access from outside the cluster. | `bool` | `false` | -| `externalMethod` | Method to pass through traffic to the VM. | `string` | `PortList` | -| `externalPorts` | Ports to forward from outside the cluster. | `[]int` | `[22]` | -| `runStrategy` | Requested running state of the VirtualMachineInstance | `string` | `Always` | -| `instanceType` | Virtual Machine instance type. | `string` | `u1.medium` | -| `instanceProfile` | Virtual Machine preferences profile. | `string` | `ubuntu` | -| `systemDisk` | System disk configuration. | `object` | `{}` | -| `systemDisk.image` | The base image for the virtual machine. | `string` | `ubuntu` | -| `systemDisk.storage` | The size of the disk allocated for the virtual machine. | `string` | `5Gi` | -| `systemDisk.storageClass` | StorageClass used to store the data. | `string` | `replicated` | -| `subnets` | Additional subnets | `[]object` | `[]` | -| `subnets[i].name` | Subnet name | `string` | `""` | -| `gpus` | List of GPUs to attach. | `[]object` | `[]` | -| `gpus[i].name` | The name of the GPU resource to attach. | `string` | `""` | -| `cpuModel` | Model specifies the CPU model inside the VMI. List of available models https://github.com/libvirt/libvirt/tree/master/src/cpu_map | `string` | `""` | -| `resources` | Resource configuration for the virtual machine. | `object` | `{}` | -| `resources.cpu` | Number of CPU cores allocated. | `quantity` | `""` | -| `resources.sockets` | Number of CPU sockets (vCPU topology). | `quantity` | `""` | -| `resources.memory` | Amount of memory allocated. | `quantity` | `""` | -| `sshKeys` | List of SSH public keys for authentication. | `[]string` | `[]` | -| `cloudInit` | Cloud-init user data. | `string` | `""` | -| `cloudInitSeed` | Seed string to generate SMBIOS UUID for the VM. | `string` | `""` | - - -## U Series - -The U Series is quite neutral and provides resources for -general purpose applications. - -*U* is the abbreviation for "Universal", hinting at the universal -attitude towards workloads. - -VMs of instance types will share physical CPU cores on a -time-slice basis with other VMs. - -### U Series Characteristics - -Specific characteristics of this series are: -- *Burstable CPU performance* - The workload has a baseline compute - performance but is permitted to burst beyond this baseline, if - excess compute resources are available. -- *vCPU-To-Memory Ratio (1:4)* - A vCPU-to-Memory ratio of 1:4, for less - noise per node. - -## O Series - -The O Series is based on the U Series, with the only difference -being that memory is overcommitted. - -*O* is the abbreviation for "Overcommitted". - -### UO Series Characteristics - -Specific characteristics of this series are: -- *Burstable CPU performance* - The workload has a baseline compute - performance but is permitted to burst beyond this baseline, if - excess compute resources are available. -- *Overcommitted Memory* - Memory is over-committed in order to achieve - a higher workload density. -- *vCPU-To-Memory Ratio (1:4)* - A vCPU-to-Memory ratio of 1:4, for less - noise per node. - -## CX Series - -The CX Series provides exclusive compute resources for compute -intensive applications. - -*CX* is the abbreviation of "Compute Exclusive". - -The exclusive resources are given to the compute threads of the -VM. In order to ensure this, some additional cores (depending -on the number of disks and NICs) will be requested to offload -the IO threading from cores dedicated to the workload. -In addition, in this series, the NUMA topology of the used -cores is provided to the VM. - -### CX Series Characteristics - -Specific characteristics of this series are: -- *Hugepages* - Hugepages are used in order to improve memory - performance. -- *Dedicated CPU* - Physical cores are exclusively assigned to every - vCPU in order to provide fixed and high compute guarantees to the - workload. -- *Isolated emulator threads* - Hypervisor emulator threads are isolated - from the vCPUs in order to reduce emaulation related impact on the - workload. -- *vNUMA* - Physical NUMA topology is reflected in the guest in order to - optimize guest sided cache utilization. -- *vCPU-To-Memory Ratio (1:2)* - A vCPU-to-Memory ratio of 1:2. - -## M Series - -The M Series provides resources for memory intensive -applications. - -*M* is the abbreviation of "Memory". - -### M Series Characteristics - -Specific characteristics of this series are: -- *Hugepages* - Hugepages are used in order to improve memory - performance. -- *Burstable CPU performance* - The workload has a baseline compute - performance but is permitted to burst beyond this baseline, if - excess compute resources are available. -- *vCPU-To-Memory Ratio (1:8)* - A vCPU-to-Memory ratio of 1:8, for much - less noise per node. - -## RT Series - -The RT Series provides resources for realtime applications, like Oslat. - -*RT* is the abbreviation for "realtime". - -This series of instance types requires nodes capable of running -realtime applications. - -### RT Series Characteristics - -Specific characteristics of this series are: -- *Hugepages* - Hugepages are used in order to improve memory - performance. -- *Dedicated CPU* - Physical cores are exclusively assigned to every - vCPU in order to provide fixed and high compute guarantees to the - workload. -- *Isolated emulator threads* - Hypervisor emulator threads are isolated - from the vCPUs in order to reduce emaulation related impact on the - workload. -- *vCPU-To-Memory Ratio (1:4)* - A vCPU-to-Memory ratio of 1:4 starting from - the medium size. - -## Development - -To get started with customizing or creating your own instancetypes and preferences -see [DEVELOPMENT.md](./DEVELOPMENT.md). - -## Resources - -The following instancetype resources are provided by Cozystack: - -Name | vCPUs | Memory ------|-------|------- -cx1.2xlarge | 8 | 16Gi -cx1.4xlarge | 16 | 32Gi -cx1.8xlarge | 32 | 64Gi -cx1.large | 2 | 4Gi -cx1.medium | 1 | 2Gi -cx1.xlarge | 4 | 8Gi -gn1.2xlarge | 8 | 32Gi -gn1.4xlarge | 16 | 64Gi -gn1.8xlarge | 32 | 128Gi -gn1.xlarge | 4 | 16Gi -m1.2xlarge | 8 | 64Gi -m1.4xlarge | 16 | 128Gi -m1.8xlarge | 32 | 256Gi -m1.large | 2 | 16Gi -m1.xlarge | 4 | 32Gi -n1.2xlarge | 16 | 32Gi -n1.4xlarge | 32 | 64Gi -n1.8xlarge | 64 | 128Gi -n1.large | 4 | 8Gi -n1.medium | 4 | 4Gi -n1.xlarge | 8 | 16Gi -o1.2xlarge | 8 | 32Gi -o1.4xlarge | 16 | 64Gi -o1.8xlarge | 32 | 128Gi -o1.large | 2 | 8Gi -o1.medium | 1 | 4Gi -o1.micro | 1 | 1Gi -o1.nano | 1 | 512Mi -o1.small | 1 | 2Gi -o1.xlarge | 4 | 16Gi -rt1.2xlarge | 8 | 32Gi -rt1.4xlarge | 16 | 64Gi -rt1.8xlarge | 32 | 128Gi -rt1.large | 2 | 8Gi -rt1.medium | 1 | 4Gi -rt1.micro | 1 | 1Gi -rt1.small | 1 | 2Gi -rt1.xlarge | 4 | 16Gi -u1.2xlarge | 8 | 32Gi -u1.2xmedium | 2 | 4Gi -u1.4xlarge | 16 | 64Gi -u1.8xlarge | 32 | 128Gi -u1.large | 2 | 8Gi -u1.medium | 1 | 4Gi -u1.micro | 1 | 1Gi -u1.nano | 1 | 512Mi -u1.small | 1 | 2Gi -u1.xlarge | 4 | 16Gi - -The following preference resources are provided by Cozystack: - -Name | Guest OS ------|--------- -alpine | Alpine -centos.7 | CentOS 7 -centos.7.desktop | CentOS 7 -centos.stream10 | CentOS Stream 10 -centos.stream10.desktop | CentOS Stream 10 -centos.stream8 | CentOS Stream 8 -centos.stream8.desktop | CentOS Stream 8 -centos.stream8.dpdk | CentOS Stream 8 -centos.stream9 | CentOS Stream 9 -centos.stream9.desktop | CentOS Stream 9 -centos.stream9.dpdk | CentOS Stream 9 -cirros | Cirros -fedora | Fedora (amd64) -fedora.arm64 | Fedora (arm64) -opensuse.leap | OpenSUSE Leap -opensuse.tumbleweed | OpenSUSE Tumbleweed -rhel.10 | Red Hat Enterprise Linux 10 Beta (amd64) -rhel.10.arm64 | Red Hat Enterprise Linux 10 Beta (arm64) -rhel.7 | Red Hat Enterprise Linux 7 -rhel.7.desktop | Red Hat Enterprise Linux 7 -rhel.8 | Red Hat Enterprise Linux 8 -rhel.8.desktop | Red Hat Enterprise Linux 8 -rhel.8.dpdk | Red Hat Enterprise Linux 8 -rhel.9 | Red Hat Enterprise Linux 9 (amd64) -rhel.9.arm64 | Red Hat Enterprise Linux 9 (arm64) -rhel.9.desktop | Red Hat Enterprise Linux 9 Desktop (amd64) -rhel.9.dpdk | Red Hat Enterprise Linux 9 DPDK (amd64) -rhel.9.realtime | Red Hat Enterprise Linux 9 Realtime (amd64) -sles | SUSE Linux Enterprise Server -ubuntu | Ubuntu -windows.10 | Microsoft Windows 10 -windows.10.virtio | Microsoft Windows 10 (virtio) -windows.11 | Microsoft Windows 11 -windows.11.virtio | Microsoft Windows 11 (virtio) -windows.2k16 | Microsoft Windows Server 2016 -windows.2k16.virtio | Microsoft Windows Server 2016 (virtio) -windows.2k19 | Microsoft Windows Server 2019 -windows.2k19.virtio | Microsoft Windows Server 2019 (virtio) -windows.2k22 | Microsoft Windows Server 2022 -windows.2k22.virtio | Microsoft Windows Server 2022 (virtio) -windows.2k25 | Microsoft Windows Server 2025 -windows.2k25.virtio | Microsoft Windows Server 2025 (virtio) diff --git a/packages/apps/virtual-machine/charts/cozy-lib b/packages/apps/virtual-machine/charts/cozy-lib deleted file mode 120000 index e1813509..00000000 --- a/packages/apps/virtual-machine/charts/cozy-lib +++ /dev/null @@ -1 +0,0 @@ -../../../library/cozy-lib \ No newline at end of file diff --git a/packages/apps/virtual-machine/hack/update-instance-types.sh b/packages/apps/virtual-machine/hack/update-instance-types.sh deleted file mode 100755 index 1a248525..00000000 --- a/packages/apps/virtual-machine/hack/update-instance-types.sh +++ /dev/null @@ -1 +0,0 @@ -#!/bin/sh diff --git a/packages/apps/virtual-machine/logos/vm.svg b/packages/apps/virtual-machine/logos/vm.svg deleted file mode 100644 index 9c3e34d9..00000000 --- a/packages/apps/virtual-machine/logos/vm.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/apps/virtual-machine/templates/_helpers.tpl b/packages/apps/virtual-machine/templates/_helpers.tpl deleted file mode 100644 index ddea90fe..00000000 --- a/packages/apps/virtual-machine/templates/_helpers.tpl +++ /dev/null @@ -1,126 +0,0 @@ -{{/* -Expand the name of the chart. -*/}} -{{- define "virtual-machine.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Create a default fully qualified app name. -We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). -If release name contains chart name it will be used as a full name. -*/}} -{{- define "virtual-machine.fullname" -}} -{{- if .Values.fullnameOverride }} -{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- $name := default .Chart.Name .Values.nameOverride }} -{{- if contains $name .Release.Name }} -{{- .Release.Name | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} -{{- end }} -{{- end }} -{{- end }} - -{{/* -Create chart name and version as used by the chart label. -*/}} -{{- define "virtual-machine.chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Common labels -*/}} -{{- define "virtual-machine.labels" -}} -helm.sh/chart: {{ include "virtual-machine.chart" . }} -{{ include "virtual-machine.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} -{{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} -{{- end }} - -{{/* -Selector labels -*/}} -{{- define "virtual-machine.selectorLabels" -}} -app.kubernetes.io/name: {{ include "virtual-machine.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end }} - -{{/* -Generate a stable UUID for cloud-init re-initialization upon upgrade. -*/}} -{{- define "virtual-machine.stableUuid" -}} -{{- $source := printf "%s-%s-%s" .Release.Namespace (include "virtual-machine.fullname" .) .Values.cloudInitSeed }} -{{- $hash := sha256sum $source }} -{{- $uuid := printf "%s-%s-4%s-9%s-%s" (substr 0 8 $hash) (substr 8 12 $hash) (substr 13 16 $hash) (substr 17 20 $hash) (substr 20 32 $hash) }} -{{- if eq .Values.cloudInitSeed "" }} - {{- /* Try to save previous uuid to not trigger full cloud-init again if user decided to remove the seed. */}} - {{- $vmResource := lookup "kubevirt.io/v1" "VirtualMachine" .Release.Namespace (include "virtual-machine.fullname" .) -}} - {{- if $vmResource }} - {{- $existingUuid := $vmResource | dig "spec" "template" "spec" "domain" "firmware" "uuid" "" }} - {{- if $existingUuid }} - {{- $uuid = $existingUuid }} - {{- end }} - {{- end }} -{{- end }} -{{- $uuid }} -{{- end }} - -{{/* -Domain resources (cpu, memory) as a JSON object. -Used in vm.yaml for rendering and in the update hook for merge patches. -*/}} -{{- define "virtual-machine.domainResources" -}} -{{- $result := dict -}} -{{- if or .Values.cpuModel (and .Values.resources .Values.resources.cpu .Values.resources.sockets) -}} - {{- $cpu := dict -}} - {{- if and .Values.resources .Values.resources.cpu .Values.resources.sockets -}} - {{- $_ := set $cpu "cores" (.Values.resources.cpu | int64) -}} - {{- $_ := set $cpu "sockets" (.Values.resources.sockets | int64) -}} - {{- end -}} - {{- if .Values.cpuModel -}} - {{- $_ := set $cpu "model" .Values.cpuModel -}} - {{- end -}} - {{- $_ := set $result "cpu" $cpu -}} -{{- end -}} -{{- if and .Values.resources .Values.resources.memory -}} - {{- $_ := set $result "resources" (dict "requests" (dict "memory" .Values.resources.memory)) -}} -{{- end -}} -{{- $result | toJson -}} -{{- end -}} - -{{/* -Node Affinity for Windows VMs -*/}} -{{- define "virtual-machine.nodeAffinity" -}} -{{- if .Values._cluster.scheduling -}} -{{- $dedicatedNodesForWindowsVMs := get .Values._cluster.scheduling "dedicatedNodesForWindowsVMs" -}} -{{- if eq $dedicatedNodesForWindowsVMs "true" -}} -{{- $isWindows := hasPrefix "windows" (toString .Values.instanceProfile) -}} -affinity: - nodeAffinity: - {{- if $isWindows }} - requiredDuringSchedulingIgnoredDuringExecution: - nodeSelectorTerms: - - matchExpressions: - - key: scheduling.cozystack.io/vm-windows - operator: In - values: - - "true" - {{- else }} - preferredDuringSchedulingIgnoredDuringExecution: - - weight: 100 - preference: - matchExpressions: - - key: scheduling.cozystack.io/vm-windows - operator: NotIn - values: - - "true" - {{- end }} -{{- end -}} -{{- end -}} -{{- end -}} diff --git a/packages/apps/virtual-machine/templates/dashboard-resourcemap.yaml b/packages/apps/virtual-machine/templates/dashboard-resourcemap.yaml deleted file mode 100644 index 4beac5dc..00000000 --- a/packages/apps/virtual-machine/templates/dashboard-resourcemap.yaml +++ /dev/null @@ -1,43 +0,0 @@ -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: {{ .Release.Name }}-dashboard-resources -rules: -- apiGroups: - - "" - resources: - - services - resourceNames: - - {{ include "virtual-machine.fullname" . }} - verbs: ["get", "list", "watch"] -- apiGroups: - - cozystack.io - resources: - - workloadmonitors - resourceNames: - - {{ .Release.Name }} - verbs: ["get", "list", "watch"] ---- -kind: RoleBinding -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: {{ .Release.Name }}-dashboard-resources -subjects: -{{ include "cozy-lib.rbac.subjectsForTenantAndAccessLevel" (list "use" .Release.Namespace) }} -roleRef: - kind: Role - name: {{ .Release.Name }}-dashboard-resources - apiGroup: rbac.authorization.k8s.io ---- -apiVersion: cozystack.io/v1alpha1 -kind: WorkloadMonitor -metadata: - name: {{ $.Release.Name }} -spec: - replicas: 1 - minReplicas: 1 - kind: virtual-machine - type: virtual-machine - selector: - app.kubernetes.io/instance: {{ .Release.Name }} - version: {{ $.Chart.Version }} diff --git a/packages/apps/virtual-machine/templates/secret-network-config.yaml b/packages/apps/virtual-machine/templates/secret-network-config.yaml deleted file mode 100644 index 9800ceac..00000000 --- a/packages/apps/virtual-machine/templates/secret-network-config.yaml +++ /dev/null @@ -1,40 +0,0 @@ -# if subnets are specified and image is either ubunu or alpine -{{- if and .Values.subnets (or (eq .Values.systemDisk.image "ubuntu") (eq .Values.systemDisk.image "alpine")) }} ---- -apiVersion: v1 -kind: Secret -metadata: - name: {{ include "virtual-machine.fullname" $ }}-network-config -stringData: - networkData: | - network: - version: 1 - config: - {{- if eq .Values.systemDisk.image "ubuntu" }} - # main iface - - type: physical - name: enp1s0 - subnets: - - type: dhcp - # additional ifaces attached to subnets - {{- range $i, $subnet := .Values.subnets }} - - type: physical - name: enp{{ add 2 $i }}s0 - subnets: - - type: dhcp - {{- end }} - {{- else if eq .Values.systemDisk.image "alpine" }} - #main iface - - type: physical - name: eth0 - subnets: - - type: dhcp - # additional ifaces attached to subnets - {{- range $i, $subnet := .Values.subnets }} - - type: physical - name: eth{{ add1 $i }} - subnets: - - type: dhcp - {{- end }} - {{- end }} -{{- end }} diff --git a/packages/apps/virtual-machine/templates/secret.yaml b/packages/apps/virtual-machine/templates/secret.yaml deleted file mode 100644 index 73cd92bf..00000000 --- a/packages/apps/virtual-machine/templates/secret.yaml +++ /dev/null @@ -1,33 +0,0 @@ -{{- if .Values.sshKeys }} ---- -apiVersion: v1 -kind: Secret -metadata: - name: {{ include "virtual-machine.fullname" $ }}-ssh-keys -stringData: - {{- range $k, $v := .Values.sshKeys }} - key{{ $k }}: {{ quote $v }} - {{- end }} -{{- end }} -{{- if or .Values.cloudInit .Values.sshKeys }} ---- -apiVersion: v1 -kind: Secret -metadata: - name: {{ include "virtual-machine.fullname" . }}-cloud-init -stringData: - userdata: | - {{- if .Values.cloudInit }} - {{- .Values.cloudInit | nindent 4 }} - {{- else if and (.Values.sshKeys) (not .Values.cloudInit) }} - {{- /* - We usually provide ssh keys in cloud-init metadata, because userdata it not typed and can be used for any purpose. - However, if user provides ssh keys but not cloud-init, we still need to provide a minimal cloud-init config to avoid errors. - */}} - #cloud-config - ssh_authorized_keys: - {{- range .Values.sshKeys }} - - {{ quote . }} - {{- end }} - {{- end }} -{{- end }} diff --git a/packages/apps/virtual-machine/templates/service.yaml b/packages/apps/virtual-machine/templates/service.yaml deleted file mode 100644 index b12f7612..00000000 --- a/packages/apps/virtual-machine/templates/service.yaml +++ /dev/null @@ -1,38 +0,0 @@ ---- -apiVersion: v1 -kind: Service -metadata: - name: {{ include "virtual-machine.fullname" . }} - labels: - apps.cozystack.io/user-service: "true" - {{- include "virtual-machine.labels" . | nindent 4 }} -{{- if .Values.external }} - annotations: - networking.cozystack.io/wholeIP: "true" -{{- end }} -spec: - type: {{ ternary "LoadBalancer" "ClusterIP" .Values.external }} -{{- if .Values.external }} - externalTrafficPolicy: Local - {{- if ((include "cozy-lib.network.disableLoadBalancerNodePorts" $) | fromYaml) }} - allocateLoadBalancerNodePorts: false - {{- end }} -{{- else }} - clusterIP: None -{{- end }} - selector: - {{- include "virtual-machine.selectorLabels" . | nindent 4 }} - ports: -{{- if .Values.external }} - {{- if and (eq .Values.externalMethod "WholeIP") (not .Values.externalPorts) }} - - port: 65535 - {{- else }} - {{- range .Values.externalPorts }} - - name: port-{{ . }} - port: {{ . }} - targetPort: {{ . }} - {{- end }} - {{- end }} -{{- else }} - - port: 65535 -{{- end }} diff --git a/packages/apps/virtual-machine/templates/vm-update-hook.yaml b/packages/apps/virtual-machine/templates/vm-update-hook.yaml deleted file mode 100644 index 65ee02f0..00000000 --- a/packages/apps/virtual-machine/templates/vm-update-hook.yaml +++ /dev/null @@ -1,170 +0,0 @@ -{{- $vmName := include "virtual-machine.fullname" . -}} -{{- $namespace := .Release.Namespace -}} - -{{- $existingVM := lookup "kubevirt.io/v1" "VirtualMachine" $namespace $vmName -}} -{{- $existingPVC := lookup "v1" "PersistentVolumeClaim" $namespace $vmName -}} -{{- $existingService := lookup "v1" "Service" $namespace $vmName -}} - -{{- $instanceType := .Values.instanceType | default "" -}} -{{- $instanceProfile := .Values.instanceProfile | default "" -}} -{{- $desiredStorage := .Values.systemDisk.storage | default "" -}} -{{- $desiredServiceType := ternary "LoadBalancer" "ClusterIP" .Values.external -}} - -{{- $needUpdateType := false -}} -{{- $needUpdateProfile := false -}} -{{- $needResizePVC := false -}} -{{- $needRecreateService := false -}} -{{- $needRemoveInstanceType := false -}} -{{- $needRemoveCustomResources := false -}} - -{{- $existingHasInstanceType := and $existingVM $existingVM.spec.instancetype -}} -{{- if and $existingHasInstanceType (not $instanceType) -}} - {{- $needRemoveInstanceType = true -}} -{{- else if and $existingHasInstanceType $instanceType -}} - {{- if not (eq $existingVM.spec.instancetype.name $instanceType) -}} - {{- $needUpdateType = true -}} - {{- end -}} -{{- else if and $existingVM (not $existingHasInstanceType) $instanceType -}} - {{- $needRemoveCustomResources = true -}} -{{- end -}} - -{{- if and $existingVM $existingVM.spec.preference $instanceProfile -}} - {{- if not (eq $existingVM.spec.preference.name $instanceProfile) -}} - {{- $needUpdateProfile = true -}} - {{- end -}} -{{- end -}} - -{{- if and $existingPVC $desiredStorage -}} - {{- $currentStorage := $existingPVC.spec.resources.requests.storage | toString -}} - {{- if not (eq $currentStorage $desiredStorage) -}} - {{- $oldSize := (include "cozy-lib.resources.toFloat" $currentStorage) | float64 -}} - {{- $newSize := (include "cozy-lib.resources.toFloat" $desiredStorage) | float64 -}} - {{- if gt $newSize $oldSize -}} - {{- $needResizePVC = true -}} - {{- end -}} - {{- end -}} -{{- end -}} - -{{- if $existingService -}} - {{- $currentServiceType := $existingService.spec.type -}} - {{- if ne $currentServiceType $desiredServiceType -}} - {{- $needRecreateService = true -}} - {{- end -}} -{{- end -}} - -{{- if or $needUpdateType $needUpdateProfile $needResizePVC $needRecreateService $needRemoveInstanceType $needRemoveCustomResources }} -apiVersion: batch/v1 -kind: Job -metadata: - name: "{{ $.Release.Name }}-update-hook" - annotations: - helm.sh/hook: pre-install,pre-upgrade - helm.sh/hook-weight: "0" - helm.sh/hook-delete-policy: hook-succeeded,before-hook-creation -spec: - template: - metadata: - labels: - app: "{{ $.Release.Name }}-update-hook" - policy.cozystack.io/allow-to-apiserver: "true" - spec: - serviceAccountName: {{ $.Release.Name }}-update-hook - restartPolicy: Never - containers: - - name: update-resources - image: docker.io/alpine/k8s:1.33.4 - resources: - requests: - memory: "16Mi" - cpu: "10m" - limits: - memory: "128Mi" - cpu: "100m" - command: ["sh", "-exc"] - args: - - | - {{- if $needUpdateType }} - echo "Patching VirtualMachine for instancetype update..." - kubectl patch virtualmachines.kubevirt.io {{ $vmName }} -n {{ $namespace }} \ - --type merge \ - -p '{"spec":{"instancetype":{"name": "{{ $instanceType }}", "revisionName": null}}}' - {{- end }} - - {{- if $needUpdateProfile }} - echo "Patching VirtualMachine for preference update..." - kubectl patch virtualmachines.kubevirt.io {{ $vmName }} -n {{ $namespace }} \ - --type merge \ - -p '{"spec":{"preference":{"name": "{{ $instanceProfile }}", "revisionName": null}}}' - {{- end }} - - {{- if $needRemoveInstanceType }} - echo "Removing instancetype from VM (switching to custom resources)..." - kubectl patch virtualmachines.kubevirt.io {{ $vmName }} -n {{ $namespace }} \ - --type merge \ - -p '{"spec":{"instancetype":null{{- if not $instanceProfile }},"preference":null{{- end }},"template":{"spec":{"domain":{{ include "virtual-machine.domainResources" . }}}}}}' - {{- end }} - - {{- if $needRemoveCustomResources }} - echo "Removing custom CPU/memory from domain (switching to instancetype)..." - kubectl patch virtualmachines.kubevirt.io {{ $vmName }} -n {{ $namespace }} \ - --type merge \ - -p '{"spec":{"instancetype":{"name":"{{ $instanceType }}","revisionName":null},"template":{"spec":{"domain":{"cpu":null,"resources":null}}}}}' - {{- end }} - - {{- if $needResizePVC }} - echo "Patching PVC for storage resize..." - kubectl patch pvc {{ $vmName }} -n {{ $namespace }} \ - --type merge \ - -p '{"spec":{"resources":{"requests":{"storage":"{{ $desiredStorage }}"}}}}' - {{- end }} - - {{- if $needRecreateService }} - echo "Removing Service..." - kubectl delete service --cascade=orphan -n {{ $namespace }} {{ $vmName }} - {{- end }} ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ $.Release.Name }}-update-hook - annotations: - helm.sh/hook: pre-install,pre-upgrade - helm.sh/hook-weight: "-5" - helm.sh/hook-delete-policy: before-hook-creation ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: {{ $.Release.Name }}-update-hook - annotations: - helm.sh/hook: pre-install,pre-upgrade - helm.sh/hook-weight: "-5" - helm.sh/hook-delete-policy: before-hook-creation -rules: - - apiGroups: ["kubevirt.io"] - resources: ["virtualmachines"] - verbs: ["patch", "get", "list", "watch"] - - apiGroups: [""] - resources: ["persistentvolumeclaims"] - verbs: ["patch", "get", "list", "watch"] - - apiGroups: [""] - resources: ["services"] - resourceNames: ["{{ $vmName }}"] - verbs: ["delete"] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: {{ $.Release.Name }}-update-hook - annotations: - helm.sh/hook: pre-install,pre-upgrade - helm.sh/hook-weight: "-5" - helm.sh/hook-delete-policy: before-hook-creation -subjects: - - kind: ServiceAccount - name: {{ $.Release.Name }}-update-hook -roleRef: - kind: Role - name: {{ $.Release.Name }}-update-hook - apiGroup: rbac.authorization.k8s.io -{{- end }} diff --git a/packages/apps/virtual-machine/templates/vm.yaml b/packages/apps/virtual-machine/templates/vm.yaml deleted file mode 100644 index c1b67344..00000000 --- a/packages/apps/virtual-machine/templates/vm.yaml +++ /dev/null @@ -1,159 +0,0 @@ -{{- if and .Values.instanceType (not (lookup "instancetype.kubevirt.io/v1beta1" "VirtualMachineClusterInstancetype" "" .Values.instanceType)) }} -{{- fail (printf "Specified instancetype not exists in cluster: %s" .Values.instanceType) }} -{{- end }} -{{- if and .Values.instanceProfile (not (lookup "instancetype.kubevirt.io/v1beta1" "VirtualMachineClusterPreference" "" .Values.instanceProfile)) }} -{{- fail (printf "Specified profile not exists in cluster: %s" .Values.instanceProfile) }} -{{- end }} -{{- if and (not .Values.instanceType) (not (and .Values.resources .Values.resources.cpu .Values.resources.sockets .Values.resources.memory)) }} -{{- fail "Either instanceType or resources (cpu, sockets, memory) must be specified" }} -{{- end }} - -apiVersion: kubevirt.io/v1 -kind: VirtualMachine -metadata: - name: {{ include "virtual-machine.fullname" . }} - labels: - {{- include "virtual-machine.labels" . | nindent 4 }} -spec: - {{- if hasKey .Values "runStrategy" }} - runStrategy: {{ .Values.runStrategy }} - {{- else }} - runStrategy: {{ ternary "Always" "Halted" (.Values.running | default true) }} - {{- end }} - - {{- with .Values.instanceType }} - instancetype: - kind: VirtualMachineClusterInstancetype - name: {{ . }} - {{- end }} - {{- with .Values.instanceProfile }} - preference: - kind: VirtualMachineClusterPreference - name: {{ . }} - {{- end }} - - dataVolumeTemplates: - - metadata: - name: {{ include "virtual-machine.fullname" . }} - labels: - app.kubernetes.io/instance: {{ .Release.Name }} - spec: - storage: - resources: - requests: - storage: {{ .Values.systemDisk.storage | quote }} - {{- with .Values.systemDisk.storageClass }} - storageClassName: {{ . }} - {{- end }} - source: - {{- $dv := lookup "cdi.kubevirt.io/v1beta1" "DataVolume" "cozy-public" (printf "vm-image-%s" .Values.systemDisk.image) }} - {{- if $dv }} - pvc: - name: vm-image-{{ .Values.systemDisk.image }} - namespace: cozy-public - {{- else }} - http: - {{- if eq .Values.systemDisk.image "cirros" }} - url: https://download.cirros-cloud.net/0.6.2/cirros-0.6.2-x86_64-disk.img - {{- else if eq .Values.systemDisk.image "ubuntu" }} - url: https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img - {{- else if eq .Values.systemDisk.image "fedora" }} - url: https://download.fedoraproject.org/pub/fedora/linux/releases/42/Cloud/x86_64/images/Fedora-Cloud-Base-Generic-42-1.1.x86_64.qcow2 - {{- else if eq .Values.systemDisk.image "alpine" }} - url: https://dl-cdn.alpinelinux.org/alpine/v3.20/releases/cloud/nocloud_alpine-3.20.8-x86_64-bios-cloudinit-r0.qcow2 - {{- else if eq .Values.systemDisk.image "talos" }} - url: https://github.com/siderolabs/talos/releases/download/v1.7.6/nocloud-amd64.raw.xz - {{- end }} - {{- end }} - - template: - metadata: - annotations: - kubevirt.io/allow-pod-bridge-network-live-migration: "true" - labels: - {{- include "virtual-machine.labels" . | nindent 8 }} - spec: - domain: - {{- $domainRes := include "virtual-machine.domainResources" . | fromJson -}} - {{- with $domainRes.cpu }} - cpu: {{- . | toYaml | nindent 10 }} - {{- end }} - {{- with $domainRes.resources }} - resources: {{- . | toYaml | nindent 10 }} - {{- end }} - firmware: - uuid: {{ include "virtual-machine.stableUuid" . }} - devices: - {{- if .Values.gpus }} - gpus: - {{- range $i, $gpu := .Values.gpus }} - - name: gpu{{ add $i 1 }} - deviceName: {{ $gpu.name }} - {{- end }} - {{- end }} - - disks: - - disk: - bus: scsi - name: systemdisk - {{- if or .Values.cloudInit .Values.sshKeys (and .Values.subnets (or (eq .Values.systemDisk.image "ubuntu") (eq .Values.systemDisk.image "alpine"))) }} - - disk: - bus: virtio - name: cloudinitdisk - {{- end }} - - interfaces: - - name: default - bridge: {} - {{- if .Values.subnets }} - {{- range $i, $subnet := .Values.subnets }} - - name: {{ $subnet.name }} - bridge: {} - {{- end }} - {{- end }} - - machine: - type: "" - - {{- if .Values.sshKeys }} - accessCredentials: - - sshPublicKey: - source: - secret: - secretName: {{ include "virtual-machine.fullname" $ }}-ssh-keys - propagationMethod: - # keys will be injected into metadata part of cloud-init disk - noCloud: {} - {{- end }} - - terminationGracePeriodSeconds: 30 - - {{- include "virtual-machine.nodeAffinity" . | nindent 6 }} - - volumes: - - name: systemdisk - dataVolume: - name: {{ include "virtual-machine.fullname" . }} - {{- if or .Values.cloudInit .Values.sshKeys (and .Values.subnets (or (eq .Values.systemDisk.image "ubuntu") (eq .Values.systemDisk.image "alpine"))) }} - - name: cloudinitdisk - cloudInitNoCloud: - {{- if or .Values.cloudInit .Values.sshKeys }} - secretRef: - name: {{ include "virtual-machine.fullname" . }}-cloud-init - {{- end }} - {{- if and .Values.subnets (or (eq .Values.systemDisk.image "ubuntu") (eq .Values.systemDisk.image "alpine")) }} - networkDataSecretRef: - name: {{ include "virtual-machine.fullname" . }}-network-config - {{- end }} - {{- end }} - - networks: - - name: default - pod: {} - {{- if .Values.subnets }} - {{- range $i, $subnet := .Values.subnets }} - - name: {{ $subnet.name }} - multus: - networkName: {{ $.Release.Namespace }}/{{ $subnet.name }} - {{- end }} - {{- end }} diff --git a/packages/apps/virtual-machine/values.schema.json b/packages/apps/virtual-machine/values.schema.json deleted file mode 100644 index 3cd6c226..00000000 --- a/packages/apps/virtual-machine/values.schema.json +++ /dev/null @@ -1,230 +0,0 @@ -{ - "title": "Chart Values", - "type": "object", - "properties": { - "cloudInit": { - "description": "Cloud-init user data.", - "type": "string", - "default": "" - }, - "cloudInitSeed": { - "description": "Seed string to generate SMBIOS UUID for the VM.", - "type": "string", - "default": "" - }, - "cpuModel": { - "description": "Model specifies the CPU model inside the VMI. List of available models https://github.com/libvirt/libvirt/tree/master/src/cpu_map", - "type": "string", - "default": "" - }, - "external": { - "description": "Enable external access from outside the cluster.", - "type": "boolean", - "default": false - }, - "externalMethod": { - "description": "Method to pass through traffic to the VM.", - "type": "string", - "default": "PortList", - "enum": [ - "PortList", - "WholeIP" - ] - }, - "externalPorts": { - "description": "Ports to forward from outside the cluster.", - "type": "array", - "default": [ - 22 - ], - "items": { - "type": "integer" - } - }, - "gpus": { - "description": "List of GPUs to attach.", - "type": "array", - "default": [], - "items": { - "type": "object", - "required": [ - "name" - ], - "properties": { - "name": { - "description": "The name of the GPU resource to attach.", - "type": "string" - } - } - } - }, - "instanceProfile": { - "description": "Virtual Machine preferences profile.", - "type": "string", - "default": "ubuntu", - "enum": [ - "alpine", - "centos.7", - "centos.7.desktop", - "centos.stream10", - "centos.stream10.desktop", - "centos.stream8", - "centos.stream8.desktop", - "centos.stream8.dpdk", - "centos.stream9", - "centos.stream9.desktop", - "centos.stream9.dpdk", - "cirros", - "fedora", - "fedora.arm64", - "opensuse.leap", - "opensuse.tumbleweed", - "rhel.10", - "rhel.10.arm64", - "rhel.7", - "rhel.7.desktop", - "rhel.8", - "rhel.8.desktop", - "rhel.8.dpdk", - "rhel.9", - "rhel.9.arm64", - "rhel.9.desktop", - "rhel.9.dpdk", - "rhel.9.realtime", - "sles", - "ubuntu", - "windows.10", - "windows.10.virtio", - "windows.11", - "windows.11.virtio", - "windows.2k16", - "windows.2k16.virtio", - "windows.2k19", - "windows.2k19.virtio", - "windows.2k22", - "windows.2k22.virtio", - "windows.2k25", - "windows.2k25.virtio", - "" - ] - }, - "instanceType": { - "description": "Virtual Machine instance type.", - "type": "string", - "default": "u1.medium" - }, - "resources": { - "description": "Resource configuration for the virtual machine.", - "type": "object", - "default": {}, - "properties": { - "cpu": { - "description": "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": "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 - }, - "sockets": { - "description": "Number of CPU sockets (vCPU topology).", - "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 - } - } - }, - "runStrategy": { - "description": "Requested running state of the VirtualMachineInstance", - "type": "string", - "default": "Always", - "enum": [ - "Always", - "Halted", - "Manual", - "RerunOnFailure", - "Once" - ] - }, - "sshKeys": { - "description": "List of SSH public keys for authentication.", - "type": "array", - "default": [], - "items": { - "type": "string" - } - }, - "subnets": { - "description": "Additional subnets", - "type": "array", - "default": [], - "items": { - "type": "object", - "properties": { - "name": { - "description": "Subnet name", - "type": "string" - } - } - } - }, - "systemDisk": { - "description": "System disk configuration.", - "type": "object", - "default": {}, - "required": [ - "image", - "storage" - ], - "properties": { - "image": { - "description": "The base image for the virtual machine.", - "type": "string", - "default": "ubuntu", - "enum": [ - "ubuntu", - "cirros", - "alpine", - "fedora", - "talos" - ] - }, - "storage": { - "description": "The size of the disk allocated for the virtual machine.", - "type": "string", - "default": "5Gi" - }, - "storageClass": { - "description": "StorageClass used to store the data.", - "type": "string", - "default": "replicated" - } - } - } - } -} diff --git a/packages/apps/virtual-machine/values.yaml b/packages/apps/virtual-machine/values.yaml deleted file mode 100644 index 169d7e10..00000000 --- a/packages/apps/virtual-machine/values.yaml +++ /dev/null @@ -1,108 +0,0 @@ -## -## @section Common parameters -## - -## @enum {string} ExternalMethod - Method to pass through traffic to the VM. -## @value PortList - Forward selected ports only. -## @value WholeIP - Forward all traffic for the IP. - -## @enum {string} SystemImage - The base image for the virtual machine. -## @value ubuntu -## @value cirros -## @value alpine -## @value fedora -## @value talos - -## @typedef {struct} SystemDisk - System disk configuration. -## @field {SystemImage} image - The base image for the virtual machine. -## @field {string} storage - The size of the disk allocated for the virtual machine. -## @field {string} [storageClass] - StorageClass used to store the data. - -## @typedef {struct} Subnet - Additional subnets -## @field {string} [name] - Subnet name - -## @typedef {struct} GPU - GPU device configuration. -## @field {string} name - The name of the GPU resource to attach. - -## @typedef {struct} Resources - Resource configuration for the virtual machine. -## @field {quantity} [cpu] - Number of CPU cores allocated. -## @field {quantity} [sockets] - Number of CPU sockets (vCPU topology). -## @field {quantity} [memory] - Amount of memory allocated. - -## @param {bool} external - Enable external access from outside the cluster. -external: false - -## @param {ExternalMethod} externalMethod - Method to pass through traffic to the VM. -externalMethod: "PortList" - -## @param {[]int} externalPorts - Ports to forward from outside the cluster. -externalPorts: - - 22 - -## @enum {string} RunStrategy - Requested running state of the VirtualMachineInstance -## @value Always - VMI should always be running -## @value Halted - VMI should never be running -## @value Manual - VMI can be started/stopped using API endpoints -## @value RerunOnFailure - VMI will initially be running and restarted if a failure occurs, but will not be restarted upon successful completion -## @value Once - VMI will run once and not be restarted upon completion regardless if the completion is of phase Failure or Success - -## @param {RunStrategy} runStrategy - Requested running state of the VirtualMachineInstance -runStrategy: Always - -## @param {string} instanceType - Virtual Machine instance type. -## @param {string} instanceProfile - Virtual Machine preferences profile. -instanceType: "u1.medium" -instanceProfile: ubuntu - -## @param {SystemDisk} systemDisk - System disk configuration. -systemDisk: - image: ubuntu - storage: 5Gi - storageClass: replicated - -## @param {[]Subnet} subnets - Additional subnets -subnets: [] -## Example: -## subnets: -## - name: subnet-84dbec17 -## - name: subnet-aa8896b5 -## - name: subnet-e9b97196 - -## @param {[]GPU} gpus - List of GPUs to attach. -gpus: [] -## Example: -## gpus: -## - name: nvidia.com/GA102GL_A10 - -## @param {string} cpuModel - Model specifies the CPU model inside the VMI. List of available models https://github.com/libvirt/libvirt/tree/master/src/cpu_map -cpuModel: "" - -## @param {Resources} [resources] - Resource configuration for the virtual machine. -resources: {} -## Example: -## resources: -## cpu: "4" -## sockets: "1" -## memory: "8Gi" - -## @param {[]string} sshKeys - List of SSH public keys for authentication. -sshKeys: [] -## Example: -## sshKeys: -## - ssh-rsa ... -## - ssh-ed25519 ... -## - -## @param {string} cloudInit - Cloud-init user data. -cloudInit: "" -## Example: -## cloudInit: | -## #cloud-config -## password: ubuntu -## chpasswd: { expire: False } -## - -## @param {string} cloudInitSeed - Seed string to generate SMBIOS UUID for the VM. -cloudInitSeed: "" -## Example: -## cloudInitSeed: "upd1" diff --git a/packages/core/platform/sources/virtual-machine-application.yaml b/packages/core/platform/sources/virtual-machine-application.yaml deleted file mode 100644 index b2583e7e..00000000 --- a/packages/core/platform/sources/virtual-machine-application.yaml +++ /dev/null @@ -1,29 +0,0 @@ ---- -apiVersion: cozystack.io/v1alpha1 -kind: PackageSource -metadata: - name: cozystack.virtual-machine-application -spec: - sourceRef: - kind: OCIRepository - name: cozystack-packages - namespace: cozy-system - path: / - variants: - - name: kubevirt - dependsOn: - - cozystack.networking - - cozystack.kubevirt - - cozystack.kubevirt-cdi - libraries: - - name: cozy-lib - path: library/cozy-lib - components: - - name: virtual-machine - path: apps/virtual-machine - libraries: [cozy-lib] - - name: virtual-machine-rd - path: system/virtual-machine-rd - install: - namespace: cozy-system - releaseName: virtual-machine-rd diff --git a/packages/core/platform/templates/bundles/iaas.yaml b/packages/core/platform/templates/bundles/iaas.yaml index 7c856b8c..41dc5181 100644 --- a/packages/core/platform/templates/bundles/iaas.yaml +++ b/packages/core/platform/templates/bundles/iaas.yaml @@ -13,7 +13,6 @@ {{include "cozystack.platform.package.default" (list "cozystack.capi-provider-infra-kubevirt" $) }} {{include "cozystack.platform.package.default" (list "cozystack.bucket-application" $) }} {{include "cozystack.platform.package" (list "cozystack.kubernetes-application" "kubevirt" $) }} -{{include "cozystack.platform.package" (list "cozystack.virtual-machine-application" "kubevirt" $) }} {{include "cozystack.platform.package" (list "cozystack.virtualprivatecloud-application" "kubevirt" $) }} {{include "cozystack.platform.package" (list "cozystack.vm-disk-application" "kubevirt" $) }} {{include "cozystack.platform.package" (list "cozystack.vm-instance-application" "kubevirt" $) }} diff --git a/packages/system/virtual-machine-rd/Chart.yaml b/packages/system/virtual-machine-rd/Chart.yaml deleted file mode 100644 index 6228a221..00000000 --- a/packages/system/virtual-machine-rd/Chart.yaml +++ /dev/null @@ -1,3 +0,0 @@ -apiVersion: v2 -name: virtual-machine-rd -version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process diff --git a/packages/system/virtual-machine-rd/Makefile b/packages/system/virtual-machine-rd/Makefile deleted file mode 100644 index 79142c9b..00000000 --- a/packages/system/virtual-machine-rd/Makefile +++ /dev/null @@ -1,4 +0,0 @@ -export NAME=virtual-machine-rd -export NAMESPACE=cozy-system - -include ../../../hack/package.mk diff --git a/packages/system/virtual-machine-rd/cozyrds/virtual-machine.yaml b/packages/system/virtual-machine-rd/cozyrds/virtual-machine.yaml deleted file mode 100644 index 6b52340b..00000000 --- a/packages/system/virtual-machine-rd/cozyrds/virtual-machine.yaml +++ /dev/null @@ -1,37 +0,0 @@ -apiVersion: cozystack.io/v1alpha1 -kind: ApplicationDefinition -metadata: - name: virtual-machine -spec: - application: - kind: VirtualMachine - plural: virtualmachines - singular: virtualmachine - openAPISchema: |- - {"title":"Chart Values","type":"object","properties":{"cloudInit":{"description":"Cloud-init user data.","type":"string","default":""},"cloudInitSeed":{"description":"Seed string to generate SMBIOS UUID for the VM.","type":"string","default":""},"cpuModel":{"description":"Model specifies the CPU model inside the VMI. List of available models https://github.com/libvirt/libvirt/tree/master/src/cpu_map","type":"string","default":""},"external":{"description":"Enable external access from outside the cluster.","type":"boolean","default":false},"externalMethod":{"description":"Method to pass through traffic to the VM.","type":"string","default":"PortList","enum":["PortList","WholeIP"]},"externalPorts":{"description":"Ports to forward from outside the cluster.","type":"array","default":[22],"items":{"type":"integer"}},"gpus":{"description":"List of GPUs to attach.","type":"array","default":[],"items":{"type":"object","required":["name"],"properties":{"name":{"description":"The name of the GPU resource to attach.","type":"string"}}}},"instanceProfile":{"description":"Virtual Machine preferences profile.","type":"string","default":"ubuntu","enum":["alpine","centos.7","centos.7.desktop","centos.stream10","centos.stream10.desktop","centos.stream8","centos.stream8.desktop","centos.stream8.dpdk","centos.stream9","centos.stream9.desktop","centos.stream9.dpdk","cirros","fedora","fedora.arm64","opensuse.leap","opensuse.tumbleweed","rhel.10","rhel.10.arm64","rhel.7","rhel.7.desktop","rhel.8","rhel.8.desktop","rhel.8.dpdk","rhel.9","rhel.9.arm64","rhel.9.desktop","rhel.9.dpdk","rhel.9.realtime","sles","ubuntu","windows.10","windows.10.virtio","windows.11","windows.11.virtio","windows.2k16","windows.2k16.virtio","windows.2k19","windows.2k19.virtio","windows.2k22","windows.2k22.virtio","windows.2k25","windows.2k25.virtio",""]},"instanceType":{"description":"Virtual Machine instance type.","type":"string","default":"u1.medium"},"resources":{"description":"Resource configuration for the virtual machine.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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},"sockets":{"description":"Number of CPU sockets (vCPU topology).","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}}},"runStrategy":{"description":"Requested running state of the VirtualMachineInstance","type":"string","default":"Always","enum":["Always","Halted","Manual","RerunOnFailure","Once"]},"sshKeys":{"description":"List of SSH public keys for authentication.","type":"array","default":[],"items":{"type":"string"}},"subnets":{"description":"Additional subnets","type":"array","default":[],"items":{"type":"object","properties":{"name":{"description":"Subnet name","type":"string"}}}},"systemDisk":{"description":"System disk configuration.","type":"object","default":{},"required":["image","storage"],"properties":{"image":{"description":"The base image for the virtual machine.","type":"string","default":"ubuntu","enum":["ubuntu","cirros","alpine","fedora","talos"]},"storage":{"description":"The size of the disk allocated for the virtual machine.","type":"string","default":"5Gi"},"storageClass":{"description":"StorageClass used to store the data.","type":"string","default":"replicated"}}}}} - release: - prefix: virtual-machine- - labels: - sharding.fluxcd.io/key: tenants - chartRef: - kind: ExternalArtifact - name: cozystack-virtual-machine-application-kubevirt-virtual-machine - namespace: cozy-system - dashboard: - category: IaaS - singular: Virtual Machine - plural: Virtual Machines - description: Virtual Machine (simple) - weight: 10 - tags: - - compute - icon: PHN2ZyB3aWR0aD0iMTQ0IiBoZWlnaHQ9IjE0NCIgdmlld0JveD0iMCAwIDE0NCAxNDQiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIxNDQiIGhlaWdodD0iMTQ0IiByeD0iMjQiIGZpbGw9InVybCgjcGFpbnQwX2xpbmVhcl82ODdfMzQ1NCkiLz4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzY4N18zNDU0KSI+CjxwYXRoIGQ9Ik04OS41MDM5IDExMS43MDdINTQuNDk3QzU0LjE3MjcgMTExLjcwNyA1NC4wMTA4IDExMS4yMjEgNTQuMzM0OSAxMTEuMDU5TDU3LjI1MjIgMTA4Ljk1MkM2MC4zMzE0IDEwNi42ODMgNjEuOTUyMiAxMDIuNjMxIDYwLjk3OTcgOTguNzQxMkg4My4wMjFDODIuMDQ4NSAxMDIuNjMxIDgzLjY2OTMgMTA2LjY4MyA4Ni43NDg1IDEwOC45NTJMODkuNjY1OCAxMTEuMDU5Qzg5Ljk5IDExMS4yMjEgODkuODI3OSAxMTEuNzA3IDg5LjUwMzkgMTExLjcwN1oiIGZpbGw9IiNCMEI2QkIiLz4KPHBhdGggZD0iTTExMy4zMjggOTguNzQxSDMwLjY3MjVDMjcuNTkzMSA5OC43NDEgMjUgOTYuMTQ4IDI1IDkzLjA2ODdWMzMuMTAzMkMyNSAzMC4wMjM5IDI3LjU5MzEgMjcuNDMwNyAzMC42NzI1IDI3LjQzMDdIMTEzLjMyOEMxMTYuNDA3IDI3LjQzMDcgMTE5IDMwLjAyMzcgMTE5IDMzLjEwMzJWOTMuMDY4N0MxMTkgOTYuMTQ4IDExNi40MDcgOTguNzQxIDExMy4zMjggOTguNzQxWiIgZmlsbD0iI0U4RURFRSIvPgo8cGF0aCBkPSJNMTE5IDg0LjE1NDlIMjVWMzMuMTAzMkMyNSAzMC4wMjM5IDI3LjU5MzEgMjcuNDMwNyAzMC42NzI1IDI3LjQzMDdIMTEzLjMyOEMxMTYuNDA3IDI3LjQzMDcgMTE5IDMwLjAyMzcgMTE5IDMzLjEwMzJMMTE5IDg0LjE1NDlaIiBmaWxsPSIjMDBCM0ZGIi8+CjxwYXRoIGQ9Ik05MC42Mzc0IDExNi41NjlINTMuMzYxNkM1Mi4wNjUxIDExNi41NjkgNTAuOTMwNyAxMTUuNDM1IDUwLjkzMDcgMTE0LjEzOEM1MC45MzA3IDExMi44NDEgNTIuMDY1MSAxMTEuNzA3IDUzLjM2MTYgMTExLjcwN0g5MC42Mzc0QzkxLjkzMzkgMTExLjcwNyA5My4wNjg0IDExMi44NDEgOTMuMDY4NCAxMTQuMTM4QzkzLjA2ODQgMTE1LjQzNSA5MS45MzM5IDExNi41NjkgOTAuNjM3NCAxMTYuNTY5WiIgZmlsbD0iI0U4RURFRSIvPgo8L2c+CjxwYXRoIGQ9Ik03Mi41Mjc1IDUzLjgzNjdDNzIuNDQzMSA1My44MzUxIDcyLjM2MDUgNTMuODEyMiA3Mi4yODczIDUzLjc3MDFMNTYuNDY5OSA0NC43OTM0QzU2LjM5ODMgNDQuNzUxOSA1Ni4zMzg4IDQ0LjY5MjMgNTYuMjk3MyA0NC42MjA3QzU2LjI1NTkgNDQuNTQ5IDU2LjIzMzggNDQuNDY3OCA1Ni4yMzM0IDQ0LjM4NUM1Ni4yMzM0IDQ0LjIxNjkgNTYuMzI1OCA0NC4wNjE3IDU2LjQ2OTkgNDMuOTc4NUw3Mi4xOTEyIDM1LjA2MDlDNzIuMjYzNyAzNS4wMjEgNzIuMzQ1IDM1IDcyLjQyNzcgMzVDNzIuNTEwNSAzNSA3Mi41OTE4IDM1LjAyMSA3Mi42NjQzIDM1LjA2MDlMODguNDg3MiA0NC4wMzk1Qzg4LjU1OTEgNDQuMDgwMSA4OC42MTg4IDQ0LjEzOTIgODguNjYgNDQuMjEwN0M4OC43MDEzIDQ0LjI4MjIgODguNzIyNyA0NC4zNjM1IDg4LjcyMTkgNDQuNDQ2Qzg4LjcyMjUgNDQuNTI4NSA4OC43MDEgNDQuNjA5NyA4OC42NTk4IDQ0LjY4MTJDODguNjE4NSA0NC43NTI2IDg4LjU1ODkgNDQuODExOCA4OC40ODcyIDQ0Ljg1MjVMNzIuNzcxNCA1My43NjgzQzcyLjY5NzIgNTMuODExNCA3Mi42MTMzIDUzLjgzNDkgNzIuNTI3NSA1My44MzY3IiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBvcGFjaXR5PSIwLjciIGQ9Ik03MC4yNTUzIDc1LjY1MTdDNzAuMTcxIDc1LjY1MzUgNzAuMDg3OCA3NS42MzE3IDcwLjAxNTEgNzUuNTg4OEw1NC4yNDU4IDY2LjY0MTdDNTQuMTcxNSA2Ni42MDI0IDU0LjEwOTUgNjYuNTQzNiA1NC4wNjYxIDY2LjQ3MTZDNTQuMDIyOCA2Ni4zOTk3IDU0IDY2LjMxNzMgNTQgNjYuMjMzM1Y0OC4yNzhDNTQgNDguMTA4IDU0LjA5MjQgNDcuOTU0NiA1NC4yNDM5IDQ3Ljg2OTZDNTQuMzE3MiA0Ny44MjcxIDU0LjQwMDQgNDcuODA0NyA1NC40ODUxIDQ3LjgwNDdDNTQuNTY5NyA0Ny44MDQ3IDU0LjY1MjkgNDcuODI3MSA1NC43MjYyIDQ3Ljg2OTZMNzAuNDkzNyA1Ni44MTMxQzcwLjU2NDIgNTYuODU2NSA3MC42MjI1IDU2LjkxNyA3MC42NjMyIDU2Ljk4OTFDNzAuNzAzOSA1Ny4wNjEyIDcwLjcyNTcgNTcuMTQyNCA3MC43MjY1IDU3LjIyNTFWNzUuMTgwNUM3MC43MjU5IDc1LjI2MjggNzAuNzA0MiA3NS4zNDM2IDcwLjY2MzUgNzUuNDE1MUM3MC42MjI3IDc1LjQ4NjYgNzAuNTY0MiA3NS41NDY0IDcwLjQ5MzcgNzUuNTg4OEM3MC40MjA2IDc1LjYyOTEgNzAuMzM4NyA3NS42NTA3IDcwLjI1NTMgNzUuNjUxNyIgZmlsbD0id2hpdGUiLz4KPHBhdGggb3BhY2l0eT0iMC40IiBkPSJNNzQuNzE5OCA3NS42NTExQzc0LjYzMzMgNzUuNjUxMiA3NC41NDgyIDc1LjYyOTYgNzQuNDcyMiA3NS41ODgzQzc0LjQwMTYgNzUuNTQ2MSA3NC4zNDMyIDc1LjQ4NjIgNzQuMzAyNyA3NS40MTQ3Qzc0LjI2MjMgNzUuMzQzMSA3NC4yNDExIDc1LjI2MjIgNzQuMjQxMiA3NS4xOFY1Ny4zMzczQzc0LjI0MTIgNTcuMTcxIDc0LjMzMzYgNTcuMDE1OCA3NC40NzIyIDU2LjkyOUw5MC4yMzk3IDQ3Ljk4NTVDOTAuMzExOSA0Ny45NDM4IDkwLjM5MzggNDcuOTIxOSA5MC40NzcxIDQ3LjkyMTlDOTAuNTYwNSA0Ny45MjE5IDkwLjY0MjQgNDcuOTQzOCA5MC43MTQ2IDQ3Ljk4NTVDOTAuNzg3NiA0OC4wMjU1IDkwLjg0ODUgNDguMDg0MiA5MC44OTExIDQ4LjE1NTdDOTAuOTMzNyA0OC4yMjcyIDkwLjk1NjMgNDguMzA4OCA5MC45NTY2IDQ4LjM5MlY2Ni4yMzI4QzkwLjk1NyA2Ni4zMTY0IDkwLjkzNDcgNjYuMzk4NSA5MC44OTIxIDY2LjQ3MDRDOTAuODQ5NSA2Ni41NDI0IDkwLjc4ODEgNjYuNjAxNCA5MC43MTQ2IDY2LjY0MTFMNzQuOTUyNiA3NS41ODgzQzc0Ljg4MjUgNzUuNjMwNyA3NC44MDE4IDc1LjY1MjUgNzQuNzE5OCA3NS42NTExIiBmaWxsPSJ3aGl0ZSIvPgo8ZGVmcz4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDBfbGluZWFyXzY4N18zNDU0IiB4MT0iMTYxIiB5MT0iMTgwIiB4Mj0iMy41OTI4NGUtMDciIHkyPSI0Ljk5OTk4IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjNTk1NjU2Ii8+CjwvbGluZWFyR3JhZGllbnQ+CjxjbGlwUGF0aCBpZD0iY2xpcDBfNjg3XzM0NTQiPgo8cmVjdCB3aWR0aD0iOTQiIGhlaWdodD0iOTQiIGZpbGw9IndoaXRlIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyNSAyNSkiLz4KPC9jbGlwUGF0aD4KPC9kZWZzPgo8L3N2Zz4K - keysOrder: [["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata", "name"], ["spec", "external"], ["spec", "externalMethod"], ["spec", "externalPorts"], ["spec", "runStrategy"], ["spec", "instanceType"], ["spec", "instanceProfile"], ["spec", "systemDisk"], ["spec", "systemDisk", "image"], ["spec", "systemDisk", "storage"], ["spec", "systemDisk", "storageClass"], ["spec", "subnets"], ["spec", "gpus"], ["spec", "cpuModel"], ["spec", "resources"], ["spec", "sshKeys"], ["spec", "cloudInit"], ["spec", "cloudInitSeed"]] - secrets: - exclude: [] - include: [] - services: - exclude: [] - include: - - resourceNames: - - virtual-machine-{{ .name }} diff --git a/packages/system/virtual-machine-rd/templates/backupstrategy.yaml b/packages/system/virtual-machine-rd/templates/backupstrategy.yaml deleted file mode 100644 index ac2fe517..00000000 --- a/packages/system/virtual-machine-rd/templates/backupstrategy.yaml +++ /dev/null @@ -1,35 +0,0 @@ -{{- if .Capabilities.APIVersions.Has "strategy.backups.cozystack.io/v1alpha1" }} -apiVersion: strategy.backups.cozystack.io/v1alpha1 -kind: Velero -metadata: - name: velero-backup-strategy-for-virtualmachines -spec: - template: - spec: - csiSnapshotTimeout: 10m0s - includedNamespaces: - - '{{ printf "{{ .Application.metadata.namespace }}" }}' - orLabelSelector: - - matchLabels: - apps.cozystack.io/application.group: apps.cozystack.io - apps.cozystack.io/application.kind: '{{ printf "{{ .Application.kind }}" }}' - apps.cozystack.io/application.name: '{{ printf "{{ .Application.metadata.name }}"}}' - - matchLabels: - app.kubernetes.io/instance: 'virtual-machine-{{ printf "{{ .Application.metadata.name }}"}}' - includedResources: - - helmreleases.helm.toolkit.fluxcd.io - - virtualmachines.kubevirt.io - - virtualmachineinstances.kubevirt.io - - datavolumes.cdi.kubevirt.io - - persistentvolumeclaims - - services - - configmaps - - secrets - storageLocation: '{{ printf "{{ .Parameters.backupStorageLocationName }}" }}' - volumeSnapshotLocations: - - '{{ printf "{{ .Parameters.backupStorageLocationName }}" }}' - snapshotVolumes: true - snapshotMoveData: true - ttl: 720h0m0s - itemOperationTimeout: 24h0m0s -{{- end }} diff --git a/packages/system/virtual-machine-rd/templates/cozyrd.yaml b/packages/system/virtual-machine-rd/templates/cozyrd.yaml deleted file mode 100644 index e079ddcf..00000000 --- a/packages/system/virtual-machine-rd/templates/cozyrd.yaml +++ /dev/null @@ -1,4 +0,0 @@ -{{- range $path, $_ := .Files.Glob "cozyrds/*" }} ---- -{{ $.Files.Get $path }} -{{- end }} diff --git a/packages/system/virtual-machine-rd/values.yaml b/packages/system/virtual-machine-rd/values.yaml deleted file mode 100644 index 0967ef42..00000000 --- a/packages/system/virtual-machine-rd/values.yaml +++ /dev/null @@ -1 +0,0 @@ -{} From 13aa341a28403bab0a3e8ee0a8665d17f0f91554 Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Thu, 12 Feb 2026 15:26:18 +0100 Subject: [PATCH 040/666] fix(platform): address review comments in vm migration script Replace `|| echo ""` with `|| true` to avoid newline bugs in variable assignments. Switch `for x in $(cmd)` loops to `while read` for safer iteration over kubectl output. Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- hack/e2e-apps/virtualmachine.bats | 45 ------------------- .../platform/images/migrations/migrations/29 | 18 ++++---- 2 files changed, 10 insertions(+), 53 deletions(-) delete mode 100644 hack/e2e-apps/virtualmachine.bats diff --git a/hack/e2e-apps/virtualmachine.bats b/hack/e2e-apps/virtualmachine.bats deleted file mode 100644 index 33e87b53..00000000 --- a/hack/e2e-apps/virtualmachine.bats +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env bats - -@test "Create a Virtual Machine" { - name='test' - kubectl apply -f - </dev/null | base64 -d 2>/dev/null || echo "") + VALUES_YAML=$(kubectl -n "$NAMESPACE" get secret "$VALUES_SECRET" -o jsonpath="{.data.${VALUES_KEY}}" 2>/dev/null | base64 -d 2>/dev/null || true) if [ -z "$VALUES_YAML" ]; then - VALUES_YAML=$(kubectl -n "$NAMESPACE" get secret "$VALUES_SECRET" -o jsonpath="{.stringData.${VALUES_KEY}}" 2>/dev/null || echo "") + VALUES_YAML=$(kubectl -n "$NAMESPACE" get secret "$VALUES_SECRET" -o jsonpath="{.stringData.${VALUES_KEY}}" 2>/dev/null || true) fi if [ -n "$VALUES_YAML" ]; then EXTERNAL=$(echo "$VALUES_YAML" | yq -r '.external // false') @@ -194,7 +194,7 @@ for entry in "${INSTANCES[@]}"; do echo " --- Save LoadBalancer IP ---" LB_IP="" if [ "$EXTERNAL" = "true" ] && resource_exists "$NAMESPACE" "svc" "$OLD_NAME"; then - LB_IP=$(kubectl -n "$NAMESPACE" get svc "$OLD_NAME" -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || echo "") + LB_IP=$(kubectl -n "$NAMESPACE" get svc "$OLD_NAME" -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || true) if [ -n "$LB_IP" ]; then echo " LoadBalancer IP: ${LB_IP}" else @@ -314,8 +314,9 @@ PVCEOF # --- 2i: Clone Secrets --- echo " --- Clone Secrets ---" - for secret in $(kubectl -n "$NAMESPACE" get secret -o name 2>/dev/null \ - | grep "secret/${OLD_NAME}" | grep -v "sh.helm.release" | grep -v "values"); do + kubectl -n "$NAMESPACE" get secret -o name 2>/dev/null \ + | grep "secret/${OLD_NAME}" | grep -v "sh.helm.release" | grep -v "values" \ + | while IFS= read -r secret; do old_secret_name="${secret#secret/}" suffix="${old_secret_name#${OLD_NAME}}" new_secret_name="${NEW_INSTANCE_NAME}${suffix}" @@ -540,8 +541,9 @@ SVCEOF # --- 2q: Delete old resources --- echo " --- Delete old resources ---" - for secret in $(kubectl -n "$NAMESPACE" get secret -o name 2>/dev/null \ - | grep "secret/${OLD_NAME}" | grep -v "sh.helm.release" | grep -v "values"); do + kubectl -n "$NAMESPACE" get secret -o name 2>/dev/null \ + | grep "secret/${OLD_NAME}" | grep -v "sh.helm.release" | grep -v "values" \ + | while IFS= read -r secret; do old_secret_name="${secret#secret/}" delete_resource "$NAMESPACE" "secret" "$old_secret_name" done @@ -714,7 +716,7 @@ for entry in "${INSTANCES[@]}"; do instance="${entry#*/}" disk_name="${NEW_DISK_PREFIX}-${instance}" for i in $(seq 1 60); do - ready=$(kubectl -n "$ns" get hr "$disk_name" -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null || echo "") + ready=$(kubectl -n "$ns" get hr "$disk_name" -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null || true) if [ "$ready" = "True" ]; then echo " [OK] ${ns}/hr/${disk_name} is Ready" break From dbba5c325b662f8aa2821f1984f1c251243d9b68 Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Sat, 14 Feb 2026 10:13:09 +0100 Subject: [PATCH 041/666] fix kubernetes e2e test Signed-off-by: Andrei Kvapil --- hack/e2e-apps/run-kubernetes.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hack/e2e-apps/run-kubernetes.sh b/hack/e2e-apps/run-kubernetes.sh index 5b8b1b4b..c60fdbe4 100644 --- a/hack/e2e-apps/run-kubernetes.sh +++ b/hack/e2e-apps/run-kubernetes.sh @@ -56,7 +56,7 @@ spec: gpus: [] instanceType: u1.medium maxReplicas: 10 - minReplicas: 0 + minReplicas: 2 roles: - ingress-nginx storageClass: replicated From b6a840e8735ee2b4f039bd4d263149e8ca92fcf0 Mon Sep 17 00:00:00 2001 From: cozystack-bot <217169706+cozystack-bot@users.noreply.github.com> Date: Sat, 14 Feb 2026 09:17:24 +0000 Subject: [PATCH 042/666] Prepare release v1.0.0-beta.4 Signed-off-by: cozystack-bot <217169706+cozystack-bot@users.noreply.github.com> --- packages/apps/kubernetes/images/cluster-autoscaler.tag | 2 +- packages/apps/kubernetes/images/kubevirt-csi-driver.tag | 2 +- packages/core/installer/values.yaml | 4 ++-- packages/core/platform/values.yaml | 2 +- packages/core/testing/values.yaml | 2 +- packages/extra/bootbox/images/matchbox.tag | 2 +- packages/extra/seaweedfs/images/objectstorage-sidecar.tag | 2 +- packages/system/backup-controller/values.yaml | 2 +- packages/system/backupstrategy-controller/values.yaml | 2 +- packages/system/cozystack-api/values.yaml | 2 +- packages/system/cozystack-controller/values.yaml | 2 +- packages/system/dashboard/templates/configmap.yaml | 2 +- packages/system/dashboard/values.yaml | 6 +++--- .../system/grafana-operator/images/grafana-dashboards.tag | 2 +- packages/system/kamaji/values.yaml | 4 ++-- packages/system/kubeovn-plunger/values.yaml | 2 +- packages/system/kubeovn-webhook/values.yaml | 2 +- packages/system/kubevirt-csi-node/values.yaml | 2 +- packages/system/lineage-controller-webhook/values.yaml | 2 +- packages/system/objectstorage-controller/values.yaml | 2 +- packages/system/seaweedfs/values.yaml | 2 +- 21 files changed, 25 insertions(+), 25 deletions(-) diff --git a/packages/apps/kubernetes/images/cluster-autoscaler.tag b/packages/apps/kubernetes/images/cluster-autoscaler.tag index e3436c68..c3d6ddef 100644 --- a/packages/apps/kubernetes/images/cluster-autoscaler.tag +++ b/packages/apps/kubernetes/images/cluster-autoscaler.tag @@ -1 +1 @@ -ghcr.io/cozystack/cozystack/cluster-autoscaler:0.0.0@sha256:598331326f0c2aac420187f0cc3a49fedcb22ed5de4afe50c6ccf8e05d9fa537 +ghcr.io/cozystack/cozystack/cluster-autoscaler:0.0.0@sha256:7deeee117e7eec599cb453836ca95eadd131dfc8c875dc457ef29dc1433395e0 diff --git a/packages/apps/kubernetes/images/kubevirt-csi-driver.tag b/packages/apps/kubernetes/images/kubevirt-csi-driver.tag index ca67ba13..a8e48958 100644 --- a/packages/apps/kubernetes/images/kubevirt-csi-driver.tag +++ b/packages/apps/kubernetes/images/kubevirt-csi-driver.tag @@ -1 +1 @@ -ghcr.io/cozystack/cozystack/kubevirt-csi-driver:latest@sha256:cd58760c97ba50ef74ff940cdcda4b9a6d8554eec8305fc96c2e8bbea72b75a9 +ghcr.io/cozystack/cozystack/kubevirt-csi-driver:0.0.0@sha256:1997d623bb60a5b540027a4e0716e7ca84f668ee09f31290e9c43324f0003137 diff --git a/packages/core/installer/values.yaml b/packages/core/installer/values.yaml index e9de3f13..64ff817b 100644 --- a/packages/core/installer/values.yaml +++ b/packages/core/installer/values.yaml @@ -1,6 +1,6 @@ cozystackOperator: # Deployment variant: talos, generic, hosted variant: talos - image: ghcr.io/cozystack/cozystack/cozystack-operator:v1.0.0-beta.3@sha256:3cacecfc318e8314300bba8538b475fb8d50b2bf3106533faa8d942e4ed14448 + image: ghcr.io/cozystack/cozystack/cozystack-operator:v1.0.0-beta.4@sha256:322dd7358df369525f76e6e43512482e38caec5315d36a878399d2d60bf2f18d platformSourceUrl: 'oci://ghcr.io/cozystack/cozystack/cozystack-packages' - platformSourceRef: 'digest=sha256:e5ddcf5a02d17b17fc7ffa8c87ddeaa25a01d928a63fb297f6b25b669bb6b7c7' + platformSourceRef: 'digest=sha256:b88502242b535a31ab33c06ffc0a96d1c67230d2db2c5e873fa23f6523592ff6' diff --git a/packages/core/platform/values.yaml b/packages/core/platform/values.yaml index 38452a17..b90c001b 100644 --- a/packages/core/platform/values.yaml +++ b/packages/core/platform/values.yaml @@ -5,7 +5,7 @@ sourceRef: path: / migrations: enabled: false - image: ghcr.io/cozystack/cozystack/platform-migrations:latest + image: ghcr.io/cozystack/cozystack/platform-migrations:v1.0.0-beta.4@sha256:f404d05834907b9b2695bbb37732bd1ae2e79b03da77dc91bb3fbc0fbc53dcbf targetVersion: 30 # Bundle deployment configuration bundles: diff --git a/packages/core/testing/values.yaml b/packages/core/testing/values.yaml index 26ac2e55..73481ab1 100644 --- a/packages/core/testing/values.yaml +++ b/packages/core/testing/values.yaml @@ -1,2 +1,2 @@ e2e: - image: ghcr.io/cozystack/cozystack/e2e-sandbox:v1.0.0-beta.3@sha256:eac71ef0de3450fce96255629e77903630c63ade62b81e7055f1a689f92ee153 + image: ghcr.io/cozystack/cozystack/e2e-sandbox:v1.0.0-beta.4@sha256:eac71ef0de3450fce96255629e77903630c63ade62b81e7055f1a689f92ee153 diff --git a/packages/extra/bootbox/images/matchbox.tag b/packages/extra/bootbox/images/matchbox.tag index 133eda2a..6c3054d5 100644 --- a/packages/extra/bootbox/images/matchbox.tag +++ b/packages/extra/bootbox/images/matchbox.tag @@ -1 +1 @@ -ghcr.io/cozystack/cozystack/matchbox:v1.0.0-beta.3@sha256:185010911694809fe15491efe3c7994e0ce46a4f92ef1f7edbbd0b318861e8d4 +ghcr.io/cozystack/cozystack/matchbox:v1.0.0-beta.4@sha256:cba9a2761c40ae6645e28da4cbc6d91eb86ddc886f4523cf20b3c0e40597d593 diff --git a/packages/extra/seaweedfs/images/objectstorage-sidecar.tag b/packages/extra/seaweedfs/images/objectstorage-sidecar.tag index 71eb27b8..bd622108 100644 --- a/packages/extra/seaweedfs/images/objectstorage-sidecar.tag +++ b/packages/extra/seaweedfs/images/objectstorage-sidecar.tag @@ -1 +1 @@ -ghcr.io/cozystack/cozystack/objectstorage-sidecar:v1.0.0-beta.3@sha256:f845a2c3ec0f79a8873e9f3e65ee625ec3574c5e33e174a7bf60b15d1db49979 +ghcr.io/cozystack/cozystack/objectstorage-sidecar:v1.0.0-beta.4@sha256:235b194a531b70e266a10ef78d2955d19f5b659513f23d8b3cfbbc0dff7fc1c0 diff --git a/packages/system/backup-controller/values.yaml b/packages/system/backup-controller/values.yaml index 3e91ea6e..1bdab88b 100644 --- a/packages/system/backup-controller/values.yaml +++ b/packages/system/backup-controller/values.yaml @@ -1,5 +1,5 @@ backupController: - image: "ghcr.io/cozystack/cozystack/backup-controller:v1.0.0-beta.3@sha256:7a8d0ea7b380196fac418e0697b4218af15025eac8128dfedfd12eb933bd258d" + image: "ghcr.io/cozystack/cozystack/backup-controller:v1.0.0-beta.4@sha256:2dcd5347683ee88012b0e558b3a240dec9942230e9c673a359e0196f407a0833" replicas: 2 debug: false metrics: diff --git a/packages/system/backupstrategy-controller/values.yaml b/packages/system/backupstrategy-controller/values.yaml index 94cc6bed..21d1a9f3 100644 --- a/packages/system/backupstrategy-controller/values.yaml +++ b/packages/system/backupstrategy-controller/values.yaml @@ -1,5 +1,5 @@ backupStrategyController: - image: "ghcr.io/cozystack/cozystack/backupstrategy-controller:v1.0.0-beta.3@sha256:ae8fb6c7bf274a3812a6a9f3f87317fa4575250f7ee75e118d20d5ab0797577b" + image: "ghcr.io/cozystack/cozystack/backupstrategy-controller:v1.0.0-beta.4@sha256:c2c150918e5609f1d6663f56b6dcbdac47bee33154524230001fcd04165f4268" replicas: 2 debug: false metrics: diff --git a/packages/system/cozystack-api/values.yaml b/packages/system/cozystack-api/values.yaml index 769cd4b6..d9041546 100644 --- a/packages/system/cozystack-api/values.yaml +++ b/packages/system/cozystack-api/values.yaml @@ -1,3 +1,3 @@ cozystackAPI: - image: ghcr.io/cozystack/cozystack/cozystack-api:v1.0.0-beta.3@sha256:a263702859c36f85d097c300c1be462aaa45a3955d69e9ae72a37cabf6dadaa9 + image: ghcr.io/cozystack/cozystack/cozystack-api:v1.0.0-beta.4@sha256:42f8d8120f7fd3bfe37f114eedf5ca8df62ac69a0d922d91a2c392772f3b46ee replicas: 2 diff --git a/packages/system/cozystack-controller/values.yaml b/packages/system/cozystack-controller/values.yaml index 908a955d..53ed1033 100644 --- a/packages/system/cozystack-controller/values.yaml +++ b/packages/system/cozystack-controller/values.yaml @@ -1,4 +1,4 @@ cozystackController: - image: ghcr.io/cozystack/cozystack/cozystack-controller:v1.0.0-beta.3@sha256:00b0ba15fef7c3ce8c0801ecb11b1953665a511990e18f51c2ca3ca40127c7aa + image: ghcr.io/cozystack/cozystack/cozystack-controller:v1.0.0-beta.4@sha256:915d07ef61e1fc3bdf87e4bfc4b8ae3920e7e33d74082778c7735ba36a08cdef debug: false disableTelemetry: false diff --git a/packages/system/dashboard/templates/configmap.yaml b/packages/system/dashboard/templates/configmap.yaml index 03cd572b..1c79b2cb 100644 --- a/packages/system/dashboard/templates/configmap.yaml +++ b/packages/system/dashboard/templates/configmap.yaml @@ -1,6 +1,6 @@ {{- $brandingConfig := .Values._cluster.branding | default dict }} -{{- $tenantText := "v1.0.0-beta.3" }} +{{- $tenantText := "v1.0.0-beta.4" }} {{- $footerText := "Cozystack" }} {{- $titleText := "Cozystack Dashboard" }} {{- $logoText := "" }} diff --git a/packages/system/dashboard/values.yaml b/packages/system/dashboard/values.yaml index 95d7bdf8..0c898a91 100644 --- a/packages/system/dashboard/values.yaml +++ b/packages/system/dashboard/values.yaml @@ -1,6 +1,6 @@ openapiUI: - image: ghcr.io/cozystack/cozystack/openapi-ui:v1.0.0-beta.3@sha256:adbd07c7bde083fbf3a2fb2625ec4adbe6718a0f6f643b2505cc0029c2c0f724 + image: ghcr.io/cozystack/cozystack/openapi-ui:v1.0.0-beta.4@sha256:adbd07c7bde083fbf3a2fb2625ec4adbe6718a0f6f643b2505cc0029c2c0f724 openapiUIK8sBff: - image: ghcr.io/cozystack/cozystack/openapi-ui-k8s-bff:v1.0.0-beta.3@sha256:0ed112d44973dcff64b65f7f1438a35df98c7fdee4590343a790668f728d9024 + image: ghcr.io/cozystack/cozystack/openapi-ui-k8s-bff:v1.0.0-beta.4@sha256:1f7827a1978bd9c81ac924dd0e78f6a3ce834a9a64af55047e220812bc15a944 tokenProxy: - image: ghcr.io/cozystack/cozystack/token-proxy:v1.0.0-beta.3@sha256:2e280991e07853ea48f97b0a42946afffa10d03d6a83d41099ed83e6ffc94fdc + image: ghcr.io/cozystack/cozystack/token-proxy:v1.0.0-beta.4@sha256:2e280991e07853ea48f97b0a42946afffa10d03d6a83d41099ed83e6ffc94fdc diff --git a/packages/system/grafana-operator/images/grafana-dashboards.tag b/packages/system/grafana-operator/images/grafana-dashboards.tag index 726d78e9..cc81b24f 100644 --- a/packages/system/grafana-operator/images/grafana-dashboards.tag +++ b/packages/system/grafana-operator/images/grafana-dashboards.tag @@ -1 +1 @@ -ghcr.io/cozystack/cozystack/grafana-dashboards:v1.0.0-beta.3@sha256:e866b5b3874b9d390b341183d2ee070e1387440c14cfe51af831695def6dc2ec +ghcr.io/cozystack/cozystack/grafana-dashboards:v1.0.0-beta.4@sha256:e866b5b3874b9d390b341183d2ee070e1387440c14cfe51af831695def6dc2ec diff --git a/packages/system/kamaji/values.yaml b/packages/system/kamaji/values.yaml index bd7dfe63..26e8bfcf 100644 --- a/packages/system/kamaji/values.yaml +++ b/packages/system/kamaji/values.yaml @@ -3,7 +3,7 @@ kamaji: deploy: false image: pullPolicy: IfNotPresent - tag: v1.0.0-beta.3@sha256:fe9b6bb548edfc26be8aaac65801d598a4e2f9884ddf748083b9e509fa00259e + tag: v1.0.0-beta.4@sha256:05f8e166dc94aadb74cd6448f6f96fbd2167057a559097516a41b8f53e434918 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:v1.0.0-beta.3@sha256:fe9b6bb548edfc26be8aaac65801d598a4e2f9884ddf748083b9e509fa00259e + - --migrate-image=ghcr.io/cozystack/cozystack/kamaji:v1.0.0-beta.4@sha256:05f8e166dc94aadb74cd6448f6f96fbd2167057a559097516a41b8f53e434918 diff --git a/packages/system/kubeovn-plunger/values.yaml b/packages/system/kubeovn-plunger/values.yaml index b86cc2dc..b325a7b7 100644 --- a/packages/system/kubeovn-plunger/values.yaml +++ b/packages/system/kubeovn-plunger/values.yaml @@ -1,4 +1,4 @@ portSecurity: true routes: "" -image: ghcr.io/cozystack/cozystack/kubeovn-plunger:v1.0.0-beta.3@sha256:c89397d6a74e6de38830513a323ade3fa99a42946b40ba596136fc08930b2348 +image: ghcr.io/cozystack/cozystack/kubeovn-plunger:v1.0.0-beta.4@sha256:16b362d6fa1ca30c791ea5cfc7984e085b9ea76a24c9abb3b05dad5c8b64bd0e ovnCentralName: ovn-central diff --git a/packages/system/kubeovn-webhook/values.yaml b/packages/system/kubeovn-webhook/values.yaml index 65898067..d5d8ba93 100644 --- a/packages/system/kubeovn-webhook/values.yaml +++ b/packages/system/kubeovn-webhook/values.yaml @@ -1,3 +1,3 @@ portSecurity: true routes: "" -image: ghcr.io/cozystack/cozystack/kubeovn-webhook:v1.0.0-beta.3@sha256:e18f9fd679e38f65362a8d0042f25468272f6d081136ad47027168d8e7e07a4a +image: ghcr.io/cozystack/cozystack/kubeovn-webhook:v1.0.0-beta.4@sha256:e18f9fd679e38f65362a8d0042f25468272f6d081136ad47027168d8e7e07a4a diff --git a/packages/system/kubevirt-csi-node/values.yaml b/packages/system/kubevirt-csi-node/values.yaml index f7fd69a1..521683be 100644 --- a/packages/system/kubevirt-csi-node/values.yaml +++ b/packages/system/kubevirt-csi-node/values.yaml @@ -1,3 +1,3 @@ storageClass: replicated csiDriver: - image: ghcr.io/cozystack/cozystack/kubevirt-csi-driver:latest@sha256:cd58760c97ba50ef74ff940cdcda4b9a6d8554eec8305fc96c2e8bbea72b75a9 + image: ghcr.io/cozystack/cozystack/kubevirt-csi-driver:0.0.0@sha256:1997d623bb60a5b540027a4e0716e7ca84f668ee09f31290e9c43324f0003137 diff --git a/packages/system/lineage-controller-webhook/values.yaml b/packages/system/lineage-controller-webhook/values.yaml index 10ac0be5..00e593eb 100644 --- a/packages/system/lineage-controller-webhook/values.yaml +++ b/packages/system/lineage-controller-webhook/values.yaml @@ -1,5 +1,5 @@ lineageControllerWebhook: - image: ghcr.io/cozystack/cozystack/lineage-controller-webhook:v1.0.0-beta.3@sha256:da85b064cdef4c8a798f32a731396a9b90741654dc599a5f4f381372834765db + image: ghcr.io/cozystack/cozystack/lineage-controller-webhook:v1.0.0-beta.4@sha256:e9054f9137fd73039b2d91a979f672c9258336f91e51440a77aaa09e6e0b5254 debug: false localK8sAPIEndpoint: enabled: true diff --git a/packages/system/objectstorage-controller/values.yaml b/packages/system/objectstorage-controller/values.yaml index d5fe5bec..18dda28f 100644 --- a/packages/system/objectstorage-controller/values.yaml +++ b/packages/system/objectstorage-controller/values.yaml @@ -1,3 +1,3 @@ objectstorage: controller: - image: "ghcr.io/cozystack/cozystack/objectstorage-controller:v1.0.0-beta.3@sha256:bb2b2b95cbc3d613b077a87a6c281a3ceff8ef8655d770fb2f8fd6b5f1d0c588" + image: "ghcr.io/cozystack/cozystack/objectstorage-controller:v1.0.0-beta.4@sha256:b4c972769afda76c48b58e7acf0ac66a0abf16a622f245c60338f432872f640a" diff --git a/packages/system/seaweedfs/values.yaml b/packages/system/seaweedfs/values.yaml index cc5e8301..63199e28 100644 --- a/packages/system/seaweedfs/values.yaml +++ b/packages/system/seaweedfs/values.yaml @@ -177,7 +177,7 @@ seaweedfs: bucketClassName: "seaweedfs" region: "" sidecar: - image: "ghcr.io/cozystack/cozystack/objectstorage-sidecar:v1.0.0-beta.3@sha256:f845a2c3ec0f79a8873e9f3e65ee625ec3574c5e33e174a7bf60b15d1db49979" + image: "ghcr.io/cozystack/cozystack/objectstorage-sidecar:v1.0.0-beta.4@sha256:235b194a531b70e266a10ef78d2955d19f5b659513f23d8b3cfbbc0dff7fc1c0" certificates: commonName: "SeaweedFS CA" ipAddresses: [] From 6b407bd4039fd2e98805518267256761fd6e3b27 Mon Sep 17 00:00:00 2001 From: cozystack-bot <217169706+cozystack-bot@users.noreply.github.com> Date: Sat, 14 Feb 2026 09:23:50 +0000 Subject: [PATCH 043/666] docs: add changelog for v1.0.0-beta.4 Signed-off-by: cozystack-bot <217169706+cozystack-bot@users.noreply.github.com> --- docs/changelogs/v1.0.0-beta.4.md | 96 ++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 docs/changelogs/v1.0.0-beta.4.md diff --git a/docs/changelogs/v1.0.0-beta.4.md b/docs/changelogs/v1.0.0-beta.4.md new file mode 100644 index 00000000..c5694a73 --- /dev/null +++ b/docs/changelogs/v1.0.0-beta.4.md @@ -0,0 +1,96 @@ + + +> **⚠️ Beta Release Warning**: This is a pre-release version intended for testing and early adoption. Breaking changes may occur before the stable v1.0.0 release. + +## Major Features and Improvements + +### Virtual Machines + +* **[vm-instance] Complete migration from virtual-machine to vm-disk and vm-instance**: Completed the architectural redesign of virtual machine management by fully migrating from the legacy `virtual-machine` application to the new `vm-disk` and `vm-instance` applications. This includes automatic migration scripts (migration 28) that convert existing virtual machines, handle CDI webhook configurations, and update cloud-init references. The new architecture provides better separation of concerns between disk management and VM lifecycle, enabling more flexible VM configuration and improved resource management ([**@kvaps**](https://github.com/kvaps) in #2040). + +* **[vm-instance] Port advanced VM features**: Ported critical VM features from the legacy virtual-machine application including cpuModel field for direct CPU model specification, support for switching between instanceType and custom resource configurations, and migration from deprecated `running` field to `runStrategy` field following KubeVirt best practices ([**@kvaps**](https://github.com/kvaps) in #2040). + +### Storage and CSI + +* **[kubevirt-csi-driver] Add RWX Filesystem (NFS) support**: Added Read-Write-Many (RWX) filesystem support to kubevirt-csi-driver, enabling multiple pods to mount the same persistent volume simultaneously via NFS. This provides native NFS support for shared storage use cases without requiring external NFS provisioners, with automatic NFS server deployment per PVC and seamless integration with KubeVirt's storage layer ([**@kvaps**](https://github.com/kvaps) in #2042). + +### Platform and Infrastructure + +* **[cozystack-api] Switch from DaemonSet to Deployment**: Migrated cozystack-api from DaemonSet to Deployment with PreferClose topology spread constraints, improving resource efficiency while maintaining high availability. The Deployment approach reduces resource consumption compared to running API pods on every node, while topology spreading ensures resilient pod placement across the cluster ([**@kvaps**](https://github.com/kvaps) in #2041, #2048). + +* **[linstor] Move CRDs installation to dedicated chart**: Refactored LINSTOR CRDs installation by moving them to a dedicated `piraeus-operator-crds` chart, solving Helm's limitation with large CRD files that could cause unreliable installations. This ensures all LINSTOR CRDs (including linstorsatellites.io) are properly installed before the operator starts, preventing satellite pod creation failures. Includes automatic migration script to reassign existing CRDs to the new chart ([**@kvaps**](https://github.com/kvaps) in #2036). + +* **[installer] Unify operator templates**: Merged separate operator templates into a single variant-based template, simplifying the installation process and reducing configuration duplication. The new template supports different deployment variants (Talos, non-Talos) through a unified configuration approach ([**@kvaps**](https://github.com/kvaps) in #2034). + +### Applications + +* **[mariadb] Rename mysql application to mariadb**: Renamed the MySQL application to MariaDB to accurately reflect the underlying database engine being used. Includes automatic migration script (migration 27) that handles resource renaming and ensures seamless upgrade path for existing MySQL deployments. All resources, including databases, users, backups, and configurations, are automatically migrated to use the mariadb naming ([**@kvaps**](https://github.com/kvaps) in #2026). + +* **[ferretdb] Remove FerretDB application**: Removed the FerretDB application from the catalog as it has been superseded by native MongoDB support with improved performance and features ([**@kvaps**](https://github.com/kvaps) in #2028). + +## Improvements + +* **[rbac] Use hierarchical naming scheme**: Refactored RBAC configuration to use hierarchical naming scheme for cluster roles and role bindings, improving organization and maintainability of permission structures across the platform ([**@lllamnyp**](https://github.com/lllamnyp) in #2019). + +* **[backups] Create RBAC for backup resources**: Added comprehensive RBAC configuration for backup resources, enabling proper permission management for backup operations and restore jobs across different user roles ([**@lllamnyp**](https://github.com/lllamnyp) in #2018). + +* **[etcd-operator] Add vertical-pod-autoscaler dependency**: Added vertical-pod-autoscaler as a dependency to etcd-operator package, ensuring proper resource scaling and optimization for etcd clusters ([**@sircthulhu**](https://github.com/sircthulhu) in #2047). + +## Fixes + +* **[cozystack-operator] Preserve existing suspend field in package reconciler**: Fixed package reconciler to properly preserve the existing suspend field state during reconciliation, preventing unintended resumption of suspended packages ([**@sircthulhu**](https://github.com/sircthulhu) in #2043). + +* **[cozystack-operator] Fix namespace privileged flag resolution**: Fixed operator to correctly resolve namespace privileged flag by checking all Packages in the namespace, not just the first one. This ensures namespaces are properly marked as privileged when any package requires elevated permissions ([**@kvaps**](https://github.com/kvaps) in #2046). + +* **[cozystack-operator] Fix namespace reconciliation field ownership**: Fixed Server-Side Apply (SSA) field ownership conflicts by using per-Package field owner for namespace reconciliation, preventing conflicts when multiple packages reconcile the same namespace ([**@kvaps**](https://github.com/kvaps) in #2046). + +* **[platform] Clean up Helm secrets for removed releases**: Added cleanup logic to migration 23 to remove orphaned Helm secrets from removed -rd releases, preventing secret accumulation and reducing cluster resource usage ([**@kvaps**](https://github.com/kvaps) in #2035). + +* **[monitoring] Fix YAML parse error in vmagent template**: Fixed YAML parsing error in monitoring-agents vmagent template that could cause monitoring stack deployment failures ([**@kvaps**](https://github.com/kvaps) in #2037). + +* **[talm] Fix metadata.id type casting in physical_links_info**: Fixed Prometheus query in physical_links_info chart to properly cast metadata.id to string for regexMatch operations, preventing query failures with numeric interface IDs ([**@kvaps**](https://github.com/kvaps) in cozystack/talm#110). + +## Dependencies + +* **[kilo] Update to v0.7.1**: Updated Kilo WireGuard mesh networking to v0.7.1 with bug fixes and improvements ([**@kvaps**](https://github.com/kvaps) in #2049). + +## Development, Testing, and CI/CD + +* **[ci] Improve cozyreport functionality**: Enhanced cozyreport tool with improved reporting capabilities for CI/CD pipelines, providing better visibility into test results and build status ([**@lllamnyp**](https://github.com/lllamnyp) in #2032). + +* **[e2e] Increase HelmRelease readiness timeout for kubernetes test**: Increased HelmRelease readiness timeout in Kubernetes end-to-end tests to prevent false failures on slower hardware or during high load conditions, specifically targeting ingress-nginx component which may take longer to become ready ([**@lexfrei**](https://github.com/lexfrei) in #2033). + +## Documentation + +* **[website] Add documentation versioning**: Implemented comprehensive documentation versioning system with separate v0 and v1 documentation trees, version selector in the UI, proper URL redirects for unversioned docs, and improved navigation for users working with different Cozystack versions ([**@IvanStukov**](https://github.com/IvanStukov) in cozystack/website#415). + +* **[website] Describe upgrade to v1.0**: Added detailed upgrade instructions for migrating from v0.x to v1.0, including prerequisites, upgrade steps, and troubleshooting guidance ([**@nbykov0**](https://github.com/nbykov0) in cozystack/website@21bbe84). + +* **[website] Update support documentation**: Updated support documentation with current contact information and support channels ([**@xrmtech-isk**](https://github.com/xrmtech-isk) in cozystack/website#420). + +--- + +## Contributors + +We'd like to thank all contributors who made this release possible: + +* [**@IvanStukov**](https://github.com/IvanStukov) +* [**@kvaps**](https://github.com/kvaps) +* [**@lexfrei**](https://github.com/lexfrei) +* [**@lllamnyp**](https://github.com/lllamnyp) +* [**@nbykov0**](https://github.com/nbykov0) +* [**@sircthulhu**](https://github.com/sircthulhu) +* [**@xrmtech-isk**](https://github.com/xrmtech-isk) + +### New Contributors + +We're excited to welcome our first-time contributors: + +* [**@IvanStukov**](https://github.com/IvanStukov) - First contribution! +* [**@xrmtech-isk**](https://github.com/xrmtech-isk) - First contribution! + +--- + +**Full Changelog**: [v1.0.0-beta.3...v1.0.0-beta.4](https://github.com/cozystack/cozystack/compare/v1.0.0-beta.3...v1.0.0-beta.4) From 2bc5e01fda74d418d0bb929dc4e356f62f10b168 Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Sat, 14 Feb 2026 11:02:12 +0100 Subject: [PATCH 044/666] fix kubernetes e2e test for rwx volume Signed-off-by: Andrei Kvapil --- hack/e2e-apps/run-kubernetes.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hack/e2e-apps/run-kubernetes.sh b/hack/e2e-apps/run-kubernetes.sh index c60fdbe4..d0997ecb 100644 --- a/hack/e2e-apps/run-kubernetes.sh +++ b/hack/e2e-apps/run-kubernetes.sh @@ -260,7 +260,7 @@ spec: EOF # Wait for Pod to complete successfully - kubectl --kubeconfig tenantkubeconfig-${test_name} wait pod nfs-test-pod -n tenant-test --timeout=2m --for=jsonpath='{.status.phase}'=Succeeded + kubectl --kubeconfig tenantkubeconfig-${test_name} wait pod nfs-test-pod -n tenant-test --timeout=5m --for=jsonpath='{.status.phase}'=Succeeded # Verify NFS data integrity nfs_result=$(kubectl --kubeconfig tenantkubeconfig-${test_name} logs nfs-test-pod -n tenant-test) From b170a9f4f9d4f65ba677d22d0e14ae76bcdebf1a Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Sat, 14 Feb 2026 11:42:52 +0100 Subject: [PATCH 045/666] feat(cluster-autoscaler): enable enforce-node-group-min-size by default Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- packages/system/cluster-autoscaler/values.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/system/cluster-autoscaler/values.yaml b/packages/system/cluster-autoscaler/values.yaml index 85eacb2d..eaef53d9 100644 --- a/packages/system/cluster-autoscaler/values.yaml +++ b/packages/system/cluster-autoscaler/values.yaml @@ -1 +1,3 @@ -cluster-autoscaler: {} +cluster-autoscaler: + extraArgs: + enforce-node-group-min-size: true From fa55b5f41f84fc6ace9edc93648a9beb7c6f7bd2 Mon Sep 17 00:00:00 2001 From: Kirill Ilin Date: Sun, 15 Feb 2026 16:25:00 +0500 Subject: [PATCH 046/666] [dashboard] Upgrade dashboard to version 1.4.0 - Upgrade CRDs in CozyStack dashboard controller - Add Ingress proxy timeouts for WebSocket to work without terminations - Add CFOMapping custom resource Signed-off-by: Kirill Ilin --- api/dashboard/v1alpha1/dashboard_resources.go | 22 ++ api/dashboard/v1alpha1/groupversion_info.go | 3 + .../v1alpha1/zz_generated.deepcopy.go | 59 ++++++ internal/controller/dashboard/breadcrumb.go | 4 +- .../dashboard/customformsoverride.go | 47 +++++ internal/controller/dashboard/factory.go | 94 ++++----- internal/controller/dashboard/manager.go | 8 + .../controller/dashboard/marketplacepanel.go | 2 +- internal/controller/dashboard/navigation.go | 69 +++++++ internal/controller/dashboard/sidebar.go | 14 +- .../controller/dashboard/static_helpers.go | 2 +- .../controller/dashboard/static_processor.go | 2 + .../controller/dashboard/static_refactored.go | 191 ++++++++++++------ .../dashboard.cozystack.io_cfomappings.yaml | 118 +++++++++++ .../images/openapi-ui-k8s-bff/Dockerfile | 5 +- .../dashboard/images/openapi-ui/Dockerfile | 19 +- .../patches/flatmap-dynamic-key.diff | 90 --------- .../patches/tenantmodules.diff | 12 +- .../system/dashboard/templates/ingress.yaml | 2 + .../dashboard/templates/nginx-config.yaml | 15 +- packages/system/dashboard/templates/web.yaml | 73 +++++-- 21 files changed, 608 insertions(+), 243 deletions(-) create mode 100644 internal/controller/dashboard/navigation.go create mode 100644 packages/system/cozystack-controller/definitions/dashboard.cozystack.io_cfomappings.yaml delete mode 100644 packages/system/dashboard/images/openapi-ui/openapi-k8s-toolkit/patches/flatmap-dynamic-key.diff diff --git a/api/dashboard/v1alpha1/dashboard_resources.go b/api/dashboard/v1alpha1/dashboard_resources.go index 5af3b359..afac0b05 100644 --- a/api/dashboard/v1alpha1/dashboard_resources.go +++ b/api/dashboard/v1alpha1/dashboard_resources.go @@ -253,3 +253,25 @@ type FactoryList struct { metav1.ListMeta `json:"metadata,omitempty"` Items []Factory `json:"items"` } + +// ----------------------------------------------------------------------------- +// CustomFormsOverrideMapping +// ----------------------------------------------------------------------------- + +// +kubebuilder:object:root=true +// +kubebuilder:resource:path=cfomappings,scope=Cluster +// +kubebuilder:subresource:status +type CFOMapping struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ArbitrarySpec `json:"spec"` + Status CommonStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true +type CFOMappingList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []CFOMapping `json:"items"` +} diff --git a/api/dashboard/v1alpha1/groupversion_info.go b/api/dashboard/v1alpha1/groupversion_info.go index b3c38e00..659401a7 100644 --- a/api/dashboard/v1alpha1/groupversion_info.go +++ b/api/dashboard/v1alpha1/groupversion_info.go @@ -69,6 +69,9 @@ func addKnownTypes(scheme *runtime.Scheme) error { &Factory{}, &FactoryList{}, + + &CFOMapping{}, + &CFOMappingList{}, ) metav1.AddToGroupVersion(scheme, GroupVersion) return nil diff --git a/api/dashboard/v1alpha1/zz_generated.deepcopy.go b/api/dashboard/v1alpha1/zz_generated.deepcopy.go index 45b1d9d7..568a1780 100644 --- a/api/dashboard/v1alpha1/zz_generated.deepcopy.go +++ b/api/dashboard/v1alpha1/zz_generated.deepcopy.go @@ -417,6 +417,65 @@ func (in *FactoryList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CFOMapping) DeepCopyInto(out *CFOMapping) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CFOMapping. +func (in *CFOMapping) DeepCopy() *CFOMapping { + if in == nil { + return nil + } + out := new(CFOMapping) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CFOMapping) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CFOMappingList) DeepCopyInto(out *CFOMappingList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]CFOMapping, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CFOMappingList. +func (in *CFOMappingList) DeepCopy() *CFOMappingList { + if in == nil { + return nil + } + out := new(CFOMappingList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CFOMappingList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MarketplacePanel) DeepCopyInto(out *MarketplacePanel) { *out = *in diff --git a/internal/controller/dashboard/breadcrumb.go b/internal/controller/dashboard/breadcrumb.go index aadcacac..6d65f2db 100644 --- a/internal/controller/dashboard/breadcrumb.go +++ b/internal/controller/dashboard/breadcrumb.go @@ -33,12 +33,12 @@ func (m *Manager) ensureBreadcrumb(ctx context.Context, crd *cozyv1alpha1.Applic key := plural // e.g., "virtualmachines" label := labelPlural - link := fmt.Sprintf("/openapi-ui/{clusterName}/{namespace}/api-table/%s/%s/%s", strings.ToLower(group), strings.ToLower(version), plural) + link := fmt.Sprintf("/openapi-ui/{cluster}/{namespace}/api-table/%s/%s/%s", strings.ToLower(group), strings.ToLower(version), plural) // If this is a module, change the first breadcrumb item to "Tenant Modules" if crd.Spec.Dashboard != nil && crd.Spec.Dashboard.Module { key = "tenantmodules" label = "Tenant Modules" - link = "/openapi-ui/{clusterName}/{namespace}/api-table/core.cozystack.io/v1alpha1/tenantmodules" + link = "/openapi-ui/{cluster}/{namespace}/api-table/core.cozystack.io/v1alpha1/tenantmodules" } items := []any{ diff --git a/internal/controller/dashboard/customformsoverride.go b/internal/controller/dashboard/customformsoverride.go index 60bc82fc..dfbccf5c 100644 --- a/internal/controller/dashboard/customformsoverride.go +++ b/internal/controller/dashboard/customformsoverride.go @@ -84,6 +84,53 @@ func (m *Manager) ensureCustomFormsOverride(ctx context.Context, crd *cozyv1alph return err } +// ensureCFOMapping updates the CFOMapping resource to include a mapping for the given CRD +func (m *Manager) ensureCFOMapping(ctx context.Context, crd *cozyv1alpha1.ApplicationDefinition) error { + g, v, kind := pickGVK(crd) + plural := pickPlural(kind, crd) + + resourcePath := fmt.Sprintf("/%s/%s/%s", g, v, plural) + customizationID := fmt.Sprintf("default-%s", resourcePath) + + obj := &dashv1alpha1.CFOMapping{} + obj.SetName("cfomapping") + + _, err := controllerutil.CreateOrUpdate(ctx, m.Client, obj, func() error { + // Parse existing mappings + mappings := make(map[string]string) + if obj.Spec.JSON.Raw != nil { + var spec map[string]any + if err := json.Unmarshal(obj.Spec.JSON.Raw, &spec); err == nil { + if m, ok := spec["mappings"].(map[string]any); ok { + for k, val := range m { + if s, ok := val.(string); ok { + mappings[k] = s + } + } + } + } + } + + // Add/update the mapping for this CRD + mappings[resourcePath] = customizationID + + specData := map[string]any{ + "mappings": mappings, + } + b, err := json.Marshal(specData) + if err != nil { + return err + } + + newSpec := dashv1alpha1.ArbitrarySpec{JSON: apiextv1.JSON{Raw: b}} + if !compareArbitrarySpecs(obj.Spec, newSpec) { + obj.Spec = newSpec + } + return nil + }) + return err +} + // buildMultilineStringSchema parses OpenAPI schema and creates schema with multilineString // for all string fields inside spec that don't have enum func buildMultilineStringSchema(openAPISchema string) (map[string]any, error) { diff --git a/internal/controller/dashboard/factory.go b/internal/controller/dashboard/factory.go index e55aedc7..66ee111d 100644 --- a/internal/controller/dashboard/factory.go +++ b/internal/controller/dashboard/factory.go @@ -47,7 +47,7 @@ func (m *Manager) ensureFactory(ctx context.Context, crd *cozyv1alpha1.Applicati if prefix, ok := vncTabPrefix(kind); ok { tabs = append(tabs, vncTab(prefix)) } - tabs = append(tabs, yamlTab(plural)) + tabs = append(tabs, yamlTab(g, v, plural)) // Use unified factory creation config := UnifiedResourceConfig{ @@ -160,11 +160,11 @@ func detailsTab(kind, endpoint, schemaJSON string, keysOrder [][]string) map[str map[string]any{ "type": "EnrichedTable", "data": map[string]any{ - "id": "vpc-subnets-table", - "baseprefix": "/openapi-ui", - "clusterNamePartOfUrl": "{2}", - "customizationId": "virtualprivatecloud-subnets", - "fetchUrl": "/api/clusters/{2}/k8s/api/v1/namespaces/{3}/configmaps", + "id": "vpc-subnets-table", + "baseprefix": "/openapi-ui", + "cluster": "{2}", + "customizationId": "virtualprivatecloud-subnets", + "fetchUrl": "/api/clusters/{2}/k8s/api/v1/namespaces/{3}/configmaps", "fieldSelector": map[string]any{ "metadata.name": "virtualprivatecloud-{6}-subnets", }, @@ -188,12 +188,12 @@ func detailsTab(kind, endpoint, schemaJSON string, keysOrder [][]string) map[str map[string]any{ "type": "EnrichedTable", "data": map[string]any{ - "id": "resource-quotas-table", - "baseprefix": "/openapi-ui", - "clusterNamePartOfUrl": "{2}", - "customizationId": "factory-resource-quotas", - "fetchUrl": "/api/clusters/{2}/k8s/api/v1/namespaces/{reqsJsonPath[0]['.status.namespace']}/resourcequotas", - "pathToItems": []any{`items`}, + "id": "resource-quotas-table", + "baseprefix": "/openapi-ui", + "cluster": "{2}", + "customizationId": "factory-resource-quotas", + "fetchUrl": "/api/clusters/{2}/k8s/api/v1/namespaces/{reqsJsonPath[0]['.status.namespace']}/resourcequotas", + "pathToItems": []any{`items`}, }, }, }), @@ -242,13 +242,13 @@ func detailsTab(kind, endpoint, schemaJSON string, keysOrder [][]string) map[str map[string]any{ "type": "EnrichedTable", "data": map[string]any{ - "id": "conditions-table", - "fetchUrl": endpoint, - "clusterNamePartOfUrl": "{2}", - "customizationId": "factory-status-conditions", - "baseprefix": "/openapi-ui", - "withoutControls": true, - "pathToItems": []any{"status", "conditions"}, + "id": "conditions-table", + "fetchUrl": endpoint, + "cluster": "{2}", + "customizationId": "factory-status-conditions", + "baseprefix": "/openapi-ui", + "withoutControls": true, + "pathToItems": []any{"status", "conditions"}, }, }, }), @@ -264,12 +264,12 @@ func workloadsTab(kind string) map[string]any { map[string]any{ "type": "EnrichedTable", "data": map[string]any{ - "id": "workloads-table", - "fetchUrl": "/api/clusters/{2}/k8s/apis/cozystack.io/v1alpha1/namespaces/{3}/workloadmonitors", - "clusterNamePartOfUrl": "{2}", - "baseprefix": "/openapi-ui", - "customizationId": "factory-details-v1alpha1.cozystack.io.workloadmonitors", - "pathToItems": []any{"items"}, + "id": "workloads-table", + "fetchUrl": "/api/clusters/{2}/k8s/apis/cozystack.io/v1alpha1/namespaces/{3}/workloadmonitors", + "cluster": "{2}", + "baseprefix": "/openapi-ui", + "customizationId": "factory-details-v1alpha1.cozystack.io.workloadmonitors", + "pathToItems": []any{"items"}, "labelSelector": map[string]any{ "apps.cozystack.io/application.group": "apps.cozystack.io", "apps.cozystack.io/application.kind": kind, @@ -289,12 +289,12 @@ func servicesTab(kind string) map[string]any { map[string]any{ "type": "EnrichedTable", "data": map[string]any{ - "id": "services-table", - "fetchUrl": "/api/clusters/{2}/k8s/api/v1/namespaces/{3}/services", - "clusterNamePartOfUrl": "{2}", - "baseprefix": "/openapi-ui", - "customizationId": "factory-details-v1.services", - "pathToItems": []any{"items"}, + "id": "services-table", + "fetchUrl": "/api/clusters/{2}/k8s/api/v1/namespaces/{3}/services", + "cluster": "{2}", + "baseprefix": "/openapi-ui", + "customizationId": "factory-details-v1.services", + "pathToItems": []any{"items"}, "labelSelector": map[string]any{ "apps.cozystack.io/application.group": "apps.cozystack.io", "apps.cozystack.io/application.kind": kind, @@ -315,12 +315,12 @@ func ingressesTab(kind string) map[string]any { map[string]any{ "type": "EnrichedTable", "data": map[string]any{ - "id": "ingresses-table", - "fetchUrl": "/api/clusters/{2}/k8s/apis/networking.k8s.io/v1/namespaces/{3}/ingresses", - "clusterNamePartOfUrl": "{2}", - "baseprefix": "/openapi-ui", - "customizationId": "factory-details-networking.k8s.io.v1.ingresses", - "pathToItems": []any{"items"}, + "id": "ingresses-table", + "fetchUrl": "/api/clusters/{2}/k8s/apis/networking.k8s.io/v1/namespaces/{3}/ingresses", + "cluster": "{2}", + "baseprefix": "/openapi-ui", + "customizationId": "factory-details-networking.k8s.io.v1.ingresses", + "pathToItems": []any{"items"}, "labelSelector": map[string]any{ "apps.cozystack.io/application.group": "apps.cozystack.io", "apps.cozystack.io/application.kind": kind, @@ -341,12 +341,12 @@ func secretsTab(kind string) map[string]any { map[string]any{ "type": "EnrichedTable", "data": map[string]any{ - "id": "secrets-table", - "fetchUrl": "/api/clusters/{2}/k8s/apis/core.cozystack.io/v1alpha1/namespaces/{3}/tenantsecrets", - "clusterNamePartOfUrl": "{2}", - "baseprefix": "/openapi-ui", - "customizationId": "factory-details-v1alpha1.core.cozystack.io.tenantsecrets", - "pathToItems": []any{"items"}, + "id": "secrets-table", + "fetchUrl": "/api/clusters/{2}/k8s/apis/core.cozystack.io/v1alpha1/namespaces/{3}/tenantsecrets", + "cluster": "{2}", + "baseprefix": "/openapi-ui", + "customizationId": "factory-details-v1alpha1.core.cozystack.io.tenantsecrets", + "pathToItems": []any{"items"}, "labelSelector": map[string]any{ "apps.cozystack.io/application.group": "apps.cozystack.io", "apps.cozystack.io/application.kind": kind, @@ -358,7 +358,7 @@ func secretsTab(kind string) map[string]any { } } -func yamlTab(plural string) map[string]any { +func yamlTab(group, version, plural string) map[string]any { return map[string]any{ "key": "yaml", "label": "YAML", @@ -369,8 +369,10 @@ func yamlTab(plural string) map[string]any { "id": "yaml-editor", "cluster": "{2}", "isNameSpaced": true, - "type": "builtin", - "typeName": plural, + "type": "apis", + "apiGroup": group, + "apiVersion": version, + "plural": plural, "prefillValuesRequestIndex": float64(0), "readOnly": true, "substractHeight": float64(400), diff --git a/internal/controller/dashboard/manager.go b/internal/controller/dashboard/manager.go index 23897586..b0a4cd1b 100644 --- a/internal/controller/dashboard/manager.go +++ b/internal/controller/dashboard/manager.go @@ -132,6 +132,10 @@ func (m *Manager) EnsureForAppDef(ctx context.Context, crd *cozyv1alpha1.Applica return reconcile.Result{}, err } + if err := m.ensureCFOMapping(ctx, crd); err != nil { + return reconcile.Result{}, err + } + if err := m.ensureSidebar(ctx, crd); err != nil { return reconcile.Result{}, err } @@ -139,6 +143,10 @@ func (m *Manager) EnsureForAppDef(ctx context.Context, crd *cozyv1alpha1.Applica if err := m.ensureFactory(ctx, crd); err != nil { return reconcile.Result{}, err } + + if err := m.ensureNavigation(ctx, crd); err != nil { + return reconcile.Result{}, err + } return reconcile.Result{}, nil } diff --git a/internal/controller/dashboard/marketplacepanel.go b/internal/controller/dashboard/marketplacepanel.go index 6bfce752..fcfff799 100644 --- a/internal/controller/dashboard/marketplacepanel.go +++ b/internal/controller/dashboard/marketplacepanel.go @@ -74,7 +74,7 @@ func (m *Manager) ensureMarketplacePanel(ctx context.Context, crd *cozyv1alpha1. "type": "nonCrd", "apiGroup": "apps.cozystack.io", "apiVersion": "v1alpha1", - "typeName": app.Plural, // e.g., "buckets" + "plural": app.Plural, // e.g., "buckets" "disabled": false, "hidden": false, "tags": tags, diff --git a/internal/controller/dashboard/navigation.go b/internal/controller/dashboard/navigation.go new file mode 100644 index 00000000..5e4bad2e --- /dev/null +++ b/internal/controller/dashboard/navigation.go @@ -0,0 +1,69 @@ +package dashboard + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + dashv1alpha1 "github.com/cozystack/cozystack/api/dashboard/v1alpha1" + cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1" + + apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +// ensureNavigation updates the Navigation resource to include a baseFactoriesMapping entry for the given CRD +func (m *Manager) ensureNavigation(ctx context.Context, crd *cozyv1alpha1.ApplicationDefinition) error { + g, v, kind := pickGVK(crd) + plural := pickPlural(kind, crd) + + lowerKind := strings.ToLower(kind) + factoryKey := fmt.Sprintf("%s-details", lowerKind) + + // All CRD resources are namespaced API resources + mappingKey := fmt.Sprintf("base-factory-namespaced-api-%s-%s-%s", g, v, plural) + + obj := &dashv1alpha1.Navigation{} + obj.SetName("navigation") + + _, err := controllerutil.CreateOrUpdate(ctx, m.Client, obj, func() error { + // Parse existing spec + spec := make(map[string]any) + if obj.Spec.JSON.Raw != nil { + if err := json.Unmarshal(obj.Spec.JSON.Raw, &spec); err != nil { + spec = make(map[string]any) + } + } + + // Get or create baseFactoriesMapping + var mappings map[string]string + if existing, ok := spec["baseFactoriesMapping"].(map[string]any); ok { + mappings = make(map[string]string, len(existing)) + for k, val := range existing { + if s, ok := val.(string); ok { + mappings[k] = s + } + } + } else { + mappings = make(map[string]string) + } + + // Add/update the mapping for this CRD + mappings[mappingKey] = factoryKey + + spec["baseFactoriesMapping"] = mappings + + b, err := json.Marshal(spec) + if err != nil { + return err + } + + newSpec := dashv1alpha1.ArbitrarySpec{JSON: apiextv1.JSON{Raw: b}} + if !compareArbitrarySpecs(obj.Spec, newSpec) { + obj.Spec = newSpec + } + return nil + }) + return err +} diff --git a/internal/controller/dashboard/sidebar.go b/internal/controller/dashboard/sidebar.go index a6ade387..b7a6eb70 100644 --- a/internal/controller/dashboard/sidebar.go +++ b/internal/controller/dashboard/sidebar.go @@ -22,8 +22,8 @@ import ( // // Menu rules: // - The first section is "Marketplace" with two hardcoded entries: -// - Marketplace (/openapi-ui/{clusterName}/{namespace}/factory/marketplace) -// - Tenant Info (/openapi-ui/{clusterName}/{namespace}/factory/info-details/info) +// - Marketplace (/openapi-ui/{cluster}/{namespace}/factory/marketplace) +// - Tenant Info (/openapi-ui/{cluster}/{namespace}/factory/info-details/info) // - All other sections are built from CRDs where spec.dashboard != nil. // - Categories are ordered strictly as: // Marketplace, IaaS, PaaS, NaaS, , Resources, Backups, Administration @@ -91,7 +91,7 @@ func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.Applicati // Weight (default 0) weight := def.Spec.Dashboard.Weight - link := fmt.Sprintf("/openapi-ui/{clusterName}/{namespace}/api-table/%s/%s/%s", g, v, plural) + link := fmt.Sprintf("/openapi-ui/{cluster}/{namespace}/api-table/%s/%s/%s", g, v, plural) categories[cat] = append(categories[cat], item{ Key: plural, @@ -146,7 +146,7 @@ func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.Applicati map[string]any{ "key": "marketplace", "label": "Marketplace", - "link": "/openapi-ui/{clusterName}/{namespace}/factory/marketplace", + "link": "/openapi-ui/{cluster}/{namespace}/factory/marketplace", }, }, }, @@ -205,12 +205,12 @@ func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.Applicati map[string]any{ "key": "info", "label": "Info", - "link": "/openapi-ui/{clusterName}/{namespace}/factory/info-details/info", + "link": "/openapi-ui/{cluster}/{namespace}/factory/info-details/info", }, map[string]any{ "key": "modules", "label": "Modules", - "link": "/openapi-ui/{clusterName}/{namespace}/api-table/core.cozystack.io/v1alpha1/tenantmodules", + "link": "/openapi-ui/{cluster}/{namespace}/api-table/core.cozystack.io/v1alpha1/tenantmodules", }, map[string]any{ "key": "loadbalancer-services", @@ -220,7 +220,7 @@ func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.Applicati map[string]any{ "key": "tenants", "label": "Tenants", - "link": "/openapi-ui/{clusterName}/{namespace}/api-table/apps.cozystack.io/v1alpha1/tenants", + "link": "/openapi-ui/{cluster}/{namespace}/api-table/apps.cozystack.io/v1alpha1/tenants", }, }, }) diff --git a/internal/controller/dashboard/static_helpers.go b/internal/controller/dashboard/static_helpers.go index af46674b..1a1018cb 100644 --- a/internal/controller/dashboard/static_helpers.go +++ b/internal/controller/dashboard/static_helpers.go @@ -1134,7 +1134,7 @@ func yamlEditor(id, cluster string, isNameSpaced bool, typeName string, prefillV "cluster": cluster, "isNameSpaced": isNameSpaced, "type": "builtin", - "typeName": typeName, + "plural": typeName, "prefillValuesRequestIndex": prefillValuesRequestIndex, "substractHeight": float64(400), }, diff --git a/internal/controller/dashboard/static_processor.go b/internal/controller/dashboard/static_processor.go index 902e3818..d4b270f4 100644 --- a/internal/controller/dashboard/static_processor.go +++ b/internal/controller/dashboard/static_processor.go @@ -49,6 +49,8 @@ func (m *Manager) ensureStaticResource(ctx context.Context, obj client.Object) e resource.(*dashv1alpha1.Navigation).Spec = o.Spec case *dashv1alpha1.TableUriMapping: resource.(*dashv1alpha1.TableUriMapping).Spec = o.Spec + case *dashv1alpha1.CFOMapping: + resource.(*dashv1alpha1.CFOMapping).Spec = o.Spec } // Ensure labels are always set m.addDashboardLabels(resource, nil, ResourceTypeStatic) diff --git a/internal/controller/dashboard/static_refactored.go b/internal/controller/dashboard/static_refactored.go index 4b244396..d47c88da 100644 --- a/internal/controller/dashboard/static_refactored.go +++ b/internal/controller/dashboard/static_refactored.go @@ -17,111 +17,111 @@ func CreateAllBreadcrumbs() []*dashboardv1alpha1.Breadcrumb { return []*dashboardv1alpha1.Breadcrumb{ // Stock project factory configmap details createBreadcrumb("stock-project-factory-configmap-details", []map[string]any{ - createBreadcrumbItem("configmaps", "v1/configmaps", "/openapi-ui/{clusterName}/{namespace}/builtin-table/configmaps"), + createBreadcrumbItem("configmaps", "v1/configmaps", "/openapi-ui/{cluster}/{namespace}/builtin-table/configmaps"), createBreadcrumbItem("configmap", "{6}"), }), // Stock cluster factory namespace details createBreadcrumb("stock-cluster-factory-namespace-details", []map[string]any{ - createBreadcrumbItem("namespaces", "v1/namespaces", "/openapi-ui/{clusterName}/builtin-table/namespaces"), + createBreadcrumbItem("namespaces", "v1/namespaces", "/openapi-ui/{cluster}/builtin-table/namespaces"), createBreadcrumbItem("namespace", "{5}"), }), // Stock cluster factory node details createBreadcrumb("stock-cluster-factory-node-details", []map[string]any{ - createBreadcrumbItem("node", "v1/nodes", "/openapi-ui/{clusterName}/builtin-table/nodes"), + createBreadcrumbItem("node", "v1/nodes", "/openapi-ui/{cluster}/builtin-table/nodes"), createBreadcrumbItem("node", "{5}"), }), // Stock project factory pod details createBreadcrumb("stock-project-factory-pod-details", []map[string]any{ - createBreadcrumbItem("pods", "v1/pods", "/openapi-ui/{clusterName}/{namespace}/builtin-table/pods"), + createBreadcrumbItem("pods", "v1/pods", "/openapi-ui/{cluster}/{namespace}/builtin-table/pods"), createBreadcrumbItem("pod", "{6}"), }), // Stock project factory secret details createBreadcrumb("stock-project-factory-kube-secret-details", []map[string]any{ - createBreadcrumbItem("secrets", "v1/secrets", "/openapi-ui/{clusterName}/{namespace}/builtin-table/secrets"), + createBreadcrumbItem("secrets", "v1/secrets", "/openapi-ui/{cluster}/{namespace}/builtin-table/secrets"), createBreadcrumbItem("secret", "{6}"), }), // Stock project factory service details createBreadcrumb("stock-project-factory-kube-service-details", []map[string]any{ - createBreadcrumbItem("services", "v1/services", "/openapi-ui/{clusterName}/{namespace}/builtin-table/services"), + createBreadcrumbItem("services", "v1/services", "/openapi-ui/{cluster}/{namespace}/builtin-table/services"), createBreadcrumbItem("service", "{6}"), }), // Stock project factory ingress details createBreadcrumb("stock-project-factory-kube-ingress-details", []map[string]any{ - createBreadcrumbItem("ingresses", "networking.k8s.io/v1/ingresses", "/openapi-ui/{clusterName}/{namespace}/builtin-table/ingresses"), + createBreadcrumbItem("ingresses", "networking.k8s.io/v1/ingresses", "/openapi-ui/{cluster}/{namespace}/builtin-table/ingresses"), createBreadcrumbItem("ingress", "{6}"), }), // Stock cluster api table createBreadcrumb("stock-cluster-api-table", []map[string]any{ - createBreadcrumbItem("api", "{apiGroup}/{apiVersion}/{typeName}"), + createBreadcrumbItem("api", "{apiGroup}/{apiVersion}/{plural}"), }), // Stock cluster api form createBreadcrumb("stock-cluster-api-form", []map[string]any{ - createBreadcrumbItem("create-api-res-namespaced-table", "{apiGroup}/{apiVersion}/{typeName}", "/openapi-ui/{clusterName}/api-table/{apiGroup}/{apiVersion}/{typeName}"), + createBreadcrumbItem("create-api-res-namespaced-table", "{apiGroup}/{apiVersion}/{plural}", "/openapi-ui/{cluster}/api-table/{apiGroup}/{apiVersion}/{plural}"), createBreadcrumbItem("create-api-res-namespaced-typename", "Create"), }), // Stock cluster api form edit createBreadcrumb("stock-cluster-api-form-edit", []map[string]any{ - createBreadcrumbItem("create-api-res-namespaced-table", "{apiGroup}/{apiVersion}/{typeName}", "/openapi-ui/{clusterName}/api-table/{apiGroup}/{apiVersion}/{typeName}"), + createBreadcrumbItem("create-api-res-namespaced-table", "{apiGroup}/{apiVersion}/{plural}", "/openapi-ui/{cluster}/api-table/{apiGroup}/{apiVersion}/{plural}"), createBreadcrumbItem("create-api-res-namespaced-typename", "Update"), }), // Stock cluster builtin table createBreadcrumb("stock-cluster-builtin-table", []map[string]any{ - createBreadcrumbItem("api", "v1/{typeName}"), + createBreadcrumbItem("api", "v1/{plural}"), }), // Stock cluster builtin form createBreadcrumb("stock-cluster-builtin-form", []map[string]any{ - createBreadcrumbItem("create-api-res-namespaced-table", "v1/{typeName}", "/openapi-ui/{clusterName}/builtin-table/{typeName}"), + createBreadcrumbItem("create-api-res-namespaced-table", "v1/{plural}", "/openapi-ui/{cluster}/builtin-table/{plural}"), createBreadcrumbItem("create-api-res-namespaced-typename", "Create"), }), // Stock cluster builtin form edit createBreadcrumb("stock-cluster-builtin-form-edit", []map[string]any{ - createBreadcrumbItem("create-api-res-namespaced-table", "v1/{typeName}", "/openapi-ui/{clusterName}/builtin-table/{typeName}"), + createBreadcrumbItem("create-api-res-namespaced-table", "v1/{plural}", "/openapi-ui/{cluster}/builtin-table/{plural}"), createBreadcrumbItem("create-api-res-namespaced-typename", "Update"), }), // Stock project api table createBreadcrumb("stock-project-api-table", []map[string]any{ - createBreadcrumbItem("api", "{apiGroup}/{apiVersion}/{typeName}"), + createBreadcrumbItem("api", "{apiGroup}/{apiVersion}/{plural}"), }), // Stock project api form createBreadcrumb("stock-project-api-form", []map[string]any{ - createBreadcrumbItem("create-api-res-namespaced-table", "{apiGroup}/{apiVersion}/{typeName}", "/openapi-ui/{clusterName}/{namespace}/api-table/{apiGroup}/{apiVersion}/{typeName}"), + createBreadcrumbItem("create-api-res-namespaced-table", "{apiGroup}/{apiVersion}/{plural}", "/openapi-ui/{cluster}/{namespace}/api-table/{apiGroup}/{apiVersion}/{plural}"), createBreadcrumbItem("create-api-res-namespaced-typename", "Create"), }), // Stock project api form edit createBreadcrumb("stock-project-api-form-edit", []map[string]any{ - createBreadcrumbItem("create-api-res-namespaced-table", "{apiGroup}/{apiVersion}/{typeName}", "/openapi-ui/{clusterName}/{namespace}/api-table/{apiGroup}/{apiVersion}/{typeName}"), + createBreadcrumbItem("create-api-res-namespaced-table", "{apiGroup}/{apiVersion}/{plural}", "/openapi-ui/{cluster}/{namespace}/api-table/{apiGroup}/{apiVersion}/{plural}"), createBreadcrumbItem("create-api-res-namespaced-typename", "Update"), }), // Stock project builtin table createBreadcrumb("stock-project-builtin-table", []map[string]any{ - createBreadcrumbItem("api", "v1/{typeName}"), + createBreadcrumbItem("api", "v1/{plural}"), }), // Stock project builtin form createBreadcrumb("stock-project-builtin-form", []map[string]any{ - createBreadcrumbItem("create-api-res-namespaced-table", "v1/{typeName}", "/openapi-ui/{clusterName}/{namespace}/builtin-table/{typeName}"), + createBreadcrumbItem("create-api-res-namespaced-table", "v1/{plural}", "/openapi-ui/{cluster}/{namespace}/builtin-table/{plural}"), createBreadcrumbItem("create-api-res-namespaced-typename", "Create"), }), // Stock project builtin form edit createBreadcrumb("stock-project-builtin-form-edit", []map[string]any{ - createBreadcrumbItem("create-api-res-namespaced-table", "v1/{typeName}", "/openapi-ui/{clusterName}/{namespace}/builtin-table/{typeName}"), + createBreadcrumbItem("create-api-res-namespaced-table", "v1/{plural}", "/openapi-ui/{cluster}/{namespace}/builtin-table/{plural}"), createBreadcrumbItem("create-api-res-namespaced-typename", "Update"), }), } @@ -535,14 +535,14 @@ func CreateAllFactories() []*dashboardv1alpha1.Factory { }, []any{ map[string]any{ "data": map[string]any{ - "baseApiVersion": "v1alpha1", - "baseprefix": "openapi-ui", - "clusterNamePartOfUrl": "{2}", - "id": 311, - "mpResourceKind": "MarketplacePanel", - "mpResourceName": "marketplacepanels", - "namespacePartOfUrl": "{3}", - "baseApiGroup": "dashboard.cozystack.io", + "baseApiVersion": "v1alpha1", + "baseprefix": "openapi-ui", + "cluster": "{2}", + "id": 311, + "marketplaceKind": "MarketplacePanel", + "marketplacePlural": "marketplacepanels", + "namespace": "{3}", + "baseApiGroup": "dashboard.cozystack.io", }, "type": "MarketplaceCard", }, @@ -855,7 +855,7 @@ func CreateAllFactories() []*dashboardv1alpha1.Factory { "prefillValuesRequestIndex": 0, "substractHeight": float64(400), "type": "builtin", - "typeName": "secrets", + "plural": "secrets", "readOnly": true, }, }, @@ -1085,13 +1085,13 @@ func CreateAllFactories() []*dashboardv1alpha1.Factory { map[string]any{ "type": "EnrichedTable", "data": map[string]any{ - "id": "service-port-mapping-table", - "baseprefix": "/openapi-ui", - "clusterNamePartOfUrl": "{2}", - "customizationId": "factory-kube-service-details-port-mapping", - "fetchUrl": "/api/clusters/{2}/k8s/api/v1/namespaces/{3}/services/{6}", - "pathToItems": ".spec.ports", - "withoutControls": true, + "id": "service-port-mapping-table", + "baseprefix": "/openapi-ui", + "cluster": "{2}", + "customizationId": "factory-kube-service-details-port-mapping", + "fetchUrl": "/api/clusters/{2}/k8s/api/v1/namespaces/{3}/services/{6}", + "pathToItems": ".spec.ports", + "withoutControls": true, }, }, }), @@ -1111,11 +1111,11 @@ func CreateAllFactories() []*dashboardv1alpha1.Factory { map[string]any{ "type": "EnrichedTable", "data": map[string]any{ - "id": "service-pod-serving-table", - "baseprefix": "/openapi-ui", - "clusterNamePartOfUrl": "{2}", - "customizationId": "factory-kube-service-details-endpointslice", - "fetchUrl": "/api/clusters/{2}/k8s/apis/discovery.k8s.io/v1/namespaces/{3}/endpointslices", + "id": "service-pod-serving-table", + "baseprefix": "/openapi-ui", + "cluster": "{2}", + "customizationId": "factory-kube-service-details-endpointslice", + "fetchUrl": "/api/clusters/{2}/k8s/apis/discovery.k8s.io/v1/namespaces/{3}/endpointslices", "labelSelector": map[string]any{ "kubernetes.io/service-name": "{reqsJsonPath[0]['.metadata.name']['-']}", }, @@ -1145,7 +1145,7 @@ func CreateAllFactories() []*dashboardv1alpha1.Factory { "prefillValuesRequestIndex": 0, "substractHeight": float64(400), "type": "builtin", - "typeName": "services", + "plural": "services", }, }, }, @@ -1168,11 +1168,11 @@ func CreateAllFactories() []*dashboardv1alpha1.Factory { map[string]any{ "type": "EnrichedTable", "data": map[string]any{ - "id": "pods-table", - "baseprefix": "/openapi-ui", - "clusterNamePartOfUrl": "{2}", - "customizationId": "factory-node-details-/v1/pods", - "fetchUrl": "/api/clusters/{2}/k8s/api/v1/namespaces/{3}/pods", + "id": "pods-table", + "baseprefix": "/openapi-ui", + "cluster": "{2}", + "customizationId": "factory-node-details-/v1/pods", + "fetchUrl": "/api/clusters/{2}/k8s/api/v1/namespaces/{3}/pods", "labelSelectorFull": map[string]any{ "pathToLabels": ".spec.selector", "reqIndex": 0, @@ -1300,13 +1300,13 @@ func CreateAllFactories() []*dashboardv1alpha1.Factory { map[string]any{ "type": "EnrichedTable", "data": map[string]any{ - "id": "rules-table", - "fetchUrl": "/api/clusters/{2}/k8s/apis/networking.k8s.io/v1/namespaces/{3}/ingresses/{6}", - "clusterNamePartOfUrl": "{2}", - "customizationId": "factory-kube-ingress-details-rules", - "baseprefix": "/openapi-ui", - "withoutControls": true, - "pathToItems": []any{"spec", "rules"}, + "id": "rules-table", + "fetchUrl": "/api/clusters/{2}/k8s/apis/networking.k8s.io/v1/namespaces/{3}/ingresses/{6}", + "cluster": "{2}", + "customizationId": "factory-kube-ingress-details-rules", + "baseprefix": "/openapi-ui", + "withoutControls": true, + "pathToItems": []any{"spec", "rules"}, }, }, }), @@ -1322,8 +1322,10 @@ func CreateAllFactories() []*dashboardv1alpha1.Factory { "id": "yaml-editor", "cluster": "{2}", "isNameSpaced": true, - "type": "builtin", - "typeName": "ingresses", + "type": "apis", + "apiGroup": "networking.k8s.io", + "apiVersion": "v1", + "plural": "ingresses", "prefillValuesRequestIndex": float64(0), "substractHeight": float64(400), }, @@ -1452,11 +1454,11 @@ func CreateAllFactories() []*dashboardv1alpha1.Factory { map[string]any{ "type": "EnrichedTable", "data": map[string]any{ - "id": "workloads-table", - "baseprefix": "/openapi-ui", - "clusterNamePartOfUrl": "{2}", - "customizationId": "factory-details-v1alpha1.cozystack.io.workloads", - "fetchUrl": "/api/clusters/{2}/k8s/apis/cozystack.io/v1alpha1/namespaces/{3}/workloads", + "id": "workloads-table", + "baseprefix": "/openapi-ui", + "cluster": "{2}", + "customizationId": "factory-details-v1alpha1.cozystack.io.workloads", + "fetchUrl": "/api/clusters/{2}/k8s/apis/cozystack.io/v1alpha1/namespaces/{3}/workloads", "labelSelector": map[string]any{ "workloads.cozystack.io/monitor": "{reqs[0]['metadata','name']}", }, @@ -1477,8 +1479,10 @@ func CreateAllFactories() []*dashboardv1alpha1.Factory { "isNameSpaced": true, "prefillValuesRequestIndex": 0, "substractHeight": float64(400), - "type": "builtin", - "typeName": "workloadmonitors", + "type": "apis", + "apiGroup": "cozystack.io", + "apiVersion": "v1alpha1", + "plural": "workloadmonitors", }, }, }, @@ -1960,12 +1964,27 @@ func CreateAllFactories() []*dashboardv1alpha1.Factory { // CreateAllNavigations creates all navigation resources using helper functions func CreateAllNavigations() []*dashboardv1alpha1.Navigation { + // Build baseFactoriesMapping for static (built-in) factories + baseFactoriesMapping := map[string]string{ + // Cluster-scoped builtin resources + "base-factory-clusterscoped-builtin-v1-namespaces": "namespace-details", + "base-factory-clusterscoped-builtin-v1-nodes": "node-details", + // Namespaced builtin resources + "base-factory-namespaced-builtin-v1-pods": "pod-details", + "base-factory-namespaced-builtin-v1-secrets": "kube-secret-details", + "base-factory-namespaced-builtin-v1-services": "kube-service-details", + // Namespaced API resources + "base-factory-namespaced-api-networking.k8s.io-v1-ingresses": "kube-ingress-details", + "base-factory-namespaced-api-cozystack.io-v1alpha1-workloadmonitors": "workloadmonitor-details", + } + return []*dashboardv1alpha1.Navigation{ createNavigation("navigation", map[string]any{ "namespaces": map[string]any{ "change": "/openapi-ui/{selectedCluster}/{value}/factory/marketplace", "clear": "/openapi-ui/{selectedCluster}/api-table/core.cozystack.io/v1alpha1/tenantnamespaces", }, + "baseFactoriesMapping": baseFactoriesMapping, }), } } @@ -2342,6 +2361,51 @@ func createWorkloadmonitorHeader() map[string]any { } } +// CreateStaticCFOMapping creates the CFOMapping resource with mappings from static CustomFormsOverrides +func CreateStaticCFOMapping() *dashboardv1alpha1.CFOMapping { + // Build mappings from static CustomFormsOverrides + customFormsOverrides := CreateAllCustomFormsOverrides() + mappings := make(map[string]string, len(customFormsOverrides)) + for _, cfo := range customFormsOverrides { + var spec map[string]any + if err := json.Unmarshal(cfo.Spec.JSON.Raw, &spec); err != nil { + continue + } + customizationID, ok := spec["customizationId"].(string) + if !ok { + continue + } + // Extract the resource path from customizationId (remove "default-" prefix) + resourcePath := strings.TrimPrefix(customizationID, "default-") + mappings[resourcePath] = customizationID + } + + return createCFOMapping("cfomapping", mappings) +} + +// createCFOMapping creates a CFOMapping resource +func createCFOMapping(name string, mappings map[string]string) *dashboardv1alpha1.CFOMapping { + spec := map[string]any{ + "mappings": mappings, + } + jsonData, _ := json.Marshal(spec) + + return &dashboardv1alpha1.CFOMapping{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "dashboard.cozystack.io/v1alpha1", + Kind: "CFOMapping", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Spec: dashboardv1alpha1.ArbitrarySpec{ + JSON: v1.JSON{ + Raw: jsonData, + }, + }, + } +} + // ---------------- Complete resource creation function ---------------- // CreateAllStaticResources creates all static dashboard resources using helper functions @@ -2378,5 +2442,8 @@ func CreateAllStaticResources() []client.Object { resources = append(resources, tableUriMapping) } + // Add CFOMapping + resources = append(resources, CreateStaticCFOMapping()) + return resources } diff --git a/packages/system/cozystack-controller/definitions/dashboard.cozystack.io_cfomappings.yaml b/packages/system/cozystack-controller/definitions/dashboard.cozystack.io_cfomappings.yaml new file mode 100644 index 00000000..a46f6a23 --- /dev/null +++ b/packages/system/cozystack-controller/definitions/dashboard.cozystack.io_cfomappings.yaml @@ -0,0 +1,118 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.4 + name: cfomappings.dashboard.cozystack.io +spec: + group: dashboard.cozystack.io + names: + kind: CFOMapping + listKind: CFOMappingList + plural: cfomappings + singular: cfomapping + scope: Cluster + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + ArbitrarySpec holds schemaless user data and preserves unknown fields. + We map the entire .spec to a single JSON payload to mirror the CRDs you provided. + NOTE: Using apiextensionsv1.JSON avoids losing arbitrary structure during round-trips. + type: object + x-kubernetes-preserve-unknown-fields: true + status: + description: CommonStatus is a generic Status block with Kubernetes conditions. + properties: + conditions: + description: Conditions represent the latest available observations + of an object's state. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + observedGeneration: + description: ObservedGeneration reflects the most recent generation + observed by the controller. + format: int64 + type: integer + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/packages/system/dashboard/images/openapi-ui-k8s-bff/Dockerfile b/packages/system/dashboard/images/openapi-ui-k8s-bff/Dockerfile index ef698698..ea0c900d 100644 --- a/packages/system/dashboard/images/openapi-ui-k8s-bff/Dockerfile +++ b/packages/system/dashboard/images/openapi-ui-k8s-bff/Dockerfile @@ -1,9 +1,10 @@ -# imported from https://github.com/cozystack/openapi-ui-k8s-bff +# imported from https://github.com/PRO-Robotech/openapi-ui-k8s-bff ARG NODE_VERSION=20.18.1 FROM node:${NODE_VERSION}-alpine AS builder WORKDIR /src -ARG COMMIT_REF=183dc9dcbb0f8a1833dad642c35faa385c71e58d +# release/1.4.0 +ARG COMMIT_REF=92e4b618eb9ad17b19827b5a2b7ceab33e8cf534 RUN wget -O- https://github.com/PRO-Robotech/openapi-ui-k8s-bff/archive/${COMMIT_REF}.tar.gz | tar xzf - --strip-components=1 ENV PATH=/src/node_modules/.bin:$PATH diff --git a/packages/system/dashboard/images/openapi-ui/Dockerfile b/packages/system/dashboard/images/openapi-ui/Dockerfile index f430f8e6..4b80c960 100644 --- a/packages/system/dashboard/images/openapi-ui/Dockerfile +++ b/packages/system/dashboard/images/openapi-ui/Dockerfile @@ -1,12 +1,13 @@ ARG NODE_VERSION=20.18.1 # openapi-k8s-toolkit -# imported from https://github.com/cozystack/openapi-k8s-toolkit +# imported from https://github.com/PRO-Robotech/openapi-k8s-toolkit FROM node:${NODE_VERSION}-alpine AS openapi-k8s-toolkit-builder RUN apk add git WORKDIR /src -ARG COMMIT=cb2f122caafaa2fd5455750213d9e633017ec555 -RUN wget -O- https://github.com/cozystack/openapi-k8s-toolkit/archive/${COMMIT}.tar.gz | tar -xzvf- --strip-components=1 +# release/1.4.0 +ARG COMMIT=c67029cc7b7495c65ee0406033576e773a73bb01 +RUN wget -O- https://github.com/PRO-Robotech/openapi-k8s-toolkit/archive/${COMMIT}.tar.gz | tar -xzvf- --strip-components=1 COPY openapi-k8s-toolkit/patches /patches RUN git apply /patches/*.diff @@ -17,12 +18,13 @@ RUN npm run build # openapi-ui -# imported from https://github.com/cozystack/openapi-ui +# imported from https://github.com/PRO-Robotech/openapi-ui FROM node:${NODE_VERSION}-alpine AS builder #RUN apk add git WORKDIR /src -ARG COMMIT_REF=3cfbbf2156b6a5e4a1f283a032019530c0c2d37d +# release/1.4.0 +ARG COMMIT_REF=6addca6939264ef2e39801baa88c1460cc1aa53e RUN wget -O- https://github.com/PRO-Robotech/openapi-ui/archive/${COMMIT_REF}.tar.gz | tar xzf - --strip-components=1 #COPY openapi-ui/patches /patches @@ -56,5 +58,12 @@ COPY --from=builder2 /src/node_modules /app/node_modules COPY --from=builder2 /src/build /app/build EXPOSE 8080 RUN sed -i -e 's|OpenAPI UI|Cozystack|g' build/index.html +# Fix Factory component: return null while loading instead of showing "Factory Not Found" 404 +RUN APP_JS=$(find build -name "App-react.js" -type f | head -1) && \ + if [ -n "$APP_JS" ]; then \ + sed -i 's|const { data: factoryData } = useK8sSmartResource({|const { data: factoryData, isLoading: factoryIsLoading } = useK8sSmartResource({|' "$APP_JS" && \ + sed -i '/Factory Not Found/s/return /return factoryIsLoading ? null : /' "$APP_JS" && \ + echo "Factory loading patch applied to $APP_JS"; \ + fi USER 1001 CMD ["node", "/app/build/index.js"] diff --git a/packages/system/dashboard/images/openapi-ui/openapi-k8s-toolkit/patches/flatmap-dynamic-key.diff b/packages/system/dashboard/images/openapi-ui/openapi-k8s-toolkit/patches/flatmap-dynamic-key.diff deleted file mode 100644 index 03212014..00000000 --- a/packages/system/dashboard/images/openapi-ui/openapi-k8s-toolkit/patches/flatmap-dynamic-key.diff +++ /dev/null @@ -1,90 +0,0 @@ -diff --git a/src/components/molecules/EnrichedTable/organisms/EnrichedTableProvider/utils.ts b/src/components/molecules/EnrichedTable/organisms/EnrichedTableProvider/utils.ts -index 87a0f12..fb2e1cc 100644 ---- a/src/components/molecules/EnrichedTable/organisms/EnrichedTableProvider/utils.ts -+++ b/src/components/molecules/EnrichedTable/organisms/EnrichedTableProvider/utils.ts -@@ -134,22 +134,6 @@ export const prepare = ({ - // impossible in k8s - return {} - }) -- if (customFields.length > 0) { -- dataSource = dataSource.map((el: TJSON) => { -- const newFieldsForComplexJsonPath: Record = {} -- customFields.forEach(({ dataIndex, jsonPath }) => { -- const jpQueryResult = jp.query(el, `$${jsonPath}`) -- newFieldsForComplexJsonPath[dataIndex] = -- Array.isArray(jpQueryResult) && jpQueryResult.length === 1 ? jpQueryResult[0] : jpQueryResult -- }) -- if (typeof el === 'object') { -- return { ...el, ...newFieldsForComplexJsonPath } -- } -- // impossible in k8s -- return { ...newFieldsForComplexJsonPath } -- }) -- } -- - // Handle flatMap: expand rows for map objects - // Process all flatMap columns sequentially - if (flatMapColumns.length > 0 && dataSource) { -@@ -204,6 +188,62 @@ export const prepare = ({ - currentDataSource = expandedDataSource - }) - dataSource = currentDataSource -+ } -+ -+ if (customFields.length > 0) { -+ dataSource = dataSource.map((el: TJSON) => { -+ const newFieldsForComplexJsonPath: Record = {} -+ customFields.forEach(({ dataIndex, jsonPath }) => { -+ let fieldValue: TJSON = null -+ let handled = false -+ -+ const flatMapMatch = jsonPath.match(/^(.*)\[(_flatMap[^\]]+_Key)\](.*)$/) -+ if (flatMapMatch && el && typeof el === 'object' && !Array.isArray(el)) { -+ const basePath = flatMapMatch[1] -+ const keyField = flatMapMatch[2] -+ const tailPath = flatMapMatch[3] -+ const keyValue = (el as Record)[keyField] -+ if (keyValue !== null && keyValue !== undefined) { -+ const baseResult = jp.query(el, `$${basePath}`)[0] -+ if (baseResult && typeof baseResult === 'object' && !Array.isArray(baseResult)) { -+ const baseValue = (baseResult as Record)[String(keyValue)] -+ if (tailPath) { -+ const normalizedTailPath = -+ tailPath.startsWith('.') || tailPath.startsWith('[') ? tailPath : `.${tailPath}` -+ const tailResult = jp.query(baseValue, `$${normalizedTailPath}`) -+ fieldValue = Array.isArray(tailResult) && tailResult.length === 1 ? tailResult[0] : tailResult -+ } else { -+ fieldValue = baseValue as TJSON -+ } -+ handled = true -+ } -+ } -+ } -+ -+ if (!handled) { -+ let resolvedJsonPath = jsonPath -+ if (el && typeof el === 'object' && !Array.isArray(el)) { -+ resolvedJsonPath = jsonPath.replace(/\[(_flatMap[^\]]+_Key)\]/g, (match, keyField) => { -+ const keyValue = (el as Record)[keyField] -+ if (keyValue === null || keyValue === undefined) { -+ return match -+ } -+ const escaped = String(keyValue).replace(/'/g, "\\'") -+ return `['${escaped}']` -+ }) -+ } -+ const jpQueryResult = jp.query(el, `$${resolvedJsonPath}`) -+ fieldValue = Array.isArray(jpQueryResult) && jpQueryResult.length === 1 ? jpQueryResult[0] : jpQueryResult -+ } -+ -+ newFieldsForComplexJsonPath[dataIndex] = fieldValue -+ }) -+ if (typeof el === 'object') { -+ return { ...el, ...newFieldsForComplexJsonPath } -+ } -+ // impossible in k8s -+ return { ...newFieldsForComplexJsonPath } -+ }) - } - } else { - dataSource = dataItems.map((el: TJSON) => { diff --git a/packages/system/dashboard/images/openapi-ui/openapi-k8s-toolkit/patches/tenantmodules.diff b/packages/system/dashboard/images/openapi-ui/openapi-k8s-toolkit/patches/tenantmodules.diff index 2817a8e0..d028f09a 100644 --- a/packages/system/dashboard/images/openapi-ui/openapi-k8s-toolkit/patches/tenantmodules.diff +++ b/packages/system/dashboard/images/openapi-ui/openapi-k8s-toolkit/patches/tenantmodules.diff @@ -33,18 +33,18 @@ index 8bcef4d..2551e92 100644 if (key === 'edit') { + // Special case: redirect tenantmodules from core.cozystack.io to apps.cozystack.io with plural form + let apiGroupAndVersion = value.apiGroupAndVersion -+ let typeName = value.typeName -+ if (apiGroupAndVersion?.startsWith('core.cozystack.io/') && typeName === 'tenantmodules') { ++ let plural = value.plural ++ if (apiGroupAndVersion?.startsWith('core.cozystack.io/') && plural === 'tenantmodules') { + const appsApiVersion = apiGroupAndVersion.replace('core.cozystack.io/', 'apps.cozystack.io/') -+ const pluralTypeName = getPluralForm(value.entryName) ++ const pluralName = getPluralForm(value.name) + apiGroupAndVersion = appsApiVersion -+ typeName = pluralTypeName ++ plural = pluralName + } navigate( `${baseprefix}/${value.cluster}${value.namespace ? `/${value.namespace}` : ''}${ value.syntheticProject ? `/${value.syntheticProject}` : '' -- }/${value.pathPrefix}/${value.apiGroupAndVersion}/${value.typeName}/${value.entryName}?backlink=${ -+ }/${value.pathPrefix}/${apiGroupAndVersion}/${typeName}/${value.entryName}?backlink=${ +- }/${value.pathPrefix}/${value.apiGroupAndVersion}/${value.plural}/${value.name}?backlink=${ ++ }/${value.pathPrefix}/${apiGroupAndVersion}/${plural}/${value.name}?backlink=${ value.backlink }`, ) diff --git a/packages/system/dashboard/templates/ingress.yaml b/packages/system/dashboard/templates/ingress.yaml index aacc1bc1..b3079a72 100644 --- a/packages/system/dashboard/templates/ingress.yaml +++ b/packages/system/dashboard/templates/ingress.yaml @@ -18,6 +18,8 @@ metadata: nginx.ingress.kubernetes.io/proxy-body-size: 100m nginx.ingress.kubernetes.io/proxy-buffer-size: 100m nginx.ingress.kubernetes.io/proxy-buffers-number: "4" + nginx.ingress.kubernetes.io/proxy-read-timeout: "86400" + nginx.ingress.kubernetes.io/proxy-send-timeout: "86400" name: dashboard-web-ingress spec: ingressClassName: {{ $exposeIngress }} diff --git a/packages/system/dashboard/templates/nginx-config.yaml b/packages/system/dashboard/templates/nginx-config.yaml index 696e9ce9..989f1470 100644 --- a/packages/system/dashboard/templates/nginx-config.yaml +++ b/packages/system/dashboard/templates/nginx-config.yaml @@ -32,6 +32,19 @@ data: proxy_pass http://incloud-web-nginx.{{ .Release.Namespace }}.svc:8080; } + location /k8s/clusters/default/ { + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $host; + proxy_read_timeout 86400s; + proxy_send_timeout 86400s; + + rewrite /k8s/clusters/default/(.*) /$1 break; + proxy_pass https://kubernetes.default.svc:443; + } + location /k8s { proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; @@ -40,7 +53,7 @@ data: proxy_set_header Host $host; proxy_read_timeout 86400s; proxy_send_timeout 86400s; - + rewrite /k8s/(.*) /$1 break; proxy_pass https://kubernetes.default.svc:443; } diff --git a/packages/system/dashboard/templates/web.yaml b/packages/system/dashboard/templates/web.yaml index d1c789b6..defb087b 100644 --- a/packages/system/dashboard/templates/web.yaml +++ b/packages/system/dashboard/templates/web.yaml @@ -45,6 +45,24 @@ spec: value: v1alpha1 - name: BASE_NAMESPACE_FULL_PATH value: "/apis/core.cozystack.io/v1alpha1/tenantnamespaces" + - name: BASE_NAVIGATION_RESOURCE_PLURAL + value: navigations + - name: BASE_NAVIGATION_RESOURCE_NAME + value: navigation + - name: BASE_FRONTEND_PREFIX + value: /openapi-ui + - name: BASE_FACTORY_NAMESPACED_API_KEY + value: base-factory-namespaced-api + - name: BASE_FACTORY_CLUSTERSCOPED_API_KEY + value: base-factory-clusterscoped-api + - name: BASE_FACTORY_NAMESPACED_BUILTIN_KEY + value: base-factory-namespaced-builtin + - name: BASE_FACTORY_CLUSTERSCOPED_BUILTIN_KEY + value: base-factory-clusterscoped-builtin + - name: BASE_NAMESPACE_FACTORY_KEY + value: base-factory-clusterscoped-builtin + - name: BASE_ALLOWED_AUTH_HEADERS + value: user-agent,accept,content-type,origin,referer,accept-encoding,cookie,authorization - name: LOGGER value: "true" - name: LOGGER_WITH_HEADERS @@ -73,7 +91,7 @@ spec: name: bff ports: - containerPort: 64231 - name: http + name: http-bff protocol: TCP resources: limits: @@ -98,7 +116,6 @@ spec: type: RuntimeDefault terminationMessagePath: /dev/termination-log terminationMessagePolicy: File - volumeMounts: null - env: - name: BASEPREFIX value: /openapi-ui @@ -108,40 +125,60 @@ spec: value: dashboard.cozystack.io - name: CUSTOMIZATION_API_VERSION value: v1alpha1 - - name: CUSTOMIZATION_NAVIGATION_RESOURCE - value: navigation + - name: CUSTOMIZATION_CFOMAPPING_RESOURCE_NAME + value: cfomapping + - name: CUSTOMIZATION_CFOMAPPING_RESOURCE_PLURAL + value: cfomappings + - name: CUSTOMIZATION_CFO_FALLBACK_ID + value: "" - name: CUSTOMIZATION_NAVIGATION_RESOURCE_NAME + value: navigation + - name: CUSTOMIZATION_NAVIGATION_RESOURCE_PLURAL value: navigations + - name: CUSTOMIZATION_SIDEBAR_FALLBACK_ID + value: stock-project-api-table + - name: CUSTOMIZATION_BREADCRUMBS_FALLBACK_ID + value: stock-project-api-table - name: INSTANCES_API_GROUP value: dashboard.cozystack.io - - name: INSTANCES_RESOURCE_NAME - value: instances - - name: INSTANCES_VERSION + - name: INSTANCES_API_VERSION value: v1alpha1 - - name: MARKETPLACE_GROUP - value: dashboard.cozystack.io + - name: INSTANCES_PLURAL + value: instances - name: MARKETPLACE_KIND value: MarketplacePanel - - name: MARKETPLACE_RESOURCE_NAME + - name: MARKETPLACE_PLURAL value: marketplacepanels - - name: MARKETPLACE_VERSION - value: v1alpha1 - name: NAVIGATE_FROM_CLUSTERLIST value: /openapi-ui/~recordValue~/api-table/core.cozystack.io/v1alpha1/tenantnamespaces - name: PROJECTS_API_GROUP value: core.cozystack.io - - name: PROJECTS_RESOURCE_NAME - value: tenantnamespaces - - name: PROJECTS_VERSION + - name: PROJECTS_API_VERSION value: v1alpha1 + - name: PROJECTS_PLURAL + value: tenantnamespaces - name: CUSTOM_NAMESPACE_API_RESOURCE_API_GROUP value: core.cozystack.io - name: CUSTOM_NAMESPACE_API_RESOURCE_API_VERSION value: v1alpha1 - - name: CUSTOM_NAMESPACE_API_RESOURCE_RESOURCE_NAME + - name: CUSTOM_NAMESPACE_API_RESOURCE_PLURAL value: tenantnamespaces + - name: BASE_FACTORY_NAMESPACED_API_KEY + value: base-factory-namespaced-api + - name: BASE_FACTORY_CLUSTERSCOPED_API_KEY + value: base-factory-clusterscoped-api + - name: BASE_FACTORY_NAMESPACED_BUILTIN_KEY + value: base-factory-namespaced-builtin + - name: BASE_FACTORY_CLUSTERSCOPED_BUILTIN_KEY + value: base-factory-clusterscoped-builtin + - name: BASE_NAMESPACE_FACTORY_KEY + value: base-factory-clusterscoped-builtin - name: USE_NAMESPACE_NAV value: "true" + - name: USE_NEW_NAVIGATION + value: "true" + - name: HIDE_NAVIGATION + value: "true" - name: LOGIN_URL value: "/oauth2/userinfo" - name: LOGOUT_URL @@ -225,17 +262,13 @@ spec: type: RuntimeDefault terminationMessagePath: /dev/termination-log terminationMessagePolicy: File - volumeMounts: null dnsPolicy: ClusterFirst enableServiceLinks: false hostIPC: false hostNetwork: false hostPID: false - preemptionPolicy: null priorityClassName: system-cluster-critical restartPolicy: Always - runtimeClassName: null schedulerName: default-scheduler serviceAccountName: incloud-web-web terminationGracePeriodSeconds: 30 - volumes: null From 8e8bea039f79f5b15d7358a74111df23225eb502 Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Sun, 15 Feb 2026 21:34:19 +0100 Subject: [PATCH 047/666] feat(vpc): migrate subnets definition from map to array format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change VPC subnets from map[string]Subnet to []Subnet with explicit name field, aligning with the vm-instance subnet format. Map format: subnets: {mysubnet: {cidr: "x"}} Array format: subnets: [{name: mysubnet, cidr: "x"}] Subnet ID generation (sha256 of namespace/vpcId/subnetName) remains unchanged — subnetName now comes from .name field instead of map key. ConfigMap output format stays the same. Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- packages/apps/vpc/templates/vpc.yaml | 16 ++++++++-------- packages/apps/vpc/values.schema.json | 13 ++++++++++--- packages/apps/vpc/values.yaml | 9 +++++---- .../cozyrds/virtualprivatecloud.yaml | 2 +- 4 files changed, 24 insertions(+), 16 deletions(-) diff --git a/packages/apps/vpc/templates/vpc.yaml b/packages/apps/vpc/templates/vpc.yaml index eb113b4f..6c0d2249 100644 --- a/packages/apps/vpc/templates/vpc.yaml +++ b/packages/apps/vpc/templates/vpc.yaml @@ -16,8 +16,8 @@ spec: namespaces: - {{ .Release.Namespace }} -{{- range $subnetName, $subnetConfig := .Values.subnets }} -{{- $subnetId := print "subnet-" (print $.Release.Namespace "/" $vpcId "/" $subnetName | sha256sum | trunc 8) }} +{{- range .Values.subnets }} +{{- $subnetId := print "subnet-" (print $.Release.Namespace "/" $vpcId "/" .name | sha256sum | trunc 8) }} --- apiVersion: k8s.cni.cncf.io/v1 kind: NetworkAttachmentDefinition @@ -25,7 +25,7 @@ metadata: name: {{ $subnetId }} namespace: {{ $.Release.Namespace }} labels: - cozystack.io/subnetName: {{ $subnetName }} + cozystack.io/subnetName: {{ .name }} cozystack.io/vpcId: {{ $vpcId }} cozystack.io/vpcName: {{ $.Release.Name }} cozystack.io/tenantName: {{ $.Release.Namespace }} @@ -42,13 +42,13 @@ kind: Subnet metadata: name: {{ $subnetId }} labels: - cozystack.io/subnetName: {{ $subnetName }} + cozystack.io/subnetName: {{ .name }} cozystack.io/vpcId: {{ $vpcId }} cozystack.io/vpcName: {{ $.Release.Name }} cozystack.io/tenantName: {{ $.Release.Namespace }} spec: vpc: {{ $vpcId }} - cidrBlock: {{ $subnetConfig.cidr }} + cidrBlock: {{ .cidr }} provider: "{{ $subnetId }}.{{ $.Release.Namespace }}.ovn" protocol: IPv4 enableLb: false @@ -66,9 +66,9 @@ metadata: cozystack.io/vpcId: {{ $vpcId }} cozystack.io/tenantName: {{ $.Release.Namespace }} data: - {{- range $subnetName, $subnetConfig := .Values.subnets }} - {{ $subnetName }}.ID: {{ print "subnet-" (print $.Release.Namespace "/" $vpcId "/" $subnetName | sha256sum | trunc 8) }} - {{ $subnetName }}.CIDR: {{ $subnetConfig.cidr }} + {{- range .Values.subnets }} + {{ .name }}.ID: {{ print "subnet-" (print $.Release.Namespace "/" $vpcId "/" .name | sha256sum | trunc 8) }} + {{ .name }}.CIDR: {{ .cidr }} {{- end }} --- apiVersion: rbac.authorization.k8s.io/v1 diff --git a/packages/apps/vpc/values.schema.json b/packages/apps/vpc/values.schema.json index 3e888de4..a975d2d7 100644 --- a/packages/apps/vpc/values.schema.json +++ b/packages/apps/vpc/values.schema.json @@ -4,14 +4,21 @@ "properties": { "subnets": { "description": "Subnets of a VPC", - "type": "object", - "default": {}, - "additionalProperties": { + "type": "array", + "default": [], + "items": { "type": "object", + "required": [ + "name" + ], "properties": { "cidr": { "description": "IP address range", "type": "string" + }, + "name": { + "description": "Subnet name", + "type": "string" } } } diff --git a/packages/apps/vpc/values.yaml b/packages/apps/vpc/values.yaml index e2bd5faa..3af8b26b 100644 --- a/packages/apps/vpc/values.yaml +++ b/packages/apps/vpc/values.yaml @@ -3,13 +3,14 @@ ## ## @typedef {struct} Subnet - Subnet of a VPC +## @field {string} name - Subnet name ## @field {string} [cidr] - IP address range -## @param {map[string]Subnet} subnets - Subnets of a VPC -subnets: {} +## @param {[]Subnet} subnets - Subnets of a VPC +subnets: [] ## Example: ## subnets: -## mysubnet0: +## - name: mysubnet0 ## cidr: "172.16.0.0/24" -## mysubnet1: +## - name: mysubnet1 ## cidr: "172.16.1.0/24" diff --git a/packages/system/virtualprivatecloud-rd/cozyrds/virtualprivatecloud.yaml b/packages/system/virtualprivatecloud-rd/cozyrds/virtualprivatecloud.yaml index 3f53f984..d5694a2c 100644 --- a/packages/system/virtualprivatecloud-rd/cozyrds/virtualprivatecloud.yaml +++ b/packages/system/virtualprivatecloud-rd/cozyrds/virtualprivatecloud.yaml @@ -8,7 +8,7 @@ spec: plural: virtualprivateclouds singular: virtualprivatecloud openAPISchema: |- - {"title":"Chart Values","type":"object","properties":{"subnets":{"description":"Subnets of a VPC","type":"object","default":{},"additionalProperties":{"type":"object","properties":{"cidr":{"description":"IP address range","type":"string"}}}}}} + {"title":"Chart Values","type":"object","properties":{"subnets":{"description":"Subnets of a VPC","type":"array","default":[],"items":{"type":"object","required":["name"],"properties":{"cidr":{"description":"IP address range","type":"string"},"name":{"description":"Subnet name","type":"string"}}}}}} release: prefix: "virtualprivatecloud-" labels: From 9031de05380be6b1560dae57c8c2e79441f50c33 Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Sun, 15 Feb 2026 21:34:26 +0100 Subject: [PATCH 048/666] feat(platform): add migration 30 for VPC subnets map-to-array conversion Add migration script that converts VPC HelmRelease values from map format to array format. The script discovers all VirtualPrivateCloud HelmReleases, reads their values Secrets, and converts subnets using yq. Idempotent: skips if subnets are already an array or null. Bumps migration targetVersion from 30 to 31. Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- .../platform/images/migrations/migrations/30 | 116 ++++++++++++++++++ packages/core/platform/values.yaml | 2 +- 2 files changed, 117 insertions(+), 1 deletion(-) create mode 100755 packages/core/platform/images/migrations/migrations/30 diff --git a/packages/core/platform/images/migrations/migrations/30 b/packages/core/platform/images/migrations/migrations/30 new file mode 100755 index 00000000..64b89305 --- /dev/null +++ b/packages/core/platform/images/migrations/migrations/30 @@ -0,0 +1,116 @@ +#!/bin/bash +# Migration 30 --> 31 +# Convert VPC subnets from map format to array format in HelmRelease values. +# Map format: subnets: {name: {cidr: x}} +# Array format: subnets: [{name: name, cidr: x}] +# Idempotent: skips if subnets is already an array or empty/null. + +set -euo pipefail + +# ============================================================ +# STEP 1: Discover all VirtualPrivateCloud HelmReleases +# ============================================================ +echo "=== Discovering VirtualPrivateCloud HelmReleases ===" +INSTANCES=() +while IFS=/ read -r ns name; do + [ -z "$ns" ] && continue + INSTANCES+=("${ns}/${name}") + echo " Found: ${ns}/${name}" +done < <(kubectl get hr -A -l "apps.cozystack.io/application.kind=VirtualPrivateCloud" \ + -o jsonpath='{range .items[*]}{.metadata.namespace}/{.metadata.name}{"\n"}{end}' 2>/dev/null) + +if [ ${#INSTANCES[@]} -eq 0 ]; then + echo " No VirtualPrivateCloud HelmReleases found. Nothing to migrate." + kubectl create configmap -n cozy-system cozystack-version \ + --from-literal=version=31 --dry-run=client -o yaml | kubectl apply -f- + exit 0 +fi +echo " Total: ${#INSTANCES[@]} instance(s)" + +# ============================================================ +# STEP 2: Migrate each instance +# ============================================================ +for entry in "${INSTANCES[@]}"; do + NAMESPACE="${entry%%/*}" + HR_NAME="${entry#*/}" + + echo "" + echo "======================================================================" + echo "=== Processing: ${HR_NAME} in ${NAMESPACE}" + echo "======================================================================" + + # --- Find values Secret --- + VALUES_SECRET=$(kubectl -n "$NAMESPACE" get hr "$HR_NAME" -o json | \ + jq -r '.spec.valuesFrom // [] | map(select(.kind == "Secret" and (.name | test("cozystack-values") | not))) | .[0].name // ""') + VALUES_KEY=$(kubectl -n "$NAMESPACE" get hr "$HR_NAME" -o json | \ + jq -r '.spec.valuesFrom // [] | map(select(.kind == "Secret" and (.name | test("cozystack-values") | not))) | .[0].valuesKey // "values.yaml"') + + if [ -z "$VALUES_SECRET" ]; then + echo " [SKIP] No values Secret found for hr/${HR_NAME}" + continue + fi + + if ! kubectl -n "$NAMESPACE" get secret "$VALUES_SECRET" --no-headers 2>/dev/null | grep -q .; then + echo " [SKIP] Secret ${VALUES_SECRET} not found" + continue + fi + + echo " Reading values from secret: ${VALUES_SECRET} (key: ${VALUES_KEY})" + + # --- Decode current values --- + VALUES_YAML=$(kubectl -n "$NAMESPACE" get secret "$VALUES_SECRET" \ + -o jsonpath="{.data.${VALUES_KEY}}" 2>/dev/null | base64 -d 2>/dev/null || true) + if [ -z "$VALUES_YAML" ]; then + VALUES_YAML=$(kubectl -n "$NAMESPACE" get secret "$VALUES_SECRET" \ + -o jsonpath="{.stringData.${VALUES_KEY}}" 2>/dev/null || true) + fi + + if [ -z "$VALUES_YAML" ]; then + echo " [SKIP] Could not read values from secret" + continue + fi + + # --- Check subnets type --- + SUBNETS_TYPE=$(echo "$VALUES_YAML" | yq -r '.subnets | type') + + case "$SUBNETS_TYPE" in + "!!map"|"object") + echo " [CONVERT] subnets is a map, converting to array" + ;; + "!!seq"|"array") + echo " [SKIP] subnets is already an array" + continue + ;; + "!!null"|"null") + echo " [SKIP] subnets is null/empty" + continue + ;; + *) + echo " [SKIP] subnets has unexpected type: ${SUBNETS_TYPE}" + continue + ;; + esac + + # --- Convert map to array --- + # {name: {cidr: x}} -> [{name: name, cidr: x}] + NEW_VALUES=$(echo "$VALUES_YAML" | yq ' + .subnets = ([.subnets | to_entries[] | {"name": .key} + .value]) + ') + + # --- Patch the Secret --- + NEW_VALUES_B64=$(echo "$NEW_VALUES" | base64) + echo " [PATCH] Updating secret/${VALUES_SECRET}" + kubectl -n "$NAMESPACE" get secret "$VALUES_SECRET" -o json | \ + jq --arg key "$VALUES_KEY" --arg val "$NEW_VALUES_B64" \ + '.data[$key] = $val' | \ + kubectl apply -f - + + echo " [OK] Converted subnets for ${HR_NAME}" +done + +echo "" +echo "=== Migration complete (${#INSTANCES[@]} instance(s)) ===" + +# Stamp version +kubectl create configmap -n cozy-system cozystack-version \ + --from-literal=version=31 --dry-run=client -o yaml | kubectl apply -f- diff --git a/packages/core/platform/values.yaml b/packages/core/platform/values.yaml index b90c001b..74758eb0 100644 --- a/packages/core/platform/values.yaml +++ b/packages/core/platform/values.yaml @@ -6,7 +6,7 @@ sourceRef: migrations: enabled: false image: ghcr.io/cozystack/cozystack/platform-migrations:v1.0.0-beta.4@sha256:f404d05834907b9b2695bbb37732bd1ae2e79b03da77dc91bb3fbc0fbc53dcbf - targetVersion: 30 + targetVersion: 31 # Bundle deployment configuration bundles: system: From cf505c580d26ceea10b53c2df76fd2b2357a0115 Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Sun, 15 Feb 2026 22:50:24 +0100 Subject: [PATCH 049/666] Update kilo v0.8.0 Signed-off-by: Andrei Kvapil --- packages/system/kilo/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/system/kilo/values.yaml b/packages/system/kilo/values.yaml index 762f3c26..e648e32e 100644 --- a/packages/system/kilo/values.yaml +++ b/packages/system/kilo/values.yaml @@ -1,7 +1,7 @@ kilo: image: pullPolicy: IfNotPresent - tag: v0.7.1@sha256:c2160097ef3aa4e9e460430665b743a14b8f19480211edab6f946264718340ff + tag: v0.8.0@sha256:d673c46ba0ff762bc31491f53b81c7e5f59b6b58560eb258df7cf55e480ebdd4 repository: ghcr.io/cozystack/cozystack/kilo podCIDR: 10.244.0.0/16 serviceCIDR: 10.96.0.0/16 From ef040c2ed283ff38022727782fef0652b50b84e8 Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Mon, 16 Feb 2026 15:16:38 +0100 Subject: [PATCH 050/666] feat(kilo): add Cilium compatibility variant Add a new "cilium" variant to the kilo PackageSource that deploys kilo with --compatibility=cilium flag. This enables Cilium-aware IPIP encapsulation, routing outer packets through Cilium's VxLAN overlay instead of the host network. Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- packages/core/platform/sources/kilo.yaml | 13 +++++++++++++ packages/system/kilo/templates/kilo.yaml | 3 +++ packages/system/kilo/values-cilium.yaml | 2 ++ packages/system/kilo/values.yaml | 2 +- 4 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 packages/system/kilo/values-cilium.yaml diff --git a/packages/core/platform/sources/kilo.yaml b/packages/core/platform/sources/kilo.yaml index 72602164..95770667 100644 --- a/packages/core/platform/sources/kilo.yaml +++ b/packages/core/platform/sources/kilo.yaml @@ -20,3 +20,16 @@ spec: privileged: true namespace: cozy-kilo releaseName: kilo + - name: cilium + dependsOn: + - cozystack.networking + components: + - name: kilo + path: system/kilo + valuesFiles: + - values.yaml + - values-cilium.yaml + install: + privileged: true + namespace: cozy-kilo + releaseName: kilo diff --git a/packages/system/kilo/templates/kilo.yaml b/packages/system/kilo/templates/kilo.yaml index e209aed2..fef7a718 100644 --- a/packages/system/kilo/templates/kilo.yaml +++ b/packages/system/kilo/templates/kilo.yaml @@ -29,6 +29,9 @@ spec: - --hostname=$(NODE_NAME) - --cni=false - --clean-up-interface={{ .Values.kilo.cleanUpInterface | default "false" }} + {{- with .Values.kilo.compatibility }} + - --compatibility={{ . }} + {{- end }} - --encapsulate=crosssubnet - --local=false - --mesh-granularity={{ .Values.kilo.meshGranularity | default "location" }} diff --git a/packages/system/kilo/values-cilium.yaml b/packages/system/kilo/values-cilium.yaml new file mode 100644 index 00000000..1a75fc41 --- /dev/null +++ b/packages/system/kilo/values-cilium.yaml @@ -0,0 +1,2 @@ +kilo: + compatibility: cilium diff --git a/packages/system/kilo/values.yaml b/packages/system/kilo/values.yaml index 762f3c26..4057c284 100644 --- a/packages/system/kilo/values.yaml +++ b/packages/system/kilo/values.yaml @@ -1,7 +1,7 @@ kilo: image: pullPolicy: IfNotPresent - tag: v0.7.1@sha256:c2160097ef3aa4e9e460430665b743a14b8f19480211edab6f946264718340ff + tag: v0.8.1@sha256:56602fa796eccea0d0e005c29e5c7094ae46d15bd5306b02b829672b178dcd5c repository: ghcr.io/cozystack/cozystack/kilo podCIDR: 10.244.0.0/16 serviceCIDR: 10.96.0.0/16 From 7ca6e5ce9e4ea49a73b28ae86ab46bc38dafd0fa Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Mon, 16 Feb 2026 18:47:04 +0300 Subject: [PATCH 051/666] feat(installer): add variant-aware templates for generic Kubernetes support Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- Makefile | 5 +++-- packages/core/installer/.helmignore | 11 +++++++++++ packages/core/installer/Makefile | 8 +++++++- .../installer/templates/cozystack-operator.yaml | 15 +++------------ .../core/installer/templates/packagesource.yaml | 4 ++++ packages/core/installer/values.yaml | 10 ++++++++++ 6 files changed, 38 insertions(+), 15 deletions(-) create mode 100644 packages/core/installer/.helmignore diff --git a/Makefile b/Makefile index e6c50458..12554861 100644 --- a/Makefile +++ b/Makefile @@ -48,15 +48,16 @@ manifests: > _out/assets/cozystack-operator-talos.yaml # Generic Kubernetes variant (k3s, kubeadm, RKE2) helm template installer packages/core/installer -n cozy-system \ + --set cozystackOperator.variant=generic \ + --set cozystack.apiServerHost=REPLACE_ME \ -s templates/cozystack-operator.yaml \ -s templates/packagesource.yaml \ - --set cozystackOperator.variant=generic \ > _out/assets/cozystack-operator-generic.yaml # Hosted variant (managed Kubernetes) helm template installer packages/core/installer -n cozy-system \ + --set cozystackOperator.variant=hosted \ -s templates/cozystack-operator.yaml \ -s templates/packagesource.yaml \ - --set cozystackOperator.variant=hosted \ > _out/assets/cozystack-operator-hosted.yaml cozypkg: diff --git a/packages/core/installer/.helmignore b/packages/core/installer/.helmignore new file mode 100644 index 00000000..ba88ffe8 --- /dev/null +++ b/packages/core/installer/.helmignore @@ -0,0 +1,11 @@ +# VCS and IDE +.git +.gitignore + +# Build artifacts +Makefile +images/ +example/ +*.tgz + +# NOTE: definitions/ is intentionally NOT excluded — needed by crds.yaml via .Files.Glob diff --git a/packages/core/installer/Makefile b/packages/core/installer/Makefile index ad9982b5..7038bb9c 100644 --- a/packages/core/installer/Makefile +++ b/packages/core/installer/Makefile @@ -15,7 +15,7 @@ apply: diff: cozyhr show --namespace $(NAMESPACE) $(NAME) --plain | kubectl diff -f - -image: pre-checks image-operator image-packages +image: pre-checks image-operator image-packages chart image-operator: docker buildx build -f images/cozystack-operator/Dockerfile ../../.. \ @@ -43,3 +43,9 @@ image-packages: test -n "$$DIGEST" && \ yq -i '.cozystackOperator.platformSourceUrl = strenv(REPO)' values.yaml && \ yq -i '.cozystackOperator.platformSourceRef = "digest=" + strenv(DIGEST)' values.yaml + +chart: + set -e; \ + PKG=$$(helm package . --version $(COZYSTACK_VERSION) | awk '{print $$NF}'); \ + trap 'rm -f "$$PKG"' EXIT; \ + if [ "$(PUSH)" = "1" ]; then helm push "$$PKG" oci://$(REGISTRY); fi diff --git a/packages/core/installer/templates/cozystack-operator.yaml b/packages/core/installer/templates/cozystack-operator.yaml index 96ce9b87..4568e18b 100644 --- a/packages/core/installer/templates/cozystack-operator.yaml +++ b/packages/core/installer/templates/cozystack-operator.yaml @@ -72,20 +72,11 @@ spec: value: "7445" {{- else if eq .Values.cozystackOperator.variant "generic" }} env: - # Generic Kubernetes: read from ConfigMap - # Create cozystack-operator-config ConfigMap before applying this manifest + # Generic Kubernetes: API server endpoint - name: KUBERNETES_SERVICE_HOST - valueFrom: - configMapKeyRef: - name: cozystack-operator-config - key: KUBERNETES_SERVICE_HOST - optional: false + value: {{ required "cozystack.apiServerHost is required in generic mode" .Values.cozystack.apiServerHost | quote }} - name: KUBERNETES_SERVICE_PORT - valueFrom: - configMapKeyRef: - name: cozystack-operator-config - key: KUBERNETES_SERVICE_PORT - optional: false + value: {{ .Values.cozystack.apiServerPort | quote }} {{- else if eq .Values.cozystackOperator.variant "hosted" }} # Hosted: use in-cluster service account, no env override needed env: [] diff --git a/packages/core/installer/templates/packagesource.yaml b/packages/core/installer/templates/packagesource.yaml index f4a5f6d0..65b1e4bf 100644 --- a/packages/core/installer/templates/packagesource.yaml +++ b/packages/core/installer/templates/packagesource.yaml @@ -1,3 +1,7 @@ +{{- $validVariants := list "talos" "generic" "hosted" -}} +{{- if not (has .Values.cozystackOperator.variant $validVariants) -}} +{{- fail (printf "Invalid cozystackOperator.variant %q: must be one of talos, generic, hosted" .Values.cozystackOperator.variant) -}} +{{- end -}} --- apiVersion: cozystack.io/v1alpha1 kind: PackageSource diff --git a/packages/core/installer/values.yaml b/packages/core/installer/values.yaml index 64ff817b..95633e8e 100644 --- a/packages/core/installer/values.yaml +++ b/packages/core/installer/values.yaml @@ -4,3 +4,13 @@ cozystackOperator: image: ghcr.io/cozystack/cozystack/cozystack-operator:v1.0.0-beta.4@sha256:322dd7358df369525f76e6e43512482e38caec5315d36a878399d2d60bf2f18d platformSourceUrl: 'oci://ghcr.io/cozystack/cozystack/cozystack-packages' platformSourceRef: 'digest=sha256:b88502242b535a31ab33c06ffc0a96d1c67230d2db2c5e873fa23f6523592ff6' + +# Generic variant configuration (only used when cozystackOperator.variant=generic) +cozystack: + # Kubernetes API server host (IP only, no protocol/port) + # Must be the INTERNAL IP of the control-plane node + # (the IP visible on the node's network interface, not a public/NAT IP) + # Used by the operator and networking components (cilium, kube-ovn) + apiServerHost: "" + # Kubernetes API server port + apiServerPort: "6443" From bae70596fc0e121afa31d5d9a8e35961e430cf17 Mon Sep 17 00:00:00 2001 From: cozystack-bot <217169706+cozystack-bot@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:01:30 +0000 Subject: [PATCH 052/666] Prepare release v1.0.0-beta.5 Signed-off-by: cozystack-bot <217169706+cozystack-bot@users.noreply.github.com> --- packages/core/installer/values.yaml | 5 ++--- packages/core/platform/values.yaml | 2 +- packages/core/testing/values.yaml | 2 +- packages/extra/bootbox/images/matchbox.tag | 2 +- packages/extra/seaweedfs/images/objectstorage-sidecar.tag | 2 +- packages/system/backup-controller/values.yaml | 2 +- packages/system/backupstrategy-controller/values.yaml | 2 +- packages/system/cozystack-api/values.yaml | 2 +- packages/system/cozystack-controller/values.yaml | 2 +- packages/system/dashboard/templates/configmap.yaml | 2 +- packages/system/dashboard/values.yaml | 6 +++--- .../system/grafana-operator/images/grafana-dashboards.tag | 2 +- packages/system/kamaji/values.yaml | 4 ++-- packages/system/kubeovn-plunger/values.yaml | 2 +- packages/system/kubeovn-webhook/values.yaml | 2 +- packages/system/lineage-controller-webhook/values.yaml | 2 +- packages/system/objectstorage-controller/values.yaml | 2 +- packages/system/seaweedfs/values.yaml | 2 +- 18 files changed, 22 insertions(+), 23 deletions(-) diff --git a/packages/core/installer/values.yaml b/packages/core/installer/values.yaml index 95633e8e..3dd0f1d6 100644 --- a/packages/core/installer/values.yaml +++ b/packages/core/installer/values.yaml @@ -1,10 +1,9 @@ cozystackOperator: # Deployment variant: talos, generic, hosted variant: talos - image: ghcr.io/cozystack/cozystack/cozystack-operator:v1.0.0-beta.4@sha256:322dd7358df369525f76e6e43512482e38caec5315d36a878399d2d60bf2f18d + image: ghcr.io/cozystack/cozystack/cozystack-operator:v1.0.0-beta.5@sha256:9f3089cb13b3e19dab14b8edc1220efde693f6066d3474c0137953e1873d16f3 platformSourceUrl: 'oci://ghcr.io/cozystack/cozystack/cozystack-packages' - platformSourceRef: 'digest=sha256:b88502242b535a31ab33c06ffc0a96d1c67230d2db2c5e873fa23f6523592ff6' - + platformSourceRef: 'digest=sha256:86daf23f7b2ff2f448b273153733c55ac2f57c2bbe72c779ff14865d71e623cb' # Generic variant configuration (only used when cozystackOperator.variant=generic) cozystack: # Kubernetes API server host (IP only, no protocol/port) diff --git a/packages/core/platform/values.yaml b/packages/core/platform/values.yaml index 74758eb0..ca2d9d1b 100644 --- a/packages/core/platform/values.yaml +++ b/packages/core/platform/values.yaml @@ -5,7 +5,7 @@ sourceRef: path: / migrations: enabled: false - image: ghcr.io/cozystack/cozystack/platform-migrations:v1.0.0-beta.4@sha256:f404d05834907b9b2695bbb37732bd1ae2e79b03da77dc91bb3fbc0fbc53dcbf + image: ghcr.io/cozystack/cozystack/platform-migrations:v1.0.0-beta.5@sha256:e7a9e0f0adc33e0be007af42f50ed3af064aa965ad628e48cc6c7943f31f239d targetVersion: 31 # Bundle deployment configuration bundles: diff --git a/packages/core/testing/values.yaml b/packages/core/testing/values.yaml index 73481ab1..627d3096 100644 --- a/packages/core/testing/values.yaml +++ b/packages/core/testing/values.yaml @@ -1,2 +1,2 @@ e2e: - image: ghcr.io/cozystack/cozystack/e2e-sandbox:v1.0.0-beta.4@sha256:eac71ef0de3450fce96255629e77903630c63ade62b81e7055f1a689f92ee153 + image: ghcr.io/cozystack/cozystack/e2e-sandbox:v1.0.0-beta.5@sha256:eac71ef0de3450fce96255629e77903630c63ade62b81e7055f1a689f92ee153 diff --git a/packages/extra/bootbox/images/matchbox.tag b/packages/extra/bootbox/images/matchbox.tag index 6c3054d5..19014ae6 100644 --- a/packages/extra/bootbox/images/matchbox.tag +++ b/packages/extra/bootbox/images/matchbox.tag @@ -1 +1 @@ -ghcr.io/cozystack/cozystack/matchbox:v1.0.0-beta.4@sha256:cba9a2761c40ae6645e28da4cbc6d91eb86ddc886f4523cf20b3c0e40597d593 +ghcr.io/cozystack/cozystack/matchbox:v1.0.0-beta.5@sha256:8facb6bbbaf336ff3dd606d6bcbf65f5d1fb7f079bec09966544398632005b47 diff --git a/packages/extra/seaweedfs/images/objectstorage-sidecar.tag b/packages/extra/seaweedfs/images/objectstorage-sidecar.tag index bd622108..9f5222a7 100644 --- a/packages/extra/seaweedfs/images/objectstorage-sidecar.tag +++ b/packages/extra/seaweedfs/images/objectstorage-sidecar.tag @@ -1 +1 @@ -ghcr.io/cozystack/cozystack/objectstorage-sidecar:v1.0.0-beta.4@sha256:235b194a531b70e266a10ef78d2955d19f5b659513f23d8b3cfbbc0dff7fc1c0 +ghcr.io/cozystack/cozystack/objectstorage-sidecar:v1.0.0-beta.5@sha256:235b194a531b70e266a10ef78d2955d19f5b659513f23d8b3cfbbc0dff7fc1c0 diff --git a/packages/system/backup-controller/values.yaml b/packages/system/backup-controller/values.yaml index 1bdab88b..63c49ed1 100644 --- a/packages/system/backup-controller/values.yaml +++ b/packages/system/backup-controller/values.yaml @@ -1,5 +1,5 @@ backupController: - image: "ghcr.io/cozystack/cozystack/backup-controller:v1.0.0-beta.4@sha256:2dcd5347683ee88012b0e558b3a240dec9942230e9c673a359e0196f407a0833" + image: "ghcr.io/cozystack/cozystack/backup-controller:v1.0.0-beta.5@sha256:c99ff8ab11c5c016841acd9dca72c7a643dba72a98b8f816ea67ba7fa3549f88" replicas: 2 debug: false metrics: diff --git a/packages/system/backupstrategy-controller/values.yaml b/packages/system/backupstrategy-controller/values.yaml index 21d1a9f3..5ff8efd5 100644 --- a/packages/system/backupstrategy-controller/values.yaml +++ b/packages/system/backupstrategy-controller/values.yaml @@ -1,5 +1,5 @@ backupStrategyController: - image: "ghcr.io/cozystack/cozystack/backupstrategy-controller:v1.0.0-beta.4@sha256:c2c150918e5609f1d6663f56b6dcbdac47bee33154524230001fcd04165f4268" + image: "ghcr.io/cozystack/cozystack/backupstrategy-controller:v1.0.0-beta.5@sha256:cd92a2620c9965512977b57c2829d931e7ecb7a8afc0693d7c8ab39bd8ff77d8" replicas: 2 debug: false metrics: diff --git a/packages/system/cozystack-api/values.yaml b/packages/system/cozystack-api/values.yaml index d9041546..511d1f33 100644 --- a/packages/system/cozystack-api/values.yaml +++ b/packages/system/cozystack-api/values.yaml @@ -1,3 +1,3 @@ cozystackAPI: - image: ghcr.io/cozystack/cozystack/cozystack-api:v1.0.0-beta.4@sha256:42f8d8120f7fd3bfe37f114eedf5ca8df62ac69a0d922d91a2c392772f3b46ee + image: ghcr.io/cozystack/cozystack/cozystack-api:v1.0.0-beta.5@sha256:b217c3d1cca7e35b9e6ee32297ebe923fee12ca48d94062484a9dfcffda0e2e3 replicas: 2 diff --git a/packages/system/cozystack-controller/values.yaml b/packages/system/cozystack-controller/values.yaml index 53ed1033..5ead5eae 100644 --- a/packages/system/cozystack-controller/values.yaml +++ b/packages/system/cozystack-controller/values.yaml @@ -1,4 +1,4 @@ cozystackController: - image: ghcr.io/cozystack/cozystack/cozystack-controller:v1.0.0-beta.4@sha256:915d07ef61e1fc3bdf87e4bfc4b8ae3920e7e33d74082778c7735ba36a08cdef + image: ghcr.io/cozystack/cozystack/cozystack-controller:v1.0.0-beta.5@sha256:edeb1788395d650f5f14a935c8f6b95ede7ca548cba198c18c75cc5eafd89104 debug: false disableTelemetry: false diff --git a/packages/system/dashboard/templates/configmap.yaml b/packages/system/dashboard/templates/configmap.yaml index 1c79b2cb..c32fc638 100644 --- a/packages/system/dashboard/templates/configmap.yaml +++ b/packages/system/dashboard/templates/configmap.yaml @@ -1,6 +1,6 @@ {{- $brandingConfig := .Values._cluster.branding | default dict }} -{{- $tenantText := "v1.0.0-beta.4" }} +{{- $tenantText := "v1.0.0-beta.5" }} {{- $footerText := "Cozystack" }} {{- $titleText := "Cozystack Dashboard" }} {{- $logoText := "" }} diff --git a/packages/system/dashboard/values.yaml b/packages/system/dashboard/values.yaml index 0c898a91..a2f9c642 100644 --- a/packages/system/dashboard/values.yaml +++ b/packages/system/dashboard/values.yaml @@ -1,6 +1,6 @@ openapiUI: - image: ghcr.io/cozystack/cozystack/openapi-ui:v1.0.0-beta.4@sha256:adbd07c7bde083fbf3a2fb2625ec4adbe6718a0f6f643b2505cc0029c2c0f724 + image: ghcr.io/cozystack/cozystack/openapi-ui:v1.0.0-beta.5@sha256:0d331986ac06de1fa5b660d69670cba0f2a59fcdab6cf43cb136827ecb8fcfe8 openapiUIK8sBff: - image: ghcr.io/cozystack/cozystack/openapi-ui-k8s-bff:v1.0.0-beta.4@sha256:1f7827a1978bd9c81ac924dd0e78f6a3ce834a9a64af55047e220812bc15a944 + image: ghcr.io/cozystack/cozystack/openapi-ui-k8s-bff:v1.0.0-beta.5@sha256:1b3ea6d4c7dbbe6a8def3b2807fffdfab2ac4afc39d7a846e57dd491fa168f92 tokenProxy: - image: ghcr.io/cozystack/cozystack/token-proxy:v1.0.0-beta.4@sha256:2e280991e07853ea48f97b0a42946afffa10d03d6a83d41099ed83e6ffc94fdc + image: ghcr.io/cozystack/cozystack/token-proxy:v1.0.0-beta.5@sha256:2e280991e07853ea48f97b0a42946afffa10d03d6a83d41099ed83e6ffc94fdc diff --git a/packages/system/grafana-operator/images/grafana-dashboards.tag b/packages/system/grafana-operator/images/grafana-dashboards.tag index cc81b24f..34111c9f 100644 --- a/packages/system/grafana-operator/images/grafana-dashboards.tag +++ b/packages/system/grafana-operator/images/grafana-dashboards.tag @@ -1 +1 @@ -ghcr.io/cozystack/cozystack/grafana-dashboards:v1.0.0-beta.4@sha256:e866b5b3874b9d390b341183d2ee070e1387440c14cfe51af831695def6dc2ec +ghcr.io/cozystack/cozystack/grafana-dashboards:v1.0.0-beta.5@sha256:e866b5b3874b9d390b341183d2ee070e1387440c14cfe51af831695def6dc2ec diff --git a/packages/system/kamaji/values.yaml b/packages/system/kamaji/values.yaml index 26e8bfcf..cbf71457 100644 --- a/packages/system/kamaji/values.yaml +++ b/packages/system/kamaji/values.yaml @@ -3,7 +3,7 @@ kamaji: deploy: false image: pullPolicy: IfNotPresent - tag: v1.0.0-beta.4@sha256:05f8e166dc94aadb74cd6448f6f96fbd2167057a559097516a41b8f53e434918 + tag: v1.0.0-beta.5@sha256:05f8e166dc94aadb74cd6448f6f96fbd2167057a559097516a41b8f53e434918 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:v1.0.0-beta.4@sha256:05f8e166dc94aadb74cd6448f6f96fbd2167057a559097516a41b8f53e434918 + - --migrate-image=ghcr.io/cozystack/cozystack/kamaji:v1.0.0-beta.5@sha256:05f8e166dc94aadb74cd6448f6f96fbd2167057a559097516a41b8f53e434918 diff --git a/packages/system/kubeovn-plunger/values.yaml b/packages/system/kubeovn-plunger/values.yaml index b325a7b7..0abcc7db 100644 --- a/packages/system/kubeovn-plunger/values.yaml +++ b/packages/system/kubeovn-plunger/values.yaml @@ -1,4 +1,4 @@ portSecurity: true routes: "" -image: ghcr.io/cozystack/cozystack/kubeovn-plunger:v1.0.0-beta.4@sha256:16b362d6fa1ca30c791ea5cfc7984e085b9ea76a24c9abb3b05dad5c8b64bd0e +image: ghcr.io/cozystack/cozystack/kubeovn-plunger:v1.0.0-beta.5@sha256:67d615f86c3230e8c643b6bd347093cdb1e575c99f7c63599fd43e0c4ffc249a ovnCentralName: ovn-central diff --git a/packages/system/kubeovn-webhook/values.yaml b/packages/system/kubeovn-webhook/values.yaml index d5d8ba93..26613989 100644 --- a/packages/system/kubeovn-webhook/values.yaml +++ b/packages/system/kubeovn-webhook/values.yaml @@ -1,3 +1,3 @@ portSecurity: true routes: "" -image: ghcr.io/cozystack/cozystack/kubeovn-webhook:v1.0.0-beta.4@sha256:e18f9fd679e38f65362a8d0042f25468272f6d081136ad47027168d8e7e07a4a +image: ghcr.io/cozystack/cozystack/kubeovn-webhook:v1.0.0-beta.5@sha256:e6334c29d3aaf0dea766c88e3e05b53ad623d1bb497b3c836e6f76adade45b29 diff --git a/packages/system/lineage-controller-webhook/values.yaml b/packages/system/lineage-controller-webhook/values.yaml index 00e593eb..d5f28f89 100644 --- a/packages/system/lineage-controller-webhook/values.yaml +++ b/packages/system/lineage-controller-webhook/values.yaml @@ -1,5 +1,5 @@ lineageControllerWebhook: - image: ghcr.io/cozystack/cozystack/lineage-controller-webhook:v1.0.0-beta.4@sha256:e9054f9137fd73039b2d91a979f672c9258336f91e51440a77aaa09e6e0b5254 + image: ghcr.io/cozystack/cozystack/lineage-controller-webhook:v1.0.0-beta.5@sha256:9dd5411d222fe7d7b0583a8e6807bf7745eb1412bf180827c5f1957e6ebbfeb4 debug: false localK8sAPIEndpoint: enabled: true diff --git a/packages/system/objectstorage-controller/values.yaml b/packages/system/objectstorage-controller/values.yaml index 18dda28f..b3c9d4c6 100644 --- a/packages/system/objectstorage-controller/values.yaml +++ b/packages/system/objectstorage-controller/values.yaml @@ -1,3 +1,3 @@ objectstorage: controller: - image: "ghcr.io/cozystack/cozystack/objectstorage-controller:v1.0.0-beta.4@sha256:b4c972769afda76c48b58e7acf0ac66a0abf16a622f245c60338f432872f640a" + image: "ghcr.io/cozystack/cozystack/objectstorage-controller:v1.0.0-beta.5@sha256:b4c972769afda76c48b58e7acf0ac66a0abf16a622f245c60338f432872f640a" diff --git a/packages/system/seaweedfs/values.yaml b/packages/system/seaweedfs/values.yaml index 63199e28..94aba339 100644 --- a/packages/system/seaweedfs/values.yaml +++ b/packages/system/seaweedfs/values.yaml @@ -177,7 +177,7 @@ seaweedfs: bucketClassName: "seaweedfs" region: "" sidecar: - image: "ghcr.io/cozystack/cozystack/objectstorage-sidecar:v1.0.0-beta.4@sha256:235b194a531b70e266a10ef78d2955d19f5b659513f23d8b3cfbbc0dff7fc1c0" + image: "ghcr.io/cozystack/cozystack/objectstorage-sidecar:v1.0.0-beta.5@sha256:235b194a531b70e266a10ef78d2955d19f5b659513f23d8b3cfbbc0dff7fc1c0" certificates: commonName: "SeaweedFS CA" ipAddresses: [] From bfba9fb5e70fa2cb33f2ceae1d318ea5780a3f5e Mon Sep 17 00:00:00 2001 From: cozystack-bot <217169706+cozystack-bot@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:08:18 +0000 Subject: [PATCH 053/666] docs: add changelog for v1.0.0-beta.5 Signed-off-by: cozystack-bot <217169706+cozystack-bot@users.noreply.github.com> --- docs/changelogs/v1.0.0-beta.5.md | 36 ++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 docs/changelogs/v1.0.0-beta.5.md diff --git a/docs/changelogs/v1.0.0-beta.5.md b/docs/changelogs/v1.0.0-beta.5.md new file mode 100644 index 00000000..b8bc306c --- /dev/null +++ b/docs/changelogs/v1.0.0-beta.5.md @@ -0,0 +1,36 @@ + + +> **⚠️ Beta Release Warning**: This is a pre-release version intended for testing and early adoption. Breaking changes may occur before the stable v1.0.0 release. + +## Features and Improvements + +* **[installer] Add variant-aware templates for generic Kubernetes support**: Extended the installer chart to support generic and hosted Kubernetes deployments via the existing `cozystackOperator.variant` parameter. When using `variant=generic`, the installer now renders separate templates for the Cozystack operator, skipping Talos-specific components. This enables users to deploy Cozystack on standard Kubernetes distributions and hosted Kubernetes services, expanding platform compatibility beyond Talos Linux ([**@lexfrei**](https://github.com/lexfrei) in #2010). + +* **[kilo] Add Cilium compatibility variant**: Added a new `cilium` variant to the kilo PackageSource that deploys kilo with the `--compatibility=cilium` flag. This enables Cilium-aware IPIP encapsulation where the outer packet IP matches the inner packet source, allowing Cilium's network policies to function correctly with kilo's WireGuard mesh networking. Users can now run kilo alongside Cilium CNI while maintaining full network policy enforcement capabilities ([**@kvaps**](https://github.com/kvaps) in #2055). + +* **[cluster-autoscaler] Enable enforce-node-group-min-size by default**: Enabled the `enforce-node-group-min-size` option for the system cluster-autoscaler chart. This ensures node groups are always scaled up to their configured minimum size, even when current workload demands are lower, preventing unexpected scale-down below minimum thresholds and improving cluster stability for production workloads ([**@kvaps**](https://github.com/kvaps) in #2050). + +* **[dashboard] Upgrade dashboard to version 1.4.0**: Updated the Cozystack dashboard to version 1.4.0 with new features and improvements for better user experience and cluster management capabilities ([**@sircthulhu**](https://github.com/sircthulhu) in #2051). + +## Breaking Changes & Upgrade Notes + +* **[vpc] Migrate subnets definition from map to array format**: Migrated VPC subnets definition from map format (`map[string]Subnet`) to array format (`[]Subnet`) with an explicit `name` field. This aligns VPC subnet definitions with the vm-instance `networks` field pattern and provides more intuitive configuration. Existing VPC deployments are automatically migrated via migration 30, which converts the subnet map to an array while preserving all existing subnet configurations and network connectivity ([**@kvaps**](https://github.com/kvaps) in #2052). + +## Dependencies + +* **[kilo] Update to v0.8.0**: Updated Kilo WireGuard mesh networking to v0.8.0 with performance improvements, bug fixes, and new compatibility features ([**@kvaps**](https://github.com/kvaps) in #2053). + +* **[talm] Skip config loading for __complete command**: Fixed CLI completion behavior by skipping config loading for the `__complete` command, preventing errors during shell completion when configuration files are not available or misconfigured ([**@kitsunoff**](https://github.com/kitsunoff) in cozystack/talm#109). + +## Contributors + +We'd like to thank all contributors who made this release possible: + +* [**@kitsunoff**](https://github.com/kitsunoff) +* [**@kvaps**](https://github.com/kvaps) +* [**@lexfrei**](https://github.com/lexfrei) +* [**@sircthulhu**](https://github.com/sircthulhu) + +**Full Changelog**: https://github.com/cozystack/cozystack/compare/v1.0.0-beta.4...v1.0.0-beta.5 From 73b8946a7e2c398f22bb02342788ee6cf64831e6 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Mon, 16 Feb 2026 23:01:22 +0300 Subject: [PATCH 054/666] chore(codegen): regenerate stale deepcopy and CRD definitions Run make generate to bring generated files up to date with current API types. This was pre-existing staleness unrelated to any code change. Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- .../v1alpha1/zz_generated.deepcopy.go | 118 +++++++++--------- .../backups.cozystack.io_backupclasses.yaml | 2 +- 2 files changed, 60 insertions(+), 60 deletions(-) diff --git a/api/dashboard/v1alpha1/zz_generated.deepcopy.go b/api/dashboard/v1alpha1/zz_generated.deepcopy.go index 568a1780..25d97a79 100644 --- a/api/dashboard/v1alpha1/zz_generated.deepcopy.go +++ b/api/dashboard/v1alpha1/zz_generated.deepcopy.go @@ -159,6 +159,65 @@ func (in *BreadcrumbList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CFOMapping) DeepCopyInto(out *CFOMapping) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CFOMapping. +func (in *CFOMapping) DeepCopy() *CFOMapping { + if in == nil { + return nil + } + out := new(CFOMapping) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CFOMapping) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CFOMappingList) DeepCopyInto(out *CFOMappingList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]CFOMapping, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CFOMappingList. +func (in *CFOMappingList) DeepCopy() *CFOMappingList { + if in == nil { + return nil + } + out := new(CFOMappingList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CFOMappingList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CommonStatus) DeepCopyInto(out *CommonStatus) { *out = *in @@ -417,65 +476,6 @@ func (in *FactoryList) DeepCopyObject() runtime.Object { return nil } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *CFOMapping) DeepCopyInto(out *CFOMapping) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - in.Status.DeepCopyInto(&out.Status) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CFOMapping. -func (in *CFOMapping) DeepCopy() *CFOMapping { - if in == nil { - return nil - } - out := new(CFOMapping) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *CFOMapping) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *CFOMappingList) DeepCopyInto(out *CFOMappingList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]CFOMapping, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CFOMappingList. -func (in *CFOMappingList) DeepCopy() *CFOMappingList { - if in == nil { - return nil - } - out := new(CFOMappingList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *CFOMappingList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MarketplacePanel) DeepCopyInto(out *MarketplacePanel) { *out = *in diff --git a/packages/system/backup-controller/definitions/backups.cozystack.io_backupclasses.yaml b/packages/system/backup-controller/definitions/backups.cozystack.io_backupclasses.yaml index b0f0fec2..4af8cf7d 100644 --- a/packages/system/backup-controller/definitions/backups.cozystack.io_backupclasses.yaml +++ b/packages/system/backup-controller/definitions/backups.cozystack.io_backupclasses.yaml @@ -60,7 +60,7 @@ spec: type: string kind: description: Kind is the kind of the application (e.g., - VirtualMachine, MySQL). + VirtualMachine, MariaDB). type: string required: - kind From 75e25fa9779b36bbd219f9318d072c6c525a0d1f Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Mon, 16 Feb 2026 23:01:38 +0300 Subject: [PATCH 055/666] fix(codegen): add gen_client to update-codegen.sh and regenerate applyconfiguration The applyconfiguration code referenced testing.TypeConverter from k8s.io/client-go/testing, which was removed in client-go v0.34.1. Root cause: hack/update-codegen.sh called gen_helpers and gen_openapi but not gen_client, so applyconfiguration was never regenerated after the client-go upgrade. Changes: - Fix THIS_PKG from sample-apiserver template leftover to correct module path - Add kube::codegen::gen_client call with --with-applyconfig flag - Regenerate applyconfiguration (now uses managedfields.TypeConverter) - Add tests for ForKind and NewTypeConverter functions Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- go.mod | 3 +- go.sum | 5 - hack/update-codegen.sh | 9 +- .../apps/v1alpha1/application.go | 42 ++++-- .../apps/v1alpha1/applicationspec.go | 48 ------- .../apps/v1alpha1/applicationstatus.go | 75 ++++++++++ .../applyconfiguration/internal/internal.go | 4 +- pkg/generated/applyconfiguration/utils.go | 18 +-- .../applyconfiguration/utils_test.go | 64 +++++++++ .../clientset/versioned/clientset.go | 120 ++++++++++++++++ .../versioned/fake/clientset_generated.go | 131 ++++++++++++++++++ pkg/generated/clientset/versioned/fake/doc.go | 20 +++ .../clientset/versioned/fake/register.go | 56 ++++++++ .../clientset/versioned/scheme/doc.go | 20 +++ .../clientset/versioned/scheme/register.go | 56 ++++++++ .../typed/apps/v1alpha1/application.go | 74 ++++++++++ .../typed/apps/v1alpha1/apps_client.go | 101 ++++++++++++++ .../versioned/typed/apps/v1alpha1/doc.go | 20 +++ .../versioned/typed/apps/v1alpha1/fake/doc.go | 20 +++ .../apps/v1alpha1/fake/fake_application.go | 53 +++++++ .../apps/v1alpha1/fake/fake_apps_client.go | 40 ++++++ .../apps/v1alpha1/generated_expansion.go | 21 +++ 22 files changed, 925 insertions(+), 75 deletions(-) delete mode 100644 pkg/generated/applyconfiguration/apps/v1alpha1/applicationspec.go create mode 100644 pkg/generated/applyconfiguration/apps/v1alpha1/applicationstatus.go create mode 100644 pkg/generated/applyconfiguration/utils_test.go create mode 100644 pkg/generated/clientset/versioned/clientset.go create mode 100644 pkg/generated/clientset/versioned/fake/clientset_generated.go create mode 100644 pkg/generated/clientset/versioned/fake/doc.go create mode 100644 pkg/generated/clientset/versioned/fake/register.go create mode 100644 pkg/generated/clientset/versioned/scheme/doc.go create mode 100644 pkg/generated/clientset/versioned/scheme/register.go create mode 100644 pkg/generated/clientset/versioned/typed/apps/v1alpha1/application.go create mode 100644 pkg/generated/clientset/versioned/typed/apps/v1alpha1/apps_client.go create mode 100644 pkg/generated/clientset/versioned/typed/apps/v1alpha1/doc.go create mode 100644 pkg/generated/clientset/versioned/typed/apps/v1alpha1/fake/doc.go create mode 100644 pkg/generated/clientset/versioned/typed/apps/v1alpha1/fake/fake_application.go create mode 100644 pkg/generated/clientset/versioned/typed/apps/v1alpha1/fake/fake_apps_client.go create mode 100644 pkg/generated/clientset/versioned/typed/apps/v1alpha1/generated_expansion.go diff --git a/go.mod b/go.mod index 9944df16..3ecda336 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,7 @@ require ( k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d sigs.k8s.io/controller-runtime v0.22.4 - sigs.k8s.io/structured-merge-diff/v4 v4.7.0 + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 ) require ( @@ -125,7 +125,6 @@ require ( sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index d2cbc779..0a69ac87 100644 --- a/go.sum +++ b/go.sum @@ -81,7 +81,6 @@ github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI= github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -324,13 +323,9 @@ sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327U sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= -sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI= -sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/hack/update-codegen.sh b/hack/update-codegen.sh index b97c2ade..293f229b 100755 --- a/hack/update-codegen.sh +++ b/hack/update-codegen.sh @@ -34,7 +34,7 @@ trap 'rm -rf ${TMPDIR}' EXIT source "${CODEGEN_PKG}/kube_codegen.sh" -THIS_PKG="k8s.io/sample-apiserver" +THIS_PKG="github.com/cozystack/cozystack" kube::codegen::gen_helpers \ --boilerplate "${SCRIPT_ROOT}/hack/boilerplate.go.txt" \ @@ -60,6 +60,13 @@ kube::codegen::gen_openapi \ --boilerplate "${SCRIPT_ROOT}/hack/boilerplate.go.txt" \ "${SCRIPT_ROOT}/pkg/apis" +kube::codegen::gen_client \ + --with-applyconfig \ + --output-dir "${SCRIPT_ROOT}/pkg/generated" \ + --output-pkg "${THIS_PKG}/pkg/generated" \ + --boilerplate "${SCRIPT_ROOT}/hack/boilerplate.go.txt" \ + "${SCRIPT_ROOT}/pkg/apis" + $CONTROLLER_GEN object:headerFile="hack/boilerplate.go.txt" paths="./api/..." $CONTROLLER_GEN rbac:roleName=manager-role crd paths="./api/..." output:crd:artifacts:config=${TMPDIR} diff --git a/pkg/generated/applyconfiguration/apps/v1alpha1/application.go b/pkg/generated/applyconfiguration/apps/v1alpha1/application.go index 908e28fd..9c5e2ab5 100644 --- a/pkg/generated/applyconfiguration/apps/v1alpha1/application.go +++ b/pkg/generated/applyconfiguration/apps/v1alpha1/application.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Cozystack Authors. +Copyright 2025 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. @@ -19,10 +19,10 @@ limitations under the License. package v1alpha1 import ( + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" types "k8s.io/apimachinery/pkg/types" v1 "k8s.io/client-go/applyconfigurations/meta/v1" - appsv1alpha1 "github.com/cozystack/cozystack/pkg/apis/apps/v1alpha1" ) // ApplicationApplyConfiguration represents a declarative configuration of the Application type for use @@ -30,8 +30,9 @@ import ( type ApplicationApplyConfiguration struct { v1.TypeMetaApplyConfiguration `json:",inline"` *v1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"` - Spec *ApplicationSpecApplyConfiguration `json:"spec,omitempty"` - Status *appsv1alpha1.ApplicationStatus `json:"status,omitempty"` + AppVersion *string `json:"appVersion,omitempty"` + Spec *apiextensionsv1.JSON `json:"spec,omitempty"` + Status *ApplicationStatusApplyConfiguration `json:"status,omitempty"` } // Application constructs a declarative configuration of the Application type for use with @@ -44,6 +45,7 @@ func Application(name, namespace string) *ApplicationApplyConfiguration { b.WithAPIVersion("apps.cozystack.io/v1alpha1") return b } +func (b ApplicationApplyConfiguration) IsApplyConfiguration() {} // WithKind sets the Kind field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. @@ -203,24 +205,48 @@ func (b *ApplicationApplyConfiguration) ensureObjectMetaApplyConfigurationExists } } +// WithAppVersion sets the AppVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the AppVersion field is set to the value of the last call. +func (b *ApplicationApplyConfiguration) WithAppVersion(value string) *ApplicationApplyConfiguration { + b.AppVersion = &value + return b +} + // WithSpec sets the Spec field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Spec field is set to the value of the last call. -func (b *ApplicationApplyConfiguration) WithSpec(value *ApplicationSpecApplyConfiguration) *ApplicationApplyConfiguration { - b.Spec = value +func (b *ApplicationApplyConfiguration) WithSpec(value apiextensionsv1.JSON) *ApplicationApplyConfiguration { + b.Spec = &value return b } // WithStatus sets the Status field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Status field is set to the value of the last call. -func (b *ApplicationApplyConfiguration) WithStatus(value appsv1alpha1.ApplicationStatus) *ApplicationApplyConfiguration { - b.Status = &value +func (b *ApplicationApplyConfiguration) WithStatus(value *ApplicationStatusApplyConfiguration) *ApplicationApplyConfiguration { + b.Status = value return b } +// GetKind retrieves the value of the Kind field in the declarative configuration. +func (b *ApplicationApplyConfiguration) GetKind() *string { + return b.TypeMetaApplyConfiguration.Kind +} + +// GetAPIVersion retrieves the value of the APIVersion field in the declarative configuration. +func (b *ApplicationApplyConfiguration) GetAPIVersion() *string { + return b.TypeMetaApplyConfiguration.APIVersion +} + // GetName retrieves the value of the Name field in the declarative configuration. func (b *ApplicationApplyConfiguration) GetName() *string { b.ensureObjectMetaApplyConfigurationExists() return b.ObjectMetaApplyConfiguration.Name } + +// GetNamespace retrieves the value of the Namespace field in the declarative configuration. +func (b *ApplicationApplyConfiguration) GetNamespace() *string { + b.ensureObjectMetaApplyConfigurationExists() + return b.ObjectMetaApplyConfiguration.Namespace +} diff --git a/pkg/generated/applyconfiguration/apps/v1alpha1/applicationspec.go b/pkg/generated/applyconfiguration/apps/v1alpha1/applicationspec.go deleted file mode 100644 index 3924ebd4..00000000 --- a/pkg/generated/applyconfiguration/apps/v1alpha1/applicationspec.go +++ /dev/null @@ -1,48 +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. -*/ - -// Code generated by applyconfiguration-gen. DO NOT EDIT. - -package v1alpha1 - -// ApplicationSpecApplyConfiguration represents a declarative configuration of the ApplicationSpec type for use -// with apply. -type ApplicationSpecApplyConfiguration struct { - Version *string `json:"version,omitempty"` - Values *string `json:"values,omitempty"` -} - -// ApplicationSpecApplyConfiguration constructs a declarative configuration of the ApplicationSpec type for use with -// apply. -func ApplicationSpec() *ApplicationSpecApplyConfiguration { - return &ApplicationSpecApplyConfiguration{} -} - -// WithVersion sets the Version field in the declarative configuration to the given value -// and returns the receiver, so that objects can be built by chaining "With" function invocations. -// If called multiple times, the Version field is set to the value of the last call. -func (b *ApplicationSpecApplyConfiguration) WithVersion(value string) *ApplicationSpecApplyConfiguration { - b.Version = &value - return b -} - -// WithValues sets the Values field in the declarative configuration to the given value -// and returns the receiver, so that objects can be built by chaining "With" function invocations. -// If called multiple times, the Values field is set to the value of the last call. -func (b *ApplicationSpecApplyConfiguration) WithValues(value string) *ApplicationSpecApplyConfiguration { - b.Values = &value - return b -} diff --git a/pkg/generated/applyconfiguration/apps/v1alpha1/applicationstatus.go b/pkg/generated/applyconfiguration/apps/v1alpha1/applicationstatus.go new file mode 100644 index 00000000..2a990861 --- /dev/null +++ b/pkg/generated/applyconfiguration/apps/v1alpha1/applicationstatus.go @@ -0,0 +1,75 @@ +/* +Copyright 2025 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. +*/ + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1 "k8s.io/client-go/applyconfigurations/meta/v1" +) + +// ApplicationStatusApplyConfiguration represents a declarative configuration of the ApplicationStatus type for use +// with apply. +type ApplicationStatusApplyConfiguration struct { + Version *string `json:"version,omitempty"` + Conditions []v1.ConditionApplyConfiguration `json:"conditions,omitempty"` + Namespace *string `json:"namespace,omitempty"` + ExternalIPsCount *int32 `json:"externalIPsCount,omitempty"` +} + +// ApplicationStatusApplyConfiguration constructs a declarative configuration of the ApplicationStatus type for use with +// apply. +func ApplicationStatus() *ApplicationStatusApplyConfiguration { + return &ApplicationStatusApplyConfiguration{} +} + +// WithVersion sets the Version field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Version field is set to the value of the last call. +func (b *ApplicationStatusApplyConfiguration) WithVersion(value string) *ApplicationStatusApplyConfiguration { + b.Version = &value + return b +} + +// WithConditions adds the given value to the Conditions field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Conditions field. +func (b *ApplicationStatusApplyConfiguration) WithConditions(values ...*v1.ConditionApplyConfiguration) *ApplicationStatusApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithConditions") + } + b.Conditions = append(b.Conditions, *values[i]) + } + return b +} + +// WithNamespace sets the Namespace field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Namespace field is set to the value of the last call. +func (b *ApplicationStatusApplyConfiguration) WithNamespace(value string) *ApplicationStatusApplyConfiguration { + b.Namespace = &value + return b +} + +// WithExternalIPsCount sets the ExternalIPsCount field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ExternalIPsCount field is set to the value of the last call. +func (b *ApplicationStatusApplyConfiguration) WithExternalIPsCount(value int32) *ApplicationStatusApplyConfiguration { + b.ExternalIPsCount = &value + return b +} diff --git a/pkg/generated/applyconfiguration/internal/internal.go b/pkg/generated/applyconfiguration/internal/internal.go index 760f1229..97f12849 100644 --- a/pkg/generated/applyconfiguration/internal/internal.go +++ b/pkg/generated/applyconfiguration/internal/internal.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Cozystack Authors. +Copyright 2025 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. @@ -22,7 +22,7 @@ import ( fmt "fmt" sync "sync" - typed "sigs.k8s.io/structured-merge-diff/v4/typed" + typed "sigs.k8s.io/structured-merge-diff/v6/typed" ) func Parser() *typed.Parser { diff --git a/pkg/generated/applyconfiguration/utils.go b/pkg/generated/applyconfiguration/utils.go index a27b3d20..ab73a5f1 100644 --- a/pkg/generated/applyconfiguration/utils.go +++ b/pkg/generated/applyconfiguration/utils.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Cozystack Authors. +Copyright 2025 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. @@ -19,12 +19,12 @@ limitations under the License. package applyconfiguration import ( + v1alpha1 "github.com/cozystack/cozystack/pkg/apis/apps/v1alpha1" + appsv1alpha1 "github.com/cozystack/cozystack/pkg/generated/applyconfiguration/apps/v1alpha1" + internal "github.com/cozystack/cozystack/pkg/generated/applyconfiguration/internal" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" - testing "k8s.io/client-go/testing" - v1alpha1 "github.com/cozystack/cozystack/pkg/apis/apps/v1alpha1" - internal "github.com/cozystack/cozystack/pkg/generated/applyconfiguration/internal" - appsv1alpha1 "github.com/cozystack/cozystack/pkg/generated/applyconfiguration/apps/v1alpha1" + managedfields "k8s.io/apimachinery/pkg/util/managedfields" ) // ForKind returns an apply configuration type for the given GroupVersionKind, or nil if no @@ -34,13 +34,13 @@ func ForKind(kind schema.GroupVersionKind) interface{} { // Group=apps.cozystack.io, Version=v1alpha1 case v1alpha1.SchemeGroupVersion.WithKind("Application"): return &appsv1alpha1.ApplicationApplyConfiguration{} - case v1alpha1.SchemeGroupVersion.WithKind("ApplicationSpec"): - return &appsv1alpha1.ApplicationSpecApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("ApplicationStatus"): + return &appsv1alpha1.ApplicationStatusApplyConfiguration{} } return nil } -func NewTypeConverter(scheme *runtime.Scheme) *testing.TypeConverter { - return &testing.TypeConverter{Scheme: scheme, TypeResolver: internal.Parser()} +func NewTypeConverter(scheme *runtime.Scheme) managedfields.TypeConverter { + return managedfields.NewSchemeTypeConverter(scheme, internal.Parser()) } diff --git a/pkg/generated/applyconfiguration/utils_test.go b/pkg/generated/applyconfiguration/utils_test.go new file mode 100644 index 00000000..dd6440b6 --- /dev/null +++ b/pkg/generated/applyconfiguration/utils_test.go @@ -0,0 +1,64 @@ +/* +Copyright 2025 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 applyconfiguration + +import ( + "testing" + + v1alpha1 "github.com/cozystack/cozystack/pkg/apis/apps/v1alpha1" + appsv1alpha1 "github.com/cozystack/cozystack/pkg/generated/applyconfiguration/apps/v1alpha1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +func TestForKind_Application(t *testing.T) { + gvk := v1alpha1.SchemeGroupVersion.WithKind("Application") + got := ForKind(gvk) + if got == nil { + t.Fatal("ForKind returned nil for Application GVK") + } + if _, ok := got.(*appsv1alpha1.ApplicationApplyConfiguration); !ok { + t.Fatalf("ForKind returned %T, want *ApplicationApplyConfiguration", got) + } +} + +func TestForKind_ApplicationStatus(t *testing.T) { + gvk := v1alpha1.SchemeGroupVersion.WithKind("ApplicationStatus") + got := ForKind(gvk) + if got == nil { + t.Fatal("ForKind returned nil for ApplicationStatus GVK") + } + if _, ok := got.(*appsv1alpha1.ApplicationStatusApplyConfiguration); !ok { + t.Fatalf("ForKind returned %T, want *ApplicationStatusApplyConfiguration", got) + } +} + +func TestForKind_Unknown(t *testing.T) { + gvk := schema.GroupVersionKind{Group: "unknown.io", Version: "v1", Kind: "Unknown"} + got := ForKind(gvk) + if got != nil { + t.Fatalf("ForKind returned %T for unknown GVK, want nil", got) + } +} + +func TestNewTypeConverter(t *testing.T) { + scheme := runtime.NewScheme() + tc := NewTypeConverter(scheme) + if tc == nil { + t.Fatal("NewTypeConverter returned nil") + } +} diff --git a/pkg/generated/clientset/versioned/clientset.go b/pkg/generated/clientset/versioned/clientset.go new file mode 100644 index 00000000..127a0032 --- /dev/null +++ b/pkg/generated/clientset/versioned/clientset.go @@ -0,0 +1,120 @@ +/* +Copyright 2025 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package versioned + +import ( + fmt "fmt" + http "net/http" + + appsv1alpha1 "github.com/cozystack/cozystack/pkg/generated/clientset/versioned/typed/apps/v1alpha1" + discovery "k8s.io/client-go/discovery" + rest "k8s.io/client-go/rest" + flowcontrol "k8s.io/client-go/util/flowcontrol" +) + +type Interface interface { + Discovery() discovery.DiscoveryInterface + AppsV1alpha1() appsv1alpha1.AppsV1alpha1Interface +} + +// Clientset contains the clients for groups. +type Clientset struct { + *discovery.DiscoveryClient + appsV1alpha1 *appsv1alpha1.AppsV1alpha1Client +} + +// AppsV1alpha1 retrieves the AppsV1alpha1Client +func (c *Clientset) AppsV1alpha1() appsv1alpha1.AppsV1alpha1Interface { + return c.appsV1alpha1 +} + +// Discovery retrieves the DiscoveryClient +func (c *Clientset) Discovery() discovery.DiscoveryInterface { + if c == nil { + return nil + } + return c.DiscoveryClient +} + +// NewForConfig creates a new Clientset for the given config. +// If config's RateLimiter is not set and QPS and Burst are acceptable, +// NewForConfig will generate a rate-limiter in configShallowCopy. +// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), +// where httpClient was generated with rest.HTTPClientFor(c). +func NewForConfig(c *rest.Config) (*Clientset, error) { + configShallowCopy := *c + + if configShallowCopy.UserAgent == "" { + configShallowCopy.UserAgent = rest.DefaultKubernetesUserAgent() + } + + // share the transport between all clients + httpClient, err := rest.HTTPClientFor(&configShallowCopy) + if err != nil { + return nil, err + } + + return NewForConfigAndClient(&configShallowCopy, httpClient) +} + +// NewForConfigAndClient creates a new Clientset for the given config and http client. +// Note the http client provided takes precedence over the configured transport values. +// If config's RateLimiter is not set and QPS and Burst are acceptable, +// NewForConfigAndClient will generate a rate-limiter in configShallowCopy. +func NewForConfigAndClient(c *rest.Config, httpClient *http.Client) (*Clientset, error) { + configShallowCopy := *c + if configShallowCopy.RateLimiter == nil && configShallowCopy.QPS > 0 { + if configShallowCopy.Burst <= 0 { + return nil, fmt.Errorf("burst is required to be greater than 0 when RateLimiter is not set and QPS is set to greater than 0") + } + configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(configShallowCopy.QPS, configShallowCopy.Burst) + } + + var cs Clientset + var err error + cs.appsV1alpha1, err = appsv1alpha1.NewForConfigAndClient(&configShallowCopy, httpClient) + if err != nil { + return nil, err + } + + cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfigAndClient(&configShallowCopy, httpClient) + if err != nil { + return nil, err + } + return &cs, nil +} + +// NewForConfigOrDie creates a new Clientset for the given config and +// panics if there is an error in the config. +func NewForConfigOrDie(c *rest.Config) *Clientset { + cs, err := NewForConfig(c) + if err != nil { + panic(err) + } + return cs +} + +// New creates a new Clientset for the given RESTClient. +func New(c rest.Interface) *Clientset { + var cs Clientset + cs.appsV1alpha1 = appsv1alpha1.New(c) + + cs.DiscoveryClient = discovery.NewDiscoveryClient(c) + return &cs +} diff --git a/pkg/generated/clientset/versioned/fake/clientset_generated.go b/pkg/generated/clientset/versioned/fake/clientset_generated.go new file mode 100644 index 00000000..3ceb93c7 --- /dev/null +++ b/pkg/generated/clientset/versioned/fake/clientset_generated.go @@ -0,0 +1,131 @@ +/* +Copyright 2025 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + applyconfiguration "github.com/cozystack/cozystack/pkg/generated/applyconfiguration" + clientset "github.com/cozystack/cozystack/pkg/generated/clientset/versioned" + appsv1alpha1 "github.com/cozystack/cozystack/pkg/generated/clientset/versioned/typed/apps/v1alpha1" + fakeappsv1alpha1 "github.com/cozystack/cozystack/pkg/generated/clientset/versioned/typed/apps/v1alpha1/fake" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/discovery" + fakediscovery "k8s.io/client-go/discovery/fake" + "k8s.io/client-go/testing" +) + +// NewSimpleClientset returns a clientset that will respond with the provided objects. +// It's backed by a very simple object tracker that processes creates, updates and deletions as-is, +// without applying any field management, validations and/or defaults. It shouldn't be considered a replacement +// for a real clientset and is mostly useful in simple unit tests. +// +// DEPRECATED: NewClientset replaces this with support for field management, which significantly improves +// server side apply testing. NewClientset is only available when apply configurations are generated (e.g. +// via --with-applyconfig). +func NewSimpleClientset(objects ...runtime.Object) *Clientset { + o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder()) + for _, obj := range objects { + if err := o.Add(obj); err != nil { + panic(err) + } + } + + cs := &Clientset{tracker: o} + cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake} + cs.AddReactor("*", "*", testing.ObjectReaction(o)) + cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) { + var opts metav1.ListOptions + if watchActcion, ok := action.(testing.WatchActionImpl); ok { + opts = watchActcion.ListOptions + } + gvr := action.GetResource() + ns := action.GetNamespace() + watch, err := o.Watch(gvr, ns, opts) + if err != nil { + return false, nil, err + } + return true, watch, nil + }) + + return cs +} + +// Clientset implements clientset.Interface. Meant to be embedded into a +// struct to get a default implementation. This makes faking out just the method +// you want to test easier. +type Clientset struct { + testing.Fake + discovery *fakediscovery.FakeDiscovery + tracker testing.ObjectTracker +} + +func (c *Clientset) Discovery() discovery.DiscoveryInterface { + return c.discovery +} + +func (c *Clientset) Tracker() testing.ObjectTracker { + return c.tracker +} + +// NewClientset returns a clientset that will respond with the provided objects. +// It's backed by a very simple object tracker that processes creates, updates and deletions as-is, +// without applying any validations and/or defaults. It shouldn't be considered a replacement +// for a real clientset and is mostly useful in simple unit tests. +func NewClientset(objects ...runtime.Object) *Clientset { + o := testing.NewFieldManagedObjectTracker( + scheme, + codecs.UniversalDecoder(), + applyconfiguration.NewTypeConverter(scheme), + ) + for _, obj := range objects { + if err := o.Add(obj); err != nil { + panic(err) + } + } + + cs := &Clientset{tracker: o} + cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake} + cs.AddReactor("*", "*", testing.ObjectReaction(o)) + cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) { + var opts metav1.ListOptions + if watchAction, ok := action.(testing.WatchActionImpl); ok { + opts = watchAction.ListOptions + } + gvr := action.GetResource() + ns := action.GetNamespace() + watch, err := o.Watch(gvr, ns, opts) + if err != nil { + return false, nil, err + } + return true, watch, nil + }) + + return cs +} + +var ( + _ clientset.Interface = &Clientset{} + _ testing.FakeClient = &Clientset{} +) + +// AppsV1alpha1 retrieves the AppsV1alpha1Client +func (c *Clientset) AppsV1alpha1() appsv1alpha1.AppsV1alpha1Interface { + return &fakeappsv1alpha1.FakeAppsV1alpha1{Fake: &c.Fake} +} diff --git a/pkg/generated/clientset/versioned/fake/doc.go b/pkg/generated/clientset/versioned/fake/doc.go new file mode 100644 index 00000000..2d395096 --- /dev/null +++ b/pkg/generated/clientset/versioned/fake/doc.go @@ -0,0 +1,20 @@ +/* +Copyright 2025 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +// This package has the automatically generated fake clientset. +package fake diff --git a/pkg/generated/clientset/versioned/fake/register.go b/pkg/generated/clientset/versioned/fake/register.go new file mode 100644 index 00000000..a05b3ada --- /dev/null +++ b/pkg/generated/clientset/versioned/fake/register.go @@ -0,0 +1,56 @@ +/* +Copyright 2025 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + appsv1alpha1 "github.com/cozystack/cozystack/pkg/apis/apps/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + serializer "k8s.io/apimachinery/pkg/runtime/serializer" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" +) + +var scheme = runtime.NewScheme() +var codecs = serializer.NewCodecFactory(scheme) + +var localSchemeBuilder = runtime.SchemeBuilder{ + appsv1alpha1.AddToScheme, +} + +// AddToScheme adds all types of this clientset into the given scheme. This allows composition +// of clientsets, like in: +// +// import ( +// "k8s.io/client-go/kubernetes" +// clientsetscheme "k8s.io/client-go/kubernetes/scheme" +// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" +// ) +// +// kclientset, _ := kubernetes.NewForConfig(c) +// _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) +// +// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types +// correctly. +var AddToScheme = localSchemeBuilder.AddToScheme + +func init() { + v1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"}) + utilruntime.Must(AddToScheme(scheme)) +} diff --git a/pkg/generated/clientset/versioned/scheme/doc.go b/pkg/generated/clientset/versioned/scheme/doc.go new file mode 100644 index 00000000..4b11a5d0 --- /dev/null +++ b/pkg/generated/clientset/versioned/scheme/doc.go @@ -0,0 +1,20 @@ +/* +Copyright 2025 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +// This package contains the scheme of the automatically generated clientset. +package scheme diff --git a/pkg/generated/clientset/versioned/scheme/register.go b/pkg/generated/clientset/versioned/scheme/register.go new file mode 100644 index 00000000..65b15149 --- /dev/null +++ b/pkg/generated/clientset/versioned/scheme/register.go @@ -0,0 +1,56 @@ +/* +Copyright 2025 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package scheme + +import ( + appsv1alpha1 "github.com/cozystack/cozystack/pkg/apis/apps/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + serializer "k8s.io/apimachinery/pkg/runtime/serializer" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" +) + +var Scheme = runtime.NewScheme() +var Codecs = serializer.NewCodecFactory(Scheme) +var ParameterCodec = runtime.NewParameterCodec(Scheme) +var localSchemeBuilder = runtime.SchemeBuilder{ + appsv1alpha1.AddToScheme, +} + +// AddToScheme adds all types of this clientset into the given scheme. This allows composition +// of clientsets, like in: +// +// import ( +// "k8s.io/client-go/kubernetes" +// clientsetscheme "k8s.io/client-go/kubernetes/scheme" +// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" +// ) +// +// kclientset, _ := kubernetes.NewForConfig(c) +// _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) +// +// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types +// correctly. +var AddToScheme = localSchemeBuilder.AddToScheme + +func init() { + v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"}) + utilruntime.Must(AddToScheme(Scheme)) +} diff --git a/pkg/generated/clientset/versioned/typed/apps/v1alpha1/application.go b/pkg/generated/clientset/versioned/typed/apps/v1alpha1/application.go new file mode 100644 index 00000000..afc81aad --- /dev/null +++ b/pkg/generated/clientset/versioned/typed/apps/v1alpha1/application.go @@ -0,0 +1,74 @@ +/* +Copyright 2025 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + + appsv1alpha1 "github.com/cozystack/cozystack/pkg/apis/apps/v1alpha1" + applyconfigurationappsv1alpha1 "github.com/cozystack/cozystack/pkg/generated/applyconfiguration/apps/v1alpha1" + scheme "github.com/cozystack/cozystack/pkg/generated/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + gentype "k8s.io/client-go/gentype" +) + +// ApplicationsGetter has a method to return a ApplicationInterface. +// A group's client should implement this interface. +type ApplicationsGetter interface { + Applications(namespace string) ApplicationInterface +} + +// ApplicationInterface has methods to work with Application resources. +type ApplicationInterface interface { + Create(ctx context.Context, application *appsv1alpha1.Application, opts v1.CreateOptions) (*appsv1alpha1.Application, error) + Update(ctx context.Context, application *appsv1alpha1.Application, opts v1.UpdateOptions) (*appsv1alpha1.Application, error) + // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + UpdateStatus(ctx context.Context, application *appsv1alpha1.Application, opts v1.UpdateOptions) (*appsv1alpha1.Application, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*appsv1alpha1.Application, error) + List(ctx context.Context, opts v1.ListOptions) (*appsv1alpha1.ApplicationList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *appsv1alpha1.Application, err error) + Apply(ctx context.Context, application *applyconfigurationappsv1alpha1.ApplicationApplyConfiguration, opts v1.ApplyOptions) (result *appsv1alpha1.Application, err error) + // Add a +genclient:noStatus comment above the type to avoid generating ApplyStatus(). + ApplyStatus(ctx context.Context, application *applyconfigurationappsv1alpha1.ApplicationApplyConfiguration, opts v1.ApplyOptions) (result *appsv1alpha1.Application, err error) + ApplicationExpansion +} + +// applications implements ApplicationInterface +type applications struct { + *gentype.ClientWithListAndApply[*appsv1alpha1.Application, *appsv1alpha1.ApplicationList, *applyconfigurationappsv1alpha1.ApplicationApplyConfiguration] +} + +// newApplications returns a Applications +func newApplications(c *AppsV1alpha1Client, namespace string) *applications { + return &applications{ + gentype.NewClientWithListAndApply[*appsv1alpha1.Application, *appsv1alpha1.ApplicationList, *applyconfigurationappsv1alpha1.ApplicationApplyConfiguration]( + "applications", + c.RESTClient(), + scheme.ParameterCodec, + namespace, + func() *appsv1alpha1.Application { return &appsv1alpha1.Application{} }, + func() *appsv1alpha1.ApplicationList { return &appsv1alpha1.ApplicationList{} }, + ), + } +} diff --git a/pkg/generated/clientset/versioned/typed/apps/v1alpha1/apps_client.go b/pkg/generated/clientset/versioned/typed/apps/v1alpha1/apps_client.go new file mode 100644 index 00000000..40ff64b9 --- /dev/null +++ b/pkg/generated/clientset/versioned/typed/apps/v1alpha1/apps_client.go @@ -0,0 +1,101 @@ +/* +Copyright 2025 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + http "net/http" + + appsv1alpha1 "github.com/cozystack/cozystack/pkg/apis/apps/v1alpha1" + scheme "github.com/cozystack/cozystack/pkg/generated/clientset/versioned/scheme" + rest "k8s.io/client-go/rest" +) + +type AppsV1alpha1Interface interface { + RESTClient() rest.Interface + ApplicationsGetter +} + +// AppsV1alpha1Client is used to interact with features provided by the apps.cozystack.io group. +type AppsV1alpha1Client struct { + restClient rest.Interface +} + +func (c *AppsV1alpha1Client) Applications(namespace string) ApplicationInterface { + return newApplications(c, namespace) +} + +// NewForConfig creates a new AppsV1alpha1Client for the given config. +// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), +// where httpClient was generated with rest.HTTPClientFor(c). +func NewForConfig(c *rest.Config) (*AppsV1alpha1Client, error) { + config := *c + setConfigDefaults(&config) + httpClient, err := rest.HTTPClientFor(&config) + if err != nil { + return nil, err + } + return NewForConfigAndClient(&config, httpClient) +} + +// NewForConfigAndClient creates a new AppsV1alpha1Client for the given config and http client. +// Note the http client provided takes precedence over the configured transport values. +func NewForConfigAndClient(c *rest.Config, h *http.Client) (*AppsV1alpha1Client, error) { + config := *c + setConfigDefaults(&config) + client, err := rest.RESTClientForConfigAndClient(&config, h) + if err != nil { + return nil, err + } + return &AppsV1alpha1Client{client}, nil +} + +// NewForConfigOrDie creates a new AppsV1alpha1Client for the given config and +// panics if there is an error in the config. +func NewForConfigOrDie(c *rest.Config) *AppsV1alpha1Client { + client, err := NewForConfig(c) + if err != nil { + panic(err) + } + return client +} + +// New creates a new AppsV1alpha1Client for the given RESTClient. +func New(c rest.Interface) *AppsV1alpha1Client { + return &AppsV1alpha1Client{c} +} + +func setConfigDefaults(config *rest.Config) { + gv := appsv1alpha1.SchemeGroupVersion + config.GroupVersion = &gv + config.APIPath = "/apis" + config.NegotiatedSerializer = rest.CodecFactoryForGeneratedClient(scheme.Scheme, scheme.Codecs).WithoutConversion() + + if config.UserAgent == "" { + config.UserAgent = rest.DefaultKubernetesUserAgent() + } +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *AppsV1alpha1Client) RESTClient() rest.Interface { + if c == nil { + return nil + } + return c.restClient +} diff --git a/pkg/generated/clientset/versioned/typed/apps/v1alpha1/doc.go b/pkg/generated/clientset/versioned/typed/apps/v1alpha1/doc.go new file mode 100644 index 00000000..b37cc8b0 --- /dev/null +++ b/pkg/generated/clientset/versioned/typed/apps/v1alpha1/doc.go @@ -0,0 +1,20 @@ +/* +Copyright 2025 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +// This package has the automatically generated typed clients. +package v1alpha1 diff --git a/pkg/generated/clientset/versioned/typed/apps/v1alpha1/fake/doc.go b/pkg/generated/clientset/versioned/typed/apps/v1alpha1/fake/doc.go new file mode 100644 index 00000000..592956cf --- /dev/null +++ b/pkg/generated/clientset/versioned/typed/apps/v1alpha1/fake/doc.go @@ -0,0 +1,20 @@ +/* +Copyright 2025 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +// Package fake has the automatically generated clients. +package fake diff --git a/pkg/generated/clientset/versioned/typed/apps/v1alpha1/fake/fake_application.go b/pkg/generated/clientset/versioned/typed/apps/v1alpha1/fake/fake_application.go new file mode 100644 index 00000000..0760721d --- /dev/null +++ b/pkg/generated/clientset/versioned/typed/apps/v1alpha1/fake/fake_application.go @@ -0,0 +1,53 @@ +/* +Copyright 2025 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1alpha1 "github.com/cozystack/cozystack/pkg/apis/apps/v1alpha1" + appsv1alpha1 "github.com/cozystack/cozystack/pkg/generated/applyconfiguration/apps/v1alpha1" + typedappsv1alpha1 "github.com/cozystack/cozystack/pkg/generated/clientset/versioned/typed/apps/v1alpha1" + gentype "k8s.io/client-go/gentype" +) + +// fakeApplications implements ApplicationInterface +type fakeApplications struct { + *gentype.FakeClientWithListAndApply[*v1alpha1.Application, *v1alpha1.ApplicationList, *appsv1alpha1.ApplicationApplyConfiguration] + Fake *FakeAppsV1alpha1 +} + +func newFakeApplications(fake *FakeAppsV1alpha1, namespace string) typedappsv1alpha1.ApplicationInterface { + return &fakeApplications{ + gentype.NewFakeClientWithListAndApply[*v1alpha1.Application, *v1alpha1.ApplicationList, *appsv1alpha1.ApplicationApplyConfiguration]( + fake.Fake, + namespace, + v1alpha1.SchemeGroupVersion.WithResource("applications"), + v1alpha1.SchemeGroupVersion.WithKind("Application"), + func() *v1alpha1.Application { return &v1alpha1.Application{} }, + func() *v1alpha1.ApplicationList { return &v1alpha1.ApplicationList{} }, + func(dst, src *v1alpha1.ApplicationList) { dst.ListMeta = src.ListMeta }, + func(list *v1alpha1.ApplicationList) []*v1alpha1.Application { + return gentype.ToPointerSlice(list.Items) + }, + func(list *v1alpha1.ApplicationList, items []*v1alpha1.Application) { + list.Items = gentype.FromPointerSlice(items) + }, + ), + fake, + } +} diff --git a/pkg/generated/clientset/versioned/typed/apps/v1alpha1/fake/fake_apps_client.go b/pkg/generated/clientset/versioned/typed/apps/v1alpha1/fake/fake_apps_client.go new file mode 100644 index 00000000..40981a9d --- /dev/null +++ b/pkg/generated/clientset/versioned/typed/apps/v1alpha1/fake/fake_apps_client.go @@ -0,0 +1,40 @@ +/* +Copyright 2025 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1alpha1 "github.com/cozystack/cozystack/pkg/generated/clientset/versioned/typed/apps/v1alpha1" + rest "k8s.io/client-go/rest" + testing "k8s.io/client-go/testing" +) + +type FakeAppsV1alpha1 struct { + *testing.Fake +} + +func (c *FakeAppsV1alpha1) Applications(namespace string) v1alpha1.ApplicationInterface { + return newFakeApplications(c, namespace) +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *FakeAppsV1alpha1) RESTClient() rest.Interface { + var ret *rest.RESTClient + return ret +} diff --git a/pkg/generated/clientset/versioned/typed/apps/v1alpha1/generated_expansion.go b/pkg/generated/clientset/versioned/typed/apps/v1alpha1/generated_expansion.go new file mode 100644 index 00000000..6a0aab6a --- /dev/null +++ b/pkg/generated/clientset/versioned/typed/apps/v1alpha1/generated_expansion.go @@ -0,0 +1,21 @@ +/* +Copyright 2025 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +type ApplicationExpansion interface{} From 25ff583f3407b76fbae6f32e7da01beb32b113f0 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Mon, 2 Feb 2026 13:11:18 +0100 Subject: [PATCH 056/666] [apps] Add managed OpenSearch service Add OpenSearch application with operator and resource definition: - App chart with multi-version support (v1/v2/v3), TLS, auth, dashboards - OpenSearch operator wrapper (opster v2.8.0) with sysctl daemonset - ApplicationDefinition for Cozystack platform integration Co-Authored-By: Claude Opus 4.5 Signed-off-by: Matthieu --- packages/apps/opensearch/.helmignore | 23 + packages/apps/opensearch/Chart.yaml | 7 + packages/apps/opensearch/Makefile | 11 + packages/apps/opensearch/README.md | 60 + packages/apps/opensearch/charts/cozy-lib | 1 + packages/apps/opensearch/files/versions.yaml | 5 + .../apps/opensearch/hack/update-versions.sh | 53 + packages/apps/opensearch/logos/opensearch.svg | 11 + packages/apps/opensearch/templates/.gitkeep | 0 .../apps/opensearch/templates/_versions.tpl | 13 + .../templates/dashboard-resourcemap.yaml | 42 + .../opensearch/templates/external-svc.yaml | 39 + .../apps/opensearch/templates/opensearch.yaml | 99 + .../apps/opensearch/templates/security.yaml | 120 + packages/apps/opensearch/templates/users.yaml | 21 + .../opensearch/tests/opensearch_test.yaml | 516 ++ .../apps/opensearch/tests/security_test.yaml | 207 + .../apps/opensearch/tests/users_test.yaml | 176 + packages/apps/opensearch/values.schema.json | 239 + packages/apps/opensearch/values.yaml | 111 + .../system/opensearch-operator/.helmignore | 23 + .../system/opensearch-operator/Chart.yaml | 3 + packages/system/opensearch-operator/Makefile | 10 + .../charts/opensearch-operator/CHANGELOG.md | 61 + .../charts/opensearch-operator/Chart.yaml | 6 + .../charts/opensearch-operator/README.md | 29 + ...arch.opster.io_opensearchactiongroups.yaml | 94 + ...ensearch.opster.io_opensearchclusters.yaml | 6372 +++++++++++++++++ ...pster.io_opensearchcomponenttemplates.yaml | 136 + ...ch.opster.io_opensearchindextemplates.yaml | 163 + ...earch.opster.io_opensearchismpolicies.yaml | 461 ++ .../opensearch.opster.io_opensearchroles.yaml | 123 + ....opster.io_opensearchsnapshotpolicies.yaml | 203 + ...pensearch.opster.io_opensearchtenants.yaml | 85 + ....opster.io_opensearchuserrolebindings.yaml | 109 + .../opensearch.opster.io_opensearchusers.yaml | 117 + .../templates/_helpers.tpl | 62 + ...perator-controller-manager-deployment.yaml | 94 + ...ontroller-manager-metrics-service-svc.yaml | 13 + ...search-operator-controller-manager-sa.yaml | 6 + .../templates/opensearch-operator-crds.yaml | 5 + ...ch-operator-leader-election-role-role.yaml | 48 + ...erator-leader-election-rolebinding-rb.yaml | 11 + ...opensearch-operator-manager-config-cm.yaml | 17 + .../opensearch-operator-manager-role-cr.yaml | 414 ++ ...ensearch-operator-manager-rolebinding.yaml | 27 + ...opensearch-operator-metrics-reader-cr.yaml | 9 + .../opensearch-operator-proxy-role-cr.yaml | 17 + ...opensearch-operator-proxy-rolebinding.yaml | 27 + .../charts/opensearch-operator/values.yaml | 123 + .../templates/sysctl-daemonset.yaml | 64 + .../system/opensearch-operator/values.yaml | 4 + packages/system/opensearch-rd/Chart.yaml | 3 + packages/system/opensearch-rd/Makefile | 4 + .../opensearch-rd/cozyrds/opensearch.yaml | 42 + .../opensearch-rd/templates/cozyrd.yaml | 4 + packages/system/opensearch-rd/values.yaml | 1 + 57 files changed, 10744 insertions(+) create mode 100644 packages/apps/opensearch/.helmignore create mode 100644 packages/apps/opensearch/Chart.yaml create mode 100644 packages/apps/opensearch/Makefile create mode 100644 packages/apps/opensearch/README.md create mode 120000 packages/apps/opensearch/charts/cozy-lib create mode 100644 packages/apps/opensearch/files/versions.yaml create mode 100755 packages/apps/opensearch/hack/update-versions.sh create mode 100644 packages/apps/opensearch/logos/opensearch.svg create mode 100644 packages/apps/opensearch/templates/.gitkeep create mode 100644 packages/apps/opensearch/templates/_versions.tpl create mode 100644 packages/apps/opensearch/templates/dashboard-resourcemap.yaml create mode 100644 packages/apps/opensearch/templates/external-svc.yaml create mode 100644 packages/apps/opensearch/templates/opensearch.yaml create mode 100644 packages/apps/opensearch/templates/security.yaml create mode 100644 packages/apps/opensearch/templates/users.yaml create mode 100644 packages/apps/opensearch/tests/opensearch_test.yaml create mode 100644 packages/apps/opensearch/tests/security_test.yaml create mode 100644 packages/apps/opensearch/tests/users_test.yaml create mode 100644 packages/apps/opensearch/values.schema.json create mode 100644 packages/apps/opensearch/values.yaml create mode 100644 packages/system/opensearch-operator/.helmignore create mode 100644 packages/system/opensearch-operator/Chart.yaml create mode 100644 packages/system/opensearch-operator/Makefile create mode 100644 packages/system/opensearch-operator/charts/opensearch-operator/CHANGELOG.md create mode 100644 packages/system/opensearch-operator/charts/opensearch-operator/Chart.yaml create mode 100644 packages/system/opensearch-operator/charts/opensearch-operator/README.md create mode 100644 packages/system/opensearch-operator/charts/opensearch-operator/files/opensearch.opster.io_opensearchactiongroups.yaml create mode 100644 packages/system/opensearch-operator/charts/opensearch-operator/files/opensearch.opster.io_opensearchclusters.yaml create mode 100644 packages/system/opensearch-operator/charts/opensearch-operator/files/opensearch.opster.io_opensearchcomponenttemplates.yaml create mode 100644 packages/system/opensearch-operator/charts/opensearch-operator/files/opensearch.opster.io_opensearchindextemplates.yaml create mode 100644 packages/system/opensearch-operator/charts/opensearch-operator/files/opensearch.opster.io_opensearchismpolicies.yaml create mode 100644 packages/system/opensearch-operator/charts/opensearch-operator/files/opensearch.opster.io_opensearchroles.yaml create mode 100644 packages/system/opensearch-operator/charts/opensearch-operator/files/opensearch.opster.io_opensearchsnapshotpolicies.yaml create mode 100644 packages/system/opensearch-operator/charts/opensearch-operator/files/opensearch.opster.io_opensearchtenants.yaml create mode 100644 packages/system/opensearch-operator/charts/opensearch-operator/files/opensearch.opster.io_opensearchuserrolebindings.yaml create mode 100644 packages/system/opensearch-operator/charts/opensearch-operator/files/opensearch.opster.io_opensearchusers.yaml create mode 100644 packages/system/opensearch-operator/charts/opensearch-operator/templates/_helpers.tpl create mode 100644 packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-controller-manager-deployment.yaml create mode 100644 packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-controller-manager-metrics-service-svc.yaml create mode 100644 packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-controller-manager-sa.yaml create mode 100644 packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-crds.yaml create mode 100644 packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-leader-election-role-role.yaml create mode 100644 packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-leader-election-rolebinding-rb.yaml create mode 100644 packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-manager-config-cm.yaml create mode 100644 packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-manager-role-cr.yaml create mode 100644 packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-manager-rolebinding.yaml create mode 100644 packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-metrics-reader-cr.yaml create mode 100644 packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-proxy-role-cr.yaml create mode 100644 packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-proxy-rolebinding.yaml create mode 100644 packages/system/opensearch-operator/charts/opensearch-operator/values.yaml create mode 100644 packages/system/opensearch-operator/templates/sysctl-daemonset.yaml create mode 100644 packages/system/opensearch-operator/values.yaml create mode 100644 packages/system/opensearch-rd/Chart.yaml create mode 100644 packages/system/opensearch-rd/Makefile create mode 100644 packages/system/opensearch-rd/cozyrds/opensearch.yaml create mode 100644 packages/system/opensearch-rd/templates/cozyrd.yaml create mode 100644 packages/system/opensearch-rd/values.yaml diff --git a/packages/apps/opensearch/.helmignore b/packages/apps/opensearch/.helmignore new file mode 100644 index 00000000..0e8a0eb3 --- /dev/null +++ b/packages/apps/opensearch/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/packages/apps/opensearch/Chart.yaml b/packages/apps/opensearch/Chart.yaml new file mode 100644 index 00000000..8abefadf --- /dev/null +++ b/packages/apps/opensearch/Chart.yaml @@ -0,0 +1,7 @@ +apiVersion: v2 +name: opensearch +description: Managed OpenSearch service +icon: /logos/opensearch.svg +type: application +version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process +appVersion: "2.11.1" diff --git a/packages/apps/opensearch/Makefile b/packages/apps/opensearch/Makefile new file mode 100644 index 00000000..9440c3fd --- /dev/null +++ b/packages/apps/opensearch/Makefile @@ -0,0 +1,11 @@ +include ../../../hack/package.mk + +.PHONY: generate update + +generate: + cozyvalues-gen -v values.yaml -s values.schema.json -r README.md + ../../../hack/update-crd.sh + +update: + hack/update-versions.sh + make generate diff --git a/packages/apps/opensearch/README.md b/packages/apps/opensearch/README.md new file mode 100644 index 00000000..d968c665 --- /dev/null +++ b/packages/apps/opensearch/README.md @@ -0,0 +1,60 @@ +# Managed OpenSearch Service + +## Parameters + +### Common parameters + +| Name | Description | Type | Value | +| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ---------- | ------- | +| `replicas` | Number of OpenSearch nodes in the cluster. | `int` | `3` | +| `resources` | Explicit CPU and memory configuration for each OpenSearch node. When omitted, the preset defined in `resourcesPreset` is applied. | `object` | `{}` | +| `resources.cpu` | CPU available to each node. | `quantity` | `""` | +| `resources.memory` | Memory (RAM) available to each node. | `quantity` | `""` | +| `resourcesPreset` | Default sizing preset used when `resources` is omitted. OpenSearch requires minimum 2Gi memory. | `string` | `large` | +| `size` | Persistent Volume Claim size available for application data. | `quantity` | `10Gi` | +| `storageClass` | StorageClass used to store the data. | `string` | `""` | +| `external` | Enable external access from outside the cluster. | `bool` | `false` | +| `topologySpreadPolicy` | How strictly to enforce pod distribution across nodes and zones. | `string` | `soft` | +| `version` | OpenSearch major version to deploy. | `string` | `v2` | + + +### Image configuration + +| Name | Description | Type | Value | +| ------------------- | -------------------------------------- | -------- | ----- | +| `images` | Container images used by the operator. | `object` | `{}` | +| `images.opensearch` | OpenSearch image. | `string` | `""` | + + +### Node roles configuration + +| Name | Description | Type | Value | +| ------------------ | ----------------------------- | -------- | ------- | +| `nodeRoles` | Node roles configuration. | `object` | `{}` | +| `nodeRoles.master` | Enable cluster_manager role. | `bool` | `true` | +| `nodeRoles.data` | Enable data role. | `bool` | `true` | +| `nodeRoles.ingest` | Enable ingest role. | `bool` | `true` | +| `nodeRoles.ml` | Enable machine learning role. | `bool` | `false` | + + +### Users configuration + +| Name | Description | Type | Value | +| ---------------------- | -------------------------------------------------- | ------------------- | ----- | +| `users` | Custom OpenSearch users configuration map. | `map[string]object` | `{}` | +| `users[name].password` | Password for the user (auto-generated if omitted). | `string` | `""` | +| `users[name].roles` | List of OpenSearch roles. | `[]string` | `[]` | + + +### OpenSearch Dashboards configuration + +| Name | Description | Type | Value | +| ----------------------------- | ----------------------------------------------------- | ---------- | -------- | +| `dashboards` | OpenSearch Dashboards configuration. | `object` | `{}` | +| `dashboards.enabled` | Enable OpenSearch Dashboards deployment. | `bool` | `false` | +| `dashboards.replicas` | Number of Dashboards replicas. | `int` | `1` | +| `dashboards.resources` | Explicit CPU and memory configuration for Dashboards. | `object` | `{}` | +| `dashboards.resources.cpu` | CPU available to each node. | `quantity` | `""` | +| `dashboards.resources.memory` | Memory (RAM) available to each node. | `quantity` | `""` | +| `dashboards.resourcesPreset` | Default sizing preset for Dashboards. | `string` | `medium` | + diff --git a/packages/apps/opensearch/charts/cozy-lib b/packages/apps/opensearch/charts/cozy-lib new file mode 120000 index 00000000..e1813509 --- /dev/null +++ b/packages/apps/opensearch/charts/cozy-lib @@ -0,0 +1 @@ +../../../library/cozy-lib \ No newline at end of file diff --git a/packages/apps/opensearch/files/versions.yaml b/packages/apps/opensearch/files/versions.yaml new file mode 100644 index 00000000..5e88df94 --- /dev/null +++ b/packages/apps/opensearch/files/versions.yaml @@ -0,0 +1,5 @@ +# OpenSearch version mapping (major version -> image tag) +# Auto-generated by hack/update-versions.sh - do not edit manually +"v3": "3.0.0" +"v2": "2.11.1" +"v1": "1.3.20" diff --git a/packages/apps/opensearch/hack/update-versions.sh b/packages/apps/opensearch/hack/update-versions.sh new file mode 100755 index 00000000..19d9e7b2 --- /dev/null +++ b/packages/apps/opensearch/hack/update-versions.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash + +set -o errexit +set -o nounset +set -o pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OPENSEARCH_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +VERSIONS_FILE="${OPENSEARCH_DIR}/files/versions.yaml" + +# Supported major versions (newest first) +SUPPORTED_MAJOR_VERSIONS="3 2 1" + +echo "Supported major versions: $SUPPORTED_MAJOR_VERSIONS" + +# Check if skopeo is installed +if ! command -v skopeo &> /dev/null; then + echo "Error: skopeo is not installed. Please install skopeo and try again." >&2 + exit 1 +fi + +# Check if jq is installed +if ! command -v jq &> /dev/null; then + echo "Error: jq is not installed. Please install jq and try again." >&2 + exit 1 +fi + +# Get available image tags from Docker Hub +IMAGE="docker.io/opensearchproject/opensearch" +echo "Fetching available image tags from registry..." +TAGS=$(skopeo list-tags "docker://${IMAGE}" | jq -r '.Tags[]') + +echo "# OpenSearch version mapping (major version -> image tag)" > "${VERSIONS_FILE}" +echo "# Auto-generated by hack/update-versions.sh - do not edit manually" >> "${VERSIONS_FILE}" + +for MAJOR in $SUPPORTED_MAJOR_VERSIONS; do + # Find the latest stable release for this major version + LATEST=$(echo "$TAGS" \ + | grep -E "^${MAJOR}\.[0-9]+\.[0-9]+$" \ + | sort -t. -k1,1n -k2,2n -k3,3n \ + | tail -1) + + if [ -n "$LATEST" ]; then + echo "v${MAJOR}: latest tag is ${LATEST}" + echo "\"v${MAJOR}\": \"${LATEST}\"" >> "${VERSIONS_FILE}" + else + echo "WARNING: No stable release found for major version ${MAJOR}" >&2 + fi +done + +echo "" +echo "Updated ${VERSIONS_FILE}:" +cat "${VERSIONS_FILE}" diff --git a/packages/apps/opensearch/logos/opensearch.svg b/packages/apps/opensearch/logos/opensearch.svg new file mode 100644 index 00000000..345fdd0a --- /dev/null +++ b/packages/apps/opensearch/logos/opensearch.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/packages/apps/opensearch/templates/.gitkeep b/packages/apps/opensearch/templates/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/packages/apps/opensearch/templates/_versions.tpl b/packages/apps/opensearch/templates/_versions.tpl new file mode 100644 index 00000000..4eae4163 --- /dev/null +++ b/packages/apps/opensearch/templates/_versions.tpl @@ -0,0 +1,13 @@ +{{/* +Version mapping helper +Loads version mapping from files/versions.yaml and returns the full version for a given major version +*/}} +{{- define "opensearch.versionMap" -}} +{{- $versions := .Files.Get "files/versions.yaml" | fromYaml -}} +{{- $version := .Values.version | default "v2" -}} +{{- if hasKey $versions $version -}} +{{- index $versions $version -}} +{{- else -}} +{{- fail (printf "Invalid version '%s'. Available versions: %s" $version (keys $versions | join ", ")) -}} +{{- end -}} +{{- end -}} diff --git a/packages/apps/opensearch/templates/dashboard-resourcemap.yaml b/packages/apps/opensearch/templates/dashboard-resourcemap.yaml new file mode 100644 index 00000000..91dbf8cb --- /dev/null +++ b/packages/apps/opensearch/templates/dashboard-resourcemap.yaml @@ -0,0 +1,42 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ .Release.Name }}-dashboard-resources +rules: +- apiGroups: + - "" + resources: + - services + resourceNames: + - {{ .Release.Name }} + - {{ .Release.Name }}-external + {{- if .Values.dashboards.enabled }} + - {{ .Release.Name }}-dashboards + - {{ .Release.Name }}-dashboards-external + {{- end }} + verbs: ["get", "list", "watch"] +- apiGroups: + - "" + resources: + - secrets + resourceNames: + - {{ .Release.Name }}-credentials + verbs: ["get", "list", "watch"] +- apiGroups: + - cozystack.io + resources: + - workloadmonitors + resourceNames: + - {{ .Release.Name }} + verbs: ["get", "list", "watch"] +--- +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: {{ .Release.Name }}-dashboard-resources +subjects: +{{ include "cozy-lib.rbac.subjectsForTenantAndAccessLevel" (list "use" .Release.Namespace) }} +roleRef: + kind: Role + name: {{ .Release.Name }}-dashboard-resources + apiGroup: rbac.authorization.k8s.io diff --git a/packages/apps/opensearch/templates/external-svc.yaml b/packages/apps/opensearch/templates/external-svc.yaml new file mode 100644 index 00000000..f28626ab --- /dev/null +++ b/packages/apps/opensearch/templates/external-svc.yaml @@ -0,0 +1,39 @@ +{{- if .Values.external }} +{{- $clusterDomain := (index .Values._cluster "cluster-domain") | default "cozy.local" }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }}-external + annotations: + external-dns.alpha.kubernetes.io/hostname: {{ .Release.Name }}.{{ .Release.Namespace }}.{{ $clusterDomain }} +spec: + type: LoadBalancer + selector: + opster.io/opensearch-cluster: {{ .Release.Name }} + opster.io/opensearch-nodepool: nodes + ports: + - name: https + port: 9200 + targetPort: 9200 + protocol: TCP +{{- if .Values.dashboards.enabled }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }}-dashboards-external + annotations: + external-dns.alpha.kubernetes.io/hostname: {{ .Release.Name }}-dashboards.{{ .Release.Namespace }}.{{ $clusterDomain }} +spec: + type: LoadBalancer + selector: + opster.io/opensearch-cluster: {{ .Release.Name }} + app.kubernetes.io/component: dashboards + ports: + - name: https + port: 5601 + targetPort: 5601 + protocol: TCP +{{- end }} +{{- end }} diff --git a/packages/apps/opensearch/templates/opensearch.yaml b/packages/apps/opensearch/templates/opensearch.yaml new file mode 100644 index 00000000..9917cf5f --- /dev/null +++ b/packages/apps/opensearch/templates/opensearch.yaml @@ -0,0 +1,99 @@ +{{- $topologyMode := .Values.topologySpreadPolicy | default "soft" }} +{{- $whenUnsatisfiable := "ScheduleAnyway" }} +{{- if eq $topologyMode "hard" }} +{{- $whenUnsatisfiable = "DoNotSchedule" }} +{{- end }} +--- +apiVersion: opensearch.opster.io/v1 +kind: OpenSearchCluster +metadata: + name: {{ .Release.Name }} +spec: + general: + serviceName: {{ .Release.Name }} + version: {{ include "opensearch.versionMap" $ }} + httpPort: 9200 + drainDataNodes: true + {{- if gt (len .Values.images.opensearch) 0 }} + image: {{ .Values.images.opensearch }} + {{- end }} + bootstrap: + resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.resourcesPreset .Values.resources $) | nindent 6 }} + security: + tls: + transport: + generate: true + perNode: true + http: + generate: true + config: + securityConfigSecret: + name: {{ .Release.Name }}-security-config + adminCredentialsSecret: + name: {{ .Release.Name }}-admin-credentials + {{- if .Values.dashboards.enabled }} + dashboards: + enable: true + version: {{ include "opensearch.versionMap" $ }} + replicas: {{ .Values.dashboards.replicas | default 1 }} + tls: + enable: true + generate: true + resources: {{- include "cozy-lib.resources.defaultingSanitize" (list (.Values.dashboards.resourcesPreset | default "medium") .Values.dashboards.resources $) | nindent 6 }} + {{- end }} + nodePools: + - component: nodes + replicas: {{ .Values.replicas }} + diskSize: {{ .Values.size }} + persistence: + pvc: + accessModes: + - ReadWriteOnce + {{- if .Values.storageClass }} + storageClass: {{ .Values.storageClass }} + {{- else if eq (int .Values.replicas) 1 }} + storageClass: replicated + {{- else }} + storageClass: local + {{- end }} + resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.resourcesPreset .Values.resources $) | nindent 8 }} + roles: + {{- if .Values.nodeRoles.master }} + - cluster_manager + {{- end }} + {{- if .Values.nodeRoles.data }} + - data + {{- end }} + {{- if .Values.nodeRoles.ingest }} + - ingest + {{- end }} + {{- if .Values.nodeRoles.ml }} + - ml + {{- end }} + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: {{ $whenUnsatisfiable }} + labelSelector: + matchLabels: + opster.io/opensearch-cluster: {{ .Release.Name }} + - maxSkew: 1 + topologyKey: topology.kubernetes.io/zone + whenUnsatisfiable: {{ $whenUnsatisfiable }} + labelSelector: + matchLabels: + opster.io/opensearch-cluster: {{ .Release.Name }} +--- +# WorkloadMonitor tracks OpenSearch pods +apiVersion: cozystack.io/v1alpha1 +kind: WorkloadMonitor +metadata: + name: {{ .Release.Name }} +spec: + replicas: {{ .Values.replicas }} + minReplicas: 1 + kind: opensearch + type: opensearch + selector: + opster.io/opensearch-cluster: {{ .Release.Name }} + version: {{ .Chart.Version }} diff --git a/packages/apps/opensearch/templates/security.yaml b/packages/apps/opensearch/templates/security.yaml new file mode 100644 index 00000000..d10d353a --- /dev/null +++ b/packages/apps/opensearch/templates/security.yaml @@ -0,0 +1,120 @@ +{{- $clusterDomain := (index .Values._cluster "cluster-domain") | default "cozy.local" }} +{{- $existingAdminCreds := lookup "v1" "Secret" .Release.Namespace (printf "%s-admin-credentials" .Release.Name) }} +{{- $existingSecurityConfig := lookup "v1" "Secret" .Release.Namespace (printf "%s-security-config" .Release.Name) }} +{{- $password := randAlphaNum 32 }} +{{- if and $existingAdminCreds (hasKey $existingAdminCreds.data "password") }} +{{- $password = index $existingAdminCreds.data "password" | b64dec }} +{{- end }} +--- +# Admin credentials for the OpenSearch operator (referenced by adminCredentialsSecret) +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Release.Name }}-admin-credentials + labels: + app.kubernetes.io/name: opensearch + app.kubernetes.io/instance: {{ .Release.Name }} +type: Opaque +stringData: + username: admin + password: {{ $password | quote }} +--- +# Security plugin configuration (referenced by securityConfigSecret) +# On upgrades, the existing secret is preserved to avoid bcrypt hash regeneration +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Release.Name }}-security-config + labels: + app.kubernetes.io/name: opensearch + app.kubernetes.io/instance: {{ .Release.Name }} +type: Opaque +{{- if and $existingSecurityConfig (hasKey $existingSecurityConfig.data "internal_users.yml") }} +data: + {{- range $key, $val := $existingSecurityConfig.data }} + {{ $key }}: {{ $val }} + {{- end }} +{{- else }} +stringData: + config.yml: | + --- + _meta: + type: "config" + config_version: 2 + config: + dynamic: + http: + anonymous_auth_enabled: false + authc: + basic_internal_auth_domain: + description: "Authenticate via HTTP Basic against internal users database" + http_enabled: true + transport_enabled: true + order: 0 + http_authenticator: + type: basic + challenge: true + authentication_backend: + type: internal + + internal_users.yml: | + --- + _meta: + type: "internalusers" + config_version: 2 + admin: + hash: {{ htpasswd "admin" $password | trimPrefix "admin:" | quote }} + reserved: true + backend_roles: + - "admin" + description: "Admin user" + + roles.yml: | + --- + _meta: + type: "roles" + config_version: 2 + + roles_mapping.yml: | + --- + _meta: + type: "rolesmapping" + config_version: 2 + all_access: + reserved: false + backend_roles: + - "admin" + + action_groups.yml: | + --- + _meta: + type: "actiongroups" + config_version: 2 + + tenants.yml: | + --- + _meta: + type: "tenants" + config_version: 2 +{{- end }} +--- +# User-facing credentials with connection URI +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Release.Name }}-credentials + labels: + app.kubernetes.io/name: opensearch + app.kubernetes.io/instance: {{ .Release.Name }} +type: Opaque +stringData: + username: admin + password: {{ $password | quote }} + host: {{ .Release.Name }}.{{ .Release.Namespace }}.svc.{{ $clusterDomain }} + port: "9200" + uri: https://admin:{{ $password | urlquery }}@{{ .Release.Name }}.{{ .Release.Namespace }}.svc.{{ $clusterDomain }}:9200 + {{- if .Values.dashboards.enabled }} + dashboards-host: {{ .Release.Name }}-dashboards.{{ .Release.Namespace }}.svc.{{ $clusterDomain }} + dashboards-port: "5601" + dashboards-uri: https://admin:{{ $password | urlquery }}@{{ .Release.Name }}-dashboards.{{ .Release.Namespace }}.svc.{{ $clusterDomain }}:5601 + {{- end }} diff --git a/packages/apps/opensearch/templates/users.yaml b/packages/apps/opensearch/templates/users.yaml new file mode 100644 index 00000000..1d326206 --- /dev/null +++ b/packages/apps/opensearch/templates/users.yaml @@ -0,0 +1,21 @@ +{{- range $username, $user := .Values.users }} +{{- $existingSecret := lookup "v1" "Secret" $.Release.Namespace (printf "%s-user-%s" $.Release.Name $username) }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ $.Release.Name }}-user-{{ $username }} + labels: + opensearch.opster.io/credentials: "true" +type: kubernetes.io/basic-auth +stringData: + username: {{ $username | quote }} + {{- if $user.password }} + password: {{ $user.password | quote }} + {{- else if and $existingSecret (hasKey $existingSecret.data "password") }} + password: {{ index $existingSecret.data "password" | b64dec | quote }} + {{- else }} + password: {{ randAlphaNum 16 | quote }} + {{- end }} + roles: {{ $user.roles | default (list "readall") | join "," | quote }} +{{- end }} diff --git a/packages/apps/opensearch/tests/opensearch_test.yaml b/packages/apps/opensearch/tests/opensearch_test.yaml new file mode 100644 index 00000000..78850134 --- /dev/null +++ b/packages/apps/opensearch/tests/opensearch_test.yaml @@ -0,0 +1,516 @@ +suite: opensearch CR tests + +templates: + - templates/opensearch.yaml + +tests: + ################### + # Basic rendering # + ################### + + - it: renders OpenSearchCluster and WorkloadMonitor + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + asserts: + - hasDocuments: + count: 2 + - isKind: + of: OpenSearchCluster + documentIndex: 0 + - isKind: + of: WorkloadMonitor + documentIndex: 1 + + - it: sets correct CR name + release: + name: my-opensearch + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + asserts: + - equal: + path: metadata.name + value: my-opensearch + documentIndex: 0 + + ################## + # Version # + ################## + + - it: defaults to version 2.11.1 + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + asserts: + - equal: + path: spec.general.version + value: "2.11.1" + documentIndex: 0 + + - it: sets version 3.0.0 when v3 selected + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + version: v3 + asserts: + - equal: + path: spec.general.version + value: "3.0.0" + documentIndex: 0 + + - it: sets version 1.3.20 when v1 selected + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + version: v1 + asserts: + - equal: + path: spec.general.version + value: "1.3.20" + documentIndex: 0 + + ##################### + # General # + ##################### + + - it: sets drainDataNodes to true + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + asserts: + - equal: + path: spec.general.drainDataNodes + value: true + documentIndex: 0 + + - it: sets httpPort to 9200 + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + asserts: + - equal: + path: spec.general.httpPort + value: 9200 + documentIndex: 0 + + ##################### + # Security # + ##################### + + - it: always includes security section with TLS + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + asserts: + - equal: + path: spec.security.tls.transport.generate + value: true + documentIndex: 0 + - equal: + path: spec.security.tls.transport.perNode + value: true + documentIndex: 0 + - equal: + path: spec.security.tls.http.generate + value: true + documentIndex: 0 + + - it: references security config secret + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + asserts: + - equal: + path: spec.security.config.securityConfigSecret.name + value: test-os-security-config + documentIndex: 0 + + - it: references admin credentials secret + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + asserts: + - equal: + path: spec.security.config.adminCredentialsSecret.name + value: test-os-admin-credentials + documentIndex: 0 + + - it: does not disable security plugin + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + asserts: + - notExists: + path: spec.general.additionalConfig + documentIndex: 0 + + ##################### + # Node Pools # + ##################### + + - it: sets default replica count to 3 + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + asserts: + - equal: + path: spec.nodePools[0].replicas + value: 3 + documentIndex: 0 + + - it: sets replica count from values + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + replicas: 5 + asserts: + - equal: + path: spec.nodePools[0].replicas + value: 5 + documentIndex: 0 + + ##################### + # Node Roles # + ##################### + + - it: enables default node roles + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + asserts: + - contains: + path: spec.nodePools[0].roles + content: cluster_manager + documentIndex: 0 + - contains: + path: spec.nodePools[0].roles + content: data + documentIndex: 0 + - contains: + path: spec.nodePools[0].roles + content: ingest + documentIndex: 0 + + - it: disables master role when configured + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + nodeRoles: + master: false + data: true + ingest: true + ml: false + asserts: + - notContains: + path: spec.nodePools[0].roles + content: cluster_manager + documentIndex: 0 + + - it: enables ml role when configured + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + nodeRoles: + master: true + data: true + ingest: true + ml: true + asserts: + - contains: + path: spec.nodePools[0].roles + content: ml + documentIndex: 0 + + ########################### + # Topology Spread Policy # + ########################### + + - it: uses soft topology spread by default (ScheduleAnyway) + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + asserts: + - equal: + path: spec.nodePools[0].topologySpreadConstraints[0].whenUnsatisfiable + value: ScheduleAnyway + documentIndex: 0 + - equal: + path: spec.nodePools[0].topologySpreadConstraints[1].whenUnsatisfiable + value: ScheduleAnyway + documentIndex: 0 + + - it: uses hard topology spread when configured (DoNotSchedule) + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + topologySpreadPolicy: hard + asserts: + - equal: + path: spec.nodePools[0].topologySpreadConstraints[0].whenUnsatisfiable + value: DoNotSchedule + documentIndex: 0 + - equal: + path: spec.nodePools[0].topologySpreadConstraints[1].whenUnsatisfiable + value: DoNotSchedule + documentIndex: 0 + + - it: sets correct topology keys + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + asserts: + - equal: + path: spec.nodePools[0].topologySpreadConstraints[0].topologyKey + value: kubernetes.io/hostname + documentIndex: 0 + - equal: + path: spec.nodePools[0].topologySpreadConstraints[1].topologyKey + value: topology.kubernetes.io/zone + documentIndex: 0 + + ##################### + # Storage # + ##################### + + - it: sets accessModes to ReadWriteOnce + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + asserts: + - contains: + path: spec.nodePools[0].persistence.pvc.accessModes + content: ReadWriteOnce + documentIndex: 0 + + - it: sets storage size from values + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + size: 50Gi + asserts: + - equal: + path: spec.nodePools[0].diskSize + value: 50Gi + documentIndex: 0 + + - it: uses local storageClass when replicas > 1 and no storageClass + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + storageClass: "" + replicas: 3 + asserts: + - equal: + path: spec.nodePools[0].persistence.pvc.storageClass + value: local + documentIndex: 0 + + - it: uses replicated storageClass when single replica and no storageClass + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + storageClass: "" + replicas: 1 + asserts: + - equal: + path: spec.nodePools[0].persistence.pvc.storageClass + value: replicated + documentIndex: 0 + + - it: uses custom storageClass when provided + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + storageClass: fast-ssd + asserts: + - equal: + path: spec.nodePools[0].persistence.pvc.storageClass + value: fast-ssd + documentIndex: 0 + + ##################### + # Dashboards # + ##################### + + - it: does not render dashboards when disabled + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + dashboards: + enabled: false + replicas: 1 + resourcesPreset: "medium" + resources: {} + asserts: + - notExists: + path: spec.dashboards + documentIndex: 0 + + - it: renders dashboards when enabled + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + dashboards: + enabled: true + replicas: 1 + resourcesPreset: "medium" + resources: {} + asserts: + - equal: + path: spec.dashboards.enable + value: true + documentIndex: 0 + - equal: + path: spec.dashboards.replicas + value: 1 + documentIndex: 0 + - equal: + path: spec.dashboards.tls.enable + value: true + documentIndex: 0 + - equal: + path: spec.dashboards.tls.generate + value: true + documentIndex: 0 + + ########################### + # WorkloadMonitor # + ########################### + + - it: creates WorkloadMonitor with correct metadata + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + asserts: + - equal: + path: metadata.name + value: test-os + documentIndex: 1 + - equal: + path: spec.kind + value: opensearch + documentIndex: 1 + - equal: + path: spec.type + value: opensearch + documentIndex: 1 + + - it: sets replicas in WorkloadMonitor + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + replicas: 5 + asserts: + - equal: + path: spec.replicas + value: 5 + documentIndex: 1 + + - it: sets minReplicas to 1 + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + asserts: + - equal: + path: spec.minReplicas + value: 1 + documentIndex: 1 + + - it: sets correct selector labels + release: + name: mydb + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + asserts: + - equal: + path: spec.selector["opster.io/opensearch-cluster"] + value: mydb + documentIndex: 1 diff --git a/packages/apps/opensearch/tests/security_test.yaml b/packages/apps/opensearch/tests/security_test.yaml new file mode 100644 index 00000000..cd8740d9 --- /dev/null +++ b/packages/apps/opensearch/tests/security_test.yaml @@ -0,0 +1,207 @@ +suite: security secrets tests + +templates: + - templates/security.yaml + +tests: + ################### + # Basic rendering # + ################### + + - it: renders three secrets (admin-credentials, security-config, credentials) + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + asserts: + - hasDocuments: + count: 3 + + ########################### + # Admin credentials # + ########################### + + - it: sets admin-credentials secret name + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + asserts: + - equal: + path: metadata.name + value: test-os-admin-credentials + documentIndex: 0 + + - it: sets admin username + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + asserts: + - equal: + path: stringData.username + value: admin + documentIndex: 0 + + - it: generates a 32-char alphanumeric password + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + asserts: + - matchRegex: + path: stringData.password + pattern: "^[a-zA-Z0-9]{32}$" + documentIndex: 0 + + ########################### + # Security config # + ########################### + + - it: sets security-config secret name + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + asserts: + - equal: + path: metadata.name + value: test-os-security-config + documentIndex: 1 + + - it: includes config.yml with basic auth + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + asserts: + - exists: + path: stringData["config.yml"] + documentIndex: 1 + + - it: includes internal_users.yml with bcrypt hash + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + asserts: + - matchRegex: + path: stringData["internal_users.yml"] + pattern: "hash:.*\\$2a\\$" + documentIndex: 1 + + - it: includes roles_mapping.yml + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + asserts: + - exists: + path: stringData["roles_mapping.yml"] + documentIndex: 1 + + ########################### + # User-facing credentials # + ########################### + + - it: sets credentials secret name + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + asserts: + - equal: + path: metadata.name + value: test-os-credentials + documentIndex: 2 + + - it: sets correct host and port + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + asserts: + - equal: + path: stringData.host + value: test-os.tenant-test.svc.cozy.local + documentIndex: 2 + - equal: + path: stringData.port + value: "9200" + documentIndex: 2 + + - it: sets https URI with credentials + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + asserts: + - matchRegex: + path: stringData.uri + pattern: "^https://admin:[a-zA-Z0-9]{32}@test-os\\.tenant-test\\.svc\\.cozy\\.local:9200$" + documentIndex: 2 + + - it: does not include dashboards fields when dashboards disabled + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + dashboards: + enabled: false + replicas: 1 + resourcesPreset: "medium" + resources: {} + asserts: + - notExists: + path: stringData.dashboards-host + documentIndex: 2 + + - it: includes dashboards fields when dashboards enabled + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + dashboards: + enabled: true + replicas: 1 + resourcesPreset: "medium" + resources: {} + asserts: + - equal: + path: stringData.dashboards-host + value: test-os-dashboards.tenant-test.svc.cozy.local + documentIndex: 2 + - equal: + path: stringData.dashboards-port + value: "5601" + documentIndex: 2 + - matchRegex: + path: stringData.dashboards-uri + pattern: "^https://admin:[a-zA-Z0-9]{32}@test-os-dashboards\\.tenant-test\\.svc\\.cozy\\.local:5601$" + documentIndex: 2 diff --git a/packages/apps/opensearch/tests/users_test.yaml b/packages/apps/opensearch/tests/users_test.yaml new file mode 100644 index 00000000..f6b4990e --- /dev/null +++ b/packages/apps/opensearch/tests/users_test.yaml @@ -0,0 +1,176 @@ +suite: users secrets tests + +templates: + - templates/users.yaml + +tests: + ################### + # Basic rendering # + ################### + + - it: does not render secrets when no users defined + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + users: {} + asserts: + - hasDocuments: + count: 0 + + - it: renders one secret per user + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + users: + user1: + roles: + - readall + user2: + roles: + - all_access + asserts: + - hasDocuments: + count: 2 + + ##################### + # Secret metadata # + ##################### + + - it: sets correct secret name + release: + name: my-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + users: + myuser: + roles: + - readall + asserts: + - equal: + path: metadata.name + value: my-os-user-myuser + + - it: sets opensearch credentials label + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + users: + myuser: + roles: + - readall + asserts: + - equal: + path: metadata.labels["opensearch.opster.io/credentials"] + value: "true" + + - it: uses basic-auth secret type + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + users: + myuser: + roles: + - readall + asserts: + - equal: + path: type + value: kubernetes.io/basic-auth + + ##################### + # Secret data # + ##################### + + - it: sets username in secret + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + users: + testuser: + roles: + - readall + asserts: + - equal: + path: stringData.username + value: testuser + + - it: uses provided password + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + users: + myuser: + password: mysecretpassword + roles: + - readall + asserts: + - equal: + path: stringData.password + value: mysecretpassword + + - it: generates password when not provided + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + users: + myuser: + roles: + - readall + asserts: + - matchRegex: + path: stringData.password + pattern: "^[a-zA-Z0-9]{16}$" + + - it: sets roles as comma-separated string + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + users: + myuser: + roles: + - readall + - reporting_user + - monitoring_user + asserts: + - equal: + path: stringData.roles + value: "readall,reporting_user,monitoring_user" + + - it: defaults to readall role when no roles specified + release: + name: test-os + namespace: tenant-test + set: + _cluster: + cluster-domain: cozy.local + users: + myuser: {} + asserts: + - equal: + path: stringData.roles + value: "readall" diff --git a/packages/apps/opensearch/values.schema.json b/packages/apps/opensearch/values.schema.json new file mode 100644 index 00000000..54a6bb00 --- /dev/null +++ b/packages/apps/opensearch/values.schema.json @@ -0,0 +1,239 @@ +{ + "title": "Chart Values", + "type": "object", + "properties": { + "dashboards": { + "description": "OpenSearch Dashboards configuration.", + "type": "object", + "default": {}, + "required": [ + "enabled", + "replicas", + "resourcesPreset" + ], + "properties": { + "enabled": { + "description": "Enable OpenSearch Dashboards deployment.", + "type": "boolean", + "default": false + }, + "replicas": { + "description": "Number of Dashboards replicas.", + "type": "integer", + "default": 1 + }, + "resources": { + "description": "Explicit CPU and memory configuration for Dashboards.", + "type": "object", + "default": {}, + "properties": { + "cpu": { + "description": "CPU available to each node.", + "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 node.", + "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 for Dashboards.", + "type": "string", + "default": "medium", + "enum": [ + "nano", + "micro", + "small", + "medium", + "large", + "xlarge", + "2xlarge" + ] + } + } + }, + "external": { + "description": "Enable external access from outside the cluster.", + "type": "boolean", + "default": false + }, + "images": { + "description": "Container images used by the operator.", + "type": "object", + "default": {}, + "required": [ + "opensearch" + ], + "properties": { + "opensearch": { + "description": "OpenSearch image.", + "type": "string", + "default": "" + } + } + }, + "nodeRoles": { + "description": "Node roles configuration.", + "type": "object", + "default": {}, + "required": [ + "data", + "ingest", + "master", + "ml" + ], + "properties": { + "data": { + "description": "Enable data role.", + "type": "boolean", + "default": true + }, + "ingest": { + "description": "Enable ingest role.", + "type": "boolean", + "default": true + }, + "master": { + "description": "Enable cluster_manager role.", + "type": "boolean", + "default": true + }, + "ml": { + "description": "Enable machine learning role.", + "type": "boolean", + "default": false + } + } + }, + "replicas": { + "description": "Number of OpenSearch nodes in the cluster.", + "type": "integer", + "default": 3 + }, + "resources": { + "description": "Explicit CPU and memory configuration for each OpenSearch node. When omitted, the preset defined in `resourcesPreset` is applied.", + "type": "object", + "default": {}, + "properties": { + "cpu": { + "description": "CPU available to each node.", + "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 node.", + "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. OpenSearch requires minimum 2Gi memory.", + "type": "string", + "default": "large", + "enum": [ + "nano", + "micro", + "small", + "medium", + "large", + "xlarge", + "2xlarge" + ] + }, + "size": { + "description": "Persistent Volume Claim size available for application data.", + "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", + "default": "" + }, + "topologySpreadPolicy": { + "description": "How strictly to enforce pod distribution across nodes and zones.", + "type": "string", + "default": "soft", + "enum": [ + "soft", + "hard" + ] + }, + "users": { + "description": "Custom OpenSearch users configuration map.", + "type": "object", + "default": {}, + "additionalProperties": { + "type": "object", + "properties": { + "password": { + "description": "Password for the user (auto-generated if omitted).", + "type": "string" + }, + "roles": { + "description": "List of OpenSearch roles.", + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "version": { + "description": "OpenSearch major version to deploy.", + "type": "string", + "default": "v2", + "enum": [ + "v3", + "v2", + "v1" + ] + } + } +} \ No newline at end of file diff --git a/packages/apps/opensearch/values.yaml b/packages/apps/opensearch/values.yaml new file mode 100644 index 00000000..8de805e3 --- /dev/null +++ b/packages/apps/opensearch/values.yaml @@ -0,0 +1,111 @@ +## +## @section Common parameters +## + +## @typedef {struct} Resources - Explicit CPU and memory configuration for each OpenSearch node. +## @field {quantity} [cpu] - CPU available to each node. +## @field {quantity} [memory] - Memory (RAM) available to each node. + +## @enum {string} ResourcesPreset - Default sizing preset. +## @value nano +## @value micro +## @value small +## @value medium +## @value large +## @value xlarge +## @value 2xlarge + +## @param {int} replicas - Number of OpenSearch nodes in the cluster. +replicas: 3 + +## @param {Resources} [resources] - Explicit CPU and memory configuration for each OpenSearch node. When omitted, the preset defined in `resourcesPreset` is applied. +resources: {} + +## @param {ResourcesPreset} resourcesPreset="large" - Default sizing preset used when `resources` is omitted. OpenSearch requires minimum 2Gi memory. +resourcesPreset: "large" + +## @param {quantity} size - Persistent Volume Claim size available for application data. +size: 10Gi + +## @param {string} storageClass - StorageClass used to store the data. +storageClass: "" + +## @param {bool} external - Enable external access from outside the cluster. +external: false + +## @enum {string} TopologySpreadPolicy - Pod distribution policy across nodes/zones. +## @value soft - Best-effort distribution (ScheduleAnyway) - pods may be scheduled on same node if needed +## @value hard - Strict distribution (DoNotSchedule) - pods will not schedule if spread cannot be achieved + +## @param {TopologySpreadPolicy} topologySpreadPolicy="soft" - How strictly to enforce pod distribution across nodes and zones. +topologySpreadPolicy: "soft" + +## +## @enum {string} Version +## @value v3 +## @value v2 +## @value v1 + +## @param {Version} version - OpenSearch major version to deploy. +version: v2 + +## +## @section Image configuration +## + +## @typedef {struct} Images - Container image configuration. +## @field {string} opensearch - OpenSearch image. + +## @param {Images} images - Container images used by the operator. +images: + opensearch: "" + +## +## @section Node roles configuration +## + +## @typedef {struct} NodeRoles - OpenSearch node roles. +## @field {bool} master - Enable cluster_manager role. +## @field {bool} data - Enable data role. +## @field {bool} ingest - Enable ingest role. +## @field {bool} ml - Enable machine learning role. + +## @param {NodeRoles} nodeRoles - Node roles configuration. +nodeRoles: + master: true + data: true + ingest: true + ml: false + +## +## @section Users configuration +## + +## @typedef {struct} User - User configuration. +## @field {string} [password] - Password for the user (auto-generated if omitted). +## @field {[]string} roles - List of OpenSearch roles. + +## @param {map[string]User} users - Custom OpenSearch users configuration map. +users: {} +## Example: +## users: +## myuser: +## roles: +## - all_access + +## +## @section OpenSearch Dashboards configuration +## + +## @typedef {struct} Dashboards - OpenSearch Dashboards deployment configuration. +## @field {bool} enabled - Enable OpenSearch Dashboards deployment. +## @field {int} replicas - Number of Dashboards replicas. +## @field {Resources} [resources] - Explicit CPU and memory configuration for Dashboards. +## @field {ResourcesPreset} resourcesPreset - Default sizing preset for Dashboards. + +## @param {Dashboards} dashboards - OpenSearch Dashboards configuration. +dashboards: + enabled: false + replicas: 1 + resources: {} + resourcesPreset: "medium" diff --git a/packages/system/opensearch-operator/.helmignore b/packages/system/opensearch-operator/.helmignore new file mode 100644 index 00000000..0e8a0eb3 --- /dev/null +++ b/packages/system/opensearch-operator/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/packages/system/opensearch-operator/Chart.yaml b/packages/system/opensearch-operator/Chart.yaml new file mode 100644 index 00000000..a991bdca --- /dev/null +++ b/packages/system/opensearch-operator/Chart.yaml @@ -0,0 +1,3 @@ +apiVersion: v2 +name: cozy-opensearch-operator +version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process diff --git a/packages/system/opensearch-operator/Makefile b/packages/system/opensearch-operator/Makefile new file mode 100644 index 00000000..71b94549 --- /dev/null +++ b/packages/system/opensearch-operator/Makefile @@ -0,0 +1,10 @@ +export NAME=opensearch-operator +export NAMESPACE=cozy-$(NAME) + +include ../../../hack/package.mk + +update: + rm -rf charts + helm repo add opensearch-operator https://opensearch-project.github.io/opensearch-k8s-operator/ + helm repo update opensearch-operator + helm pull opensearch-operator/opensearch-operator --untar --untardir charts diff --git a/packages/system/opensearch-operator/charts/opensearch-operator/CHANGELOG.md b/packages/system/opensearch-operator/charts/opensearch-operator/CHANGELOG.md new file mode 100644 index 00000000..1866ab55 --- /dev/null +++ b/packages/system/opensearch-operator/charts/opensearch-operator/CHANGELOG.md @@ -0,0 +1,61 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +--- +## [Unreleased] +### Added +- Added support for custom image used by `kubeRbacProxy`. +### Changed +### Deprecated +### Removed +### Fixed +### Security + +--- +## [2.0.0] +### Added +### Changed +- Modified `version` to `2.0.0` and `appVersion` to `v2.0`. +- Allow chart image tag to pick from `appVersion`, unless explicitly passed `tag` values in `values.yaml` file. +### Deprecated +### Removed +### Fixed +### Security + +--- +## [1.0.3] +### Added +### Changed +- Added missing spec `dashboards.additionalConfig` +### Deprecated +### Removed +### Fixed +### Security + +--- +## [1.0.2] +### Added +### Changed +- Added README.md file to charts/ folder. +### Deprecated +### Removed +### Fixed +### Security + +--- +## [1.0.1] +### Added +### Changed +- Updated version to 1.0.1 +### Deprecated +### Removed +### Fixed +### Security + +[Unreleased]: https://github.com/opensearch-project/opensearch-k8s-operator/compare/opensearch-operator-2.0.0...HEAD +[2.0.0]: https://github.com/opensearch-project/opensearch-k8s-operator/compare/opensearch-operator-1.0.3...opensearch-operator-2.0.0 +[1.0.3]: https://github.com/opensearch-project/opensearch-k8s-operator/compare/opensearch-operator-1.0.2...opensearch-operator-1.0.3 +[1.0.2]: https://github.com/opensearch-project/opensearch-k8s-operator/compare/opensearch-operator-1.0.1...opensearch-operator-1.0.2 diff --git a/packages/system/opensearch-operator/charts/opensearch-operator/Chart.yaml b/packages/system/opensearch-operator/charts/opensearch-operator/Chart.yaml new file mode 100644 index 00000000..331cf1e2 --- /dev/null +++ b/packages/system/opensearch-operator/charts/opensearch-operator/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +appVersion: 2.8.0 +description: The OpenSearch Operator Helm chart for Kubernetes +name: opensearch-operator +type: application +version: 2.8.0 diff --git a/packages/system/opensearch-operator/charts/opensearch-operator/README.md b/packages/system/opensearch-operator/charts/opensearch-operator/README.md new file mode 100644 index 00000000..dc63c250 --- /dev/null +++ b/packages/system/opensearch-operator/charts/opensearch-operator/README.md @@ -0,0 +1,29 @@ +# OpenSearch-k8s-operator + +The Kubernetes [OpenSearch Operator](https://github.com/opensearch-project/opensearch-k8s-operator) is used for automating the deployment, provisioning, management, and orchestration of OpenSearch clusters and OpenSearch dashboards. + +## Getting started + +The Operator can be easily installed using helm on any CNCF-certified Kubernetes cluster. Please refer to the [User Guide](https://github.com/opensearch-project/opensearch-k8s-operator/blob/main/docs/userguide/main.md) for more information. + +### Installation Using Helm + +#### Get Repo Info +``` +helm repo add opensearch-operator https://opensearch-project.github.io/opensearch-k8s-operator/ +helm repo update +``` +#### Install Chart +``` +helm install [RELEASE_NAME] opensearch-operator/opensearch-operator +``` +#### Uninstall Chart +``` +helm uninstall [RELEASE_NAME] +``` +#### Upgrade Chart +``` +helm repo update +helm upgrade [RELEASE_NAME] opensearch-operator/opensearch-operator +``` + diff --git a/packages/system/opensearch-operator/charts/opensearch-operator/files/opensearch.opster.io_opensearchactiongroups.yaml b/packages/system/opensearch-operator/charts/opensearch-operator/files/opensearch.opster.io_opensearchactiongroups.yaml new file mode 100644 index 00000000..8ba4c7b2 --- /dev/null +++ b/packages/system/opensearch-operator/charts/opensearch-operator/files/opensearch.opster.io_opensearchactiongroups.yaml @@ -0,0 +1,94 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.0 + name: opensearchactiongroups.opensearch.opster.io +spec: + group: opensearch.opster.io + names: + kind: OpensearchActionGroup + listKind: OpensearchActionGroupList + plural: opensearchactiongroups + shortNames: + - opensearchactiongroup + singular: opensearchactiongroup + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: OpensearchActionGroup is the Schema for the opensearchactiongroups + API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: OpensearchActionGroupSpec defines the desired state of OpensearchActionGroup + properties: + allowedActions: + items: + type: string + type: array + description: + type: string + opensearchCluster: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + type: + type: string + required: + - allowedActions + - opensearchCluster + type: object + status: + description: OpensearchActionGroupStatus defines the observed state of + OpensearchActionGroup + properties: + existingActionGroup: + type: boolean + managedCluster: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + reason: + type: string + state: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/packages/system/opensearch-operator/charts/opensearch-operator/files/opensearch.opster.io_opensearchclusters.yaml b/packages/system/opensearch-operator/charts/opensearch-operator/files/opensearch.opster.io_opensearchclusters.yaml new file mode 100644 index 00000000..063bbd00 --- /dev/null +++ b/packages/system/opensearch-operator/charts/opensearch-operator/files/opensearch.opster.io_opensearchclusters.yaml @@ -0,0 +1,6372 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.0 + name: opensearchclusters.opensearch.opster.io +spec: + group: opensearch.opster.io + names: + kind: OpenSearchCluster + listKind: OpenSearchClusterList + plural: opensearchclusters + shortNames: + - os + - opensearch + singular: opensearchcluster + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.health + name: health + type: string + - description: Available nodes + jsonPath: .status.availableNodes + name: nodes + type: integer + - description: Opensearch version + jsonPath: .status.version + name: version + type: string + - jsonPath: .status.phase + name: phase + type: string + - jsonPath: .metadata.creationTimestamp + name: age + type: date + name: v1 + schema: + openAPIV3Schema: + description: Es is the Schema for the es API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: ClusterSpec defines the desired state of OpenSearchCluster + properties: + bootstrap: + properties: + additionalConfig: + additionalProperties: + type: string + description: Extra items to add to the opensearch.yml, defaults + to General.AdditionalConfig + type: object + affinity: + description: Affinity is a group of affinity scheduling rules. + properties: + nodeAffinity: + description: Describes node affinity scheduling rules for + the pod. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node matches the corresponding matchExpressions; the + node(s) with the highest sum are the most preferred. + items: + description: |- + An empty preferred scheduling term matches all objects with implicit weight 0 + (i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op). + properties: + preference: + description: A node selector term, associated with + the corresponding weight. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + description: Weight associated with matching the + corresponding nodeSelectorTerm, in the range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to an update), the system + may or may not try to eventually evict the pod from its node. + properties: + nodeSelectorTerms: + description: Required. A list of node selector terms. + The terms are ORed. + items: + description: |- + A null or empty node selector term matches no objects. The requirements of + them are ANDed. + The TopologySelectorTerm type implements a subset of the NodeSelectorTerm. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + description: Describes pod affinity scheduling rules (e.g. + co-locate this pod in the same node, zone, etc. as some + other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the + node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both matchLabelKeys and labelSelector. + Also, matchLabelKeys cannot be set when labelSelector isn't set. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both mismatchLabelKeys and labelSelector. + Also, mismatchLabelKeys cannot be set when labelSelector isn't set. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key matches that of any node on which + a pod of the set of pods is running + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both matchLabelKeys and labelSelector. + Also, matchLabelKeys cannot be set when labelSelector isn't set. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both mismatchLabelKeys and labelSelector. + Also, mismatchLabelKeys cannot be set when labelSelector isn't set. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + description: Describes pod anti-affinity scheduling rules + (e.g. avoid putting this pod in the same node, zone, etc. + as some other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the anti-affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling anti-affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the + node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both matchLabelKeys and labelSelector. + Also, matchLabelKeys cannot be set when labelSelector isn't set. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both mismatchLabelKeys and labelSelector. + Also, mismatchLabelKeys cannot be set when labelSelector isn't set. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the anti-affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the anti-affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key matches that of any node on which + a pod of the set of pods is running + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both matchLabelKeys and labelSelector. + Also, matchLabelKeys cannot be set when labelSelector isn't set. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both mismatchLabelKeys and labelSelector. + Also, mismatchLabelKeys cannot be set when labelSelector isn't set. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + jvm: + type: string + keystore: + items: + properties: + keyMappings: + additionalProperties: + type: string + description: Key mappings from secret to keystore keys + type: object + secret: + description: Secret containing key value pairs + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + type: object + type: array + nodeSelector: + additionalProperties: + type: string + type: object + pluginsList: + items: + type: string + type: array + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + tolerations: + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple using the matching operator . + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + type: array + type: object + confMgmt: + description: ConfMgmt defines which additional services will be deployed + properties: + VerUpdate: + type: boolean + autoScaler: + type: boolean + smartScaler: + type: boolean + type: object + dashboards: + properties: + additionalConfig: + additionalProperties: + type: string + description: Additional properties for opensearch_dashboards.yaml + type: object + additionalVolumes: + items: + properties: + configMap: + description: ConfigMap to use to populate the volume + properties: + defaultMode: + description: |- + defaultMode is optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + ConfigMap will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the ConfigMap, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path within a + volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: optional specify whether the ConfigMap + or its keys must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + csi: + description: CSI object to use to populate the volume + properties: + driver: + description: |- + driver is the name of the CSI driver that handles this volume. + Consult with your admin for the correct name as registered in the cluster. + type: string + fsType: + description: |- + fsType to mount. Ex. "ext4", "xfs", "ntfs". + If not provided, the empty value is passed to the associated CSI driver + which will determine the default filesystem to apply. + type: string + nodePublishSecretRef: + description: |- + nodePublishSecretRef is a reference to the secret object containing + sensitive information to pass to the CSI driver to complete the CSI + NodePublishVolume and NodeUnpublishVolume calls. + This field is optional, and may be empty if no secret is required. If the + secret object contains more than one secret, all secret references are passed. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + readOnly: + description: |- + readOnly specifies a read-only configuration for the volume. + Defaults to false (read/write). + type: boolean + volumeAttributes: + additionalProperties: + type: string + description: |- + volumeAttributes stores driver-specific properties that are passed to the CSI + driver. Consult your driver's documentation for supported values. + type: object + required: + - driver + type: object + emptyDir: + description: EmptyDir to use to populate the volume + properties: + medium: + description: |- + medium represents what type of storage medium should back this directory. + The default is "" which means to use the node's default medium. + Must be an empty string (default) or Memory. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + description: |- + sizeLimit is the total amount of local storage required for this EmptyDir volume. + The size limit is also applicable for memory medium. + The maximum usage on memory medium EmptyDir would be the minimum value between + the SizeLimit specified here and the sum of memory limits of all containers in a pod. + The default is nil which means that the limit is undefined. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + name: + description: Name to use for the volume. Required. + type: string + path: + description: Path in the container to mount the volume at. + Required. + type: string + projected: + description: Projected object to use to populate the volume + properties: + defaultMode: + description: |- + defaultMode are the mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + sources: + description: |- + sources is the list of volume projections. Each entry in this list + handles one source. + items: + description: |- + Projection that may be projected along with other supported volume types. + Exactly one of these fields must be set. + properties: + clusterTrustBundle: + description: |- + ClusterTrustBundle allows a pod to access the `.spec.trustBundle` field + of ClusterTrustBundle objects in an auto-updating file. + + Alpha, gated by the ClusterTrustBundleProjection feature gate. + + ClusterTrustBundle objects can either be selected by name, or by the + combination of signer name and a label selector. + + Kubelet performs aggressive normalization of the PEM contents written + into the pod filesystem. Esoteric PEM features such as inter-block + comments and block headers are stripped. Certificates are deduplicated. + The ordering of certificates within the file is arbitrary, and Kubelet + may change the order over time. + properties: + labelSelector: + description: |- + Select all ClusterTrustBundles that match this label selector. Only has + effect if signerName is set. Mutually-exclusive with name. If unset, + interpreted as "match nothing". If set but empty, interpreted as "match + everything". + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + name: + description: |- + Select a single ClusterTrustBundle by object name. Mutually-exclusive + with signerName and labelSelector. + type: string + optional: + description: |- + If true, don't block pod startup if the referenced ClusterTrustBundle(s) + aren't available. If using name, then the named ClusterTrustBundle is + allowed not to exist. If using signerName, then the combination of + signerName and labelSelector is allowed to match zero + ClusterTrustBundles. + type: boolean + path: + description: Relative path from the volume + root to write the bundle. + type: string + signerName: + description: |- + Select all ClusterTrustBundles that match this signer name. + Mutually-exclusive with name. The contents of all selected + ClusterTrustBundles will be unified and deduplicated. + type: string + required: + - path + type: object + configMap: + description: configMap information about the configMap + data to project + properties: + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + ConfigMap will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the ConfigMap, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path + within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: optional specify whether the + ConfigMap or its keys must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + description: downwardAPI information about the + downwardAPI data to project + properties: + items: + description: Items is a list of DownwardAPIVolume + file + items: + description: DownwardAPIVolumeFile represents + information to create the file containing + the pod field + properties: + fieldRef: + description: 'Required: Selects a field + of the pod: only annotations, labels, + name, namespace and uid are supported.' + properties: + apiVersion: + description: Version of the schema + the FieldPath is written in terms + of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to + select in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + description: |- + Optional: mode bits used to set permissions on this file, must be an octal value + between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: 'Required: Path is the + relative path name of the file to + be created. Must not be absolute or + contain the ''..'' path. Must be utf-8 + encoded. The first item of the relative + path must not start with ''..''' + type: string + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, requests.cpu and requests.memory) are currently supported. + properties: + containerName: + description: 'Container name: required + for volumes, optional for env + vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output + format of the exposed resources, + defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource + to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + secret: + description: secret information about the secret + data to project + properties: + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path + within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: optional field specify whether + the Secret or its key must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + description: serviceAccountToken is information + about the serviceAccountToken data to project + properties: + audience: + description: |- + audience is the intended audience of the token. A recipient of a token + must identify itself with an identifier specified in the audience of the + token, and otherwise should reject the token. The audience defaults to the + identifier of the apiserver. + type: string + expirationSeconds: + description: |- + expirationSeconds is the requested duration of validity of the service + account token. As the token approaches expiration, the kubelet volume + plugin will proactively rotate the service account token. The kubelet will + start trying to rotate the token if the token is older than 80 percent of + its time to live or if the token is older than 24 hours.Defaults to 1 hour + and must be at least 10 minutes. + format: int64 + type: integer + path: + description: |- + path is the path relative to the mount point of the file to project the + token into. + type: string + required: + - path + type: object + type: object + type: array + x-kubernetes-list-type: atomic + type: object + restartPods: + description: Whether to restart the pods on content change + type: boolean + secret: + description: Secret to use populate the volume + properties: + defaultMode: + description: |- + defaultMode is Optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values + for mode bits. Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: |- + items If unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path within a + volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + optional: + description: optional field specify whether the Secret + or its keys must be defined + type: boolean + secretName: + description: |- + secretName is the name of the secret in the pod's namespace to use. + More info: https://kubernetes.io/docs/concepts/storage/volumes#secret + type: string + type: object + subPath: + description: SubPath of the referenced volume to mount. + type: string + required: + - name + - path + type: object + type: array + affinity: + description: Affinity is a group of affinity scheduling rules. + properties: + nodeAffinity: + description: Describes node affinity scheduling rules for + the pod. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node matches the corresponding matchExpressions; the + node(s) with the highest sum are the most preferred. + items: + description: |- + An empty preferred scheduling term matches all objects with implicit weight 0 + (i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op). + properties: + preference: + description: A node selector term, associated with + the corresponding weight. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + description: Weight associated with matching the + corresponding nodeSelectorTerm, in the range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to an update), the system + may or may not try to eventually evict the pod from its node. + properties: + nodeSelectorTerms: + description: Required. A list of node selector terms. + The terms are ORed. + items: + description: |- + A null or empty node selector term matches no objects. The requirements of + them are ANDed. + The TopologySelectorTerm type implements a subset of the NodeSelectorTerm. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + description: Describes pod affinity scheduling rules (e.g. + co-locate this pod in the same node, zone, etc. as some + other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the + node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both matchLabelKeys and labelSelector. + Also, matchLabelKeys cannot be set when labelSelector isn't set. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both mismatchLabelKeys and labelSelector. + Also, mismatchLabelKeys cannot be set when labelSelector isn't set. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key matches that of any node on which + a pod of the set of pods is running + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both matchLabelKeys and labelSelector. + Also, matchLabelKeys cannot be set when labelSelector isn't set. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both mismatchLabelKeys and labelSelector. + Also, mismatchLabelKeys cannot be set when labelSelector isn't set. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + description: Describes pod anti-affinity scheduling rules + (e.g. avoid putting this pod in the same node, zone, etc. + as some other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the anti-affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling anti-affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the + node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both matchLabelKeys and labelSelector. + Also, matchLabelKeys cannot be set when labelSelector isn't set. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both mismatchLabelKeys and labelSelector. + Also, mismatchLabelKeys cannot be set when labelSelector isn't set. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the anti-affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the anti-affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key matches that of any node on which + a pod of the set of pods is running + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both matchLabelKeys and labelSelector. + Also, matchLabelKeys cannot be set when labelSelector isn't set. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both mismatchLabelKeys and labelSelector. + Also, mismatchLabelKeys cannot be set when labelSelector isn't set. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + annotations: + additionalProperties: + type: string + type: object + basePath: + description: Base Path for Opensearch Clusters running behind + a reverse proxy + type: string + enable: + type: boolean + env: + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must be a + C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull + a container image + type: string + imagePullSecrets: + items: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + type: array + labels: + additionalProperties: + type: string + type: object + nodeSelector: + additionalProperties: + type: string + type: object + opensearchCredentialsSecret: + description: Secret that contains fields username and password + for dashboards to use to login to opensearch, must only be supplied + if a custom securityconfig is provided + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + pluginsList: + items: + type: string + type: array + podSecurityContext: + description: Set security context for the dashboards pods + properties: + appArmorProfile: + description: |- + appArmorProfile is the AppArmor options to use by the containers in this pod. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile loaded on the node that should be used. + The profile must be preconfigured on the node to work. + Must match the loaded name of the profile. + Must be set if and only if type is "Localhost". + type: string + type: + description: |- + type indicates which kind of AppArmor profile will be applied. + Valid options are: + Localhost - a profile pre-loaded on the node. + RuntimeDefault - the container runtime's default profile. + Unconfined - no AppArmor enforcement. + type: string + required: + - type + type: object + fsGroup: + description: |- + A special supplemental group that applies to all containers in a pod. + Some volume types allow the Kubelet to change the ownership of that volume + to be owned by the pod: + + 1. The owning GID will be the FSGroup + 2. The setgid bit is set (new files created in the volume will be owned by FSGroup) + 3. The permission bits are OR'd with rw-rw---- + + If unset, the Kubelet will not modify the ownership and permissions of any volume. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + fsGroupChangePolicy: + description: |- + fsGroupChangePolicy defines behavior of changing ownership and permission of the volume + before being exposed inside Pod. This field will only apply to + volume types which support fsGroup based ownership(and permissions). + It will have no effect on ephemeral volume types such as: secret, configmaps + and emptydir. + Valid values are "OnRootMismatch" and "Always". If not specified, "Always" is used. + Note that this field cannot be set when spec.os.name is windows. + type: string + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence + for that container. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence + for that container. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxChangePolicy: + description: |- + seLinuxChangePolicy defines how the container's SELinux label is applied to all volumes used by the Pod. + It has no effect on nodes that do not support SELinux or to volumes does not support SELinux. + Valid values are "MountOption" and "Recursive". + + "Recursive" means relabeling of all files on all Pod volumes by the container runtime. + This may be slow for large volumes, but allows mixing privileged and unprivileged Pods sharing the same volume on the same node. + + "MountOption" mounts all eligible Pod volumes with `-o context` mount option. + This requires all Pods that share the same volume to use the same SELinux label. + It is not possible to share the same volume among privileged and unprivileged Pods. + Eligible volumes are in-tree FibreChannel and iSCSI volumes, and all CSI volumes + whose CSI driver announces SELinux support by setting spec.seLinuxMount: true in their + CSIDriver instance. Other volumes are always re-labelled recursively. + "MountOption" value is allowed only when SELinuxMount feature gate is enabled. + + If not specified and SELinuxMount feature gate is enabled, "MountOption" is used. + If not specified and SELinuxMount feature gate is disabled, "MountOption" is used for ReadWriteOncePod volumes + and "Recursive" for all other volumes. + + This field affects only Pods that have SELinux label set, either in PodSecurityContext or in SecurityContext of all containers. + + All Pods that use the same volume should use the same seLinuxChangePolicy, otherwise some pods can get stuck in ContainerCreating state. + Note that this field cannot be set when spec.os.name is windows. + type: string + seLinuxOptions: + description: |- + The SELinux context to be applied to all containers. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in SecurityContext. If set in + both SecurityContext and PodSecurityContext, the value specified in SecurityContext + takes precedence for that container. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by the containers in this pod. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + supplementalGroups: + description: |- + A list of groups applied to the first process run in each container, in + addition to the container's primary GID and fsGroup (if specified). If + the SupplementalGroupsPolicy feature is enabled, the + supplementalGroupsPolicy field determines whether these are in addition + to or instead of any group memberships defined in the container image. + If unspecified, no additional groups are added, though group memberships + defined in the container image may still be used, depending on the + supplementalGroupsPolicy field. + Note that this field cannot be set when spec.os.name is windows. + items: + format: int64 + type: integer + type: array + x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + description: |- + Defines how supplemental groups of the first container processes are calculated. + Valid values are "Merge" and "Strict". If not specified, "Merge" is used. + (Alpha) Using the field requires the SupplementalGroupsPolicy feature gate to be enabled + and the container runtime must implement support for this feature. + Note that this field cannot be set when spec.os.name is windows. + type: string + sysctls: + description: |- + Sysctls hold a list of namespaced sysctls used for the pod. Pods with unsupported + sysctls (by the container runtime) might fail to launch. + Note that this field cannot be set when spec.os.name is windows. + items: + description: Sysctl defines a kernel parameter to be set + properties: + name: + description: Name of a property to set + type: string + value: + description: Value of a property to set + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options within a container's SecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + replicas: + format: int32 + type: integer + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + securityContext: + description: Set security context for the dashboards pods' container + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + appArmorProfile: + description: |- + appArmorProfile is the AppArmor options to use by this container. If set, this profile + overrides the pod's appArmorProfile. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile loaded on the node that should be used. + The profile must be preconfigured on the node to work. + Must match the loaded name of the profile. + Must be set if and only if type is "Localhost". + type: string + type: + description: |- + type indicates which kind of AppArmor profile will be applied. + Valid options are: + Localhost - a profile pre-loaded on the node. + RuntimeDefault - the container runtime's default profile. + Unconfined - no AppArmor enforcement. + type: string + required: + - type + type: object + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + x-kubernetes-list-type: atomic + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default value is Default which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + service: + properties: + labels: + additionalProperties: + type: string + type: object + loadBalancerSourceRanges: + items: + type: string + type: array + type: + default: ClusterIP + description: Service Type string describes ingress methods + for a service + enum: + - ClusterIP + - NodePort + - LoadBalancer + type: string + type: object + tls: + properties: + caSecret: + description: Optional, secret that contains the ca certificate + as ca.crt. If this and generate=true is set the existing + CA cert from that secret is used to generate the node certs. + In this case must contain ca.crt and ca.key fields + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + enable: + description: Enable HTTPS for Dashboards + type: boolean + generate: + description: Generate certificate, if false secret must be + provided + type: boolean + secret: + description: Optional, name of a TLS secret that contains + ca.crt, tls.key and tls.crt data. If ca.crt is in a different + secret provide it via the caSecret field + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + type: object + tolerations: + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple using the matching operator . + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + type: array + version: + type: string + required: + - replicas + - version + type: object + general: + description: |- + INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + Important: Run "make" to regenerate code after modifying this file + properties: + additionalConfig: + additionalProperties: + type: string + description: Extra items to add to the opensearch.yml + type: object + additionalVolumes: + description: Additional volumes to mount to all pods in the cluster + items: + properties: + configMap: + description: ConfigMap to use to populate the volume + properties: + defaultMode: + description: |- + defaultMode is optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + ConfigMap will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the ConfigMap, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path within a + volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: optional specify whether the ConfigMap + or its keys must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + csi: + description: CSI object to use to populate the volume + properties: + driver: + description: |- + driver is the name of the CSI driver that handles this volume. + Consult with your admin for the correct name as registered in the cluster. + type: string + fsType: + description: |- + fsType to mount. Ex. "ext4", "xfs", "ntfs". + If not provided, the empty value is passed to the associated CSI driver + which will determine the default filesystem to apply. + type: string + nodePublishSecretRef: + description: |- + nodePublishSecretRef is a reference to the secret object containing + sensitive information to pass to the CSI driver to complete the CSI + NodePublishVolume and NodeUnpublishVolume calls. + This field is optional, and may be empty if no secret is required. If the + secret object contains more than one secret, all secret references are passed. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + readOnly: + description: |- + readOnly specifies a read-only configuration for the volume. + Defaults to false (read/write). + type: boolean + volumeAttributes: + additionalProperties: + type: string + description: |- + volumeAttributes stores driver-specific properties that are passed to the CSI + driver. Consult your driver's documentation for supported values. + type: object + required: + - driver + type: object + emptyDir: + description: EmptyDir to use to populate the volume + properties: + medium: + description: |- + medium represents what type of storage medium should back this directory. + The default is "" which means to use the node's default medium. + Must be an empty string (default) or Memory. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + description: |- + sizeLimit is the total amount of local storage required for this EmptyDir volume. + The size limit is also applicable for memory medium. + The maximum usage on memory medium EmptyDir would be the minimum value between + the SizeLimit specified here and the sum of memory limits of all containers in a pod. + The default is nil which means that the limit is undefined. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + name: + description: Name to use for the volume. Required. + type: string + path: + description: Path in the container to mount the volume at. + Required. + type: string + projected: + description: Projected object to use to populate the volume + properties: + defaultMode: + description: |- + defaultMode are the mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + sources: + description: |- + sources is the list of volume projections. Each entry in this list + handles one source. + items: + description: |- + Projection that may be projected along with other supported volume types. + Exactly one of these fields must be set. + properties: + clusterTrustBundle: + description: |- + ClusterTrustBundle allows a pod to access the `.spec.trustBundle` field + of ClusterTrustBundle objects in an auto-updating file. + + Alpha, gated by the ClusterTrustBundleProjection feature gate. + + ClusterTrustBundle objects can either be selected by name, or by the + combination of signer name and a label selector. + + Kubelet performs aggressive normalization of the PEM contents written + into the pod filesystem. Esoteric PEM features such as inter-block + comments and block headers are stripped. Certificates are deduplicated. + The ordering of certificates within the file is arbitrary, and Kubelet + may change the order over time. + properties: + labelSelector: + description: |- + Select all ClusterTrustBundles that match this label selector. Only has + effect if signerName is set. Mutually-exclusive with name. If unset, + interpreted as "match nothing". If set but empty, interpreted as "match + everything". + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + name: + description: |- + Select a single ClusterTrustBundle by object name. Mutually-exclusive + with signerName and labelSelector. + type: string + optional: + description: |- + If true, don't block pod startup if the referenced ClusterTrustBundle(s) + aren't available. If using name, then the named ClusterTrustBundle is + allowed not to exist. If using signerName, then the combination of + signerName and labelSelector is allowed to match zero + ClusterTrustBundles. + type: boolean + path: + description: Relative path from the volume + root to write the bundle. + type: string + signerName: + description: |- + Select all ClusterTrustBundles that match this signer name. + Mutually-exclusive with name. The contents of all selected + ClusterTrustBundles will be unified and deduplicated. + type: string + required: + - path + type: object + configMap: + description: configMap information about the configMap + data to project + properties: + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + ConfigMap will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the ConfigMap, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path + within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: optional specify whether the + ConfigMap or its keys must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + description: downwardAPI information about the + downwardAPI data to project + properties: + items: + description: Items is a list of DownwardAPIVolume + file + items: + description: DownwardAPIVolumeFile represents + information to create the file containing + the pod field + properties: + fieldRef: + description: 'Required: Selects a field + of the pod: only annotations, labels, + name, namespace and uid are supported.' + properties: + apiVersion: + description: Version of the schema + the FieldPath is written in terms + of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to + select in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + description: |- + Optional: mode bits used to set permissions on this file, must be an octal value + between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: 'Required: Path is the + relative path name of the file to + be created. Must not be absolute or + contain the ''..'' path. Must be utf-8 + encoded. The first item of the relative + path must not start with ''..''' + type: string + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, requests.cpu and requests.memory) are currently supported. + properties: + containerName: + description: 'Container name: required + for volumes, optional for env + vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output + format of the exposed resources, + defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource + to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + secret: + description: secret information about the secret + data to project + properties: + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path + within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: optional field specify whether + the Secret or its key must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + description: serviceAccountToken is information + about the serviceAccountToken data to project + properties: + audience: + description: |- + audience is the intended audience of the token. A recipient of a token + must identify itself with an identifier specified in the audience of the + token, and otherwise should reject the token. The audience defaults to the + identifier of the apiserver. + type: string + expirationSeconds: + description: |- + expirationSeconds is the requested duration of validity of the service + account token. As the token approaches expiration, the kubelet volume + plugin will proactively rotate the service account token. The kubelet will + start trying to rotate the token if the token is older than 80 percent of + its time to live or if the token is older than 24 hours.Defaults to 1 hour + and must be at least 10 minutes. + format: int64 + type: integer + path: + description: |- + path is the path relative to the mount point of the file to project the + token into. + type: string + required: + - path + type: object + type: object + type: array + x-kubernetes-list-type: atomic + type: object + restartPods: + description: Whether to restart the pods on content change + type: boolean + secret: + description: Secret to use populate the volume + properties: + defaultMode: + description: |- + defaultMode is Optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values + for mode bits. Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: |- + items If unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path within a + volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + optional: + description: optional field specify whether the Secret + or its keys must be defined + type: boolean + secretName: + description: |- + secretName is the name of the secret in the pod's namespace to use. + More info: https://kubernetes.io/docs/concepts/storage/volumes#secret + type: string + type: object + subPath: + description: SubPath of the referenced volume to mount. + type: string + required: + - name + - path + type: object + type: array + annotations: + additionalProperties: + type: string + description: Adds support for annotations in services + type: object + command: + type: string + defaultRepo: + type: string + drainDataNodes: + description: Drain data nodes controls whether to drain data notes + on rolling restart operations + type: boolean + httpPort: + default: 9200 + format: int32 + type: integer + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull + a container image + type: string + imagePullSecrets: + items: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + type: array + keystore: + description: Populate opensearch keystore before startup + items: + properties: + keyMappings: + additionalProperties: + type: string + description: Key mappings from secret to keystore keys + type: object + secret: + description: Secret containing key value pairs + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + type: object + type: array + monitoring: + properties: + enable: + type: boolean + labels: + additionalProperties: + type: string + type: object + monitoringUserSecret: + type: string + pluginUrl: + type: string + scrapeInterval: + type: string + tlsConfig: + properties: + insecureSkipVerify: + type: boolean + serverName: + type: string + type: object + type: object + pluginsList: + items: + type: string + type: array + podSecurityContext: + description: Set security context for the cluster pods + properties: + appArmorProfile: + description: |- + appArmorProfile is the AppArmor options to use by the containers in this pod. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile loaded on the node that should be used. + The profile must be preconfigured on the node to work. + Must match the loaded name of the profile. + Must be set if and only if type is "Localhost". + type: string + type: + description: |- + type indicates which kind of AppArmor profile will be applied. + Valid options are: + Localhost - a profile pre-loaded on the node. + RuntimeDefault - the container runtime's default profile. + Unconfined - no AppArmor enforcement. + type: string + required: + - type + type: object + fsGroup: + description: |- + A special supplemental group that applies to all containers in a pod. + Some volume types allow the Kubelet to change the ownership of that volume + to be owned by the pod: + + 1. The owning GID will be the FSGroup + 2. The setgid bit is set (new files created in the volume will be owned by FSGroup) + 3. The permission bits are OR'd with rw-rw---- + + If unset, the Kubelet will not modify the ownership and permissions of any volume. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + fsGroupChangePolicy: + description: |- + fsGroupChangePolicy defines behavior of changing ownership and permission of the volume + before being exposed inside Pod. This field will only apply to + volume types which support fsGroup based ownership(and permissions). + It will have no effect on ephemeral volume types such as: secret, configmaps + and emptydir. + Valid values are "OnRootMismatch" and "Always". If not specified, "Always" is used. + Note that this field cannot be set when spec.os.name is windows. + type: string + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence + for that container. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence + for that container. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxChangePolicy: + description: |- + seLinuxChangePolicy defines how the container's SELinux label is applied to all volumes used by the Pod. + It has no effect on nodes that do not support SELinux or to volumes does not support SELinux. + Valid values are "MountOption" and "Recursive". + + "Recursive" means relabeling of all files on all Pod volumes by the container runtime. + This may be slow for large volumes, but allows mixing privileged and unprivileged Pods sharing the same volume on the same node. + + "MountOption" mounts all eligible Pod volumes with `-o context` mount option. + This requires all Pods that share the same volume to use the same SELinux label. + It is not possible to share the same volume among privileged and unprivileged Pods. + Eligible volumes are in-tree FibreChannel and iSCSI volumes, and all CSI volumes + whose CSI driver announces SELinux support by setting spec.seLinuxMount: true in their + CSIDriver instance. Other volumes are always re-labelled recursively. + "MountOption" value is allowed only when SELinuxMount feature gate is enabled. + + If not specified and SELinuxMount feature gate is enabled, "MountOption" is used. + If not specified and SELinuxMount feature gate is disabled, "MountOption" is used for ReadWriteOncePod volumes + and "Recursive" for all other volumes. + + This field affects only Pods that have SELinux label set, either in PodSecurityContext or in SecurityContext of all containers. + + All Pods that use the same volume should use the same seLinuxChangePolicy, otherwise some pods can get stuck in ContainerCreating state. + Note that this field cannot be set when spec.os.name is windows. + type: string + seLinuxOptions: + description: |- + The SELinux context to be applied to all containers. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in SecurityContext. If set in + both SecurityContext and PodSecurityContext, the value specified in SecurityContext + takes precedence for that container. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by the containers in this pod. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + supplementalGroups: + description: |- + A list of groups applied to the first process run in each container, in + addition to the container's primary GID and fsGroup (if specified). If + the SupplementalGroupsPolicy feature is enabled, the + supplementalGroupsPolicy field determines whether these are in addition + to or instead of any group memberships defined in the container image. + If unspecified, no additional groups are added, though group memberships + defined in the container image may still be used, depending on the + supplementalGroupsPolicy field. + Note that this field cannot be set when spec.os.name is windows. + items: + format: int64 + type: integer + type: array + x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + description: |- + Defines how supplemental groups of the first container processes are calculated. + Valid values are "Merge" and "Strict". If not specified, "Merge" is used. + (Alpha) Using the field requires the SupplementalGroupsPolicy feature gate to be enabled + and the container runtime must implement support for this feature. + Note that this field cannot be set when spec.os.name is windows. + type: string + sysctls: + description: |- + Sysctls hold a list of namespaced sysctls used for the pod. Pods with unsupported + sysctls (by the container runtime) might fail to launch. + Note that this field cannot be set when spec.os.name is windows. + items: + description: Sysctl defines a kernel parameter to be set + properties: + name: + description: Name of a property to set + type: string + value: + description: Value of a property to set + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options within a container's SecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + securityContext: + description: Set security context for the cluster pods' container + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + appArmorProfile: + description: |- + appArmorProfile is the AppArmor options to use by this container. If set, this profile + overrides the pod's appArmorProfile. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile loaded on the node that should be used. + The profile must be preconfigured on the node to work. + Must match the loaded name of the profile. + Must be set if and only if type is "Localhost". + type: string + type: + description: |- + type indicates which kind of AppArmor profile will be applied. + Valid options are: + Localhost - a profile pre-loaded on the node. + RuntimeDefault - the container runtime's default profile. + Unconfined - no AppArmor enforcement. + type: string + required: + - type + type: object + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + x-kubernetes-list-type: atomic + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default value is Default which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + serviceAccount: + type: string + serviceName: + type: string + setVMMaxMapCount: + type: boolean + snapshotRepositories: + items: + properties: + name: + type: string + settings: + additionalProperties: + type: string + type: object + type: + type: string + required: + - name + - type + type: object + type: array + vendor: + enum: + - Opensearch + - Op + - OP + - os + - opensearch + type: string + version: + type: string + required: + - serviceName + type: object + initHelper: + properties: + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull + a container image + type: string + imagePullSecrets: + items: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + type: array + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + version: + type: string + type: object + nodePools: + items: + properties: + additionalConfig: + additionalProperties: + type: string + type: object + affinity: + description: Affinity is a group of affinity scheduling rules. + properties: + nodeAffinity: + description: Describes node affinity scheduling rules for + the pod. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node matches the corresponding matchExpressions; the + node(s) with the highest sum are the most preferred. + items: + description: |- + An empty preferred scheduling term matches all objects with implicit weight 0 + (i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op). + properties: + preference: + description: A node selector term, associated + with the corresponding weight. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the + selector applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the + selector applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + description: Weight associated with matching the + corresponding nodeSelectorTerm, in the range + 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to an update), the system + may or may not try to eventually evict the pod from its node. + properties: + nodeSelectorTerms: + description: Required. A list of node selector terms. + The terms are ORed. + items: + description: |- + A null or empty node selector term matches no objects. The requirements of + them are ANDed. + The TopologySelectorTerm type implements a subset of the NodeSelectorTerm. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the + selector applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the + selector applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + description: Describes pod affinity scheduling rules (e.g. + co-locate this pod in the same node, zone, etc. as some + other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the + node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both matchLabelKeys and labelSelector. + Also, matchLabelKeys cannot be set when labelSelector isn't set. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both mismatchLabelKeys and labelSelector. + Also, mismatchLabelKeys cannot be set when labelSelector isn't set. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key matches that of any node on which + a pod of the set of pods is running + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of + label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both matchLabelKeys and labelSelector. + Also, matchLabelKeys cannot be set when labelSelector isn't set. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both mismatchLabelKeys and labelSelector. + Also, mismatchLabelKeys cannot be set when labelSelector isn't set. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of + label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + description: Describes pod anti-affinity scheduling rules + (e.g. avoid putting this pod in the same node, zone, etc. + as some other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the anti-affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling anti-affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the + node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both matchLabelKeys and labelSelector. + Also, matchLabelKeys cannot be set when labelSelector isn't set. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both mismatchLabelKeys and labelSelector. + Also, mismatchLabelKeys cannot be set when labelSelector isn't set. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the anti-affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the anti-affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key matches that of any node on which + a pod of the set of pods is running + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of + label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both matchLabelKeys and labelSelector. + Also, matchLabelKeys cannot be set when labelSelector isn't set. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both mismatchLabelKeys and labelSelector. + Also, mismatchLabelKeys cannot be set when labelSelector isn't set. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of + label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + annotations: + additionalProperties: + type: string + type: object + component: + type: string + diskSize: + type: string + env: + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must be + a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + jvm: + type: string + labels: + additionalProperties: + type: string + type: object + nodeSelector: + additionalProperties: + type: string + type: object + pdb: + properties: + enable: + type: boolean + maxUnavailable: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + minAvailable: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + type: object + persistence: + description: PersistencConfig defines options for data persistence + properties: + emptyDir: + description: |- + Represents an empty directory for a pod. + Empty directory volumes support ownership management and SELinux relabeling. + properties: + medium: + description: |- + medium represents what type of storage medium should back this directory. + The default is "" which means to use the node's default medium. + Must be an empty string (default) or Memory. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + description: |- + sizeLimit is the total amount of local storage required for this EmptyDir volume. + The size limit is also applicable for memory medium. + The maximum usage on memory medium EmptyDir would be the minimum value between + the SizeLimit specified here and the sum of memory limits of all containers in a pod. + The default is nil which means that the limit is undefined. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + hostPath: + description: |- + Represents a host path mapped into a pod. + Host path volumes do not support ownership management or SELinux relabeling. + properties: + path: + description: |- + path of the directory on the host. + If the path is a symlink, it will follow the link to the real path. + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + type: + description: |- + type for HostPath Volume + Defaults to "" + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + required: + - path + type: object + pvc: + properties: + accessModes: + items: + type: string + type: array + storageClass: + type: string + type: object + type: object + priorityClassName: + type: string + probes: + properties: + liveness: + properties: + failureThreshold: + format: int32 + type: integer + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + readiness: + properties: + failureThreshold: + format: int32 + type: integer + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + startup: + properties: + failureThreshold: + format: int32 + type: integer + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + type: object + replicas: + format: int32 + type: integer + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + roles: + items: + type: string + type: array + tolerations: + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple using the matching operator . + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + type: array + topologySpreadConstraints: + items: + description: TopologySpreadConstraint specifies how to spread + matching pods among the given topology. + properties: + labelSelector: + description: |- + LabelSelector is used to find matching pods. + Pods that match this label selector are counted to determine the number of pods + in their corresponding topology domain. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select the pods over which + spreading will be calculated. The keys are used to lookup values from the + incoming pod labels, those key-value labels are ANDed with labelSelector + to select the group of existing pods over which spreading will be calculated + for the incoming pod. The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + MatchLabelKeys cannot be set when LabelSelector isn't set. + Keys that don't exist in the incoming pod labels will + be ignored. A null or empty list means only match against labelSelector. + + This is a beta field and requires the MatchLabelKeysInPodTopologySpread feature gate to be enabled (enabled by default). + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + description: |- + MaxSkew describes the degree to which pods may be unevenly distributed. + When `whenUnsatisfiable=DoNotSchedule`, it is the maximum permitted difference + between the number of matching pods in the target topology and the global minimum. + The global minimum is the minimum number of matching pods in an eligible domain + or zero if the number of eligible domains is less than MinDomains. + For example, in a 3-zone cluster, MaxSkew is set to 1, and pods with the same + labelSelector spread as 2/2/1: + In this case, the global minimum is 1. + | zone1 | zone2 | zone3 | + | P P | P P | P | + - if MaxSkew is 1, incoming pod can only be scheduled to zone3 to become 2/2/2; + scheduling it onto zone1(zone2) would make the ActualSkew(3-1) on zone1(zone2) + violate MaxSkew(1). + - if MaxSkew is 2, incoming pod can be scheduled onto any zone. + When `whenUnsatisfiable=ScheduleAnyway`, it is used to give higher precedence + to topologies that satisfy it. + It's a required field. Default value is 1 and 0 is not allowed. + format: int32 + type: integer + minDomains: + description: |- + MinDomains indicates a minimum number of eligible domains. + When the number of eligible domains with matching topology keys is less than minDomains, + Pod Topology Spread treats "global minimum" as 0, and then the calculation of Skew is performed. + And when the number of eligible domains with matching topology keys equals or greater than minDomains, + this value has no effect on scheduling. + As a result, when the number of eligible domains is less than minDomains, + scheduler won't schedule more than maxSkew Pods to those domains. + If value is nil, the constraint behaves as if MinDomains is equal to 1. + Valid values are integers greater than 0. + When value is not nil, WhenUnsatisfiable must be DoNotSchedule. + + For example, in a 3-zone cluster, MaxSkew is set to 2, MinDomains is set to 5 and pods with the same + labelSelector spread as 2/2/2: + | zone1 | zone2 | zone3 | + | P P | P P | P P | + The number of domains is less than 5(MinDomains), so "global minimum" is treated as 0. + In this situation, new pod with the same labelSelector cannot be scheduled, + because computed skew will be 3(3 - 0) if new Pod is scheduled to any of the three zones, + it will violate MaxSkew. + format: int32 + type: integer + nodeAffinityPolicy: + description: |- + NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector + when calculating pod topology spread skew. Options are: + - Honor: only nodes matching nodeAffinity/nodeSelector are included in the calculations. + - Ignore: nodeAffinity/nodeSelector are ignored. All nodes are included in the calculations. + + If this value is nil, the behavior is equivalent to the Honor policy. + type: string + nodeTaintsPolicy: + description: |- + NodeTaintsPolicy indicates how we will treat node taints when calculating + pod topology spread skew. Options are: + - Honor: nodes without taints, along with tainted nodes for which the incoming pod + has a toleration, are included. + - Ignore: node taints are ignored. All nodes are included. + + If this value is nil, the behavior is equivalent to the Ignore policy. + type: string + topologyKey: + description: |- + TopologyKey is the key of node labels. Nodes that have a label with this key + and identical values are considered to be in the same topology. + We consider each as a "bucket", and try to put balanced number + of pods into each bucket. + We define a domain as a particular instance of a topology. + Also, we define an eligible domain as a domain whose nodes meet the requirements of + nodeAffinityPolicy and nodeTaintsPolicy. + e.g. If TopologyKey is "kubernetes.io/hostname", each Node is a domain of that topology. + And, if TopologyKey is "topology.kubernetes.io/zone", each zone is a domain of that topology. + It's a required field. + type: string + whenUnsatisfiable: + description: |- + WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy + the spread constraint. + - DoNotSchedule (default) tells the scheduler not to schedule it. + - ScheduleAnyway tells the scheduler to schedule the pod in any location, + but giving higher precedence to topologies that would help reduce the + skew. + A constraint is considered "Unsatisfiable" for an incoming pod + if and only if every possible node assignment for that pod would violate + "MaxSkew" on some topology. + For example, in a 3-zone cluster, MaxSkew is set to 1, and pods with the same + labelSelector spread as 3/1/1: + | zone1 | zone2 | zone3 | + | P P P | P | P | + If WhenUnsatisfiable is set to DoNotSchedule, incoming pod can only be scheduled + to zone2(zone3) to become 3/2/1(3/1/2) as ActualSkew(2-1) on zone2(zone3) satisfies + MaxSkew(1). In other words, the cluster can still be imbalanced, but scheduler + won't make it *more* imbalanced. + It's a required field. + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + required: + - component + - replicas + - roles + type: object + type: array + security: + description: Security defines options for managing the opensearch-security + plugin + properties: + config: + properties: + adminCredentialsSecret: + description: Secret that contains fields username and password + to be used by the operator to access the opensearch cluster + for node draining. Must be set if custom securityconfig + is provided. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + adminSecret: + description: TLS Secret that contains a client certificate + (tls.key, tls.crt, ca.crt) with admin rights in the opensearch + cluster. Must be set if transport certificates are provided + by user and not generated + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + securityConfigSecret: + description: Secret that contains the differnt yml files of + the opensearch-security config (config.yml, internal_users.yml, + ...) + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + updateJob: + description: Specific configs for the SecurityConfig update + job + properties: + resources: + description: ResourceRequirements describes the compute + resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + type: object + type: object + tls: + description: Configure tls usage for transport and http interface + properties: + http: + properties: + caSecret: + description: Optional, secret that contains the ca certificate + as ca.crt. If this and generate=true is set the existing + CA cert from that secret is used to generate the node + certs. In this case must contain ca.crt and ca.key fields + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + generate: + description: If set to true the operator will generate + a CA and certificates for the cluster to use, if false + secrets with existing certificates must be supplied + type: boolean + secret: + description: Optional, name of a TLS secret that contains + ca.crt, tls.key and tls.crt data. If ca.crt is in a + different secret provide it via the caSecret field + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + type: object + transport: + properties: + adminDn: + description: DNs of certificates that should have admin + access, mainly used for securityconfig updates via securityadmin.sh, + only used when existing certificates are provided + items: + type: string + type: array + caSecret: + description: Optional, secret that contains the ca certificate + as ca.crt. If this and generate=true is set the existing + CA cert from that secret is used to generate the node + certs. In this case must contain ca.crt and ca.key fields + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + generate: + description: If set to true the operator will generate + a CA and certificates for the cluster to use, if false + secrets with existing certificates must be supplied + type: boolean + nodesDn: + description: Allowed Certificate DNs for nodes, only used + when existing certificates are provided + items: + type: string + type: array + perNode: + description: Configure transport node certificate + type: boolean + secret: + description: Optional, name of a TLS secret that contains + ca.crt, tls.key and tls.crt data. If ca.crt is in a + different secret provide it via the caSecret field + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + type: object + type: object + type: object + required: + - nodePools + type: object + status: + description: ClusterStatus defines the observed state of Es + properties: + availableNodes: + description: AvailableNodes is the number of available instances. + format: int32 + type: integer + componentsStatus: + items: + properties: + component: + type: string + conditions: + items: + type: string + type: array + description: + type: string + status: + type: string + type: object + type: array + health: + description: OpenSearchHealth is the health of the cluster as returned + by the health API. + type: string + initialized: + type: boolean + phase: + description: |- + INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + Important: Run "make" to regenerate code after modifying this file + type: string + version: + type: string + required: + - componentsStatus + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/packages/system/opensearch-operator/charts/opensearch-operator/files/opensearch.opster.io_opensearchcomponenttemplates.yaml b/packages/system/opensearch-operator/charts/opensearch-operator/files/opensearch.opster.io_opensearchcomponenttemplates.yaml new file mode 100644 index 00000000..7d4fd549 --- /dev/null +++ b/packages/system/opensearch-operator/charts/opensearch-operator/files/opensearch.opster.io_opensearchcomponenttemplates.yaml @@ -0,0 +1,136 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.0 + name: opensearchcomponenttemplates.opensearch.opster.io +spec: + group: opensearch.opster.io + names: + kind: OpensearchComponentTemplate + listKind: OpensearchComponentTemplateList + plural: opensearchcomponenttemplates + shortNames: + - opensearchcomponenttemplate + singular: opensearchcomponenttemplate + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: OpensearchComponentTemplate is the schema for the OpenSearch + component templates API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + properties: + _meta: + description: Optional user metadata about the component template + x-kubernetes-preserve-unknown-fields: true + allowAutoCreate: + description: If true, then indices can be automatically created using + this template + type: boolean + name: + description: The name of the component template. Defaults to metadata.name + type: string + opensearchCluster: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + template: + description: The template that should be applied + properties: + aliases: + additionalProperties: + description: Describes the specs of an index alias + properties: + alias: + description: The name of the alias. + type: string + filter: + description: Query used to limit documents the alias can + access. + x-kubernetes-preserve-unknown-fields: true + index: + description: The name of the index that the alias points + to. + type: string + isWriteIndex: + description: If true, the index is the write index for the + alias + type: boolean + routing: + description: Value used to route indexing and search operations + to a specific shard. + type: string + type: object + description: Aliases to add + type: object + mappings: + description: Mapping for fields in the index + x-kubernetes-preserve-unknown-fields: true + settings: + description: Configuration options for the index + x-kubernetes-preserve-unknown-fields: true + type: object + version: + description: Version number used to manage the component template + externally + type: integer + required: + - opensearchCluster + - template + type: object + status: + properties: + componentTemplateName: + description: Name of the currently managed component template + type: string + existingComponentTemplate: + type: boolean + managedCluster: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + reason: + type: string + state: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/packages/system/opensearch-operator/charts/opensearch-operator/files/opensearch.opster.io_opensearchindextemplates.yaml b/packages/system/opensearch-operator/charts/opensearch-operator/files/opensearch.opster.io_opensearchindextemplates.yaml new file mode 100644 index 00000000..37e517ee --- /dev/null +++ b/packages/system/opensearch-operator/charts/opensearch-operator/files/opensearch.opster.io_opensearchindextemplates.yaml @@ -0,0 +1,163 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.0 + name: opensearchindextemplates.opensearch.opster.io +spec: + group: opensearch.opster.io + names: + kind: OpensearchIndexTemplate + listKind: OpensearchIndexTemplateList + plural: opensearchindextemplates + shortNames: + - opensearchindextemplate + singular: opensearchindextemplate + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: OpensearchIndexTemplate is the schema for the OpenSearch index + templates API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + properties: + _meta: + description: Optional user metadata about the index template + x-kubernetes-preserve-unknown-fields: true + composedOf: + description: |- + An ordered list of component template names. Component templates are merged in the order specified, + meaning that the last component template specified has the highest precedence + items: + type: string + type: array + dataStream: + description: The dataStream config that should be applied + properties: + timestamp_field: + description: TimestampField for dataStream + properties: + name: + description: Name of the field that are used for the DataStream + type: string + required: + - name + type: object + type: object + indexPatterns: + description: Array of wildcard expressions used to match the names + of indices during creation + items: + type: string + type: array + name: + description: The name of the index template. Defaults to metadata.name + type: string + opensearchCluster: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + priority: + description: |- + Priority to determine index template precedence when a new data stream or index is created. + The index template with the highest priority is chosen + type: integer + template: + description: The template that should be applied + properties: + aliases: + additionalProperties: + description: Describes the specs of an index alias + properties: + alias: + description: The name of the alias. + type: string + filter: + description: Query used to limit documents the alias can + access. + x-kubernetes-preserve-unknown-fields: true + index: + description: The name of the index that the alias points + to. + type: string + isWriteIndex: + description: If true, the index is the write index for the + alias + type: boolean + routing: + description: Value used to route indexing and search operations + to a specific shard. + type: string + type: object + description: Aliases to add + type: object + mappings: + description: Mapping for fields in the index + x-kubernetes-preserve-unknown-fields: true + settings: + description: Configuration options for the index + x-kubernetes-preserve-unknown-fields: true + type: object + version: + description: Version number used to manage the component template + externally + type: integer + required: + - indexPatterns + - opensearchCluster + type: object + status: + properties: + existingIndexTemplate: + type: boolean + indexTemplateName: + description: Name of the currently managed index template + type: string + managedCluster: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + reason: + type: string + state: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/packages/system/opensearch-operator/charts/opensearch-operator/files/opensearch.opster.io_opensearchismpolicies.yaml b/packages/system/opensearch-operator/charts/opensearch-operator/files/opensearch.opster.io_opensearchismpolicies.yaml new file mode 100644 index 00000000..2f4b261d --- /dev/null +++ b/packages/system/opensearch-operator/charts/opensearch-operator/files/opensearch.opster.io_opensearchismpolicies.yaml @@ -0,0 +1,461 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.0 + name: opensearchismpolicies.opensearch.opster.io +spec: + group: opensearch.opster.io + names: + kind: OpenSearchISMPolicy + listKind: OpenSearchISMPolicyList + plural: opensearchismpolicies + shortNames: + - ismp + - ismpolicy + singular: opensearchismpolicy + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: ISMPolicySpec is the specification for the ISM policy for + OS. + properties: + applyToExistingIndices: + description: If true, apply the policy to existing indices that match + the index patterns in the ISM template. + type: boolean + defaultState: + description: The default starting state for each index that uses this + policy. + type: string + description: + description: A human-readable description of the policy. + type: string + errorNotification: + properties: + channel: + type: string + destination: + description: The destination URL. + properties: + amazon: + properties: + url: + type: string + type: object + chime: + properties: + url: + type: string + type: object + customWebhook: + properties: + url: + type: string + type: object + slack: + properties: + url: + type: string + type: object + type: object + messageTemplate: + description: The text of the message + properties: + source: + type: string + type: object + type: object + ismTemplate: + description: Specify an ISM template pattern that matches the index + to apply the policy. + properties: + indexPatterns: + description: Index patterns on which this policy has to be applied + items: + type: string + type: array + priority: + description: Priority of the template, defaults to 0 + type: integer + required: + - indexPatterns + type: object + opensearchCluster: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + policyId: + type: string + states: + description: The states that you define in the policy. + items: + properties: + actions: + description: The actions to execute after entering a state. + items: + description: Actions are the steps that the policy sequentially + executes on entering a specific state. + properties: + alias: + properties: + actions: + description: Allocate the index to a node with a specified + attribute. + items: + properties: + add: + properties: + aliases: + description: The name of the alias. + items: + type: string + type: array + index: + description: The name of the index that + the alias points to. + type: string + isWriteIndex: + description: Specify the index that accepts + any write operations to the alias. + type: boolean + routing: + description: Limit search to an associated + shard value + type: string + type: object + remove: + properties: + aliases: + description: The name of the alias. + items: + type: string + type: array + index: + description: The name of the index that + the alias points to. + type: string + isWriteIndex: + description: Specify the index that accepts + any write operations to the alias. + type: boolean + routing: + description: Limit search to an associated + shard value + type: string + type: object + type: object + type: array + required: + - actions + type: object + allocation: + description: Allocate the index to a node with a specific + attribute set + properties: + exclude: + description: Allocate the index to a node with a specified + attribute. + type: string + include: + description: Allocate the index to a node with any + of the specified attributes. + type: string + require: + description: Don’t allocate the index to a node with + any of the specified attributes. + type: string + waitFor: + description: Wait for the policy to execute before + allocating the index to a node with a specified + attribute. + type: string + required: + - exclude + - include + - require + - waitFor + type: object + close: + description: Closes the managed index. + type: object + delete: + description: Deletes a managed index. + type: object + forceMerge: + description: Reduces the number of Lucene segments by + merging the segments of individual shards. + properties: + maxNumSegments: + description: The number of segments to reduce the + shard to. + format: int64 + type: integer + required: + - maxNumSegments + type: object + indexPriority: + description: Set the priority for the index in a specific + state. + properties: + priority: + description: The priority for the index as soon as + it enters a state. + format: int64 + type: integer + required: + - priority + type: object + notification: + description: Name string `json:"name,omitempty"` + properties: + destination: + type: string + messageTemplate: + properties: + source: + type: string + type: object + required: + - destination + - messageTemplate + type: object + open: + description: Opens a managed index. + type: object + readOnly: + description: Sets a managed index to be read only. + type: object + readWrite: + description: Sets a managed index to be writeable. + type: object + replicaCount: + description: Sets the number of replicas to assign to + an index. + properties: + numberOfReplicas: + format: int64 + type: integer + required: + - numberOfReplicas + type: object + retry: + description: The retry configuration for the action. + properties: + backoff: + description: The backoff policy type to use when retrying. + type: string + count: + description: The number of retry counts. + format: int64 + type: integer + delay: + description: The time to wait between retries. + type: string + required: + - count + type: object + rollover: + description: Rolls an alias over to a new index when the + managed index meets one of the rollover conditions. + properties: + minDocCount: + description: The minimum number of documents required + to roll over the index. + format: int64 + type: integer + minIndexAge: + description: The minimum age required to roll over + the index. + type: string + minPrimaryShardSize: + description: The minimum storage size of a single + primary shard required to roll over the index. + type: string + minSize: + description: The minimum size of the total primary + shard storage (not counting replicas) required to + roll over the index. + type: string + type: object + rollup: + description: Periodically reduce data granularity by rolling + up old data into summarized indexes. + type: object + shrink: + description: Allows you to reduce the number of primary + shards in your indexes + properties: + forceUnsafe: + description: If true, executes the shrink action even + if there are no replicas. + type: boolean + maxShardSize: + description: The maximum size in bytes of a shard + for the target index. + type: string + numNewShards: + description: The maximum number of primary shards + in the shrunken index. + type: integer + percentageOfSourceShards: + description: Percentage of the number of original + primary shards to shrink. + format: int64 + type: integer + targetIndexNameTemplate: + description: The name of the shrunken index. + type: string + type: object + snapshot: + description: Back up your cluster’s indexes and state + properties: + repository: + description: The repository name that you register + through the native snapshot API operations. + type: string + snapshot: + description: The name of the snapshot. + type: string + required: + - repository + - snapshot + type: object + timeout: + description: The timeout period for the action. Accepts + time units for minutes, hours, and days. + type: string + type: object + type: array + name: + description: The name of the state. + type: string + transitions: + description: The next states and the conditions required to + transition to those states. If no transitions exist, the policy + assumes that it’s complete and can now stop managing the index + items: + properties: + conditions: + description: conditions for the transition. + properties: + cron: + description: The cron job that triggers the transition + if no other transition happens first. + properties: + cron: + description: A wrapper for the cron job that triggers + the transition if no other transition happens + first. This wrapper is here to adhere to the + OpenSearch API. + properties: + expression: + description: The cron expression that triggers + the transition. + type: string + timezone: + description: The timezone that triggers the + transition. + type: string + required: + - expression + - timezone + type: object + required: + - cron + type: object + minDocCount: + description: The minimum document count of the index + required to transition. + format: int64 + type: integer + minIndexAge: + description: The minimum age of the index required + to transition. + type: string + minRolloverAge: + description: The minimum age required after a rollover + has occurred to transition to the next state. + type: string + minSize: + description: The minimum size of the total primary + shard storage (not counting replicas) required to + transition. + type: string + type: object + stateName: + description: The name of the state to transition to if + the conditions are met. + type: string + required: + - conditions + - stateName + type: object + type: array + required: + - actions + - name + type: object + type: array + required: + - defaultState + - description + - states + type: object + status: + description: OpensearchISMPolicyStatus defines the observed state of OpensearchISMPolicy + properties: + existingISMPolicy: + type: boolean + managedCluster: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + policyId: + type: string + reason: + type: string + state: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/packages/system/opensearch-operator/charts/opensearch-operator/files/opensearch.opster.io_opensearchroles.yaml b/packages/system/opensearch-operator/charts/opensearch-operator/files/opensearch.opster.io_opensearchroles.yaml new file mode 100644 index 00000000..36ae514a --- /dev/null +++ b/packages/system/opensearch-operator/charts/opensearch-operator/files/opensearch.opster.io_opensearchroles.yaml @@ -0,0 +1,123 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.0 + name: opensearchroles.opensearch.opster.io +spec: + group: opensearch.opster.io + names: + kind: OpensearchRole + listKind: OpensearchRoleList + plural: opensearchroles + shortNames: + - opensearchrole + singular: opensearchrole + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: OpensearchRole is the Schema for the opensearchroles API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: OpensearchRoleSpec defines the desired state of OpensearchRole + properties: + clusterPermissions: + items: + type: string + type: array + indexPermissions: + items: + properties: + allowedActions: + items: + type: string + type: array + dls: + type: string + fls: + items: + type: string + type: array + indexPatterns: + items: + type: string + type: array + maskedFields: + items: + type: string + type: array + type: object + type: array + opensearchCluster: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + tenantPermissions: + items: + properties: + allowedActions: + items: + type: string + type: array + tenantPatterns: + items: + type: string + type: array + type: object + type: array + required: + - opensearchCluster + type: object + status: + description: OpensearchRoleStatus defines the observed state of OpensearchRole + properties: + existingRole: + type: boolean + managedCluster: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + reason: + type: string + state: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/packages/system/opensearch-operator/charts/opensearch-operator/files/opensearch.opster.io_opensearchsnapshotpolicies.yaml b/packages/system/opensearch-operator/charts/opensearch-operator/files/opensearch.opster.io_opensearchsnapshotpolicies.yaml new file mode 100644 index 00000000..2c32048d --- /dev/null +++ b/packages/system/opensearch-operator/charts/opensearch-operator/files/opensearch.opster.io_opensearchsnapshotpolicies.yaml @@ -0,0 +1,203 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.0 + name: opensearchsnapshotpolicies.opensearch.opster.io +spec: + group: opensearch.opster.io + names: + kind: OpensearchSnapshotPolicy + listKind: OpensearchSnapshotPolicyList + plural: opensearchsnapshotpolicies + singular: opensearchsnapshotpolicy + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Existing policy state + jsonPath: .status.existingSnapshotPolicy + name: existingpolicy + type: boolean + - description: Snapshot policy name + jsonPath: .status.snapshotPolicyName + name: policyName + type: string + - jsonPath: .status.state + name: state + type: string + - jsonPath: .metadata.creationTimestamp + name: age + type: date + name: v1 + schema: + openAPIV3Schema: + description: OpensearchSnapshotPolicy is the Schema for the opensearchsnapshotpolicies + API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + properties: + creation: + properties: + schedule: + properties: + cron: + properties: + expression: + type: string + timezone: + type: string + required: + - expression + - timezone + type: object + required: + - cron + type: object + timeLimit: + type: string + required: + - schedule + type: object + deletion: + properties: + deleteCondition: + properties: + maxAge: + type: string + maxCount: + type: integer + minCount: + type: integer + type: object + schedule: + properties: + cron: + properties: + expression: + type: string + timezone: + type: string + required: + - expression + - timezone + type: object + required: + - cron + type: object + timeLimit: + type: string + type: object + description: + type: string + enabled: + type: boolean + notification: + properties: + channel: + properties: + id: + type: string + required: + - id + type: object + conditions: + properties: + creation: + type: boolean + deletion: + type: boolean + failure: + type: boolean + type: object + required: + - channel + type: object + opensearchCluster: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + policyName: + type: string + snapshotConfig: + properties: + dateFormat: + type: string + dateFormatTimezone: + type: string + ignoreUnavailable: + type: boolean + includeGlobalState: + type: boolean + indices: + type: string + metadata: + additionalProperties: + type: string + type: object + partial: + type: boolean + repository: + type: string + required: + - repository + type: object + required: + - creation + - opensearchCluster + - policyName + - snapshotConfig + type: object + status: + description: OpensearchSnapshotPolicyStatus defines the observed state + of OpensearchSnapshotPolicy + properties: + existingSnapshotPolicy: + type: boolean + managedCluster: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + reason: + type: string + snapshotPolicyName: + type: string + state: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/packages/system/opensearch-operator/charts/opensearch-operator/files/opensearch.opster.io_opensearchtenants.yaml b/packages/system/opensearch-operator/charts/opensearch-operator/files/opensearch.opster.io_opensearchtenants.yaml new file mode 100644 index 00000000..d085f3ca --- /dev/null +++ b/packages/system/opensearch-operator/charts/opensearch-operator/files/opensearch.opster.io_opensearchtenants.yaml @@ -0,0 +1,85 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.0 + name: opensearchtenants.opensearch.opster.io +spec: + group: opensearch.opster.io + names: + kind: OpensearchTenant + listKind: OpensearchTenantList + plural: opensearchtenants + shortNames: + - opensearchtenant + singular: opensearchtenant + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: OpensearchTenant is the Schema for the opensearchtenants API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: OpensearchTenantSpec defines the desired state of OpensearchTenant + properties: + description: + type: string + opensearchCluster: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + required: + - opensearchCluster + type: object + status: + description: OpensearchTenantStatus defines the observed state of OpensearchTenant + properties: + existingTenant: + type: boolean + managedCluster: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + reason: + type: string + state: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/packages/system/opensearch-operator/charts/opensearch-operator/files/opensearch.opster.io_opensearchuserrolebindings.yaml b/packages/system/opensearch-operator/charts/opensearch-operator/files/opensearch.opster.io_opensearchuserrolebindings.yaml new file mode 100644 index 00000000..2e9453c6 --- /dev/null +++ b/packages/system/opensearch-operator/charts/opensearch-operator/files/opensearch.opster.io_opensearchuserrolebindings.yaml @@ -0,0 +1,109 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.0 + name: opensearchuserrolebindings.opensearch.opster.io +spec: + group: opensearch.opster.io + names: + kind: OpensearchUserRoleBinding + listKind: OpensearchUserRoleBindingList + plural: opensearchuserrolebindings + shortNames: + - opensearchuserrolebinding + singular: opensearchuserrolebinding + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: OpensearchUserRoleBinding is the Schema for the opensearchuserrolebindings + API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: OpensearchUserRoleBindingSpec defines the desired state of + OpensearchUserRoleBinding + properties: + backendRoles: + items: + type: string + type: array + opensearchCluster: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + roles: + items: + type: string + type: array + users: + items: + type: string + type: array + required: + - opensearchCluster + - roles + type: object + status: + description: OpensearchUserRoleBindingStatus defines the observed state + of OpensearchUserRoleBinding + properties: + managedCluster: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + provisionedBackendRoles: + items: + type: string + type: array + provisionedRoles: + items: + type: string + type: array + provisionedUsers: + items: + type: string + type: array + reason: + type: string + state: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/packages/system/opensearch-operator/charts/opensearch-operator/files/opensearch.opster.io_opensearchusers.yaml b/packages/system/opensearch-operator/charts/opensearch-operator/files/opensearch.opster.io_opensearchusers.yaml new file mode 100644 index 00000000..8d573ee7 --- /dev/null +++ b/packages/system/opensearch-operator/charts/opensearch-operator/files/opensearch.opster.io_opensearchusers.yaml @@ -0,0 +1,117 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.0 + name: opensearchusers.opensearch.opster.io +spec: + group: opensearch.opster.io + names: + kind: OpensearchUser + listKind: OpensearchUserList + plural: opensearchusers + shortNames: + - opensearchuser + singular: opensearchuser + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: OpensearchUser is the Schema for the opensearchusers API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: OpensearchUserSpec defines the desired state of OpensearchUser + properties: + attributes: + additionalProperties: + type: string + type: object + backendRoles: + items: + type: string + type: array + opendistroSecurityRoles: + items: + type: string + type: array + opensearchCluster: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + passwordFrom: + description: SecretKeySelector selects a key of a Secret. + properties: + key: + description: The key of the secret to select from. Must be a + valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + required: + - opensearchCluster + - passwordFrom + type: object + status: + description: OpensearchUserStatus defines the observed state of OpensearchUser + properties: + managedCluster: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + reason: + type: string + state: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/packages/system/opensearch-operator/charts/opensearch-operator/templates/_helpers.tpl b/packages/system/opensearch-operator/charts/opensearch-operator/templates/_helpers.tpl new file mode 100644 index 00000000..32f313e3 --- /dev/null +++ b/packages/system/opensearch-operator/charts/opensearch-operator/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "opensearch-operator.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "opensearch-operator.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "opensearch-operator.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "opensearch-operator.labels" -}} +helm.sh/chart: {{ include "opensearch-operator.chart" . }} +{{ include "opensearch-operator.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "opensearch-operator.selectorLabels" -}} +app.kubernetes.io/name: {{ include "opensearch-operator.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "opensearch-operator.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (printf "%s-%s" (include "opensearch-operator.fullname" .) "controller-manager") .Values.serviceAccount.name }} +{{- else }} +{{- default "opensearch-operator-controller-manager" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-controller-manager-deployment.yaml b/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-controller-manager-deployment.yaml new file mode 100644 index 00000000..4b5cb194 --- /dev/null +++ b/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-controller-manager-deployment.yaml @@ -0,0 +1,94 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + control-plane: controller-manager + name: {{ include "opensearch-operator.fullname" . }}-controller-manager +spec: + replicas: 1 + selector: + matchLabels: + control-plane: controller-manager + template: + metadata: + labels: + control-plane: controller-manager + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + containers: + {{- if or (.Values.kubeRbacProxy.enable) (eq (.Values.kubeRbacProxy.enable | toString) "") }} + - args: + - --secure-listen-address=0.0.0.0:8443 + - --upstream=http://127.0.0.1:8080/ + - --proxy-endpoints-port=10443 + - --logtostderr=true + - --v=10 + image: "{{ .Values.kubeRbacProxy.image.repository }}:{{ .Values.kubeRbacProxy.image.tag}}" + name: kube-rbac-proxy + resources: +{{- toYaml .Values.kubeRbacProxy.resources | nindent 10 }} + readinessProbe: +{{- toYaml .Values.kubeRbacProxy.readinessProbe | nindent 10 }} + livenessProbe: +{{- toYaml .Values.kubeRbacProxy.livenessProbe | nindent 10 }} + securityContext: +{{- toYaml .Values.kubeRbacProxy.securityContext | nindent 10 }} + ports: + - containerPort: 8443 + name: https + - containerPort: 10443 + name: https-proxy + protocol: TCP + {{- end}} + - args: + - --health-probe-bind-address=:8081 + - --metrics-bind-address=127.0.0.1:8080 + - --leader-elect + {{- if .Values.manager.watchNamespace }} + - --watch-namespace={{ .Values.manager.watchNamespace }} + {{- end }} + - --loglevel={{ .Values.manager.loglevel }} + command: + - /manager + image: "{{ .Values.manager.image.repository }}:{{ .Values.manager.image.tag | default .Chart.AppVersion }}" + name: operator-controller-manager + imagePullPolicy: "{{ .Values.manager.image.pullPolicy }}" + resources: +{{- toYaml .Values.manager.resources | nindent 10 }} + readinessProbe: +{{- toYaml .Values.manager.readinessProbe | nindent 10 }} + livenessProbe: +{{- toYaml .Values.manager.livenessProbe | nindent 10 }} + env: + - name: DNS_BASE + value: {{ .Values.manager.dnsBase }} + - name: PARALLEL_RECOVERY_ENABLED + value: "{{ .Values.manager.parallelRecoveryEnabled }}" + - name: PPROF_ENDPOINTS_ENABLED + value: "{{ .Values.manager.pprofEndpointsEnabled }}" + {{- if .Values.manager.extraEnv }} + {{- toYaml .Values.manager.extraEnv | nindent 8 }} + {{- end }} + securityContext: +{{- toYaml .Values.manager.securityContext | nindent 10 }} + nodeSelector: +{{- toYaml .Values.nodeSelector | nindent 8 }} + tolerations: +{{- toYaml .Values.tolerations | nindent 8 }} + securityContext: +{{- toYaml .Values.securityContext | nindent 8}} + {{- if .Values.manager.imagePullSecrets }} + imagePullSecrets: + {{- toYaml .Values.manager.imagePullSecrets | nindent 6 }} + {{- end }} + serviceAccountName: {{ include "opensearch-operator.serviceAccountName" . }} + terminationGracePeriodSeconds: 10 + {{- if .Values.priorityClassName }} + priorityClassName: {{ .Values.priorityClassName }} + {{- end }} diff --git a/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-controller-manager-metrics-service-svc.yaml b/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-controller-manager-metrics-service-svc.yaml new file mode 100644 index 00000000..e4c6e820 --- /dev/null +++ b/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-controller-manager-metrics-service-svc.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + control-plane: controller-manager + name: {{ include "opensearch-operator.fullname" . }}-controller-manager-metrics-service +spec: + ports: + - name: https + port: 8443 + targetPort: https + selector: + control-plane: controller-manager diff --git a/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-controller-manager-sa.yaml b/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-controller-manager-sa.yaml new file mode 100644 index 00000000..ee5a1ca2 --- /dev/null +++ b/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-controller-manager-sa.yaml @@ -0,0 +1,6 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "opensearch-operator.serviceAccountName" . }} +{{- end -}} diff --git a/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-crds.yaml b/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-crds.yaml new file mode 100644 index 00000000..21fdeedb --- /dev/null +++ b/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-crds.yaml @@ -0,0 +1,5 @@ +{{- if .Values.installCRDs -}} +{{- range $path, $bytes := .Files.Glob "files/*.yaml" }} +{{ $.Files.Get $path }} +{{- end }} +{{- end }} diff --git a/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-leader-election-role-role.yaml b/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-leader-election-role-role.yaml new file mode 100644 index 00000000..aa8853e0 --- /dev/null +++ b/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-leader-election-role-role.yaml @@ -0,0 +1,48 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ include "opensearch-operator.fullname" . }}-leader-election-role +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +- apiGroups: + - monitoring.coreos.com + resources: + - servicemonitors + verbs: + - create + - delete + - get + - list + - patch + - update + - watch diff --git a/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-leader-election-rolebinding-rb.yaml b/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-leader-election-rolebinding-rb.yaml new file mode 100644 index 00000000..654a0a7d --- /dev/null +++ b/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-leader-election-rolebinding-rb.yaml @@ -0,0 +1,11 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "opensearch-operator.fullname" . }}-leader-election-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "opensearch-operator.fullname" . }}-leader-election-role +subjects: +- kind: ServiceAccount + name: {{ include "opensearch-operator.serviceAccountName" . }} diff --git a/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-manager-config-cm.yaml b/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-manager-config-cm.yaml new file mode 100644 index 00000000..c2e9a13f --- /dev/null +++ b/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-manager-config-cm.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +data: + controller_manager_config.yaml: | + apiVersion: controller-runtime.sigs.k8s.io/v1alpha1 + kind: ControllerManagerConfig + health: + healthProbeBindAddress: :8081 + metrics: + bindAddress: 127.0.0.1:8080 + webhook: + port: 9443 + leaderElection: + leaderElect: true + resourceName: a867c7dc.opensearch.opster.io +kind: ConfigMap +metadata: + name: {{ include "opensearch-operator.fullname" . }}-manager-config diff --git a/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-manager-role-cr.yaml b/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-manager-role-cr.yaml new file mode 100644 index 00000000..3daf70d4 --- /dev/null +++ b/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-manager-role-cr.yaml @@ -0,0 +1,414 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: {{ include "opensearch-operator.fullname" . }}-{{ .Release.Namespace }}-manager-role +rules: +- apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - apps + resources: + - statefulsets + - statefulsets/status + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - batch + resources: + - jobs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - configmaps + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch + - update +- apiGroups: + - "" + resources: + - namespaces + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - pods + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - secrets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - services + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "policy" + resources: + - poddisruptionbudgets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - opensearch.opster.io + resources: + - events + verbs: + - create + - patch +- apiGroups: + - opensearch.opster.io + resources: + - opensearchactiongroups + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - opensearch.opster.io + resources: + - opensearchactiongroups/finalizers + verbs: + - update +- apiGroups: + - opensearch.opster.io + resources: + - opensearchactiongroups/status + verbs: + - get + - patch + - update +- apiGroups: + - opensearch.opster.io + resources: + - opensearchclusters + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - opensearch.opster.io + resources: + - opensearchclusters/finalizers + verbs: + - update +- apiGroups: + - opensearch.opster.io + resources: + - opensearchclusters/status + verbs: + - get + - patch + - update +- apiGroups: + - opensearch.opster.io + resources: + - opensearchcomponenttemplates + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - opensearch.opster.io + resources: + - opensearchcomponenttemplates/finalizers + verbs: + - update +- apiGroups: + - opensearch.opster.io + resources: + - opensearchcomponenttemplates/status + verbs: + - get + - patch + - update +- apiGroups: + - opensearch.opster.io + resources: + - opensearchindextemplates + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - opensearch.opster.io + resources: + - opensearchindextemplates/finalizers + verbs: + - update +- apiGroups: + - opensearch.opster.io + resources: + - opensearchindextemplates/status + verbs: + - get + - patch + - update +- apiGroups: + - opensearch.opster.io + resources: + - opensearchroles + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - opensearch.opster.io + resources: + - opensearchroles/finalizers + verbs: + - update +- apiGroups: + - opensearch.opster.io + resources: + - opensearchroles/status + verbs: + - get + - patch + - update +- apiGroups: + - opensearch.opster.io + resources: + - opensearchtenants + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - opensearch.opster.io + resources: + - opensearchtenants/finalizers + verbs: + - update +- apiGroups: + - opensearch.opster.io + resources: + - opensearchtenants/status + verbs: + - get + - patch + - update +- apiGroups: + - opensearch.opster.io + resources: + - opensearchuserrolebindings + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - opensearch.opster.io + resources: + - opensearchuserrolebindings/finalizers + verbs: + - update +- apiGroups: + - opensearch.opster.io + resources: + - opensearchuserrolebindings/status + verbs: + - get + - patch + - update +- apiGroups: + - opensearch.opster.io + resources: + - opensearchismpolicies/status + verbs: + - get + - patch + - update +- apiGroups: + - opensearch.opster.io + resources: + - opensearchismpolicies/finalizers + verbs: + - update +- apiGroups: + - opensearch.opster.io + resources: + - opensearchusers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - opensearch.opster.io + resources: + - opensearchismpolicies + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - opensearch.opster.io + resources: + - opensearchusers/finalizers + verbs: + - update +- apiGroups: + - opensearch.opster.io + resources: + - opensearchusers/status + verbs: + - get + - patch + - update +- apiGroups: + - "" + resources: + - persistentvolumeclaims + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - monitoring.coreos.com + resources: + - servicemonitors + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - opensearch.opster.io + resources: + - opensearchsnapshotpolicies + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - opensearch.opster.io + resources: + - opensearchsnapshotpolicies/status + verbs: + - get + - patch + - update +- apiGroups: + - opensearch.opster.io + resources: + - opensearchsnapshotpolicies/finalizers + verbs: + - update \ No newline at end of file diff --git a/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-manager-rolebinding.yaml b/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-manager-rolebinding.yaml new file mode 100644 index 00000000..a528ffdf --- /dev/null +++ b/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-manager-rolebinding.yaml @@ -0,0 +1,27 @@ +{{- if .Values.useRoleBindings }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "opensearch-operator.fullname" . }}-{{ .Release.Namespace }}-manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "opensearch-operator.fullname" . }}-{{ .Release.Namespace }}-manager-role +subjects: +- kind: ServiceAccount + name: {{ include "opensearch-operator.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +{{- else }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "opensearch-operator.fullname" . }}-{{ .Release.Namespace }}-manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "opensearch-operator.fullname" . }}-{{ .Release.Namespace }}-manager-role +subjects: +- kind: ServiceAccount + name: {{ include "opensearch-operator.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +{{- end }} diff --git a/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-metrics-reader-cr.yaml b/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-metrics-reader-cr.yaml new file mode 100644 index 00000000..c8f9d89c --- /dev/null +++ b/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-metrics-reader-cr.yaml @@ -0,0 +1,9 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "opensearch-operator.fullname" . }}-{{ .Release.Namespace }}-metrics-reader +rules: +- nonResourceURLs: + - /metrics + verbs: + - get diff --git a/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-proxy-role-cr.yaml b/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-proxy-role-cr.yaml new file mode 100644 index 00000000..cbd6cb77 --- /dev/null +++ b/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-proxy-role-cr.yaml @@ -0,0 +1,17 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "opensearch-operator.fullname" . }}-{{ .Release.Namespace }}-proxy-role +rules: +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create diff --git a/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-proxy-rolebinding.yaml b/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-proxy-rolebinding.yaml new file mode 100644 index 00000000..d9a5d339 --- /dev/null +++ b/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-proxy-rolebinding.yaml @@ -0,0 +1,27 @@ +{{- if .Values.useRoleBindings }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "opensearch-operator.fullname" . }}-{{ .Release.Namespace }}-proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "opensearch-operator.fullname" . }}-{{ .Release.Namespace }}-proxy-role +subjects: +- kind: ServiceAccount + name: {{ include "opensearch-operator.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +{{- else }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "opensearch-operator.fullname" . }}-{{ .Release.Namespace }}-proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "opensearch-operator.fullname" . }}-{{ .Release.Namespace }}-proxy-role +subjects: +- kind: ServiceAccount + name: {{ include "opensearch-operator.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +{{- end }} diff --git a/packages/system/opensearch-operator/charts/opensearch-operator/values.yaml b/packages/system/opensearch-operator/charts/opensearch-operator/values.yaml new file mode 100644 index 00000000..1ee839bb --- /dev/null +++ b/packages/system/opensearch-operator/charts/opensearch-operator/values.yaml @@ -0,0 +1,123 @@ +nameOverride: "" +fullnameOverride: "" + +podAnnotations: {} +podLabels: {} +nodeSelector: {} +tolerations: [] +securityContext: + runAsNonRoot: true +priorityClassName: "" +manager: + securityContext: + allowPrivilegeEscalation: false + extraEnv: [] + resources: + limits: + cpu: 200m + memory: 500Mi + requests: + cpu: 100m + memory: 350Mi + + livenessProbe: + failureThreshold: 3 + httpGet: + path: /healthz + port: 8081 + periodSeconds: 15 + successThreshold: 1 + timeoutSeconds: 3 + initialDelaySeconds: 10 + + readinessProbe: + failureThreshold: 3 + httpGet: + path: /readyz + port: 8081 + periodSeconds: 15 + successThreshold: 1 + timeoutSeconds: 3 + initialDelaySeconds: 10 + + # Set this to false to disable the experimental parallel recovery in case you are experiencing problems + parallelRecoveryEnabled: true + # Set this to true to enable the standard go pprof endpoints on port 6060 (https://pkg.go.dev/net/http/pprof) + # Should only be used for debugging purposes + pprofEndpointsEnabled: false + + image: + repository: opensearchproject/opensearch-operator + ## tag default uses appVersion from Chart.yaml, to override specify tag tag: "v1.1" + tag: "" + pullPolicy: "Always" + + ## Optional array of imagePullSecrets containing private registry credentials + imagePullSecrets: [] + # - name: secretName + + dnsBase: cluster.local + + # Log level of the operator. Possible values: debug, info, warn, error + loglevel: info + + # If a watchNamespace is specified, the manager's cache will be restricted to + # watch objects in the desired namespace. Defaults is to watch all namespaces. + watchNamespace: + +# Install the Custom Resource Definitions with Helm +installCRDs: true + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Override the service account name. Defaults to opensearch-operator-controller-manager + name: "" + +kubeRbacProxy: + enable: true + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + resources: + limits: + cpu: 50m + memory: 50Mi + requests: + cpu: 25m + memory: 25Mi + + livenessProbe: + failureThreshold: 3 + httpGet: + path: /healthz + port: 10443 + scheme: HTTPS + periodSeconds: 15 + successThreshold: 1 + timeoutSeconds: 3 + initialDelaySeconds: 10 + + readinessProbe: + failureThreshold: 3 + httpGet: + path: /healthz + port: 10443 + scheme: HTTPS + periodSeconds: 15 + successThreshold: 1 + timeoutSeconds: 3 + initialDelaySeconds: 10 + + image: + repository: "gcr.io/kubebuilder/kube-rbac-proxy" + tag: "v0.15.0" + +## If this is set to true, RoleBindings will be used instead of ClusterRoleBindings, inorder to restrict ClusterRoles +## to the namespace where the operator and OpenSearch cluster are in. In that case, specify the namespace where they +## are in in manager.watchNamespace field. +## If false, ClusterRoleBindings will be used +useRoleBindings: false diff --git a/packages/system/opensearch-operator/templates/sysctl-daemonset.yaml b/packages/system/opensearch-operator/templates/sysctl-daemonset.yaml new file mode 100644 index 00000000..76c3569e --- /dev/null +++ b/packages/system/opensearch-operator/templates/sysctl-daemonset.yaml @@ -0,0 +1,64 @@ +--- +# DaemonSet to configure vm.max_map_count on all nodes for OpenSearch +# This runs once on each node to ensure the kernel parameter is set +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: opensearch-sysctl-setter + labels: + app.kubernetes.io/name: opensearch-sysctl-setter + app.kubernetes.io/component: sysctl +spec: + selector: + matchLabels: + app.kubernetes.io/name: opensearch-sysctl-setter + template: + metadata: + labels: + app.kubernetes.io/name: opensearch-sysctl-setter + spec: + initContainers: + - name: sysctl + image: busybox:1.36 + securityContext: + privileged: true + resources: + requests: + cpu: 1m + memory: 1Mi + limits: + cpu: 50m + memory: 16Mi + command: + - sh + - -c + - | + sysctl -w vm.max_map_count=262144 + # Keep the value persistent by writing to sysctl.conf if writable + if [ -w /host-etc/sysctl.conf ]; then + grep -q "vm.max_map_count" /host-etc/sysctl.conf || echo "vm.max_map_count=262144" >> /host-etc/sysctl.conf + fi + volumeMounts: + - name: host-etc + mountPath: /host-etc + readOnly: false + containers: + - name: pause + image: registry.k8s.io/pause:3.9 + resources: + requests: + cpu: 1m + memory: 1Mi + limits: + cpu: 10m + memory: 10Mi + volumes: + - name: host-etc + hostPath: + path: /etc + type: Directory + tolerations: + - operator: Exists + effect: NoSchedule + - operator: Exists + effect: NoExecute diff --git a/packages/system/opensearch-operator/values.yaml b/packages/system/opensearch-operator/values.yaml new file mode 100644 index 00000000..619aa99b --- /dev/null +++ b/packages/system/opensearch-operator/values.yaml @@ -0,0 +1,4 @@ +opensearch-operator: + manager: + # Enable cluster-wide mode to watch all namespaces + watchNamespace: "" diff --git a/packages/system/opensearch-rd/Chart.yaml b/packages/system/opensearch-rd/Chart.yaml new file mode 100644 index 00000000..c0ca3b86 --- /dev/null +++ b/packages/system/opensearch-rd/Chart.yaml @@ -0,0 +1,3 @@ +apiVersion: v2 +name: opensearch-rd +version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process diff --git a/packages/system/opensearch-rd/Makefile b/packages/system/opensearch-rd/Makefile new file mode 100644 index 00000000..7d3c0311 --- /dev/null +++ b/packages/system/opensearch-rd/Makefile @@ -0,0 +1,4 @@ +export NAME=opensearch-rd +export NAMESPACE=cozy-system + +include ../../../hack/package.mk diff --git a/packages/system/opensearch-rd/cozyrds/opensearch.yaml b/packages/system/opensearch-rd/cozyrds/opensearch.yaml new file mode 100644 index 00000000..be15d8a1 --- /dev/null +++ b/packages/system/opensearch-rd/cozyrds/opensearch.yaml @@ -0,0 +1,42 @@ +apiVersion: cozystack.io/v1alpha1 +kind: ApplicationDefinition +metadata: + name: opensearch +spec: + application: + kind: OpenSearch + singular: opensearch + plural: opensearches + openAPISchema: |- + {"title":"Chart Values","type":"object","properties":{"dashboards":{"description":"OpenSearch Dashboards configuration.","type":"object","default":{},"required":["enabled","replicas","resourcesPreset"],"properties":{"enabled":{"description":"Enable OpenSearch Dashboards deployment.","type":"boolean","default":false},"replicas":{"description":"Number of Dashboards replicas.","type":"integer","default":1},"resources":{"description":"Explicit CPU and memory configuration for Dashboards.","type":"object","default":{},"properties":{"cpu":{"description":"CPU available to each node.","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 node.","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 for Dashboards.","type":"string","default":"medium","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]}}},"external":{"description":"Enable external access from outside the cluster.","type":"boolean","default":false},"images":{"description":"Container images used by the operator.","type":"object","default":{},"required":["opensearch"],"properties":{"opensearch":{"description":"OpenSearch image.","type":"string","default":""}}},"nodeRoles":{"description":"Node roles configuration.","type":"object","default":{},"required":["data","ingest","master","ml"],"properties":{"data":{"description":"Enable data role.","type":"boolean","default":true},"ingest":{"description":"Enable ingest role.","type":"boolean","default":true},"master":{"description":"Enable cluster_manager role.","type":"boolean","default":true},"ml":{"description":"Enable machine learning role.","type":"boolean","default":false}}},"replicas":{"description":"Number of OpenSearch nodes in the cluster.","type":"integer","default":3},"resources":{"description":"Explicit CPU and memory configuration for each OpenSearch node. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"CPU available to each node.","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 node.","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. OpenSearch requires minimum 2Gi memory.","type":"string","default":"large","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]},"size":{"description":"Persistent Volume Claim size available for application data.","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","default":""},"topologySpreadPolicy":{"description":"How strictly to enforce pod distribution across nodes and zones.","type":"string","default":"soft","enum":["soft","hard"]},"users":{"description":"Custom OpenSearch users configuration map.","type":"object","default":{},"additionalProperties":{"type":"object","properties":{"password":{"description":"Password for the user (auto-generated if omitted).","type":"string"},"roles":{"description":"List of OpenSearch roles.","type":"array","items":{"type":"string"}}}}},"version":{"description":"OpenSearch major version to deploy.","type":"string","default":"v2","enum":["v3","v2","v1"]}}} + release: + prefix: opensearch- + labels: + sharding.fluxcd.io/key: tenants + chartRef: + kind: ExternalArtifact + name: cozystack-opensearch-application-default-opensearch + namespace: cozy-system + dashboard: + category: PaaS + singular: OpenSearch + plural: OpenSearch Clusters + description: Managed OpenSearch service + tags: + - database + - search + icon: PHN2ZyB3aWR0aD0iMTQ0IiBoZWlnaHQ9IjE0NCIgdmlld0JveD0iMCAwIDE0NCAxNDQiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIxNDQiIGhlaWdodD0iMTQ0IiByeD0iMjQiIGZpbGw9InVybCgjcGFpbnQwX2xpbmVhcl9vcGVuc2VhcmNoKSIvPgo8cGF0aCBkPSJNNzIgMzZDNDQgMzYgMjggNTIgMjggNzJDMjggOTIgNDQgMTA4IDcyIDEwOEMxMDAgMTA4IDExNiA5MiAxMTYgNzIiIHN0cm9rZT0iIzAwNUVCOCIgc3Ryb2tlLXdpZHRoPSI4IiBzdHJva2UtbGluZWNhcD0icm91bmQiIGZpbGw9Im5vbmUiLz4KPGNpcmNsZSBjeD0iMTE2IiBjeT0iNzIiIHI9IjgiIGZpbGw9IiMwMDVFQjgiLz4KPGRlZnM+CjxsaW5lYXJHcmFkaWVudCBpZD0icGFpbnQwX2xpbmVhcl9vcGVuc2VhcmNoIiB4MT0iMTQwIiB5MT0iMTMwLjUiIHgyPSI0IiB5Mj0iOS40OTk5OSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBzdG9wLWNvbG9yPSIjMDAzQjVDIi8+CjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzAwNUVCOCIvPgo8L2xpbmVhckdyYWRpZW50Pgo8L2RlZnM+Cjwvc3ZnPgo= + keysOrder: [["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata", "name"], ["spec", "replicas"], ["spec", "resources"], ["spec", "resourcesPreset"], ["spec", "size"], ["spec", "storageClass"], ["spec", "external"], ["spec", "topologySpreadPolicy"], ["spec", "version"], ["spec", "images"], ["spec", "images", "opensearch"], ["spec", "nodeRoles"], ["spec", "nodeRoles", "master"], ["spec", "nodeRoles", "data"], ["spec", "nodeRoles", "ingest"], ["spec", "nodeRoles", "ml"], ["spec", "users"], ["spec", "dashboards"], ["spec", "dashboards", "enabled"], ["spec", "dashboards", "replicas"], ["spec", "dashboards", "resources"], ["spec", "dashboards", "resourcesPreset"]] + secrets: + exclude: [] + include: + - resourceNames: + - opensearch-{{ .name }}-credentials + services: + exclude: [] + include: + - resourceNames: + - opensearch-{{ .name }} + - opensearch-{{ .name }}-external + - opensearch-{{ .name }}-dashboards + - opensearch-{{ .name }}-dashboards-external diff --git a/packages/system/opensearch-rd/templates/cozyrd.yaml b/packages/system/opensearch-rd/templates/cozyrd.yaml new file mode 100644 index 00000000..e079ddcf --- /dev/null +++ b/packages/system/opensearch-rd/templates/cozyrd.yaml @@ -0,0 +1,4 @@ +{{- range $path, $_ := .Files.Glob "cozyrds/*" }} +--- +{{ $.Files.Get $path }} +{{- end }} diff --git a/packages/system/opensearch-rd/values.yaml b/packages/system/opensearch-rd/values.yaml new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/packages/system/opensearch-rd/values.yaml @@ -0,0 +1 @@ +{} From 4e59e5e656e59ca9b9055ae04cc1ea836d132756 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Mon, 16 Feb 2026 22:28:35 +0100 Subject: [PATCH 057/666] Add PackageSource definitions for OpenSearch Add operator and application PackageSource CRs to expose OpenSearch in the Cozystack platform: - opensearch-operator: operator deployment in cozy-opensearch-operator namespace - opensearch-application: app + resource definition with cozy-lib integration This enables OpenSearch to appear in the platform dashboard and be deployed by tenants. Co-Authored-By: Claude Sonnet 4.5 Signed-off-by: Matthieu --- .../sources/opensearch-application.yaml | 28 +++++++++++++++++++ .../platform/sources/opensearch-operator.yaml | 22 +++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 packages/core/platform/sources/opensearch-application.yaml create mode 100644 packages/core/platform/sources/opensearch-operator.yaml diff --git a/packages/core/platform/sources/opensearch-application.yaml b/packages/core/platform/sources/opensearch-application.yaml new file mode 100644 index 00000000..2b499cd0 --- /dev/null +++ b/packages/core/platform/sources/opensearch-application.yaml @@ -0,0 +1,28 @@ +--- +apiVersion: cozystack.io/v1alpha1 +kind: PackageSource +metadata: + name: cozystack.opensearch-application +spec: + sourceRef: + kind: OCIRepository + name: cozystack-packages + namespace: cozy-system + path: / + variants: + - name: default + dependsOn: + - cozystack.networking + - cozystack.opensearch-operator + libraries: + - name: cozy-lib + path: library/cozy-lib + components: + - name: opensearch + path: apps/opensearch + libraries: ["cozy-lib"] + - name: opensearch-rd + path: system/opensearch-rd + install: + namespace: cozy-system + releaseName: opensearch-rd diff --git a/packages/core/platform/sources/opensearch-operator.yaml b/packages/core/platform/sources/opensearch-operator.yaml new file mode 100644 index 00000000..21dd7a43 --- /dev/null +++ b/packages/core/platform/sources/opensearch-operator.yaml @@ -0,0 +1,22 @@ +--- +apiVersion: cozystack.io/v1alpha1 +kind: PackageSource +metadata: + name: cozystack.opensearch-operator +spec: + sourceRef: + kind: OCIRepository + name: cozystack-packages + namespace: cozy-system + path: / + variants: + - name: default + dependsOn: + - cozystack.networking + - cozystack.prometheus-operator-crds + components: + - name: opensearch-operator + path: system/opensearch-operator + install: + namespace: cozy-opensearch-operator + releaseName: opensearch-operator From 7181fd1c937a05e94073519c21135cc2d3dbbd8b Mon Sep 17 00:00:00 2001 From: Matthieu Date: Mon, 16 Feb 2026 22:40:01 +0100 Subject: [PATCH 058/666] Fix critical issues in OpenSearch operator templates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address CodeRabbit review feedback: - Fix extraEnv indentation (nindent 8 → 10) in controller-manager deployment - Fix imagePullSecrets indentation (nindent 6 → 8) for proper YAML alignment - Change proxy-rolebinding from conditional RoleBinding to always ClusterRoleBinding (required for cluster-scoped tokenreviews/subjectaccessreviews permissions) - Pin operator chart version to 2.8.0 in Makefile for reproducible builds Co-Authored-By: Claude Sonnet 4.5 Signed-off-by: Matthieu --- packages/system/opensearch-operator/Makefile | 2 +- ...ch-operator-controller-manager-deployment.yaml | 4 ++-- .../opensearch-operator-proxy-rolebinding.yaml | 15 --------------- 3 files changed, 3 insertions(+), 18 deletions(-) diff --git a/packages/system/opensearch-operator/Makefile b/packages/system/opensearch-operator/Makefile index 71b94549..36522495 100644 --- a/packages/system/opensearch-operator/Makefile +++ b/packages/system/opensearch-operator/Makefile @@ -7,4 +7,4 @@ update: rm -rf charts helm repo add opensearch-operator https://opensearch-project.github.io/opensearch-k8s-operator/ helm repo update opensearch-operator - helm pull opensearch-operator/opensearch-operator --untar --untardir charts + helm pull opensearch-operator/opensearch-operator --version 2.8.0 --untar --untardir charts diff --git a/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-controller-manager-deployment.yaml b/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-controller-manager-deployment.yaml index 4b5cb194..43a70d65 100644 --- a/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-controller-manager-deployment.yaml +++ b/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-controller-manager-deployment.yaml @@ -73,7 +73,7 @@ spec: - name: PPROF_ENDPOINTS_ENABLED value: "{{ .Values.manager.pprofEndpointsEnabled }}" {{- if .Values.manager.extraEnv }} - {{- toYaml .Values.manager.extraEnv | nindent 8 }} + {{- toYaml .Values.manager.extraEnv | nindent 10 }} {{- end }} securityContext: {{- toYaml .Values.manager.securityContext | nindent 10 }} @@ -85,7 +85,7 @@ spec: {{- toYaml .Values.securityContext | nindent 8}} {{- if .Values.manager.imagePullSecrets }} imagePullSecrets: - {{- toYaml .Values.manager.imagePullSecrets | nindent 6 }} + {{- toYaml .Values.manager.imagePullSecrets | nindent 8 }} {{- end }} serviceAccountName: {{ include "opensearch-operator.serviceAccountName" . }} terminationGracePeriodSeconds: 10 diff --git a/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-proxy-rolebinding.yaml b/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-proxy-rolebinding.yaml index d9a5d339..5cba0693 100644 --- a/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-proxy-rolebinding.yaml +++ b/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-proxy-rolebinding.yaml @@ -1,17 +1,3 @@ -{{- if .Values.useRoleBindings }} -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: {{ include "opensearch-operator.fullname" . }}-{{ .Release.Namespace }}-proxy-rolebinding -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: {{ include "opensearch-operator.fullname" . }}-{{ .Release.Namespace }}-proxy-role -subjects: -- kind: ServiceAccount - name: {{ include "opensearch-operator.serviceAccountName" . }} - namespace: {{ .Release.Namespace }} -{{- else }} apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: @@ -24,4 +10,3 @@ subjects: - kind: ServiceAccount name: {{ include "opensearch-operator.serviceAccountName" . }} namespace: {{ .Release.Namespace }} -{{- end }} From f7767d1ee695d9ef25e8a79d0e46617a1c018ab6 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Mon, 16 Feb 2026 22:48:28 +0100 Subject: [PATCH 059/666] Fix YAML style issues in OpenSearch packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address CodeRabbit nitpick feedback: - Fix inconsistent indentation in opensearch-application.yaml (4 spaces → 2 spaces) to match opensearch-operator.yaml style - Add missing space in controller-manager-deployment.yaml template (nindent 8}} → nindent 8 }}) for consistency Co-Authored-By: Claude Sonnet 4.5 Signed-off-by: Matthieu --- .../sources/opensearch-application.yaml | 32 +++++++++---------- ...perator-controller-manager-deployment.yaml | 2 +- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/core/platform/sources/opensearch-application.yaml b/packages/core/platform/sources/opensearch-application.yaml index 2b499cd0..19358c21 100644 --- a/packages/core/platform/sources/opensearch-application.yaml +++ b/packages/core/platform/sources/opensearch-application.yaml @@ -10,19 +10,19 @@ spec: namespace: cozy-system path: / variants: - - name: default - dependsOn: - - cozystack.networking - - cozystack.opensearch-operator - libraries: - - name: cozy-lib - path: library/cozy-lib - components: - - name: opensearch - path: apps/opensearch - libraries: ["cozy-lib"] - - name: opensearch-rd - path: system/opensearch-rd - install: - namespace: cozy-system - releaseName: opensearch-rd + - name: default + dependsOn: + - cozystack.networking + - cozystack.opensearch-operator + libraries: + - name: cozy-lib + path: library/cozy-lib + components: + - name: opensearch + path: apps/opensearch + libraries: ["cozy-lib"] + - name: opensearch-rd + path: system/opensearch-rd + install: + namespace: cozy-system + releaseName: opensearch-rd diff --git a/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-controller-manager-deployment.yaml b/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-controller-manager-deployment.yaml index 43a70d65..114e6b20 100644 --- a/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-controller-manager-deployment.yaml +++ b/packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-controller-manager-deployment.yaml @@ -82,7 +82,7 @@ spec: tolerations: {{- toYaml .Values.tolerations | nindent 8 }} securityContext: -{{- toYaml .Values.securityContext | nindent 8}} +{{- toYaml .Values.securityContext | nindent 8 }} {{- if .Values.manager.imagePullSecrets }} imagePullSecrets: {{- toYaml .Values.manager.imagePullSecrets | nindent 8 }} From 315e5dc0bd9a532eaf83f7aaf3fd3edc16c17d07 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Tue, 17 Feb 2026 01:32:05 +0300 Subject: [PATCH 060/666] fix(e2e): make kubernetes test retries effective by cleaning up stale resources When the kubernetes E2E test fails at the deployment wait step, set -eu causes immediate exit before cleanup. On retry, kubectl apply outputs "unchanged" for the stuck deployment, making retries 2 and 3 guaranteed to fail against the same stuck pod. Add pre-creation cleanup of backend deployment/service and NFS test resources using --ignore-not-found, so retries start fresh. Also increase the deployment wait timeout from 90s to 300s to handle CI resource pressure, aligning with other timeouts in the same function. Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- hack/e2e-apps/run-kubernetes.sh | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/hack/e2e-apps/run-kubernetes.sh b/hack/e2e-apps/run-kubernetes.sh index d0997ecb..09e50924 100644 --- a/hack/e2e-apps/run-kubernetes.sh +++ b/hack/e2e-apps/run-kubernetes.sh @@ -132,8 +132,14 @@ metadata: name: tenant-test EOF + # Clean up backend resources from any previous failed attempt + kubectl delete deployment --kubeconfig "tenantkubeconfig-${test_name}" "${test_name}-backend" \ + -n tenant-test --ignore-not-found --timeout=60s || true + kubectl delete service --kubeconfig "tenantkubeconfig-${test_name}" "${test_name}-backend" \ + -n tenant-test --ignore-not-found --timeout=60s || true + # Backend 1 - kubectl apply --kubeconfig tenantkubeconfig-${test_name} -f- < Date: Tue, 17 Feb 2026 02:00:44 +0300 Subject: [PATCH 061/666] style(e2e): consistently quote kubeconfig variable references Quote all tenantkubeconfig-${test_name} references in run-kubernetes.sh for consistent shell scripting style. The only exception is line 195 inside a sh -ec "..." double-quoted string where inner quotes would break the outer quoting. Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- hack/e2e-apps/run-kubernetes.sh | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/hack/e2e-apps/run-kubernetes.sh b/hack/e2e-apps/run-kubernetes.sh index 09e50924..511fcc17 100644 --- a/hack/e2e-apps/run-kubernetes.sh +++ b/hack/e2e-apps/run-kubernetes.sh @@ -80,10 +80,10 @@ EOF # Wait for the machine deployment to scale to 2 replicas (timeout after 1 minute) kubectl wait machinedeployment kubernetes-${test_name}-md0 -n tenant-test --timeout=1m --for=jsonpath='{.status.replicas}'=2 # Get the admin kubeconfig and save it to a file - kubectl get secret kubernetes-${test_name}-admin-kubeconfig -ojsonpath='{.data.super-admin\.conf}' -n tenant-test | base64 -d > tenantkubeconfig-${test_name} + kubectl get secret kubernetes-${test_name}-admin-kubeconfig -ojsonpath='{.data.super-admin\.conf}' -n tenant-test | base64 -d > "tenantkubeconfig-${test_name}" # Update the kubeconfig to use localhost for the API server - yq -i ".clusters[0].cluster.server = \"https://localhost:${port}\"" tenantkubeconfig-${test_name} + yq -i ".clusters[0].cluster.server = \"https://localhost:${port}\"" "tenantkubeconfig-${test_name}" # Set up port forwarding to the Kubernetes API server for a 200 second timeout @@ -98,8 +98,8 @@ EOF done ' # Verify the nodes are ready - kubectl --kubeconfig tenantkubeconfig-${test_name} wait node --all --timeout=2m --for=condition=Ready - kubectl --kubeconfig tenantkubeconfig-${test_name} get nodes -o wide + kubectl --kubeconfig "tenantkubeconfig-${test_name}" wait node --all --timeout=2m --for=condition=Ready + kubectl --kubeconfig "tenantkubeconfig-${test_name}" get nodes -o wide # Verify the kubelet version matches what we expect versions=$(kubectl --kubeconfig "tenantkubeconfig-${test_name}" \ @@ -125,7 +125,7 @@ EOF fi - kubectl --kubeconfig tenantkubeconfig-${test_name} apply -f - <&2 - kubectl --kubeconfig tenantkubeconfig-${test_name} delete pod nfs-test-pod -n tenant-test --wait=false 2>/dev/null || true - kubectl --kubeconfig tenantkubeconfig-${test_name} delete pvc nfs-test-pvc -n tenant-test --wait=false 2>/dev/null || true + kubectl --kubeconfig "tenantkubeconfig-${test_name}" delete pod nfs-test-pod -n tenant-test --wait=false 2>/dev/null || true + kubectl --kubeconfig "tenantkubeconfig-${test_name}" delete pvc nfs-test-pvc -n tenant-test --wait=false 2>/dev/null || true exit 1 fi # Cleanup NFS test resources in tenant cluster - kubectl --kubeconfig tenantkubeconfig-${test_name} delete pod nfs-test-pod -n tenant-test --wait - kubectl --kubeconfig tenantkubeconfig-${test_name} delete pvc nfs-test-pvc -n tenant-test + kubectl --kubeconfig "tenantkubeconfig-${test_name}" delete pod nfs-test-pod -n tenant-test --wait + kubectl --kubeconfig "tenantkubeconfig-${test_name}" delete pvc nfs-test-pvc -n tenant-test # Wait for all machine deployment replicas to be ready (timeout after 10 minutes) kubectl wait machinedeployment kubernetes-${test_name}-md0 -n tenant-test --timeout=10m --for=jsonpath='{.status.v1beta2.readyReplicas}'=2 From 32aff887eb8b43b4160582b6b61f434a3ceac2c2 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Tue, 17 Feb 2026 20:03:01 +0300 Subject: [PATCH 062/666] feat(openbao): add system chart with vendored upstream Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- packages/system/openbao/Chart.yaml | 3 + packages/system/openbao/Makefile | 10 + .../system/openbao/charts/openbao/.helmignore | 28 + .../system/openbao/charts/openbao/Chart.yaml | 30 + .../system/openbao/charts/openbao/README.md | 365 +++ .../openbao/grafana/dashboards/dashboard.json | 2559 +++++++++++++++++ .../charts/openbao/templates/NOTES.txt | 18 + .../charts/openbao/templates/_helpers.tpl | 1212 ++++++++ .../templates/csi-agent-configmap.yaml | 34 + .../openbao/templates/csi-clusterrole.yaml | 23 + .../templates/csi-clusterrolebinding.yaml | 24 + .../openbao/templates/csi-daemonset.yaml | 157 + .../charts/openbao/templates/csi-role.yaml | 32 + .../openbao/templates/csi-rolebinding.yaml | 25 + .../openbao/templates/csi-serviceaccount.yaml | 21 + .../openbao/templates/extra-objects.yaml | 8 + .../grafana/configmap-dashboard.yaml | 30 + .../templates/injector-certs-secret.yaml | 19 + .../templates/injector-clusterrole.yaml | 30 + .../injector-clusterrolebinding.yaml | 24 + .../templates/injector-deployment.yaml | 179 ++ .../templates/injector-disruptionbudget.yaml | 25 + .../templates/injector-mutating-webhook.yaml | 44 + .../templates/injector-network-policy.yaml | 30 + .../openbao/templates/injector-psp-role.yaml | 25 + .../templates/injector-psp-rolebinding.yaml | 26 + .../openbao/templates/injector-psp.yaml | 51 + .../openbao/templates/injector-role.yaml | 34 + .../templates/injector-rolebinding.yaml | 27 + .../openbao/templates/injector-service.yaml | 30 + .../templates/injector-serviceaccount.yaml | 18 + .../templates/prometheus-prometheusrules.yaml | 32 + .../templates/prometheus-servicemonitor.yaml | 62 + .../templates/server-backendtlspolicy.yaml | 43 + .../templates/server-clusterrolebinding.yaml | 29 + .../templates/server-config-configmap.yaml | 31 + .../templates/server-discovery-role.yaml | 26 + .../server-discovery-rolebinding.yaml | 34 + .../templates/server-disruptionbudget.yaml | 31 + .../templates/server-ha-active-service.yaml | 68 + .../templates/server-ha-standby-service.yaml | 67 + .../templates/server-headless-service.yaml | 46 + .../openbao/templates/server-httproute.yaml | 50 + .../openbao/templates/server-ingress.yaml | 67 + .../templates/server-network-policy.yaml | 24 + .../openbao/templates/server-psp-role.yaml | 25 + .../templates/server-psp-rolebinding.yaml | 26 + .../charts/openbao/templates/server-psp.yaml | 54 + .../openbao/templates/server-route.yaml | 39 + .../openbao/templates/server-service.yaml | 64 + .../server-serviceaccount-secret.yaml | 21 + .../templates/server-serviceaccount.yaml | 22 + .../openbao/templates/server-statefulset.yaml | 228 ++ .../openbao/templates/server-tlsroute.yaml | 41 + .../templates/snapshotagent-configmap.yaml | 31 + .../templates/snapshotagent-cronjob.yaml | 66 + .../snapshotagent-serviceaccount.yaml | 16 + .../openbao/templates/tests/server-test.yaml | 56 + .../charts/openbao/templates/ui-service.yaml | 53 + .../charts/openbao/values.openshift.yaml | 30 + .../openbao/charts/openbao/values.schema.json | 1207 ++++++++ .../system/openbao/charts/openbao/values.yaml | 1583 ++++++++++ packages/system/openbao/values.yaml | 5 + 63 files changed, 9318 insertions(+) create mode 100644 packages/system/openbao/Chart.yaml create mode 100644 packages/system/openbao/Makefile create mode 100644 packages/system/openbao/charts/openbao/.helmignore create mode 100644 packages/system/openbao/charts/openbao/Chart.yaml create mode 100644 packages/system/openbao/charts/openbao/README.md create mode 100644 packages/system/openbao/charts/openbao/grafana/dashboards/dashboard.json create mode 100644 packages/system/openbao/charts/openbao/templates/NOTES.txt create mode 100644 packages/system/openbao/charts/openbao/templates/_helpers.tpl create mode 100644 packages/system/openbao/charts/openbao/templates/csi-agent-configmap.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/csi-clusterrole.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/csi-clusterrolebinding.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/csi-daemonset.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/csi-role.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/csi-rolebinding.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/csi-serviceaccount.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/extra-objects.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/grafana/configmap-dashboard.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/injector-certs-secret.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/injector-clusterrole.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/injector-clusterrolebinding.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/injector-deployment.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/injector-disruptionbudget.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/injector-mutating-webhook.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/injector-network-policy.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/injector-psp-role.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/injector-psp-rolebinding.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/injector-psp.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/injector-role.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/injector-rolebinding.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/injector-service.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/injector-serviceaccount.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/prometheus-prometheusrules.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/prometheus-servicemonitor.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/server-backendtlspolicy.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/server-clusterrolebinding.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/server-config-configmap.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/server-discovery-role.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/server-discovery-rolebinding.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/server-disruptionbudget.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/server-ha-active-service.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/server-ha-standby-service.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/server-headless-service.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/server-httproute.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/server-ingress.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/server-network-policy.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/server-psp-role.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/server-psp-rolebinding.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/server-psp.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/server-route.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/server-service.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/server-serviceaccount-secret.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/server-serviceaccount.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/server-statefulset.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/server-tlsroute.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/snapshotagent-configmap.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/snapshotagent-cronjob.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/snapshotagent-serviceaccount.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/tests/server-test.yaml create mode 100644 packages/system/openbao/charts/openbao/templates/ui-service.yaml create mode 100644 packages/system/openbao/charts/openbao/values.openshift.yaml create mode 100644 packages/system/openbao/charts/openbao/values.schema.json create mode 100644 packages/system/openbao/charts/openbao/values.yaml create mode 100644 packages/system/openbao/values.yaml diff --git a/packages/system/openbao/Chart.yaml b/packages/system/openbao/Chart.yaml new file mode 100644 index 00000000..104a5e95 --- /dev/null +++ b/packages/system/openbao/Chart.yaml @@ -0,0 +1,3 @@ +apiVersion: v2 +name: cozy-openbao +version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process diff --git a/packages/system/openbao/Makefile b/packages/system/openbao/Makefile new file mode 100644 index 00000000..4ae6190e --- /dev/null +++ b/packages/system/openbao/Makefile @@ -0,0 +1,10 @@ +export NAME=openbao +export NAMESPACE=cozy-openbao +export REPO_NAME=openbao +export REPO_URL=https://openbao.github.io/openbao-helm +export CHART_NAME=openbao +export CHART_VERSION=^0.25 + +include ../../../hack/package.mk + +update: clean openbao-update diff --git a/packages/system/openbao/charts/openbao/.helmignore b/packages/system/openbao/charts/openbao/.helmignore new file mode 100644 index 00000000..4007e243 --- /dev/null +++ b/packages/system/openbao/charts/openbao/.helmignore @@ -0,0 +1,28 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.terraform/ +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj + +# CI and test +.circleci/ +.github/ +.gitlab-ci.yml +test/ diff --git a/packages/system/openbao/charts/openbao/Chart.yaml b/packages/system/openbao/charts/openbao/Chart.yaml new file mode 100644 index 00000000..4914439e --- /dev/null +++ b/packages/system/openbao/charts/openbao/Chart.yaml @@ -0,0 +1,30 @@ +annotations: + artifacthub.io/changes: | + - kind: changed + description: | + fix: Add extraPorts to server Service in ha + artifacthub.io/containsSecurityUpdates: "false" + charts.openshift.io/name: Openbao +apiVersion: v2 +appVersion: v2.5.0 +description: Official OpenBao Chart +home: https://github.com/openbao/openbao-helm +icon: https://raw.githubusercontent.com/openbao/artwork/refs/heads/main/color/openbao-color.svg +keywords: +- vault +- openbao +- security +- encryption +- secrets +- management +- automation +- infrastructure +kubeVersion: '>= 1.30.0-0' +maintainers: +- email: openbao-security@lists.openssf.org + name: OpenBao + url: https://openbao.org +name: openbao +sources: +- https://github.com/openbao/openbao-helm +version: 0.25.3 diff --git a/packages/system/openbao/charts/openbao/README.md b/packages/system/openbao/charts/openbao/README.md new file mode 100644 index 00000000..bcdf92b1 --- /dev/null +++ b/packages/system/openbao/charts/openbao/README.md @@ -0,0 +1,365 @@ +# openbao + +![Version: 0.25.3](https://img.shields.io/badge/Version-0.25.3-informational?style=flat-square) ![AppVersion: v2.5.0](https://img.shields.io/badge/AppVersion-v2.5.0-informational?style=flat-square) + +Official OpenBao Chart + +**Homepage:** + +## Maintainers + +| Name | Email | Url | +| ---- | ------ | --- | +| OpenBao | | | + +## Source Code + +* + +## Requirements + +Kubernetes: `>= 1.30.0-0` + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| csi.agent.enabled | bool | `true` | | +| csi.agent.extraArgs | list | `[]` | | +| csi.agent.image.pullPolicy | string | `"IfNotPresent"` | image pull policy to use for agent image. if tag is "latest", set to "Always" | +| csi.agent.image.registry | string | `"quay.io"` | image registry to use for agent image | +| csi.agent.image.repository | string | `"openbao/openbao"` | image repo to use for agent image | +| csi.agent.image.tag | string | `""` | image tag to use for agent image - defaults to chart appVersion | +| csi.agent.logFormat | string | `"standard"` | | +| csi.agent.logLevel | string | `"info"` | | +| csi.agent.resources | object | `{}` | | +| csi.daemonSet.annotations | object | `{}` | | +| csi.daemonSet.endpoint | string | `"/provider/openbao.sock"` | | +| csi.daemonSet.extraLabels | object | `{}` | | +| csi.daemonSet.kubeletRootDir | string | `"/var/lib/kubelet"` | | +| csi.daemonSet.providersDir | string | `"/etc/kubernetes/secrets-store-csi-providers"` | | +| csi.daemonSet.securityContext.container | object | `{}` | | +| csi.daemonSet.securityContext.pod | object | `{}` | | +| csi.daemonSet.updateStrategy.maxUnavailable | string | `""` | | +| csi.daemonSet.updateStrategy.type | string | `"RollingUpdate"` | | +| csi.debug | bool | `false` | | +| csi.enabled | bool | `false` | True if you want to install a openbao-csi-provider daemonset. Requires installing the secrets-store-csi-driver separately, see: https://secrets-store-csi-driver.sigs.k8s.io/getting-started/installation With the driver and provider installed, you can mount OpenBao secrets into volumes similar to the OpenBao Agent injector, and you can also sync those secrets into Kubernetes secrets. | +| csi.extraArgs | list | `[]` | | +| csi.hmacSecretName | string | `""` | | +| csi.image.pullPolicy | string | `"IfNotPresent"` | image pull policy to use for csi image. if tag is "latest", set to "Always" | +| csi.image.registry | string | `"quay.io"` | image registry to use for csi image | +| csi.image.repository | string | `"openbao/openbao-csi-provider"` | image repo to use for csi image | +| csi.image.tag | string | `"2.0.0"` | image tag to use for csi image | +| csi.livenessProbe.failureThreshold | int | `2` | | +| csi.livenessProbe.initialDelaySeconds | int | `5` | | +| csi.livenessProbe.periodSeconds | int | `5` | | +| csi.livenessProbe.successThreshold | int | `1` | | +| csi.livenessProbe.timeoutSeconds | int | `3` | | +| csi.pod.affinity | object | `{}` | | +| csi.pod.annotations | object | `{}` | | +| csi.pod.extraLabels | object | `{}` | | +| csi.pod.nodeSelector | object | `{}` | | +| csi.pod.tolerations | list | `[]` | | +| csi.priorityClassName | string | `""` | | +| csi.readinessProbe.failureThreshold | int | `2` | | +| csi.readinessProbe.initialDelaySeconds | int | `5` | | +| csi.readinessProbe.periodSeconds | int | `5` | | +| csi.readinessProbe.successThreshold | int | `1` | | +| csi.readinessProbe.timeoutSeconds | int | `3` | | +| csi.resources | object | `{}` | | +| csi.serviceAccount.annotations | object | `{}` | | +| csi.serviceAccount.extraLabels | object | `{}` | | +| csi.volumeMounts | list | `[]` | volumeMounts is a list of volumeMounts for the main server container. These are rendered via toYaml rather than pre-processed like the extraVolumes value. The purpose is to make it easy to share volumes between containers. | +| csi.volumes | list | `[]` | volumes is a list of volumes made available to all containers. These are rendered via toYaml rather than pre-processed like the extraVolumes value. The purpose is to make it easy to share volumes between containers. | +| extraObjects | list | `[]` | | +| global.enabled | bool | `true` | enabled is the master enabled switch. Setting this to true or false will enable or disable all the components within this chart by default. | +| global.externalBaoAddr | string | `""` | External openbao server address for the injector and CSI provider to use. Setting this will disable deployment of a openbao server. | +| global.externalVaultAddr | string | `""` | Deprecated: Please use global.externalBaoAddr instead. | +| global.imagePullSecrets | list | `[]` | Image pull secret to use for registry authentication. Alternatively, the value may be specified as an array of strings. | +| global.namespace | string | `""` | The namespace to deploy to. Defaults to the `helm` installation namespace. | +| global.openshift | bool | `false` | If deploying to OpenShift | +| global.psp | object | `{"annotations":"seccomp.security.alpha.kubernetes.io/allowedProfileNames: docker/default,runtime/default\napparmor.security.beta.kubernetes.io/allowedProfileNames: runtime/default\nseccomp.security.alpha.kubernetes.io/defaultProfileName: runtime/default\napparmor.security.beta.kubernetes.io/defaultProfileName: runtime/default\n","enable":false}` | Create PodSecurityPolicy for pods | +| global.psp.annotations | string | `"seccomp.security.alpha.kubernetes.io/allowedProfileNames: docker/default,runtime/default\napparmor.security.beta.kubernetes.io/allowedProfileNames: runtime/default\nseccomp.security.alpha.kubernetes.io/defaultProfileName: runtime/default\napparmor.security.beta.kubernetes.io/defaultProfileName: runtime/default\n"` | Annotation for PodSecurityPolicy. This is a multi-line templated string map, and can also be set as YAML. | +| global.serverTelemetry.prometheusOperator | bool | `false` | Enable integration with the Prometheus Operator See the top level serverTelemetry section below before enabling this feature. | +| global.tlsDisable | bool | `true` | TLS for end-to-end encrypted transport | +| injector.affinity | string | `"podAntiAffinity:\n requiredDuringSchedulingIgnoredDuringExecution:\n - labelSelector:\n matchLabels:\n app.kubernetes.io/name: {{ template \"openbao.name\" . }}-agent-injector\n app.kubernetes.io/instance: \"{{ .Release.Name }}\"\n component: webhook\n topologyKey: kubernetes.io/hostname\n"` | | +| injector.agentDefaults.cpuLimit | string | `"500m"` | | +| injector.agentDefaults.cpuRequest | string | `"250m"` | | +| injector.agentDefaults.memLimit | string | `"128Mi"` | | +| injector.agentDefaults.memRequest | string | `"64Mi"` | | +| injector.agentDefaults.template | string | `"map"` | | +| injector.agentDefaults.templateConfig.exitOnRetryFailure | bool | `true` | | +| injector.agentDefaults.templateConfig.staticSecretRenderInterval | string | `""` | | +| injector.agentImage | object | `{"pullPolicy":"IfNotPresent","registry":"quay.io","repository":"openbao/openbao","tag":""}` | agentImage sets the repo and tag of the OpenBao image to use for the OpenBao Agent containers. This should be set to the official OpenBao image. OpenBao 1.3.1+ is required. | +| injector.agentImage.pullPolicy | string | `"IfNotPresent"` | image pull policy to use for agent image. if tag is "latest", set to "Always" | +| injector.agentImage.registry | string | `"quay.io"` | image registry to use for agent image | +| injector.agentImage.repository | string | `"openbao/openbao"` | image repo to use for agent image | +| injector.agentImage.tag | string | `""` | image tag to use for agent image - defaults to chart appVersion | +| injector.annotations | object | `{}` | | +| injector.authPath | string | `"auth/kubernetes"` | | +| injector.certs.caBundle | string | `""` | | +| injector.certs.certName | string | `"tls.crt"` | | +| injector.certs.keyName | string | `"tls.key"` | | +| injector.certs.secretName | string | `nil` | | +| injector.enabled | string | `"-"` | True if you want to enable openbao agent injection. @default: global.enabled | +| injector.externalVaultAddr | string | `""` | Deprecated: Please use global.externalBaoAddr instead. | +| injector.extraEnvironmentVars | object | `{}` | | +| injector.extraLabels | object | `{}` | | +| injector.failurePolicy | string | `"Ignore"` | | +| injector.hostNetwork | bool | `false` | | +| injector.image.pullPolicy | string | `"IfNotPresent"` | image pull policy to use for k8s image. if tag is "latest", set to "Always" | +| injector.image.registry | string | `"docker.io"` | image registry to use for k8s image | +| injector.image.repository | string | `"hashicorp/vault-k8s"` | image repo to use for k8s image | +| injector.image.tag | string | `"1.7.2"` | image tag to use for k8s image | +| injector.leaderElector | object | `{"enabled":true}` | If multiple replicas are specified, by default a leader will be determined so that only one injector attempts to create TLS certificates. | +| injector.livenessProbe.failureThreshold | int | `2` | When a probe fails, Kubernetes will try failureThreshold times before giving up | +| injector.livenessProbe.initialDelaySeconds | int | `5` | Number of seconds after the container has started before probe initiates | +| injector.livenessProbe.periodSeconds | int | `2` | How often (in seconds) to perform the probe | +| injector.livenessProbe.successThreshold | int | `1` | Minimum consecutive successes for the probe to be considered successful after having failed | +| injector.livenessProbe.timeoutSeconds | int | `5` | Number of seconds after which the probe times out. | +| injector.logFormat | string | `"standard"` | Configures the log format of the injector. Supported log formats: "standard", "json". | +| injector.logLevel | string | `"info"` | Configures the log verbosity of the injector. Supported log levels include: trace, debug, info, warn, error | +| injector.metrics | object | `{"enabled":false}` | If true, will enable a node exporter metrics endpoint at /metrics. | +| injector.namespaceSelector | object | `{}` | | +| injector.nodeSelector | object | `{}` | | +| injector.objectSelector | object | `{}` | | +| injector.podDisruptionBudget | object | `{}` | | +| injector.port | int | `8080` | Configures the port the injector should listen on | +| injector.priorityClassName | string | `""` | | +| injector.readinessProbe.failureThreshold | int | `2` | When a probe fails, Kubernetes will try failureThreshold times before giving up | +| injector.readinessProbe.initialDelaySeconds | int | `5` | Number of seconds after the container has started before probe initiates | +| injector.readinessProbe.periodSeconds | int | `2` | How often (in seconds) to perform the probe | +| injector.readinessProbe.successThreshold | int | `1` | Minimum consecutive successes for the probe to be considered successful after having failed | +| injector.readinessProbe.timeoutSeconds | int | `5` | Number of seconds after which the probe times out. | +| injector.replicas | int | `1` | | +| injector.resources | object | `{}` | | +| injector.revokeOnShutdown | bool | `false` | | +| injector.securityContext.container | object | `{}` | | +| injector.securityContext.pod | object | `{}` | | +| injector.service.annotations | object | `{}` | | +| injector.service.extraLabels | object | `{}` | | +| injector.serviceAccount.annotations | object | `{}` | | +| injector.startupProbe.failureThreshold | int | `12` | When a probe fails, Kubernetes will try failureThreshold times before giving up | +| injector.startupProbe.initialDelaySeconds | int | `5` | Number of seconds after the container has started before probe initiates | +| injector.startupProbe.periodSeconds | int | `5` | How often (in seconds) to perform the probe | +| injector.startupProbe.successThreshold | int | `1` | Minimum consecutive successes for the probe to be considered successful after having failed | +| injector.startupProbe.timeoutSeconds | int | `5` | Number of seconds after which the probe times out. | +| injector.strategy | object | `{}` | | +| injector.tolerations | list | `[]` | | +| injector.topologySpreadConstraints | list | `[]` | | +| injector.webhook.annotations | object | `{}` | | +| injector.webhook.failurePolicy | string | `"Ignore"` | | +| injector.webhook.matchPolicy | string | `"Exact"` | | +| injector.webhook.namespaceSelector | object | `{}` | | +| injector.webhook.objectSelector | string | `"matchExpressions:\n- key: app.kubernetes.io/name\n operator: NotIn\n values:\n - {{ template \"openbao.name\" . }}-agent-injector\n"` | | +| injector.webhook.timeoutSeconds | int | `30` | | +| injector.webhookAnnotations | object | `{}` | | +| server.affinity | string | `"podAntiAffinity:\n requiredDuringSchedulingIgnoredDuringExecution:\n - labelSelector:\n matchLabels:\n app.kubernetes.io/name: {{ template \"openbao.name\" . }}\n app.kubernetes.io/instance: \"{{ .Release.Name }}\"\n component: server\n topologyKey: kubernetes.io/hostname\n"` | | +| server.annotations | object | `{}` | | +| server.auditStorage.accessMode | string | `"ReadWriteOnce"` | | +| server.auditStorage.annotations | object | `{}` | | +| server.auditStorage.enabled | bool | `false` | | +| server.auditStorage.labels | object | `{}` | | +| server.auditStorage.mountPath | string | `"/openbao/audit"` | | +| server.auditStorage.size | string | `"10Gi"` | | +| server.auditStorage.storageClass | string | `nil` | | +| server.authDelegator.enabled | bool | `true` | | +| server.configAnnotation | bool | `false` | | +| server.dataStorage.accessMode | string | `"ReadWriteOnce"` | | +| server.dataStorage.annotations | object | `{}` | | +| server.dataStorage.enabled | bool | `true` | | +| server.dataStorage.labels | object | `{}` | | +| server.dataStorage.mountPath | string | `"/openbao/data"` | | +| server.dataStorage.size | string | `"10Gi"` | | +| server.dataStorage.storageClass | string | `nil` | | +| server.dev.devRootToken | string | `"root"` | | +| server.dev.enabled | bool | `false` | | +| server.enabled | string | `"-"` | | +| server.extraArgs | string | `""` | extraArgs is a string containing additional OpenBao server arguments. | +| server.extraContainers | string | `nil` | | +| server.extraEnvironmentVars | object | `{}` | | +| server.extraInitContainers | list | `[]` | extraInitContainers is a list of init containers. Specified as a YAML list. This is useful if you need to run a script to provision TLS certificates or write out configuration files in a dynamic way. | +| server.extraLabels | object | `{}` | | +| server.extraPorts | list | `[]` | extraPorts is a list of extra ports. Specified as a YAML list. This is useful if you need to add additional ports to the statefulset in dynamic way. | +| server.extraSecretEnvironmentVars | list | `[]` | | +| server.extraVolumes | list | `[]` | | +| server.gateway.httpRoute.activeService | bool | `true` | | +| server.gateway.httpRoute.annotations | object | `{}` | | +| server.gateway.httpRoute.apiVersion | string | `"gateway.networking.k8s.io/v1"` | | +| server.gateway.httpRoute.enabled | bool | `false` | | +| server.gateway.httpRoute.filters | list | `[]` | | +| server.gateway.httpRoute.hosts[0] | string | `"chart-example.local"` | | +| server.gateway.httpRoute.labels | object | `{}` | | +| server.gateway.httpRoute.matches.path.type | string | `"PathPrefix"` | | +| server.gateway.httpRoute.matches.path.value | string | `"/"` | | +| server.gateway.httpRoute.matches.timeouts | object | `{}` | | +| server.gateway.httpRoute.parentRefs | list | `[]` | | +| server.gateway.tlsPolicy.activeService | bool | `true` | | +| server.gateway.tlsPolicy.annotations | object | `{}` | | +| server.gateway.tlsPolicy.apiVersion | string | `"gateway.networking.k8s.io/v1"` | | +| server.gateway.tlsPolicy.enabled | bool | `false` | | +| server.gateway.tlsPolicy.labels | object | `{}` | | +| server.gateway.tlsPolicy.targetRefs | list | `[]` | | +| server.gateway.tlsPolicy.validation | object | `{}` | | +| server.gateway.tlsRoute.activeService | bool | `true` | | +| server.gateway.tlsRoute.annotations | object | `{}` | | +| server.gateway.tlsRoute.apiVersion | string | `"gateway.networking.k8s.io/v1alpha3"` | | +| server.gateway.tlsRoute.enabled | bool | `false` | | +| server.gateway.tlsRoute.hosts | list | `[]` | | +| server.gateway.tlsRoute.labels | object | `{}` | | +| server.gateway.tlsRoute.parentRefs | list | `[]` | | +| server.ha.apiAddr | string | `nil` | | +| server.ha.clusterAddr | string | `nil` | | +| server.ha.config | string | `"ui = true\n\nlistener \"tcp\" {\n tls_disable = 1\n address = \"[::]:8200\"\n cluster_address = \"[::]:8201\"\n}\nstorage \"consul\" {\n path = \"openbao\"\n address = \"HOST_IP:8500\"\n}\n\nservice_registration \"kubernetes\" {}\n\n# Example configuration for using auto-unseal, using Google Cloud KMS. The\n# GKMS keys must already exist, and the cluster must have a service account\n# that is authorized to access GCP KMS.\n#seal \"gcpckms\" {\n# project = \"openbao-helm-dev-246514\"\n# region = \"global\"\n# key_ring = \"openbao-helm-unseal-kr\"\n# crypto_key = \"openbao-helm-unseal-key\"\n#}\n\n# Example configuration for enabling Prometheus metrics.\n# If you are using Prometheus Operator you can enable a ServiceMonitor resource below.\n# You may wish to enable unauthenticated metrics in the listener block above.\n#telemetry {\n# prometheus_retention_time = \"30s\"\n# disable_hostname = true\n#}\n"` | | +| server.ha.disruptionBudget.enabled | bool | `true` | | +| server.ha.disruptionBudget.maxUnavailable | string | `nil` | | +| server.ha.enabled | bool | `false` | | +| server.ha.raft.config | string | `"ui = true\n\nlistener \"tcp\" {\n tls_disable = 1\n address = \"[::]:8200\"\n cluster_address = \"[::]:8201\"\n # Enable unauthenticated metrics access (necessary for Prometheus Operator)\n #telemetry {\n # unauthenticated_metrics_access = \"true\"\n #}\n}\n\nstorage \"raft\" {\n path = \"/openbao/data\"\n}\n\nservice_registration \"kubernetes\" {}\n"` | | +| server.ha.raft.enabled | bool | `false` | | +| server.ha.raft.setNodeId | bool | `false` | | +| server.ha.replicas | int | `3` | | +| server.hostAliases | list | `[]` | | +| server.hostNetwork | bool | `false` | | +| server.image.pullPolicy | string | `"IfNotPresent"` | image pull policy to use for server image. if tag is "latest", set to "Always" | +| server.image.registry | string | `"quay.io"` | image registry to use for server image | +| server.image.repository | string | `"openbao/openbao"` | image repo to use for server image | +| server.image.tag | string | `""` | image tag to use for server image - defaults to chart appVersion | +| server.ingress.activeService | bool | `true` | | +| server.ingress.annotations | object | `{}` | | +| server.ingress.enabled | bool | `false` | | +| server.ingress.extraPaths | list | `[]` | | +| server.ingress.hosts[0].host | string | `"chart-example.local"` | | +| server.ingress.hosts[0].paths | list | `[]` | | +| server.ingress.ingressClassName | string | `""` | | +| server.ingress.labels | object | `{}` | | +| server.ingress.pathType | string | `"Prefix"` | | +| server.ingress.tls | list | `[]` | | +| server.livenessProbe.enabled | bool | `false` | | +| server.livenessProbe.execCommand | list | `[]` | | +| server.livenessProbe.failureThreshold | int | `2` | | +| server.livenessProbe.initialDelaySeconds | int | `60` | | +| server.livenessProbe.path | string | `"/v1/sys/health?standbyok=true"` | | +| server.livenessProbe.periodSeconds | int | `5` | | +| server.livenessProbe.port | int | `8200` | | +| server.livenessProbe.successThreshold | int | `1` | | +| server.livenessProbe.timeoutSeconds | int | `3` | | +| server.logFormat | string | `""` | | +| server.logLevel | string | `""` | | +| server.networkPolicy.egress | list | `[]` | | +| server.networkPolicy.enabled | bool | `false` | | +| server.networkPolicy.ingress[0].from[0].namespaceSelector | object | `{}` | | +| server.networkPolicy.ingress[0].ports[0].port | int | `8200` | | +| server.networkPolicy.ingress[0].ports[0].protocol | string | `"TCP"` | | +| server.networkPolicy.ingress[0].ports[1].port | int | `8201` | | +| server.networkPolicy.ingress[0].ports[1].protocol | string | `"TCP"` | | +| server.nodeSelector | object | `{}` | | +| server.persistentVolumeClaimRetentionPolicy | object | `{}` | | +| server.podManagementPolicy | string | `"OrderedReady"` | | +| server.postStart | list | `[]` | | +| server.preStopSleepSeconds | int | `5` | | +| server.priorityClassName | string | `""` | | +| server.readinessProbe.enabled | bool | `true` | | +| server.readinessProbe.failureThreshold | int | `2` | | +| server.readinessProbe.initialDelaySeconds | int | `5` | | +| server.readinessProbe.periodSeconds | int | `5` | | +| server.readinessProbe.port | int | `8200` | | +| server.readinessProbe.successThreshold | int | `1` | | +| server.readinessProbe.timeoutSeconds | int | `3` | | +| server.resources | object | `{}` | | +| server.route.activeService | bool | `true` | | +| server.route.annotations | object | `{}` | | +| server.route.enabled | bool | `false` | | +| server.route.host | string | `"chart-example.local"` | | +| server.route.labels | object | `{}` | | +| server.route.tls.termination | string | `"passthrough"` | | +| server.service.active.annotations | object | `{}` | | +| server.service.active.enabled | bool | `true` | | +| server.service.active.extraLabels | object | `{}` | | +| server.service.annotations | object | `{}` | | +| server.service.enabled | bool | `true` | | +| server.service.externalTrafficPolicy | string | `"Cluster"` | | +| server.service.extraLabels | object | `{}` | | +| server.service.extraPorts | list | `[]` | extraPorts is a list of extra ports. Specified as a YAML list. This is useful if you need to add additional ports to the server service in dynamic way. | +| server.service.instanceSelector.enabled | bool | `true` | | +| server.service.ipFamilies | list | `[]` | | +| server.service.ipFamilyPolicy | string | `""` | | +| server.service.port | int | `8200` | | +| server.service.publishNotReadyAddresses | bool | `true` | | +| server.service.standby.annotations | object | `{}` | | +| server.service.standby.enabled | bool | `true` | | +| server.service.standby.extraLabels | object | `{}` | | +| server.service.targetPort | int | `8200` | | +| server.serviceAccount.annotations | object | `{}` | | +| server.serviceAccount.create | bool | `true` | | +| server.serviceAccount.createSecret | bool | `false` | | +| server.serviceAccount.extraLabels | object | `{}` | | +| server.serviceAccount.name | string | `""` | | +| server.serviceAccount.serviceDiscovery.enabled | bool | `true` | | +| server.shareProcessNamespace | bool | `false` | shareProcessNamespace enables process namespace sharing between OpenBao and the extraContainers This is useful if OpenBao must be signaled, e.g. to send a SIGHUP for a log rotation | +| server.standalone.config | string | `"ui = true\n\nlistener \"tcp\" {\n tls_disable = 1\n address = \"[::]:8200\"\n cluster_address = \"[::]:8201\"\n # Enable unauthenticated metrics access (necessary for Prometheus Operator)\n #telemetry {\n # unauthenticated_metrics_access = \"true\"\n #}\n}\nstorage \"file\" {\n path = \"/openbao/data\"\n}\n\n# Example configuration for using auto-unseal, using Google Cloud KMS. The\n# GKMS keys must already exist, and the cluster must have a service account\n# that is authorized to access GCP KMS.\n#seal \"gcpckms\" {\n# project = \"openbao-helm-dev\"\n# region = \"global\"\n# key_ring = \"openbao-helm-unseal-kr\"\n# crypto_key = \"openbao-helm-unseal-key\"\n#}\n\n# Example configuration for enabling Prometheus metrics in your config.\n#telemetry {\n# prometheus_retention_time = \"30s\"\n# disable_hostname = true\n#}\n"` | | +| server.standalone.enabled | string | `"-"` | | +| server.statefulSet.annotations | object | `{}` | | +| server.statefulSet.securityContext.container | object | `{}` | | +| server.statefulSet.securityContext.pod | object | `{}` | | +| server.terminationGracePeriodSeconds | int | `10` | | +| server.tolerations | list | `[]` | | +| server.topologySpreadConstraints | list | `[]` | | +| server.updateStrategyType | string | `"OnDelete"` | | +| server.volumeMounts | string | `nil` | | +| server.volumes | string | `nil` | | +| serverTelemetry.grafanaDashboard.defaultLabel | bool | `true` | | +| serverTelemetry.grafanaDashboard.enabled | bool | `false` | | +| serverTelemetry.grafanaDashboard.extraAnnotations | object | `{}` | | +| serverTelemetry.grafanaDashboard.extraLabel | object | `{}` | | +| serverTelemetry.prometheusRules.enabled | bool | `false` | | +| serverTelemetry.prometheusRules.rules | list | `[]` | | +| serverTelemetry.prometheusRules.selectors | object | `{}` | | +| serverTelemetry.serviceMonitor.authorization | object | `{}` | | +| serverTelemetry.serviceMonitor.enabled | bool | `false` | | +| serverTelemetry.serviceMonitor.interval | string | `"30s"` | | +| serverTelemetry.serviceMonitor.port | string | `""` | Port which Prometheus uses when scraping metrics. If empty will use `openbao.scheme` helper for its value | +| serverTelemetry.serviceMonitor.scheme | string | `""` | scheme to use when Prometheus scrapes metrics. If empty will use `openbao.scheme` helper for its value | +| serverTelemetry.serviceMonitor.scrapeClass | string | `""` | | +| serverTelemetry.serviceMonitor.scrapeTimeout | string | `"10s"` | | +| serverTelemetry.serviceMonitor.selectors | object | `{}` | | +| serverTelemetry.serviceMonitor.tlsConfig | object | `{}` | | +| snapshotAgent.annotations | object | `{}` | | +| snapshotAgent.config.baoAuthPath | string | `"kubernetes"` | | +| snapshotAgent.config.baoRole | string | `"snapshot"` | | +| snapshotAgent.config.s3Bucket | string | `"openbao-snapshots"` | | +| snapshotAgent.config.s3ExpireDays | string | `"14"` | | +| snapshotAgent.config.s3Host | string | `"s3.eu-east-1.amazonaws.com"` | | +| snapshotAgent.config.s3Uri | string | `"s3://openbao-snapshots"` | | +| snapshotAgent.config.s3cmdExtraFlag | string | `"-v"` | | +| snapshotAgent.enabled | bool | `false` | | +| snapshotAgent.extraEnvironmentVars | object | `{}` | Map of extra environment variables to set in the snapshot-agent cronjob | +| snapshotAgent.extraSecretEnvironmentVars | list | `[]` | List of extra environment variables to set in the snapshot-agent cronjob These variables take value from existing Secret objects. | +| snapshotAgent.extraVolumeMounts | list | `[]` | List of additional volumeMounts for the snapshot cronjob container. | +| snapshotAgent.extraVolumes | list | `[]` | List of extraVolumes made available to the snapshot cronjob container. | +| snapshotAgent.image.repository | string | `"ghcr.io/openbao/openbao-snapshot-agent"` | | +| snapshotAgent.image.tag | string | `"0.2.4"` | | +| snapshotAgent.resources | object | `{}` | | +| snapshotAgent.restartPolicy | string | `"OnFailure"` | | +| snapshotAgent.s3CredentialsSecret | string | `"my-s3-credentials"` | | +| snapshotAgent.schedule | string | `"*/15 * * * *"` | | +| snapshotAgent.securityContext.container | object | `{}` | | +| snapshotAgent.securityContext.pod | object | `{}` | | +| snapshotAgent.serviceAccount.annotations | object | `{}` | | +| snapshotAgent.serviceAccount.create | bool | `true` | | +| snapshotAgent.serviceAccount.extraLabels | object | `{}` | | +| snapshotAgent.serviceAccount.name | string | `""` | | +| ui.activeOpenbaoPodOnly | bool | `false` | | +| ui.annotations | object | `{}` | | +| ui.enabled | bool | `false` | | +| ui.externalPort | int | `8200` | | +| ui.externalTrafficPolicy | string | `"Cluster"` | | +| ui.extraLabels | object | `{}` | | +| ui.publishNotReadyAddresses | bool | `true` | | +| ui.serviceIPFamilies | list | `[]` | | +| ui.serviceIPFamilyPolicy | string | `""` | | +| ui.serviceNodePort | string | `nil` | | +| ui.serviceType | string | `"ClusterIP"` | | +| ui.targetPort | int | `8200` | | + +---------------------------------------------- +Autogenerated from chart metadata using [helm-docs v1.14.2](https://github.com/norwoodj/helm-docs/releases/v1.14.2) diff --git a/packages/system/openbao/charts/openbao/grafana/dashboards/dashboard.json b/packages/system/openbao/charts/openbao/grafana/dashboards/dashboard.json new file mode 100644 index 00000000..b4eb93b9 --- /dev/null +++ b/packages/system/openbao/charts/openbao/grafana/dashboards/dashboard.json @@ -0,0 +1,2559 @@ +{ + "__inputs": [ + { + "name": "DS_PROMXY", + "label": "Prometheus", + "description": "", + "type": "datasource", + "pluginId": "prometheus", + "pluginName": "Prometheus" + } + ], + "__requires": [ + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "7.0.3" + }, + { + "type": "panel", + "id": "grafana-piechart-panel", + "name": "Pie Chart", + "version": "1.5.0" + }, + { + "type": "panel", + "id": "graph", + "name": "Graph", + "version": "" + }, + { + "type": "datasource", + "id": "prometheus", + "name": "Prometheus", + "version": "1.0.0" + }, + { + "type": "panel", + "id": "stat", + "name": "Stat", + "version": "" + }, + { + "type": "panel", + "id": "table", + "name": "Table", + "version": "" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "${DS_PROMXY}", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "description": " OpenBao Metrics", + "editable": true, + "graphTooltip": 1, + "id": null, + "iteration": 1602255075192, + "links": [], + "panels": [ + { + "datasource": "${DS_PROMXY}", + "fieldConfig": { + "defaults": { + "custom": { + "align": null + }, + "mappings": [ + { + "from": "", + "id": 0, + "operator": "", + "text": "Standby", + "to": "", + "type": 1, + "value": "0" + }, + { + "from": "", + "id": 1, + "operator": "", + "text": "Active", + "to": "", + "type": 1, + "value": "1" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "green", + "value": 1 + } + ] + }, + "unit": "Misc" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 9, + "x": 0, + "y": 0 + }, + "id": 39, + "maxDataPoints": 100, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["last"], + "fields": "", + "values": false + } + }, + "pluginVersion": "7.0.3", + "targets": [ + { + "expr": "up{job=\"openbao-internal\"}", + "format": "time_series", + "interval": "", + "legendFormat": "{{ instance }}", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Healthy Status", + "type": "stat" + }, + { + "datasource": "${DS_PROMXY}", + "fieldConfig": { + "defaults": { + "custom": { + "align": null, + "displayMode": "auto" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Mount Path" + }, + "properties": [ + { + "id": "custom.width", + "value": 166 + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 5, + "x": 9, + "y": 0 + }, + "id": 59, + "maxDataPoints": 100, + "options": { + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "Number of Entries" + } + ] + }, + "pluginVersion": "7.0.3", + "targets": [ + { + "expr": "sum by (exported_namespace,mount_point) (${metrics_prefix}_secret_kv_count)", + "format": "table", + "instant": true, + "interval": "", + "legendFormat": "{{ mount_point }}", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Secrets", + "transformations": [ + { + "id": "seriesToColumns", + "options": { + "byField": "mount_point" + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true, + "__name__": true, + "cluster": true, + "env": true, + "instance": true, + "job": true, + "namespace": true, + "project": true + }, + "indexByName": {}, + "renameByName": { + "Value": "Number of Entries", + "mount_point": "Mount Path", + "exported_namespace": "Namespace" + } + } + } + ], + "type": "table" + }, + { + "datasource": "${DS_PROMXY}", + "fieldConfig": { + "defaults": { + "custom": {}, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 5, + "x": 14, + "y": 0 + }, + "id": 78, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + } + }, + "pluginVersion": "7.0.3", + "targets": [ + { + "expr": "avg(${metrics_prefix}_identity_num_entities)", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Number of Identity Entities", + "type": "stat" + }, + { + "aliasColors": {}, + "breakPoint": "50%", + "cacheTimeout": null, + "combine": { + "label": "Others", + "threshold": 0 + }, + "datasource": "${DS_PROMXY}", + "decimals": null, + "fieldConfig": { + "defaults": { + "custom": { + "align": null + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "fontSize": "80%", + "format": "short", + "gridPos": { + "h": 8, + "w": 5, + "x": 19, + "y": 0 + }, + "id": 49, + "interval": null, + "legend": { + "header": "count", + "percentage": false, + "show": true, + "sideWidth": null, + "values": true + }, + "legendType": "Right side", + "links": [], + "maxDataPoints": 1, + "nullPointMode": "connected", + "pieType": "pie", + "pluginVersion": "7.0.3", + "strokeWidth": "3", + "targets": [ + { + "expr": "avg without(instance) (${metrics_prefix}_identity_entity_alias_count)", + "format": "time_series", + "interval": "", + "legendFormat": "{{ auth_method }}", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Identity Entities Aliases by Method", + "type": "grafana-piechart-panel", + "valueName": "current" + }, + { + "datasource": "${DS_PROMXY}", + "fieldConfig": { + "defaults": { + "custom": {}, + "mappings": [ + { + "from": "", + "id": 0, + "operator": "", + "text": "UNSEALED", + "to": "", + "type": 1, + "value": "2" + }, + { + "from": "", + "id": 1, + "operator": "", + "text": "SEALED", + "to": "", + "type": 1, + "value": "1" + } + ], + "noValue": "N/A", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "yellow", + "value": 1 + }, + { + "color": "green", + "value": 2 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 9, + "x": 0, + "y": 4 + }, + "id": 47, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["last"], + "fields": "", + "values": false + } + }, + "pluginVersion": "7.0.3", + "targets": [ + { + "expr": "max(1 + ${metrics_prefix}_core_unsealed{})", + "format": "time_series", + "interval": "", + "legendFormat": "{{ instance }}", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Sealed Status", + "type": "stat" + }, + { + "datasource": "${DS_PROMXY}", + "fieldConfig": { + "defaults": { + "custom": {}, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "#EAB839", + "value": 100 + }, + { + "color": "#EF843C", + "value": 200 + }, + { + "color": "red", + "value": 400 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 5, + "x": 14, + "y": 4 + }, + "id": 95, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + } + }, + "pluginVersion": "7.0.3", + "targets": [ + { + "expr": "avg(${metrics_prefix}_expire_num_leases)", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Number of Leases", + "type": "stat" + }, + { + "collapsed": true, + "datasource": "${DS_PROMXY}", + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 8 + }, + "id": 74, + "panels": [ + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_PROMXY}", + "decimals": 0, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 9 + }, + "hiddenSeries": false, + "id": 24, + "legend": { + "alignAsTable": true, + "avg": false, + "current": false, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": false, + "rightSide": false, + "show": true, + "sort": null, + "sortDesc": null, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null as zero", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "scopedVars": { + "mountpoint": { + "selected": true, + "text": "secret", + "value": "secret" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "expr": "avg(increase(${metrics_prefix}_route_create_${mountpoint}__count[5m]))", + "format": "time_series", + "hide": false, + "interval": "5m", + "legendFormat": "Create", + "refId": "A" + }, + { + "expr": "avg(increase(${metrics_prefix}_route_delete_${mountpoint}__count[5m]))", + "hide": false, + "interval": "5m", + "legendFormat": "Delete", + "refId": "B" + }, + { + "expr": "avg(increase(${metrics_prefix}_route_read_${mountpoint}__count[5m]))", + "format": "time_series", + "hide": false, + "instant": false, + "interval": "5m", + "intervalFactor": 1, + "legendFormat": "Read", + "refId": "C" + }, + { + "expr": "avg(increase(${metrics_prefix}_route_list_${mountpoint}__count[5m]))", + "format": "time_series", + "hide": false, + "interval": "5m", + "legendFormat": "List", + "refId": "D" + }, + { + "expr": "avg(increase(${metrics_prefix}_route_rollback_${mountpoint}__count[5m]))", + "hide": true, + "interval": "5m", + "legendFormat": "Rollback", + "refId": "E" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Number of Operations in \"$mountpoint\"", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:182", + "decimals": 0, + "format": "short", + "label": "Operations in 5 minute", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:183", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_PROMXY}", + "decimals": 0, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 9 + }, + "hiddenSeries": false, + "id": 35, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": false, + "rightSide": false, + "show": true, + "sort": null, + "sortDesc": null, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null as zero", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "scopedVars": { + "mountpoint": { + "selected": true, + "text": "secret", + "value": "secret" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "avg(rate(${metrics_prefix}_route_create_${mountpoint}__sum[1m]) / rate(${metrics_prefix}_route_create_${mountpoint}__count[1m]) * 1000)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Create", + "refId": "A" + }, + { + "expr": "avg(rate(${metrics_prefix}_route_delete_${mountpoint}__sum[1m]) / rate(${metrics_prefix}_route_delete_${mountpoint}__count[1m]) * 1000)", + "interval": "", + "legendFormat": "Delete", + "refId": "B" + }, + { + "expr": "avg(rate(${metrics_prefix}_route_read_${mountpoint}__sum[1m]) / rate(${metrics_prefix}_route_read_${mountpoint}__count[1m]) * 1000)", + "hide": false, + "interval": "", + "legendFormat": "Read", + "refId": "C" + }, + { + "expr": "avg(rate(${metrics_prefix}_route_list_${mountpoint}__sum[1m]) / rate(${metrics_prefix}_route_list_${mountpoint}__count[1m]) * 1000)", + "hide": false, + "interval": "", + "legendFormat": "List", + "refId": "D" + }, + { + "expr": "avg(rate(${metrics_prefix}_route_rollback_${mountpoint}__sum[1m]) / rate(${metrics_prefix}_route_rollback_${mountpoint}__count[1m]) * 1000)", + "hide": true, + "interval": "", + "legendFormat": "Rollback", + "refId": "E" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Time of Operations in \"$mountpoint\"", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:82", + "decimals": 0, + "format": "µs", + "label": "Time of one operation", + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "$$hashKey": "object:83", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "repeat": "mountpoint", + "title": "Path Info: $mountpoint", + "type": "row" + }, + { + "collapsed": false, + "datasource": "${DS_PROMXY}", + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 9 + }, + "id": 45, + "panels": [], + "repeat": null, + "title": "CPU/Mem Info: $node", + "type": "row" + }, + { + "datasource": "${DS_PROMXY}", + "description": "", + "fieldConfig": { + "defaults": { + "custom": {}, + "mappings": [], + "noValue": "N/A", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 0.2 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 3, + "x": 0, + "y": 10 + }, + "id": 41, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["last"], + "fields": "", + "values": false + } + }, + "pluginVersion": "7.0.3", + "targets": [ + { + "expr": "avg(${metrics_prefix}_runtime_heap_objects{} / ${metrics_prefix}_runtime_malloc_count{})", + "interval": "", + "intervalFactor": 10, + "legendFormat": "", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Heap Objects Used", + "type": "stat" + }, + { + "datasource": "${DS_PROMXY}", + "fieldConfig": { + "defaults": { + "custom": {}, + "mappings": [], + "noValue": "N/A", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "#EAB839", + "value": 70 + }, + { + "color": "#EF843C", + "value": 100 + }, + { + "color": "#E24D42", + "value": 150 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 3, + "x": 3, + "y": 10 + }, + "id": 76, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["last"], + "fields": "", + "values": false + } + }, + "pluginVersion": "7.0.3", + "targets": [ + { + "expr": "avg(${metrics_prefix}_runtime_num_goroutines{})", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Number of Goroutines", + "type": "stat" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_PROMXY}", + "decimals": 3, + "fieldConfig": { + "defaults": { + "custom": {}, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 5, + "w": 18, + "x": 6, + "y": 10 + }, + "hiddenSeries": false, + "id": 43, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pluginVersion": "7.0.3", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "avg(${metrics_prefix}_runtime_alloc_bytes{})", + "interval": "", + "intervalFactor": 5, + "legendFormat": "$node", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Allocated MB", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:373", + "format": "decbytes", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:374", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "collapsed": false, + "datasource": "${DS_PROMXY}", + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 15 + }, + "id": 16, + "panels": [], + "title": "Token", + "type": "row" + }, + { + "datasource": "${DS_PROMXY}", + "fieldConfig": { + "defaults": { + "custom": {}, + "mappings": [], + "noValue": "N/A", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 0, + "y": 16 + }, + "id": 53, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + } + }, + "pluginVersion": "7.0.3", + "targets": [ + { + "expr": "avg(${metrics_prefix}_token_count)", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Available Tokens", + "type": "stat" + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_PROMXY}", + "decimals": 0, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 21, + "x": 3, + "y": 16 + }, + "hiddenSeries": false, + "id": 104, + "legend": { + "alignAsTable": true, + "avg": false, + "current": true, + "max": true, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "connected", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "avg without(instance) (${metrics_prefix}_token_count_by_policy)", + "interval": "", + "legendFormat": "{{ policy }}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Tokens by Policy", + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "series", + "name": null, + "show": true, + "values": ["current"] + }, + "yaxes": [ + { + "$$hashKey": "object:575", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:576", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "cacheTimeout": null, + "datasource": "${DS_PROMXY}", + "fieldConfig": { + "defaults": { + "custom": {}, + "mappings": [], + "noValue": "0", + "nullValueMode": "connected", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "orange", + "value": 1 + }, + { + "color": "red", + "value": 3 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 0, + "y": 20 + }, + "id": 8, + "interval": null, + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "value", + "fieldOptions": { + "calcs": ["lastNotNull"] + }, + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": ["last"], + "fields": "", + "values": false + } + }, + "pluginVersion": "7.0.3", + "targets": [ + { + "expr": "avg(${metrics_prefix}_token_create_count - ${metrics_prefix}_token_store_count)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Pending Tokens", + "type": "stat" + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_PROMXY}", + "decimals": 0, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 24 + }, + "hiddenSeries": false, + "id": 102, + "legend": { + "alignAsTable": true, + "avg": false, + "current": true, + "max": true, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "connected", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "avg without(instance) (${metrics_prefix}_token_count_by_ttl)", + "format": "time_series", + "interval": "", + "legendFormat": "{{ creation_ttl }}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Tokens by TTL", + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "series", + "name": null, + "show": true, + "values": ["current"] + }, + "yaxes": [ + { + "$$hashKey": "object:390", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:391", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_PROMXY}", + "decimals": 0, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 24 + }, + "hiddenSeries": false, + "id": 100, + "legend": { + "alignAsTable": true, + "avg": false, + "current": true, + "max": true, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "connected", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "avg without(instance) (${metrics_prefix}_token_count_by_auth)", + "interval": "", + "legendFormat": "{{ auth_method }}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Tokens by Auth Method", + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "series", + "name": null, + "show": true, + "values": ["current"] + }, + "yaxes": [ + { + "$$hashKey": "object:136", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:137", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_PROMXY}", + "decimals": 0, + "fieldConfig": { + "defaults": { + "custom": { + "align": null + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 6, + "w": 12, + "x": 0, + "y": 32 + }, + "hiddenSeries": false, + "id": 65, + "legend": { + "alignAsTable": false, + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 1, + "maxDataPoints": 100, + "nullPointMode": "null as zero", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pluginVersion": "7.0.3", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "expr": "avg by(auth_method, creation_ttl) (${metrics_prefix}_token_creation)", + "format": "time_series", + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ auth_method }} - {{ creation_ttl }}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Tokens Creation by Method & TTL", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:1956", + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:1957", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": { + "Create": "rgb(84, 183, 90)", + "Store": "#0a437c" + }, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_PROMXY}", + "decimals": 0, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 6, + "w": 12, + "x": 12, + "y": 32 + }, + "hiddenSeries": false, + "id": 6, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "expr": "avg without(instance) (${metrics_prefix}_token_create_count)", + "format": "time_series", + "instant": false, + "interval": "", + "intervalFactor": 10, + "legendFormat": "Create", + "refId": "A" + }, + { + "expr": "avg without(instance) (${metrics_prefix}_token_store_count)", + "format": "time_series", + "hide": false, + "instant": false, + "interval": "", + "intervalFactor": 10, + "legendFormat": "Store", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Token Creation/Storage", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:877", + "decimals": 0, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:878", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": { + "Lookup": "#0a50a1" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_PROMXY}", + "decimals": 3, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 6, + "w": 24, + "x": 0, + "y": 38 + }, + "hiddenSeries": false, + "id": 14, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "avg(irate(${metrics_prefix}_token_lookup_count[1m]))", + "hide": false, + "interval": "", + "legendFormat": "Lookups", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Token Lookups", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:330", + "decimals": 0, + "format": "short", + "label": "Lookups per second", + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "$$hashKey": "object:331", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "collapsed": false, + "datasource": "${DS_PROMXY}", + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 44 + }, + "id": 20, + "panels": [], + "title": "Audit", + "type": "row" + }, + { + "datasource": "${DS_PROMXY}", + "description": "", + "fieldConfig": { + "defaults": { + "custom": {}, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 1 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 3, + "x": 0, + "y": 45 + }, + "id": 97, + "maxDataPoints": 100, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["max"], + "fields": "", + "values": false + } + }, + "pluginVersion": "7.0.3", + "targets": [ + { + "expr": "max(idelta(${metrics_prefix}_audit_log_request_failure[1m]))", + "format": "time_series", + "interval": "", + "legendFormat": "Request", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Log Request Failures", + "type": "stat" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_PROMXY}", + "decimals": 3, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 10, + "w": 11, + "x": 3, + "y": 45 + }, + "hiddenSeries": false, + "id": 4, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "avg(irate(${metrics_prefix}_audit_log_request_count[1m]))", + "format": "time_series", + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Request ", + "refId": "A" + }, + { + "expr": "avg(irate(${metrics_prefix}_audit_log_response_count[1m]))", + "format": "time_series", + "hide": false, + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Response", + "refId": "B" + }, + { + "expr": "avg(irate(${metrics_prefix}_core_handle_request_count[1m]))", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Handled", + "refId": "C" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Log Requests", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:109", + "decimals": 0, + "format": "short", + "label": "Requests per second", + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "$$hashKey": "object:110", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_PROMXY}", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 10, + "w": 10, + "x": 14, + "y": 45 + }, + "hiddenSeries": false, + "id": 61, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null as zero", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "avg(irate(${metrics_prefix}_consul_get_count[1m]))", + "interval": "", + "intervalFactor": 1, + "legendFormat": "GET", + "refId": "A" + }, + { + "expr": "avg(irate(${metrics_prefix}_consul_put_count[1m]))", + "interval": "", + "legendFormat": "PUT", + "refId": "B" + }, + { + "expr": "avg(irate(${metrics_prefix}_consul_delete_count[1m]))", + "interval": "", + "legendFormat": "DELETE", + "refId": "C" + }, + { + "expr": "irate(${metrics_prefix}_consul_list_count{instance=\"$node:$port\"}[1m])", + "interval": "", + "legendFormat": "LIST", + "refId": "D" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Consul Requests", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:949", + "format": "short", + "label": "Requests per second", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:950", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "datasource": "${DS_PROMXY}", + "description": "", + "fieldConfig": { + "defaults": { + "custom": {}, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 1 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 3, + "x": 0, + "y": 50 + }, + "id": 98, + "maxDataPoints": 100, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["max"], + "fields": "", + "values": false + } + }, + "pluginVersion": "7.0.3", + "targets": [ + { + "expr": "max(idelta(${metrics_prefix}_audit_log_response_failure[1m]))", + "format": "time_series", + "interval": "", + "legendFormat": "Request", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Log Response Failures", + "type": "stat" + }, + { + "collapsed": false, + "datasource": "${DS_PROMXY}", + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 55 + }, + "id": 18, + "panels": [], + "title": "Policy", + "type": "row" + }, + { + "aliasColors": { + "set": "#629e51" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_PROMXY}", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 6, + "w": 12, + "x": 0, + "y": 56 + }, + "hiddenSeries": false, + "id": 10, + "legend": { + "alignAsTable": false, + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "avg(irate(${metrics_prefix}_policy_set_policy_count[1m]))", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "SET", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Policy Set", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:1834", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "$$hashKey": "object:1835", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": { + "GET": "#1f78c1" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_PROMXY}", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 6, + "w": 12, + "x": 12, + "y": 56 + }, + "hiddenSeries": false, + "id": 12, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "avg(irate(${metrics_prefix}_policy_get_policy_count[1m]))", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "GET", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Policy Get", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:2132", + "decimals": 0, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "$$hashKey": "object:2133", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "refresh": false, + "schemaVersion": 25, + "style": "dark", + "tags": ["openbao"], + "templating": { + "list": [ + { + "current": {}, + "hide": 0, + "includeAll": false, + "multi": false, + "name": "DS_PROMXY", + "label": "datasource", + "options": [], + "query": "prometheus", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + }, + { + "allValue": null, + "current": {}, + "datasource": "${DS_PROMXY}", + "definition": "label_values(up{job=\"openbao-internal\"}, instance)", + "hide": 2, + "includeAll": false, + "label": "Host:", + "multi": false, + "name": "node", + "options": [], + "query": "label_values(up{job=\"openbao-internal\"}, instance)", + "refresh": 1, + "regex": "/([^:]+):.*/", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": null, + "current": {}, + "datasource": "${DS_PROMXY}", + "definition": "label_values(up{job=\"openbao-internal\",instance=~\"$node:(.*)\"}, instance)", + "hide": 2, + "includeAll": false, + "label": null, + "multi": false, + "name": "port", + "options": [], + "query": "label_values(up{job=\"openbao-internal\",instance=~\"$node:(.*)\"}, instance)", + "refresh": 1, + "regex": "/[^:]+:(.*)/", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": "", + "current": {}, + "datasource": "${DS_PROMXY}", + "definition": "label_values(${metrics_prefix}_secret_kv_count, mount_point)", + "hide": 0, + "includeAll": true, + "label": "Mount Point:", + "multi": true, + "name": "mountpoint", + "options": [], + "query": "label_values(${metrics_prefix}_secret_kv_count, mount_point)", + "refresh": 2, + "regex": "/(.*)//", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": "", + "datasource": "${DS_PROMXY}", + "hide": 0, + "includeAll": true, + "label": "Metrics Prefix", + "current": { + "text": "vault", + "value": "vault" + }, + "description": "Metrics Prefix defined in the OpenBao configuration with `metrics_prefix`", + "name": "metrics_prefix", + "refresh": 2, + "options": [ + { + "selected": true, + "text": "vault", + "value": "vault" + } + ], + "query": "vault", + "type": "textbox" + } + ] + }, + "time": { + "from": "now-30m", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + }, + "timezone": "", + "title": "OpenBao", + "uid": "openbao", + "version": 1 +} diff --git a/packages/system/openbao/charts/openbao/templates/NOTES.txt b/packages/system/openbao/charts/openbao/templates/NOTES.txt new file mode 100644 index 00000000..fce87c26 --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/NOTES.txt @@ -0,0 +1,18 @@ + +Thank you for installing OpenBao! + +Now that you have deployed OpenBao, you should look over the docs on using +OpenBao with Kubernetes available here: + +https://openbao.org/docs/ + + +Your release is named {{ .Release.Name }}. To learn more about the release, try: + + $ helm status {{ .Release.Name }} + $ helm get manifest {{ .Release.Name }} + +{{ if and (not .Values.global.tlsDisable) .Values.server.gateway.httpRoute.enabled }} +WARNING: Terminating TLS before reaching the OpenBao Server is not recommended +and may break things like certificate authentication. Prefer usage of `TLSRoute`. +{{- end }} diff --git a/packages/system/openbao/charts/openbao/templates/_helpers.tpl b/packages/system/openbao/charts/openbao/templates/_helpers.tpl new file mode 100644 index 00000000..3597697b --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/_helpers.tpl @@ -0,0 +1,1212 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to +this (by the DNS naming spec). If release name contains chart name it will +be used as a full name. +*/}} +{{- define "openbao.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "openbao.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Expand the name of the chart. +*/}} +{{- define "openbao.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Allow the release namespace to be overridden +*/}} +{{- define "openbao.namespace" -}} +{{- default .Release.Namespace .Values.global.namespace -}} +{{- end -}} + +{{/* +Compute if the csi driver is enabled. +*/}} +{{- define "openbao.csiEnabled" -}} +{{- $_ := set . "csiEnabled" (or + (eq (.Values.csi.enabled | toString) "true") + (and (eq (.Values.csi.enabled | toString) "-") (eq (.Values.global.enabled | toString) "true"))) -}} +{{- end -}} + +{{/* +Compute if the injector is enabled. +*/}} +{{- define "openbao.injectorEnabled" -}} +{{- $_ := set . "injectorEnabled" (or + (eq (.Values.injector.enabled | toString) "true") + (and (eq (.Values.injector.enabled | toString) "-") (eq (.Values.global.enabled | toString) "true"))) -}} +{{- end -}} + +{{/* +Compute if the server is enabled. +*/}} +{{- define "openbao.serverEnabled" -}} +{{- $_ := set . "serverEnabled" (or + (eq (.Values.server.enabled | toString) "true") + (and (eq (.Values.server.enabled | toString) "-") (eq (.Values.global.enabled | toString) "true"))) -}} +{{- end -}} + +{{/* +Compute if the server serviceaccount is enabled. +*/}} +{{- define "openbao.serverServiceAccountEnabled" -}} +{{- $_ := set . "serverServiceAccountEnabled" + (and + (eq (.Values.server.serviceAccount.create | toString) "true" ) + (or + (eq (.Values.server.enabled | toString) "true") + (eq (.Values.global.enabled | toString) "true"))) -}} +{{- end -}} + +{{/* +Compute if the server serviceaccount should have a token created and mounted to the serviceaccount. +*/}} +{{- define "openbao.serverServiceAccountSecretCreationEnabled" -}} +{{- $_ := set . "serverServiceAccountSecretCreationEnabled" + (and + (eq (.Values.server.serviceAccount.create | toString) "true") + (eq (.Values.server.serviceAccount.createSecret | toString) "true")) -}} +{{- end -}} + + +{{/* +Compute if the server auth delegator serviceaccount is enabled. +*/}} +{{- define "openbao.serverAuthDelegator" -}} +{{- $_ := set . "serverAuthDelegator" + (and + (eq (.Values.server.authDelegator.enabled | toString) "true" ) + (or (eq (.Values.server.serviceAccount.create | toString) "true") + (not (eq .Values.server.serviceAccount.name ""))) + (or + (eq (.Values.server.enabled | toString) "true") + (eq (.Values.global.enabled | toString) "true"))) -}} +{{- end -}} + +{{/* +Compute if the server service is enabled. +*/}} +{{- define "openbao.serverServiceEnabled" -}} +{{- template "openbao.serverEnabled" . -}} +{{- $_ := set . "serverServiceEnabled" (and .serverEnabled (eq (.Values.server.service.enabled | toString) "true")) -}} +{{- end -}} + +{{/* +Compute if the ui is enabled. +*/}} +{{- define "openbao.uiEnabled" -}} +{{- $_ := set . "uiEnabled" (or + (eq (.Values.ui.enabled | toString) "true") + (and (eq (.Values.ui.enabled | toString) "-") (eq (.Values.global.enabled | toString) "true"))) -}} +{{- end -}} + +{{/* +Compute the maximum number of unavailable replicas for the PodDisruptionBudget. +This defaults to (n/2)-1 where n is the number of members of the server cluster. +Add a special case for replicas=1, where it should default to 0 as well. +*/}} +{{- define "openbao.pdb.maxUnavailable" -}} +{{- if eq (int .Values.server.ha.replicas) 1 -}} +{{ 0 }} +{{- else if .Values.server.ha.disruptionBudget.maxUnavailable -}} +{{ .Values.server.ha.disruptionBudget.maxUnavailable -}} +{{- else -}} +{{- div (sub (div (mul (int .Values.server.ha.replicas) 10) 2) 1) 10 -}} +{{- end -}} +{{- end -}} + +{{/* +Resolve the external OpenBao/Vault address by checking global and injector values in order of precedence: +1. global.externalBaoAddr +2. global.externalVaultAddr +*/}} + +{{- define "openbao.externalAddr" -}} + {{- if .Values.global.externalBaoAddr -}} + {{- .Values.global.externalBaoAddr -}} + {{- else -}} + {{- .Values.global.externalVaultAddr -}} + {{- end -}} +{{- end -}} + +{{/* +Set the variable 'mode' to the server mode requested by the user to simplify +template logic. +*/}} +{{- define "openbao.mode" -}} + {{- template "openbao.serverEnabled" . -}} + {{- if or (.Values.injector.externalVaultAddr) (.Values.global.externalVaultAddr) (.Values.global.externalBaoAddr) -}} + {{- $_ := set . "mode" "external" -}} + {{- else if not .serverEnabled -}} + {{- $_ := set . "mode" "external" -}} + {{- else if eq (.Values.server.dev.enabled | toString) "true" -}} + {{- $_ := set . "mode" "dev" -}} + {{- else if eq (.Values.server.ha.enabled | toString) "true" -}} + {{- $_ := set . "mode" "ha" -}} + {{- else if or (eq (.Values.server.standalone.enabled | toString) "true") (eq (.Values.server.standalone.enabled | toString) "-") -}} + {{- $_ := set . "mode" "standalone" -}} + {{- else -}} + {{- $_ := set . "mode" "" -}} + {{- end -}} +{{- end -}} + +{{/* +Set's the replica count based on the different modes configured by user +*/}} +{{- define "openbao.replicas" -}} + {{ if eq .mode "standalone" }} + {{- default 1 -}} + {{ else if eq .mode "ha" }} + {{- if or (kindIs "int64" .Values.server.ha.replicas) (kindIs "float64" .Values.server.ha.replicas) -}} + {{- .Values.server.ha.replicas -}} + {{ else }} + {{- 3 -}} + {{- end -}} + {{ else }} + {{- default 1 -}} + {{ end }} +{{- end -}} + +{{/* +Set's up configmap mounts if this isn't a dev deployment and the user +defined a custom configuration. Additionally iterates over any +extra volumes the user may have specified (such as a secret with TLS). +*/}} +{{- define "openbao.volumes" -}} + {{- if and (ne .mode "dev") (or (.Values.server.standalone.config) (.Values.server.ha.config)) }} + - name: config + configMap: + name: {{ template "openbao.fullname" . }}-config + {{ end }} + {{- range .Values.server.extraVolumes }} + - name: userconfig-{{ .name }} + {{ .type }}: + {{- if (eq .type "configMap") }} + name: {{ .name }} + {{- else if (eq .type "secret") }} + secretName: {{ .name }} + {{- end }} + defaultMode: {{ .defaultMode | default 420 }} + {{- end }} + {{- if .Values.server.volumes }} + {{- toYaml .Values.server.volumes | nindent 8}} + {{- end }} +{{- end -}} + +{{/* +Set's the args for custom command to render the OpenBao configuration +file with IP addresses to make the out of box experience easier +for users looking to use this chart with Consul Helm. +*/}} +{{- define "openbao.args" -}} + {{ if or (eq .mode "standalone") (eq .mode "ha") }} + - | + cp /openbao/config/extraconfig-from-values.hcl /tmp/storageconfig.hcl; + [ -n "${HOST_IP}" ] && sed -Ei "s|HOST_IP|${HOST_IP?}|g" /tmp/storageconfig.hcl; + [ -n "${POD_IP}" ] && sed -Ei "s|POD_IP|${POD_IP?}|g" /tmp/storageconfig.hcl; + [ -n "${HOSTNAME}" ] && sed -Ei "s|HOSTNAME|${HOSTNAME?}|g" /tmp/storageconfig.hcl; + [ -n "${API_ADDR}" ] && sed -Ei "s|API_ADDR|${API_ADDR?}|g" /tmp/storageconfig.hcl; + [ -n "${TRANSIT_ADDR}" ] && sed -Ei "s|TRANSIT_ADDR|${TRANSIT_ADDR?}|g" /tmp/storageconfig.hcl; + [ -n "${RAFT_ADDR}" ] && sed -Ei "s|RAFT_ADDR|${RAFT_ADDR?}|g" /tmp/storageconfig.hcl; + /usr/local/bin/docker-entrypoint.sh bao server -config=/tmp/storageconfig.hcl {{ .Values.server.extraArgs }} + {{ else if eq .mode "dev" }} + - | + /usr/local/bin/docker-entrypoint.sh bao server -dev {{ .Values.server.extraArgs }} + {{ end }} +{{- end -}} + +{{/* +Set's additional environment variables based on the mode. +*/}} +{{- define "openbao.envs" -}} + {{ if eq .mode "dev" }} + - name: VAULT_DEV_ROOT_TOKEN_ID + value: {{ .Values.server.dev.devRootToken }} + - name: VAULT_DEV_LISTEN_ADDRESS + value: "[::]:8200" + {{ end }} +{{- end -}} + +{{/* +Set's which additional volumes should be mounted to the container +based on the mode configured. +*/}} +{{- define "openbao.mounts" -}} + {{ if eq (.Values.server.auditStorage.enabled | toString) "true" }} + - name: audit + mountPath: {{ .Values.server.auditStorage.mountPath }} + {{ end }} + {{ if or (eq .mode "standalone") (and (eq .mode "ha") (eq (.Values.server.ha.raft.enabled | toString) "true")) }} + {{ if eq (.Values.server.dataStorage.enabled | toString) "true" }} + - name: data + mountPath: {{ .Values.server.dataStorage.mountPath }} + {{ end }} + {{ end }} + {{ if and (ne .mode "dev") (or (.Values.server.standalone.config) (.Values.server.ha.config)) }} + - name: config + mountPath: /openbao/config + {{ end }} + {{- range .Values.server.extraVolumes }} + - name: userconfig-{{ .name }} + readOnly: true + mountPath: {{ .path | default "/openbao/userconfig" }}/{{ .name }} + {{- end }} + {{- if .Values.server.volumeMounts }} + {{- toYaml .Values.server.volumeMounts | nindent 12}} + {{- end }} +{{- end -}} + +{{/* +Set's up the volumeClaimTemplates when data or audit storage is required. HA +might not use data storage since Consul is likely it's backend, however, audit +storage might be desired by the user. +*/}} +{{- define "openbao.volumeclaims" -}} + {{- if and (ne .mode "dev") (or .Values.server.dataStorage.enabled .Values.server.auditStorage.enabled) }} + volumeClaimTemplates: + {{- if and (eq (.Values.server.dataStorage.enabled | toString) "true") (or (eq .mode "standalone") (eq (.Values.server.ha.raft.enabled | toString ) "true" )) }} + - apiVersion: v1 + kind: PersistentVolumeClaim + metadata: + name: data + {{- include "openbao.dataVolumeClaim.annotations" . | nindent 6 }} + {{- include "openbao.dataVolumeClaim.labels" . | nindent 6 }} + spec: + accessModes: + - {{ .Values.server.dataStorage.accessMode | default "ReadWriteOnce" }} + resources: + requests: + storage: {{ .Values.server.dataStorage.size }} + {{- if .Values.server.dataStorage.storageClass }} + storageClassName: {{ .Values.server.dataStorage.storageClass }} + {{- end }} + {{ end }} + {{- if eq (.Values.server.auditStorage.enabled | toString) "true" }} + - apiVersion: v1 + kind: PersistentVolumeClaim + metadata: + name: audit + {{- include "openbao.auditVolumeClaim.annotations" . | nindent 6 }} + {{- include "openbao.auditVolumeClaim.labels" . | nindent 6 }} + spec: + accessModes: + - {{ .Values.server.auditStorage.accessMode | default "ReadWriteOnce" }} + resources: + requests: + storage: {{ .Values.server.auditStorage.size }} + {{- if .Values.server.auditStorage.storageClass }} + storageClassName: {{ .Values.server.auditStorage.storageClass }} + {{- end }} + {{ end }} + {{ end }} +{{- end -}} + +{{/* +Set's the affinity for pod placement when running in standalone and HA modes. +*/}} +{{- define "openbao.affinity" -}} + {{- if and (ne .mode "dev") .Values.server.affinity }} + affinity: + {{ $tp := typeOf .Values.server.affinity }} + {{- if eq $tp "string" }} + {{- tpl .Values.server.affinity . | nindent 8 | trim }} + {{- else }} + {{- toYaml .Values.server.affinity | nindent 8 }} + {{- end }} + {{ end }} +{{- end -}} + +{{/* +Sets the injector affinity for pod placement +*/}} +{{- define "injector.affinity" -}} + {{- if .Values.injector.affinity }} + affinity: + {{ $tp := typeOf .Values.injector.affinity }} + {{- if eq $tp "string" }} + {{- tpl .Values.injector.affinity . | nindent 8 | trim }} + {{- else }} + {{- toYaml .Values.injector.affinity | nindent 8 }} + {{- end }} + {{ end }} +{{- end -}} + +{{/* +Sets the topologySpreadConstraints when running in standalone and HA modes. +*/}} +{{- define "openbao.topologySpreadConstraints" -}} + {{- if and (ne .mode "dev") .Values.server.topologySpreadConstraints }} + topologySpreadConstraints: + {{ $tp := typeOf .Values.server.topologySpreadConstraints }} + {{- if eq $tp "string" }} + {{- tpl .Values.server.topologySpreadConstraints . | nindent 8 | trim }} + {{- else }} + {{- toYaml .Values.server.topologySpreadConstraints | nindent 8 }} + {{- end }} + {{ end }} +{{- end -}} + +{{/* +Sets the injector topologySpreadConstraints for pod placement +*/}} +{{- define "injector.topologySpreadConstraints" -}} + {{- if .Values.injector.topologySpreadConstraints }} + topologySpreadConstraints: + {{ $tp := typeOf .Values.injector.topologySpreadConstraints }} + {{- if eq $tp "string" }} + {{- tpl .Values.injector.topologySpreadConstraints . | nindent 8 | trim }} + {{- else }} + {{- toYaml .Values.injector.topologySpreadConstraints | nindent 8 }} + {{- end }} + {{ end }} +{{- end -}} + +{{/* +Sets the toleration for pod placement when running in standalone and HA modes. +*/}} +{{- define "openbao.tolerations" -}} + {{- if and (ne .mode "dev") .Values.server.tolerations }} + tolerations: + {{- $tp := typeOf .Values.server.tolerations }} + {{- if eq $tp "string" }} + {{ tpl .Values.server.tolerations . | nindent 8 | trim }} + {{- else }} + {{- toYaml .Values.server.tolerations | nindent 8 }} + {{- end }} + {{- end }} +{{- end -}} + +{{/* +Sets the injector toleration for pod placement +*/}} +{{- define "injector.tolerations" -}} + {{- if .Values.injector.tolerations }} + tolerations: + {{- $tp := typeOf .Values.injector.tolerations }} + {{- if eq $tp "string" }} + {{ tpl .Values.injector.tolerations . | nindent 8 | trim }} + {{- else }} + {{- toYaml .Values.injector.tolerations | nindent 8 }} + {{- end }} + {{- end }} +{{- end -}} + +{{/* +Set's the node selector for pod placement when running in standalone and HA modes. +*/}} +{{- define "openbao.nodeselector" -}} + {{- if and (ne .mode "dev") .Values.server.nodeSelector }} + nodeSelector: + {{- $tp := typeOf .Values.server.nodeSelector }} + {{- if eq $tp "string" }} + {{ tpl .Values.server.nodeSelector . | nindent 8 | trim }} + {{- else }} + {{- toYaml .Values.server.nodeSelector | nindent 8 }} + {{- end }} + {{- end }} +{{- end -}} + +{{/* +Sets the injector node selector for pod placement +*/}} +{{- define "injector.nodeselector" -}} + {{- if .Values.injector.nodeSelector }} + nodeSelector: + {{- $tp := typeOf .Values.injector.nodeSelector }} + {{- if eq $tp "string" }} + {{ tpl .Values.injector.nodeSelector . | nindent 8 | trim }} + {{- else }} + {{- toYaml .Values.injector.nodeSelector | nindent 8 }} + {{- end }} + {{- end }} +{{- end -}} + +{{/* +Sets the injector deployment update strategy +*/}} +{{- define "injector.strategy" -}} + {{- if .Values.injector.strategy }} + strategy: + {{- $tp := typeOf .Values.injector.strategy }} + {{- if eq $tp "string" }} + {{ tpl .Values.injector.strategy . | nindent 4 | trim }} + {{- else }} + {{- toYaml .Values.injector.strategy | nindent 4 }} + {{- end }} + {{- end }} +{{- end -}} + +{{/* +Renders service annotations from either a string (templated) or a map with indent 4. +Usage: {{ include "openbao.annotations.render.4" (list . ) }} +*/}} +{{- define "openbao.annotations.render.4" -}} + {{- $ctx := index . 0 -}} + {{- $annotations := index . 1 -}} + {{- $annotationsType := typeOf $annotations -}} + {{- if eq $annotationsType "string" -}} + {{- tpl $annotations $ctx | nindent 4 -}} + {{- else -}} + {{- toYaml $annotations | nindent 4 -}} + {{- end -}} +{{- end -}} + +{{/* +Renders service annotations from either a string (templated) or a map with indent 8. +Usage: {{ include "openbao.annotations.render.4" (list . ) }} +*/}} +{{- define "openbao.annotations.render.8" -}} + {{- $ctx := index . 0 -}} + {{- $annotations := index . 1 -}} + {{- $annotationsType := typeOf $annotations -}} + {{- if eq $annotationsType "string" -}} + {{- tpl $annotations $ctx | nindent 8 -}} + {{- else -}} + {{- toYaml $annotations | nindent 8 -}} + {{- end -}} +{{- end -}} + +{{/* +Sets extra pod annotations +*/}} +{{- define "openbao.annotations" }} + {{- if or .Values.server.configAnnotation .Values.server.annotations }} + annotations: + {{- if .Values.server.configAnnotation }} + openbao.hashicorp.com/config-checksum: {{ include "openbao.config" . | sha256sum }} + {{- end }} + {{- $generic := .Values.server.annotations -}} + {{- if $generic }} + {{- include "openbao.annotations.render.8" (list . $generic) -}} + {{- end }} +{{- end -}} +{{- end -}} + +{{/* +Sets extra injector pod annotations +*/}} +{{- define "injector.annotations" -}} + {{- $generic := .Values.injector.annotations -}} + {{- if $generic }} + annotations: + {{- include "openbao.annotations.render.8" (list . $generic) -}} + {{- end }} +{{- end -}} + +{{/* +Sets extra injector service annotations +*/}} +{{- define "injector.service.annotations" -}} + {{- $generic := .Values.injector.service.annotations -}} + {{- if $generic }} + annotations: + {{- include "openbao.annotations.render.4" (list . $generic) -}} + {{- end }} +{{- end -}} + +{{/* +securityContext for the injector pod level. +*/}} +{{- define "injector.securityContext.pod" -}} + {{- if .Values.injector.securityContext.pod }} + securityContext: + {{- $tp := typeOf .Values.injector.securityContext.pod }} + {{- if eq $tp "string" }} + {{- tpl .Values.injector.securityContext.pod . | nindent 8 }} + {{- else }} + {{- toYaml .Values.injector.securityContext.pod | nindent 8 }} + {{- end }} + {{- else if not .Values.global.openshift }} + securityContext: + runAsNonRoot: true + runAsGroup: {{ .Values.injector.gid | default 1000 }} + runAsUser: {{ .Values.injector.uid | default 100 }} + fsGroup: {{ .Values.injector.gid | default 1000 }} + {{- end }} +{{- end -}} + +{{/* +securityContext for the injector container level. +*/}} +{{- define "injector.securityContext.container" -}} + {{- if .Values.injector.securityContext.container}} + securityContext: + {{- $tp := typeOf .Values.injector.securityContext.container }} + {{- if eq $tp "string" }} + {{- tpl .Values.injector.securityContext.container . | nindent 12 }} + {{- else }} + {{- toYaml .Values.injector.securityContext.container | nindent 12 }} + {{- end }} + {{- else if not .Values.global.openshift }} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + {{- end }} +{{- end -}} + +{{/* +securityContext for the statefulset pod template. +*/}} +{{- define "server.statefulSet.securityContext.pod" -}} + {{- if .Values.server.statefulSet.securityContext.pod }} + securityContext: + {{- $tp := typeOf .Values.server.statefulSet.securityContext.pod }} + {{- if eq $tp "string" }} + {{- tpl .Values.server.statefulSet.securityContext.pod . | nindent 8 }} + {{- else }} + {{- toYaml .Values.server.statefulSet.securityContext.pod | nindent 8 }} + {{- end }} + {{- else if not .Values.global.openshift }} + securityContext: + runAsNonRoot: true + runAsGroup: {{ .Values.server.gid | default 1000 }} + runAsUser: {{ .Values.server.uid | default 100 }} + fsGroup: {{ .Values.server.gid | default 1000 }} + {{- end }} +{{- end -}} + +{{/* +securityContext for the statefulset openbao container +*/}} +{{- define "server.statefulSet.securityContext.container" -}} + {{- if .Values.server.statefulSet.securityContext.container }} + securityContext: + {{- $tp := typeOf .Values.server.statefulSet.securityContext.container }} + {{- if eq $tp "string" }} + {{- tpl .Values.server.statefulSet.securityContext.container . | nindent 12 }} + {{- else }} + {{- toYaml .Values.server.statefulSet.securityContext.container | nindent 12 }} + {{- end }} + {{- else if not .Values.global.openshift }} + securityContext: + allowPrivilegeEscalation: false + {{- end }} +{{- end -}} + +{{/* +Sets extra injector service account annotations +*/}} +{{- define "injector.serviceAccount.annotations" -}} + {{- $generic := .Values.injector.serviceAccount.annotations -}} + {{- if and (ne .mode "dev") $generic }} + annotations: + {{- include "openbao.annotations.render.4" (list . $generic) -}} + {{- end }} +{{- end -}} + +{{/* +Sets extra injector webhook annotations +*/}} +{{- define "injector.webhookAnnotations" -}} + {{- $wa1 := ((.Values.injector.webhook)).annotations -}} + {{- $wa2 := .Values.injector.webhookAnnotations -}} + {{- if or $wa1 $wa2 }} + annotations: + {{- if $wa1 }} + {{- include "openbao.annotations.render.4" (list . $wa1) -}} + {{- end }} + {{- if $wa2 }} + {{- include "openbao.annotations.render.4" (list . $wa2) -}} + {{- end }} + {{- end }} +{{- end -}} + +{{/* +Set's the injector webhook objectSelector +*/}} +{{- define "injector.objectSelector" -}} + {{- $v := or (((.Values.injector.webhook)).objectSelector) (.Values.injector.objectSelector) -}} + {{ if $v }} + objectSelector: + {{- $tp := typeOf $v -}} + {{ if eq $tp "string" }} + {{ tpl $v . | indent 6 | trim }} + {{ else }} + {{ toYaml $v | indent 6 | trim }} + {{ end }} + {{ end }} +{{ end }} + +{{/* +Sets extra ui service annotations +*/}} +{{- define "openbao.ui.annotations" -}} + {{- $generic := .Values.ui.annotations -}} + {{- if $generic }} + annotations: + {{- include "openbao.annotations.render.4" (list . $generic) -}} + {{- end }} +{{- end -}} + +{{/* +Create the name of the service account to use +*/}} +{{- define "openbao.serviceAccount.name" -}} +{{- if .Values.server.serviceAccount.create -}} + {{ default (include "openbao.fullname" .) .Values.server.serviceAccount.name }} +{{- else -}} + {{ default "default" .Values.server.serviceAccount.name }} +{{- end -}} +{{- end -}} + +{{/* +Sets extra service account annotations +*/}} +{{- define "openbao.serviceAccount.annotations" -}} + {{- $generic := .Values.server.serviceAccount.annotations -}} + {{- if and (ne .mode "dev") $generic }} + annotations: + {{- include "openbao.annotations.render.4" (list . $generic) -}} + {{- end }} +{{- end -}} + +{{/* +Sets extra ingress annotations +*/}} +{{- define "openbao.ingress.annotations" -}} + {{- $generic := .Values.server.ingress.annotations -}} + {{- if $generic }} + annotations: + {{- include "openbao.annotations.render.4" (list . $generic) -}} + {{- end }} +{{- end -}} + +{{/* +Sets extra TLSRoute annotations +*/}} +{{- define "openbao.gateway.tlsRoute.annotations" -}} + {{- $generic := .Values.server.gateway.tlsRoute.annotations -}} + {{- if $generic }} + annotations: + {{- include "openbao.annotations.render.4" (list . $generic) -}} + {{- end }} +{{- end -}} + +{{/* +Sets extra HTTPRoute annotations +*/}} +{{- define "openbao.gateway.httpRoute.annotations" -}} + {{- $generic := .Values.server.gateway.httpRoute.annotations -}} + {{- if $generic }} + annotations: + {{- include "openbao.annotations.render.4" (list . $generic) -}} + {{- end }} +{{- end -}} + +{{/* +Sets extra BackendTLSPolicy annotations +*/}} +{{- define "openbao.gateway.tlsPolicy.annotations" -}} + {{- $generic := .Values.server.gateway.tlsPolicy.annotations -}} + {{- if $generic }} + annotations: + {{- include "openbao.annotations.render.4" (list . $generic) -}} + {{- end }} +{{- end -}} + +{{/* +Sets extra route annotations +*/}} +{{- define "openbao.route.annotations" -}} + {{- $generic := .Values.server.route.annotations -}} + {{- if $generic }} + annotations: + {{- include "openbao.annotations.render.4" (list . $generic) -}} + {{- end }} +{{- end -}} + +{{/* +Sets extra openbao server Service annotations +*/}} +{{- define "openbao.service.annotations" -}} + {{- $generic := .Values.server.service.annotations -}} + {{- if $generic }} + annotations: + {{- include "openbao.annotations.render.4" (list . $generic) -}} + {{- end }} +{{- end -}} + +{{/* +Sets extra openbao server Service (active) annotations +*/}} +{{- define "openbao.service.active.annotations" -}} + {{- $active := .Values.server.service.active.annotations -}} + {{- $generic := .Values.server.service.annotations -}} + {{- if or $active $generic }} + annotations: + {{- if $active }} + {{- include "openbao.annotations.render.4" (list . $active) -}} + {{- end }} + {{- if $generic }} + {{- include "openbao.annotations.render.4" (list . $generic) -}} + {{- end }} + {{- end }} +{{- end -}} + +{{/* +Sets extra openbao server Service (standby) annotations +*/}} +{{- define "openbao.service.standby.annotations" -}} + {{- $standby := .Values.server.service.standby.annotations -}} + {{- $generic := .Values.server.service.annotations -}} + {{- if or $standby $generic }} + annotations: + {{- if $standby }} + {{- include "openbao.annotations.render.4" (list . $standby) -}} + {{- end }} + {{- if $generic }} + {{- include "openbao.annotations.render.4" (list . $generic) -}} + {{- end }} + {{- end }} +{{- end -}} + +{{/* +Sets PodSecurityPolicy annotations +*/}} +{{- define "openbao.psp.annotations" -}} + {{- $generic := .Values.global.psp.annotations -}} + {{- if $generic }} + annotations: + {{- include "openbao.annotations.render.4" (list . $generic) -}} + {{- end }} +{{- end -}} + +{{/* +Sets extra statefulset annotations +*/}} +{{- define "openbao.statefulSet.annotations" -}} + {{- $generic := .Values.server.statefulSet.annotations -}} + {{- if $generic }} + annotations: + {{- include "openbao.annotations.render.4" (list . $generic) -}} + {{- end }} +{{- end -}} + +{{/* +Sets VolumeClaim annotations for data volume +*/}} +{{- define "openbao.dataVolumeClaim.annotations" -}} + {{- $generic := .Values.server.dataStorage.annotations -}} + {{- if and (ne .mode "dev") (.Values.server.dataStorage.enabled) $generic }} + annotations: + {{- include "openbao.annotations.render.4" (list . $generic) -}} + {{- end }} +{{- end -}} + +{{/* +Sets VolumeClaim labels for data volume +*/}} +{{- define "openbao.dataVolumeClaim.labels" -}} + {{- if and (ne .mode "dev") (.Values.server.dataStorage.enabled) (.Values.server.dataStorage.labels) }} + labels: + {{- $tp := typeOf .Values.server.dataStorage.labels }} + {{- if eq $tp "string" }} + {{- tpl .Values.server.dataStorage.labels . | nindent 4 }} + {{- else }} + {{- toYaml .Values.server.dataStorage.labels | nindent 4 }} + {{- end }} + {{- end }} +{{- end -}} + +{{/* +Sets VolumeClaim annotations for audit volume +*/}} +{{- define "openbao.auditVolumeClaim.annotations" -}} + {{- $generic := .Values.server.auditStorage.annotations -}} + {{- if and (ne .mode "dev") (.Values.server.auditStorage.enabled) $generic }} + annotations: + {{- include "openbao.annotations.render.4" (list . $generic) -}} + {{- end }} +{{- end -}} + +{{/* +Sets VolumeClaim labels for audit volume +*/}} +{{- define "openbao.auditVolumeClaim.labels" -}} + {{- if and (ne .mode "dev") (.Values.server.auditStorage.enabled) (.Values.server.auditStorage.labels) }} + labels: + {{- $tp := typeOf .Values.server.auditStorage.labels }} + {{- if eq $tp "string" }} + {{- tpl .Values.server.auditStorage.labels . | nindent 4 }} + {{- else }} + {{- toYaml .Values.server.auditStorage.labels | nindent 4 }} + {{- end }} + {{- end }} +{{- end -}} + +{{/* +Set's the container resources if the user has set any. +*/}} +{{- define "openbao.resources" -}} + {{- if .Values.server.resources -}} + resources: +{{ toYaml .Values.server.resources | indent 12}} + {{ end }} +{{- end -}} + +{{/* +Sets the container resources if the user has set any. +*/}} +{{- define "injector.resources" -}} + {{- if .Values.injector.resources -}} + resources: +{{ toYaml .Values.injector.resources | indent 12}} + {{ end }} +{{- end -}} + +{{/* +Sets the container resources if the user has set any. +*/}} +{{- define "csi.resources" -}} + {{- if .Values.csi.resources -}} + resources: +{{ toYaml .Values.csi.resources | indent 12}} + {{ end }} +{{- end -}} + +{{/* +Sets the container resources for CSI's Agent sidecar if the user has set any. +*/}} +{{- define "csi.agent.resources" -}} + {{- if .Values.csi.agent.resources -}} + resources: +{{ toYaml .Values.csi.agent.resources | indent 12}} + {{ end }} +{{- end -}} + +{{/* +Set's the container resources for the SnapshotAgent if the user has set any. +*/}} +{{- define "openbao.snapshotAgent.resources" -}} + {{- if .Values.snapshotAgent.resources -}} + resources: +{{ toYaml .Values.snapshotAgent.resources | indent 14}} + {{ end }} +{{- end -}} + +{{/* +Sets extra CSI daemonset annotations +*/}} +{{- define "csi.daemonSet.annotations" -}} + {{- $generic := .Values.csi.daemonSet.annotations -}} + {{- if $generic }} + annotations: + {{- include "openbao.annotations.render.4" (list . $generic) -}} + {{- end }} +{{- end -}} + +{{/* +Sets CSI daemonset securityContext for pod template +*/}} +{{- define "csi.daemonSet.securityContext.pod" -}} + {{- if .Values.csi.daemonSet.securityContext.pod }} + securityContext: + {{- $tp := typeOf .Values.csi.daemonSet.securityContext.pod }} + {{- if eq $tp "string" }} + {{- tpl .Values.csi.daemonSet.securityContext.pod . | nindent 8 }} + {{- else }} + {{- toYaml .Values.csi.daemonSet.securityContext.pod | nindent 8 }} + {{- end }} + {{- end }} +{{- end -}} + +{{/* +Sets CSI daemonset securityContext for container +*/}} +{{- define "csi.daemonSet.securityContext.container" -}} + {{- if .Values.csi.daemonSet.securityContext.container }} + securityContext: + {{- $tp := typeOf .Values.csi.daemonSet.securityContext.container }} + {{- if eq $tp "string" }} + {{- tpl .Values.csi.daemonSet.securityContext.container . | nindent 12 }} + {{- else }} + {{- toYaml .Values.csi.daemonSet.securityContext.container | nindent 12 }} + {{- end }} + {{- end }} +{{- end -}} + +{{/* +Sets the injector toleration for pod placement +*/}} +{{- define "csi.pod.tolerations" -}} + {{- if .Values.csi.pod.tolerations }} + tolerations: + {{- $tp := typeOf .Values.csi.pod.tolerations }} + {{- if eq $tp "string" }} + {{ tpl .Values.csi.pod.tolerations . | nindent 8 | trim }} + {{- else }} + {{- toYaml .Values.csi.pod.tolerations | nindent 8 }} + {{- end }} + {{- end }} +{{- end -}} + +{{/* +Sets the CSI provider nodeSelector for pod placement +*/}} +{{- define "csi.pod.nodeselector" -}} + {{- if .Values.csi.pod.nodeSelector }} + nodeSelector: + {{- $tp := typeOf .Values.csi.pod.nodeSelector }} + {{- if eq $tp "string" }} + {{ tpl .Values.csi.pod.nodeSelector . | nindent 8 | trim }} + {{- else }} + {{- toYaml .Values.csi.pod.nodeSelector | nindent 8 }} + {{- end }} + {{- end }} +{{- end -}} +{{/* +Sets the CSI provider affinity for pod placement. +*/}} +{{- define "csi.pod.affinity" -}} + {{- if .Values.csi.pod.affinity }} + affinity: + {{ $tp := typeOf .Values.csi.pod.affinity }} + {{- if eq $tp "string" }} + {{- tpl .Values.csi.pod.affinity . | nindent 8 | trim }} + {{- else }} + {{- toYaml .Values.csi.pod.affinity | nindent 8 }} + {{- end }} + {{ end }} +{{- end -}} +{{/* +Sets extra CSI provider pod annotations +*/}} +{{- define "csi.pod.annotations" -}} + {{- $generic := .Values.csi.pod.annotations -}} + {{- if $generic }} + annotations: + {{- include "openbao.annotations.render.8" (list . $generic) -}} + {{- end }} +{{- end -}} + +{{/* +Sets extra CSI service account annotations +*/}} +{{- define "csi.serviceAccount.annotations" -}} + {{- $generic := .Values.csi.serviceAccount.annotations -}} + {{- if $generic }} + annotations: + {{- include "openbao.annotations.render.4" (list . $generic) -}} + {{- end }} +{{- end -}} + +{{/* +Inject extra environment vars in the format key:value, if populated +*/}} +{{- define "openbao.extraEnvironmentVars" -}} +{{- if .extraEnvironmentVars -}} +{{- range $key, $value := .extraEnvironmentVars }} +- name: {{ printf "%s" $key | replace "." "_" | upper | quote }} + value: {{ $value | quote }} +{{- end }} +{{- end -}} +{{- end -}} + +{{/* +Inject extra environment populated by secrets, if populated +*/}} +{{- define "openbao.extraSecretEnvironmentVars" -}} +{{- if .extraSecretEnvironmentVars -}} +{{- range .extraSecretEnvironmentVars }} +- name: {{ .envName }} + valueFrom: + secretKeyRef: + name: {{ .secretName }} + key: {{ .secretKey }} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* Scheme for health check and local endpoint */}} +{{- define "openbao.scheme" -}} +{{- if .Values.global.tlsDisable -}} +{{ "http" }} +{{- else -}} +{{ "https" }} +{{- end -}} +{{- end -}} + +{{/* +imagePullSecrets generates pull secrets from either string or map values. +A map value must be indexable by the key 'name'. +*/}} +{{- define "imagePullSecrets" -}} +{{- with .Values.global.imagePullSecrets -}} +imagePullSecrets: +{{- range . -}} +{{- if typeIs "string" . }} + - name: {{ . }} +{{- else if index . "name" }} + - name: {{ .name }} +{{- end }} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +externalTrafficPolicy sets a Service's externalTrafficPolicy if applicable. +Supported inputs are Values.server.service and Values.ui +*/}} +{{- define "service.externalTrafficPolicy" -}} +{{- $type := "" -}} +{{- if .serviceType -}} +{{- $type = .serviceType -}} +{{- else if .type -}} +{{- $type = .type -}} +{{- end -}} +{{- if and .externalTrafficPolicy (or (eq $type "LoadBalancer") (eq $type "NodePort")) }} + externalTrafficPolicy: {{ .externalTrafficPolicy }} +{{- else }} +{{- end }} +{{- end -}} + +{{/* +loadBalancer configuration for the the UI service. +Supported inputs are Values.ui +*/}} +{{- define "service.loadBalancer" -}} +{{- if eq (.serviceType | toString) "LoadBalancer" }} +{{- if .loadBalancerIP }} + loadBalancerIP: {{ .loadBalancerIP }} +{{- end }} +{{- with .loadBalancerSourceRanges }} + loadBalancerSourceRanges: +{{- range . }} + - {{ . }} +{{- end }} +{{- end -}} +{{- end }} +{{- end -}} + +{{/* +config file from values +*/}} +{{- define "openbao.config" -}} + {{- if or (eq .mode "ha") (eq .mode "standalone") }} + {{- $type := typeOf (index .Values.server .mode).config }} + {{- if eq $type "string" }} + {{- if eq .mode "standalone" }} + {{ tpl .Values.server.standalone.config . | nindent 4 | trim }} + {{- else if and (eq .mode "ha") (eq (.Values.server.ha.raft.enabled | toString) "false") }} + {{ tpl .Values.server.ha.config . | nindent 4 | trim }} + {{- else if and (eq .mode "ha") (eq (.Values.server.ha.raft.enabled | toString) "true") }} + {{ tpl .Values.server.ha.raft.config . | nindent 4 | trim }} + {{ end }} + {{- else }} + {{- if and (eq .mode "ha") (eq (.Values.server.ha.raft.enabled | toString) "true") }} +{{ (index .Values.server .mode).raft.config | toPrettyJson | indent 4 }} + {{- else }} +{{ (index .Values.server .mode).config | toPrettyJson | indent 4 }} + {{- end }} + {{- end }} + {{- end }} +{{- end -}} + +{{/* +Create the name of the service account to use for the snasphot-agent +*/}} +{{- define "openbao.snapshotAgent.serviceAccount.name" -}} +{{- if .Values.snapshotAgent.serviceAccount.create -}} + {{ default (printf "%s-%s" (include "openbao.fullname" .) "snapshot") .Values.snapshotAgent.serviceAccount.name }} +{{- else -}} + {{ default "default" .Values.snapshotAgent.serviceAccount.name }} +{{- end -}} +{{- end -}} + +{{/* +Sets extra service account annotations for the snapshot-agent +*/}} +{{- define "openbao.snapshotAgent.serviceAccount.annotations" -}} + {{- if and (ne .mode "dev") .Values.snapshotAgent.serviceAccount.annotations }} + annotations: + {{- $tp := typeOf .Values.snapshotAgent.serviceAccount.annotations }} + {{- if eq $tp "string" }} + {{- tpl .Values.snapshotAgent.serviceAccount.annotations . | nindent 4 }} + {{- else }} + {{- toYaml .Values.snapshotAgent.serviceAccount.annotations | nindent 4 }} + {{- end }} + {{- end }} +{{- end -}} + +{{/* +Sets extra snapshotAgent job annotations +*/}} +{{- define "openbao.snapshotAgent.annotations" -}} + {{- if .Values.snapshotAgent.annotations }} + annotations: + {{- $tp := typeOf .Values.snapshotAgent.annotations }} + {{- if eq $tp "string" }} + {{- tpl .Values.snapshotAgent.annotations . | nindent 4 }} + {{- else }} + {{- toYaml .Values.snapshotAgent.annotations | nindent 4 }} + {{- end }} + {{- end }} +{{- end -}} + +{{/* +securityContext for the snapshotAgent pod level. +*/}} +{{- define "snapshotAgent.securityContext.pod" -}} + {{- if .Values.snapshotAgent.securityContext.pod }} + securityContext: + {{- $tp := typeOf .Values.snapshotAgent.securityContext.pod }} + {{- if eq $tp "string" }} + {{- tpl .Values.snapshotAgent.securityContext.pod . | nindent 12 }} + {{- else }} + {{- toYaml .Values.snapshotAgent.securityContext.pod | nindent 12 }} + {{- end }} + {{- else if not .Values.global.openshift }} + securityContext: + runAsNonRoot: true + runAsGroup: {{ .Values.snapshotAgent.gid | default 1000 }} + runAsUser: {{ .Values.snapshotAgent.uid | default 100 }} + fsGroup: {{ .Values.snapshotAgent.gid | default 1000 }} + {{- end }} +{{- end -}} + +{{/* +securityContext for the snapshotAgent container level. +*/}} +{{- define "snapshotAgent.securityContext.container" -}} + {{- if .Values.snapshotAgent.securityContext.container }} + securityContext: + {{- $tp := typeOf .Values.snapshotAgent.securityContext.container }} + {{- if eq $tp "string" }} + {{- tpl .Values.snapshotAgent.securityContext.container . | nindent 14 }} + {{- else }} + {{- toYaml .Values.snapshotAgent.securityContext.container | nindent 14 }} + {{- end }} + {{- else if not .Values.global.openshift }} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + {{- end }} +{{- end -}} diff --git a/packages/system/openbao/charts/openbao/templates/csi-agent-configmap.yaml b/packages/system/openbao/charts/openbao/templates/csi-agent-configmap.yaml new file mode 100644 index 00000000..cf9dded9 --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/csi-agent-configmap.yaml @@ -0,0 +1,34 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{- template "openbao.csiEnabled" . -}} +{{- if and (.csiEnabled) (eq (.Values.csi.agent.enabled | toString) "true") -}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ template "openbao.fullname" . }}-csi-provider-agent-config + namespace: {{ include "openbao.namespace" . }} + labels: + helm.sh/chart: {{ include "openbao.chart" . }} + app.kubernetes.io/name: {{ include "openbao.name" . }}-csi-provider + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +data: + config.hcl: | + vault { + {{- if include "openbao.externalAddr" . }} + "address" = "{{ include "openbao.externalAddr" . }}" + {{- else }} + "address" = "{{ include "openbao.scheme" . }}://{{ template "openbao.fullname" . }}.{{ include "openbao.namespace" . }}.svc:{{ .Values.server.service.port }}" + {{- end }} + } + + cache {} + + listener "unix" { + address = "/var/run/vault/agent.sock" + tls_disable = true + } +{{- end }} diff --git a/packages/system/openbao/charts/openbao/templates/csi-clusterrole.yaml b/packages/system/openbao/charts/openbao/templates/csi-clusterrole.yaml new file mode 100644 index 00000000..a3fbb612 --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/csi-clusterrole.yaml @@ -0,0 +1,23 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{- template "openbao.csiEnabled" . -}} +{{- if .csiEnabled -}} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ template "openbao.fullname" . }}-csi-provider-clusterrole + labels: + app.kubernetes.io/name: {{ include "openbao.name" . }}-csi-provider + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +rules: +- apiGroups: + - "" + resources: + - serviceaccounts/token + verbs: + - create +{{- end }} diff --git a/packages/system/openbao/charts/openbao/templates/csi-clusterrolebinding.yaml b/packages/system/openbao/charts/openbao/templates/csi-clusterrolebinding.yaml new file mode 100644 index 00000000..3c7847af --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/csi-clusterrolebinding.yaml @@ -0,0 +1,24 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{- template "openbao.csiEnabled" . -}} +{{- if .csiEnabled -}} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ template "openbao.fullname" . }}-csi-provider-clusterrolebinding + labels: + app.kubernetes.io/name: {{ include "openbao.name" . }}-csi-provider + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ template "openbao.fullname" . }}-csi-provider-clusterrole +subjects: +- kind: ServiceAccount + name: {{ template "openbao.fullname" . }}-csi-provider + namespace: {{ include "openbao.namespace" . }} +{{- end }} diff --git a/packages/system/openbao/charts/openbao/templates/csi-daemonset.yaml b/packages/system/openbao/charts/openbao/templates/csi-daemonset.yaml new file mode 100644 index 00000000..73168ed0 --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/csi-daemonset.yaml @@ -0,0 +1,157 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{- template "openbao.csiEnabled" . -}} +{{- if .csiEnabled -}} +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: {{ template "openbao.fullname" . }}-csi-provider + namespace: {{ include "openbao.namespace" . }} + labels: + app.kubernetes.io/name: {{ include "openbao.name" . }}-csi-provider + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + {{- if .Values.csi.daemonSet.extraLabels -}} + {{- toYaml .Values.csi.daemonSet.extraLabels | nindent 4 -}} + {{- end -}} + {{ template "csi.daemonSet.annotations" . }} +spec: + updateStrategy: + type: {{ .Values.csi.daemonSet.updateStrategy.type }} + {{- if .Values.csi.daemonSet.updateStrategy.maxUnavailable }} + rollingUpdate: + maxUnavailable: {{ .Values.csi.daemonSet.updateStrategy.maxUnavailable }} + {{- end }} + selector: + matchLabels: + app.kubernetes.io/name: {{ include "openbao.name" . }}-csi-provider + app.kubernetes.io/instance: {{ .Release.Name }} + template: + metadata: + labels: + app.kubernetes.io/name: {{ template "openbao.name" . }}-csi-provider + app.kubernetes.io/instance: {{ .Release.Name }} + {{- if .Values.csi.pod.extraLabels -}} + {{- toYaml .Values.csi.pod.extraLabels | nindent 8 -}} + {{- end -}} + {{ template "csi.pod.annotations" . }} + spec: + {{ template "csi.daemonSet.securityContext.pod" . }} + {{- if .Values.csi.priorityClassName }} + priorityClassName: {{ .Values.csi.priorityClassName }} + {{- end }} + serviceAccountName: {{ template "openbao.fullname" . }}-csi-provider + {{- template "csi.pod.tolerations" . }} + {{- template "csi.pod.nodeselector" . }} + {{- template "csi.pod.affinity" . }} + containers: + - name: {{ include "openbao.name" . }}-csi-provider + {{ template "csi.resources" . }} + {{ template "csi.daemonSet.securityContext.container" . }} + image: "{{ .Values.csi.image.registry | default "docker.io" }}/{{ .Values.csi.image.repository }}:{{ .Values.csi.image.tag }}" + imagePullPolicy: {{ .Values.csi.image.pullPolicy }} + args: + - --endpoint={{ .Values.csi.daemonSet.endpoint }} + - --debug={{ .Values.csi.debug }} + {{- if .Values.csi.hmacSecretName }} + - --hmac-secret-name={{ .Values.csi.hmacSecretName }} + {{- else }} + - --hmac-secret-name={{- include "openbao.name" . }}-csi-provider-hmac-key + {{- end }} + {{- if .Values.csi.extraArgs }} + {{- toYaml .Values.csi.extraArgs | nindent 12 }} + {{- end }} + env: + - name: VAULT_ADDR + {{- if eq (.Values.csi.agent.enabled | toString) "true" }} + value: "unix:///var/run/vault/agent.sock" + {{- else if include "openbao.externalAddr" . }} + value: "{{ include "openbao.externalAddr" . }}" + {{- else }} + value: {{ include "openbao.scheme" . }}://{{ template "openbao.fullname" . }}.{{ include "openbao.namespace" . }}.svc:{{ .Values.server.service.port }} + {{- end }} + volumeMounts: + - name: providervol + mountPath: "/provider" + {{- if eq (.Values.csi.agent.enabled | toString) "true" }} + - name: agent-unix-socket + mountPath: /var/run/vault + {{- end }} + {{- if .Values.csi.volumeMounts }} + {{- toYaml .Values.csi.volumeMounts | nindent 12}} + {{- end }} + livenessProbe: + httpGet: + path: /health/ready + port: 8080 + failureThreshold: {{ .Values.csi.livenessProbe.failureThreshold }} + initialDelaySeconds: {{ .Values.csi.livenessProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.csi.livenessProbe.periodSeconds }} + successThreshold: {{ .Values.csi.livenessProbe.successThreshold }} + timeoutSeconds: {{ .Values.csi.livenessProbe.timeoutSeconds }} + readinessProbe: + httpGet: + path: /health/ready + port: 8080 + failureThreshold: {{ .Values.csi.readinessProbe.failureThreshold }} + initialDelaySeconds: {{ .Values.csi.readinessProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.csi.readinessProbe.periodSeconds }} + successThreshold: {{ .Values.csi.readinessProbe.successThreshold }} + timeoutSeconds: {{ .Values.csi.readinessProbe.timeoutSeconds }} + {{- if eq (.Values.csi.agent.enabled | toString) "true" }} + - name: {{ include "openbao.name" . }}-agent + image: "{{ .Values.csi.agent.image.registry | default "docker.io" }}/{{ .Values.csi.agent.image.repository }}:{{ .Values.csi.agent.image.tag | default (trimPrefix "v" .Chart.AppVersion) }}" + imagePullPolicy: {{ .Values.csi.agent.image.pullPolicy }} + {{ template "csi.agent.resources" . }} + command: + - bao + args: + - agent + - -config=/etc/vault/config.hcl + {{- if .Values.csi.agent.extraArgs }} + {{- toYaml .Values.csi.agent.extraArgs | nindent 12 }} + {{- end }} + ports: + - containerPort: 8200 + env: + - name: BAO_LOG_LEVEL + value: "{{ .Values.csi.agent.logLevel }}" + - name: BAO_LOG_FORMAT + value: "{{ .Values.csi.agent.logFormat }}" + securityContext: + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsUser: 100 + runAsGroup: 1000 + volumeMounts: + - name: agent-config + mountPath: /etc/vault/config.hcl + subPath: config.hcl + readOnly: true + - name: agent-unix-socket + mountPath: /var/run/vault + {{- if .Values.csi.volumeMounts }} + {{- toYaml .Values.csi.volumeMounts | nindent 12 }} + {{- end }} + {{- end }} + volumes: + - name: providervol + hostPath: + path: {{ .Values.csi.daemonSet.providersDir }} + {{- if eq (.Values.csi.agent.enabled | toString) "true" }} + - name: agent-config + configMap: + name: {{ template "openbao.fullname" . }}-csi-provider-agent-config + - name: agent-unix-socket + emptyDir: + medium: Memory + {{- end }} + {{- if .Values.csi.volumes }} + {{- toYaml .Values.csi.volumes | nindent 8}} + {{- end }} + {{- include "imagePullSecrets" . | nindent 6 }} +{{- end }} diff --git a/packages/system/openbao/charts/openbao/templates/csi-role.yaml b/packages/system/openbao/charts/openbao/templates/csi-role.yaml new file mode 100644 index 00000000..a7554a65 --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/csi-role.yaml @@ -0,0 +1,32 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{- template "openbao.csiEnabled" . -}} +{{- if .csiEnabled -}} +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ template "openbao.fullname" . }}-csi-provider-role + namespace: {{ include "openbao.namespace" . }} + labels: + app.kubernetes.io/name: {{ include "openbao.name" . }}-csi-provider + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +rules: +- apiGroups: [""] + resources: ["secrets"] + verbs: ["get"] + resourceNames: + {{- if .Values.csi.hmacSecretName }} + - {{ .Values.csi.hmacSecretName }} + {{- else }} + - {{ include "openbao.name" . }}-csi-provider-hmac-key + {{- end }} +# 'create' permissions cannot be restricted by resource name: +# https://kubernetes.io/docs/reference/access-authn-authz/rbac/#referring-to-resources +- apiGroups: [""] + resources: ["secrets"] + verbs: ["create"] +{{- end }} diff --git a/packages/system/openbao/charts/openbao/templates/csi-rolebinding.yaml b/packages/system/openbao/charts/openbao/templates/csi-rolebinding.yaml new file mode 100644 index 00000000..c46096e1 --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/csi-rolebinding.yaml @@ -0,0 +1,25 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{- template "openbao.csiEnabled" . -}} +{{- if .csiEnabled -}} +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ template "openbao.fullname" . }}-csi-provider-rolebinding + namespace: {{ include "openbao.namespace" . }} + labels: + app.kubernetes.io/name: {{ include "openbao.name" . }}-csi-provider + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ template "openbao.fullname" . }}-csi-provider-role +subjects: +- kind: ServiceAccount + name: {{ template "openbao.fullname" . }}-csi-provider + namespace: {{ include "openbao.namespace" . }} +{{- end }} diff --git a/packages/system/openbao/charts/openbao/templates/csi-serviceaccount.yaml b/packages/system/openbao/charts/openbao/templates/csi-serviceaccount.yaml new file mode 100644 index 00000000..2f5d346b --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/csi-serviceaccount.yaml @@ -0,0 +1,21 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{- template "openbao.csiEnabled" . -}} +{{- if .csiEnabled -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ template "openbao.fullname" . }}-csi-provider + namespace: {{ include "openbao.namespace" . }} + labels: + app.kubernetes.io/name: {{ include "openbao.name" . }}-csi-provider + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + {{- if .Values.csi.serviceAccount.extraLabels -}} + {{- toYaml .Values.csi.serviceAccount.extraLabels | nindent 4 -}} + {{- end -}} + {{ template "csi.serviceAccount.annotations" . }} +{{- end }} diff --git a/packages/system/openbao/charts/openbao/templates/extra-objects.yaml b/packages/system/openbao/charts/openbao/templates/extra-objects.yaml new file mode 100644 index 00000000..b408fb07 --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/extra-objects.yaml @@ -0,0 +1,8 @@ +{{- range .Values.extraObjects }} +--- +{{- if typeIs "string" . }} +{{ tpl . $ }} +{{- else }} +{{ tpl (. | toYaml) $ }} +{{- end }} +{{- end }} diff --git a/packages/system/openbao/charts/openbao/templates/grafana/configmap-dashboard.yaml b/packages/system/openbao/charts/openbao/templates/grafana/configmap-dashboard.yaml new file mode 100644 index 00000000..970369f0 --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/grafana/configmap-dashboard.yaml @@ -0,0 +1,30 @@ +{{- if .Values.serverTelemetry.grafanaDashboard.enabled }} +{{- $files := .Files.Glob "grafana/dashboards/*.json" }} +{{- if $files }} +{{- range $path, $fileContents := $files }} +{{- $dashboardName := regexReplaceAll "(^.*/)(.*)\\.json$" $path "${2}" }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ printf "%s-%s" (include "openbao.fullname" $) $dashboardName | trunc 63 | trimSuffix "-" }} + namespace: {{ include "openbao.namespace" $ }} + labels: + app.kubernetes.io/name: {{ include "openbao.name" $ }}-grafana-dashboard + app.kubernetes.io/instance: {{ $.Release.Name }} + app.kubernetes.io/managed-by: {{ $.Release.Service }} + {{- if $.Values.serverTelemetry.grafanaDashboard.defaultLabel }} + grafana_dashboard: "1" + {{- end }} + {{- if $.Values.serverTelemetry.grafanaDashboard.extraLabels }} + {{- $.Values.serverTelemetry.grafanaDashboard.extraLabels | toYaml | nindent 4 }} + {{- end }} + {{- if $.Values.serverTelemetry.grafanaDashboard.extraAnnotations }} + annotations: + {{- $.Values.serverTelemetry.grafanaDashboard.extraAnnotations | toYaml | nindent 4 }} + {{- end }} +data: + {{ $dashboardName }}.json: {{ $.Files.Get $path | toJson }} +{{- end }} +{{- end }} +{{- end }} diff --git a/packages/system/openbao/charts/openbao/templates/injector-certs-secret.yaml b/packages/system/openbao/charts/openbao/templates/injector-certs-secret.yaml new file mode 100644 index 00000000..b5de48bf --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/injector-certs-secret.yaml @@ -0,0 +1,19 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{- template "openbao.injectorEnabled" . -}} +{{- if .injectorEnabled -}} +{{- if and (eq (.Values.injector.leaderElector.enabled | toString) "true") (gt (.Values.injector.replicas | int) 1) }} +apiVersion: v1 +kind: Secret +metadata: + name: openbao-injector-certs + namespace: {{ include "openbao.namespace" . }} + labels: + app.kubernetes.io/name: {{ include "openbao.name" . }}-agent-injector + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} +{{- end }} diff --git a/packages/system/openbao/charts/openbao/templates/injector-clusterrole.yaml b/packages/system/openbao/charts/openbao/templates/injector-clusterrole.yaml new file mode 100644 index 00000000..10ea35c1 --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/injector-clusterrole.yaml @@ -0,0 +1,30 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{- template "openbao.injectorEnabled" . -}} +{{- if .injectorEnabled -}} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ template "openbao.fullname" . }}-agent-injector-clusterrole + labels: + app.kubernetes.io/name: {{ include "openbao.name" . }}-agent-injector + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +rules: +- apiGroups: ["admissionregistration.k8s.io"] + resources: ["mutatingwebhookconfigurations"] + verbs: + - "get" + - "list" + - "watch" + - "patch" +{{- if and (eq (.Values.injector.leaderElector.enabled | toString) "true") (gt (.Values.injector.replicas | int) 1) }} +- apiGroups: [""] + resources: ["nodes"] + verbs: + - "get" +{{ end }} +{{ end }} diff --git a/packages/system/openbao/charts/openbao/templates/injector-clusterrolebinding.yaml b/packages/system/openbao/charts/openbao/templates/injector-clusterrolebinding.yaml new file mode 100644 index 00000000..353ee8ac --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/injector-clusterrolebinding.yaml @@ -0,0 +1,24 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{- template "openbao.injectorEnabled" . -}} +{{- if .injectorEnabled -}} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ template "openbao.fullname" . }}-agent-injector-binding + labels: + app.kubernetes.io/name: {{ include "openbao.name" . }}-agent-injector + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ template "openbao.fullname" . }}-agent-injector-clusterrole +subjects: +- kind: ServiceAccount + name: {{ template "openbao.fullname" . }}-agent-injector + namespace: {{ include "openbao.namespace" . }} +{{ end }} diff --git a/packages/system/openbao/charts/openbao/templates/injector-deployment.yaml b/packages/system/openbao/charts/openbao/templates/injector-deployment.yaml new file mode 100644 index 00000000..ac0fc915 --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/injector-deployment.yaml @@ -0,0 +1,179 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{- template "openbao.injectorEnabled" . -}} +{{- if .injectorEnabled -}} +# Deployment for the injector +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ template "openbao.fullname" . }}-agent-injector + namespace: {{ include "openbao.namespace" . }} + labels: + app.kubernetes.io/name: {{ include "openbao.name" . }}-agent-injector + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + component: webhook +spec: + replicas: {{ .Values.injector.replicas }} + selector: + matchLabels: + app.kubernetes.io/name: {{ template "openbao.name" . }}-agent-injector + app.kubernetes.io/instance: {{ .Release.Name }} + component: webhook + {{ template "injector.strategy" . }} + template: + metadata: + labels: + app.kubernetes.io/name: {{ template "openbao.name" . }}-agent-injector + app.kubernetes.io/instance: {{ .Release.Name }} + component: webhook + {{- if .Values.injector.extraLabels -}} + {{- toYaml .Values.injector.extraLabels | nindent 8 -}} + {{- end -}} + {{ template "injector.annotations" . }} + spec: + {{ template "injector.affinity" . }} + {{ template "injector.topologySpreadConstraints" . }} + {{ template "injector.tolerations" . }} + {{ template "injector.nodeselector" . }} + {{- if .Values.injector.priorityClassName }} + priorityClassName: {{ .Values.injector.priorityClassName }} + {{- end }} + serviceAccountName: "{{ template "openbao.fullname" . }}-agent-injector" + {{ template "injector.securityContext.pod" . -}} + {{- if not .Values.global.openshift }} + hostNetwork: {{ .Values.injector.hostNetwork }} + {{- end }} + containers: + - name: sidecar-injector + {{ template "injector.resources" . }} + image: "{{ .Values.injector.image.registry | default "docker.io" }}/{{ .Values.injector.image.repository }}:{{ .Values.injector.image.tag }}" + imagePullPolicy: "{{ .Values.injector.image.pullPolicy }}" + {{- template "injector.securityContext.container" . }} + env: + - name: AGENT_INJECT_LISTEN + value: {{ printf ":%v" .Values.injector.port }} + - name: AGENT_INJECT_LOG_LEVEL + value: {{ .Values.injector.logLevel | default "info" }} + - name: AGENT_INJECT_VAULT_ADDR + {{- if include "openbao.externalAddr" . }} + value: "{{ include "openbao.externalAddr" . }}" + {{- else if .Values.injector.externalVaultAddr }} + value: "{{ .Values.injector.externalVaultAddr }}" + {{- else }} + value: {{ include "openbao.scheme" . }}://{{ template "openbao.fullname" . }}.{{ include "openbao.namespace" . }}.svc:{{ .Values.server.service.port }} + {{- end }} + - name: AGENT_INJECT_VAULT_AUTH_PATH + value: {{ .Values.injector.authPath }} + - name: AGENT_INJECT_VAULT_IMAGE + value: "{{ .Values.injector.agentImage.registry | default "quay.io" }}/{{ .Values.injector.agentImage.repository }}:{{ .Values.injector.agentImage.tag | default (trimPrefix "v" .Chart.AppVersion) }}" + {{- if .Values.injector.certs.secretName }} + - name: AGENT_INJECT_TLS_CERT_FILE + value: "/etc/webhook/certs/{{ .Values.injector.certs.certName }}" + - name: AGENT_INJECT_TLS_KEY_FILE + value: "/etc/webhook/certs/{{ .Values.injector.certs.keyName }}" + {{- else }} + - name: AGENT_INJECT_TLS_AUTO + value: {{ template "openbao.fullname" . }}-agent-injector-cfg + - name: AGENT_INJECT_TLS_AUTO_HOSTS + value: {{ template "openbao.fullname" . }}-agent-injector-svc,{{ template "openbao.fullname" . }}-agent-injector-svc.{{ include "openbao.namespace" . }},{{ template "openbao.fullname" . }}-agent-injector-svc.{{ include "openbao.namespace" . }}.svc + {{- end }} + - name: AGENT_INJECT_LOG_FORMAT + value: {{ .Values.injector.logFormat | default "standard" }} + - name: AGENT_INJECT_REVOKE_ON_SHUTDOWN + value: "{{ .Values.injector.revokeOnShutdown | default false }}" + {{- if .Values.global.openshift }} + - name: AGENT_INJECT_SET_SECURITY_CONTEXT + value: "false" + {{- end }} + {{- if .Values.injector.metrics.enabled }} + - name: AGENT_INJECT_TELEMETRY_PATH + value: "/metrics" + {{- end }} + {{- if and (eq (.Values.injector.leaderElector.enabled | toString) "true") (gt (.Values.injector.replicas | int) 1) }} + - name: AGENT_INJECT_USE_LEADER_ELECTOR + value: "true" + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + {{- end }} + - name: AGENT_INJECT_CPU_REQUEST + value: "{{ .Values.injector.agentDefaults.cpuRequest }}" + - name: AGENT_INJECT_CPU_LIMIT + value: "{{ .Values.injector.agentDefaults.cpuLimit }}" + - name: AGENT_INJECT_MEM_REQUEST + value: "{{ .Values.injector.agentDefaults.memRequest }}" + - name: AGENT_INJECT_MEM_LIMIT + value: "{{ .Values.injector.agentDefaults.memLimit }}" + {{- if .Values.injector.agentDefaults.ephemeralRequest }} + - name: AGENT_INJECT_EPHEMERAL_REQUEST + value: "{{ .Values.injector.agentDefaults.ephemeralRequest }}" + {{- end }} + {{- if .Values.injector.agentDefaults.ephemeralLimit }} + - name: AGENT_INJECT_EPHEMERAL_LIMIT + value: "{{ .Values.injector.agentDefaults.ephemeralLimit }}" + {{- end }} + - name: AGENT_INJECT_DEFAULT_TEMPLATE + value: "{{ .Values.injector.agentDefaults.template }}" + - name: AGENT_INJECT_TEMPLATE_CONFIG_EXIT_ON_RETRY_FAILURE + value: "{{ .Values.injector.agentDefaults.templateConfig.exitOnRetryFailure }}" + {{- if .Values.injector.agentDefaults.templateConfig.staticSecretRenderInterval }} + - name: AGENT_INJECT_TEMPLATE_STATIC_SECRET_RENDER_INTERVAL + value: "{{ .Values.injector.agentDefaults.templateConfig.staticSecretRenderInterval }}" + {{- end }} + {{- include "openbao.extraEnvironmentVars" .Values.injector | nindent 12 }} + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + args: + - agent-inject + - 2>&1 + livenessProbe: + httpGet: + path: /health/ready + port: {{ .Values.injector.port }} + scheme: HTTPS + failureThreshold: {{ .Values.injector.livenessProbe.failureThreshold }} + initialDelaySeconds: {{ .Values.injector.livenessProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.injector.livenessProbe.periodSeconds }} + successThreshold: {{ .Values.injector.livenessProbe.successThreshold }} + timeoutSeconds: {{ .Values.injector.livenessProbe.timeoutSeconds }} + readinessProbe: + httpGet: + path: /health/ready + port: {{ .Values.injector.port }} + scheme: HTTPS + failureThreshold: {{ .Values.injector.readinessProbe.failureThreshold }} + initialDelaySeconds: {{ .Values.injector.readinessProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.injector.readinessProbe.periodSeconds }} + successThreshold: {{ .Values.injector.readinessProbe.successThreshold }} + timeoutSeconds: {{ .Values.injector.readinessProbe.timeoutSeconds }} + startupProbe: + httpGet: + path: /health/ready + port: {{ .Values.injector.port }} + scheme: HTTPS + failureThreshold: {{ .Values.injector.startupProbe.failureThreshold }} + initialDelaySeconds: {{ .Values.injector.startupProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.injector.startupProbe.periodSeconds }} + successThreshold: {{ .Values.injector.startupProbe.successThreshold }} + timeoutSeconds: {{ .Values.injector.startupProbe.timeoutSeconds }} +{{- if .Values.injector.certs.secretName }} + volumeMounts: + - name: webhook-certs + mountPath: /etc/webhook/certs + readOnly: true +{{- end }} +{{- if .Values.injector.certs.secretName }} + volumes: + - name: webhook-certs + secret: + secretName: "{{ .Values.injector.certs.secretName }}" +{{- end }} + {{- include "imagePullSecrets" . | nindent 6 }} +{{ end }} diff --git a/packages/system/openbao/charts/openbao/templates/injector-disruptionbudget.yaml b/packages/system/openbao/charts/openbao/templates/injector-disruptionbudget.yaml new file mode 100644 index 00000000..08749bd2 --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/injector-disruptionbudget.yaml @@ -0,0 +1,25 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{- if .Values.injector.podDisruptionBudget }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ template "openbao.fullname" . }}-agent-injector + namespace: {{ include "openbao.namespace" . }} + labels: + helm.sh/chart: {{ include "openbao.chart" . }} + app.kubernetes.io/name: {{ include "openbao.name" . }}-agent-injector + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + component: webhook +spec: + selector: + matchLabels: + app.kubernetes.io/name: {{ template "openbao.name" . }}-agent-injector + app.kubernetes.io/instance: {{ .Release.Name }} + component: webhook + {{- toYaml .Values.injector.podDisruptionBudget | nindent 2 }} +{{- end -}} diff --git a/packages/system/openbao/charts/openbao/templates/injector-mutating-webhook.yaml b/packages/system/openbao/charts/openbao/templates/injector-mutating-webhook.yaml new file mode 100644 index 00000000..8ffd2671 --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/injector-mutating-webhook.yaml @@ -0,0 +1,44 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{- template "openbao.injectorEnabled" . -}} +{{- if .injectorEnabled -}} +{{- if .Capabilities.APIVersions.Has "admissionregistration.k8s.io/v1" }} +apiVersion: admissionregistration.k8s.io/v1 +{{- else }} +apiVersion: admissionregistration.k8s.io/v1beta1 +{{- end }} +kind: MutatingWebhookConfiguration +metadata: + name: {{ template "openbao.fullname" . }}-agent-injector-cfg + labels: + app.kubernetes.io/name: {{ include "openbao.name" . }}-agent-injector + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + {{- template "injector.webhookAnnotations" . }} +webhooks: + - name: vault.hashicorp.com + failurePolicy: {{ ((.Values.injector.webhook)).failurePolicy | default .Values.injector.failurePolicy }} + matchPolicy: {{ ((.Values.injector.webhook)).matchPolicy | default "Exact" }} + sideEffects: None + timeoutSeconds: {{ ((.Values.injector.webhook)).timeoutSeconds | default "30" }} + admissionReviewVersions: ["v1", "v1beta1"] + clientConfig: + service: + name: {{ template "openbao.fullname" . }}-agent-injector-svc + namespace: {{ include "openbao.namespace" . }} + path: "/mutate" + caBundle: {{ .Values.injector.certs.caBundle | quote }} + rules: + - operations: ["CREATE", "UPDATE"] + apiGroups: [""] + apiVersions: ["v1"] + resources: ["pods"] +{{- if or (.Values.injector.namespaceSelector) (((.Values.injector.webhook)).namespaceSelector) }} + namespaceSelector: +{{ toYaml (((.Values.injector.webhook)).namespaceSelector | default .Values.injector.namespaceSelector) | indent 6}} +{{ end }} +{{- template "injector.objectSelector" . -}} +{{ end }} diff --git a/packages/system/openbao/charts/openbao/templates/injector-network-policy.yaml b/packages/system/openbao/charts/openbao/templates/injector-network-policy.yaml new file mode 100644 index 00000000..68a89754 --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/injector-network-policy.yaml @@ -0,0 +1,30 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{- template "openbao.injectorEnabled" . -}} +{{- if .injectorEnabled -}} +{{- if eq (.Values.global.openshift | toString) "true" }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ template "openbao.fullname" . }}-agent-injector + namespace: {{ include "openbao.namespace" . }} + labels: + app.kubernetes.io/name: {{ template "openbao.name" . }}-agent-injector + app.kubernetes.io/instance: {{ .Release.Name }} +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: {{ template "openbao.name" . }}-agent-injector + app.kubernetes.io/instance: {{ .Release.Name }} + component: webhook + ingress: + - from: + - namespaceSelector: {} + ports: + - port: 8080 + protocol: TCP +{{ end }} +{{ end }} diff --git a/packages/system/openbao/charts/openbao/templates/injector-psp-role.yaml b/packages/system/openbao/charts/openbao/templates/injector-psp-role.yaml new file mode 100644 index 00000000..3f42450c --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/injector-psp-role.yaml @@ -0,0 +1,25 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{- template "openbao.injectorEnabled" . -}} +{{- if .injectorEnabled -}} +{{- if eq (.Values.global.psp.enable | toString) "true" }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ template "openbao.fullname" . }}-agent-injector-psp + namespace: {{ include "openbao.namespace" . }} + labels: + app.kubernetes.io/name: {{ include "openbao.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +rules: +- apiGroups: ['policy'] + resources: ['podsecuritypolicies'] + verbs: ['use'] + resourceNames: + - {{ template "openbao.fullname" . }}-agent-injector +{{- end }} +{{- end }} diff --git a/packages/system/openbao/charts/openbao/templates/injector-psp-rolebinding.yaml b/packages/system/openbao/charts/openbao/templates/injector-psp-rolebinding.yaml new file mode 100644 index 00000000..62a609c7 --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/injector-psp-rolebinding.yaml @@ -0,0 +1,26 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{- template "openbao.injectorEnabled" . -}} +{{- if .injectorEnabled -}} +{{- if eq (.Values.global.psp.enable | toString) "true" }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ template "openbao.fullname" . }}-agent-injector-psp + namespace: {{ include "openbao.namespace" . }} + labels: + app.kubernetes.io/name: {{ include "openbao.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +roleRef: + kind: Role + name: {{ template "openbao.fullname" . }}-agent-injector-psp + apiGroup: rbac.authorization.k8s.io +subjects: + - kind: ServiceAccount + name: {{ template "openbao.fullname" . }}-agent-injector +{{- end }} +{{- end }} \ No newline at end of file diff --git a/packages/system/openbao/charts/openbao/templates/injector-psp.yaml b/packages/system/openbao/charts/openbao/templates/injector-psp.yaml new file mode 100644 index 00000000..5c1c58f7 --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/injector-psp.yaml @@ -0,0 +1,51 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{- template "openbao.injectorEnabled" . -}} +{{- if .injectorEnabled -}} +{{- if eq (.Values.global.psp.enable | toString) "true" }} +apiVersion: policy/v1beta1 +kind: PodSecurityPolicy +metadata: + name: {{ template "openbao.fullname" . }}-agent-injector + labels: + app.kubernetes.io/name: {{ include "openbao.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- template "openbao.psp.annotations" . }} +spec: + privileged: false + # Required to prevent escalations to root. + allowPrivilegeEscalation: false + volumes: + - configMap + - emptyDir + - projected + - secret + - downwardAPI + hostNetwork: false + hostIPC: false + hostPID: false + runAsUser: + # Require the container to run without root privileges. + rule: MustRunAsNonRoot + seLinux: + # This policy assumes the nodes are using AppArmor rather than SELinux. + rule: RunAsAny + supplementalGroups: + rule: MustRunAs + ranges: + # Forbid adding the root group. + - min: 1 + max: 65535 + fsGroup: + rule: MustRunAs + ranges: + # Forbid adding the root group. + - min: 1 + max: 65535 + readOnlyRootFilesystem: false +{{- end }} +{{- end }} \ No newline at end of file diff --git a/packages/system/openbao/charts/openbao/templates/injector-role.yaml b/packages/system/openbao/charts/openbao/templates/injector-role.yaml new file mode 100644 index 00000000..2e29aa7b --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/injector-role.yaml @@ -0,0 +1,34 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{- template "openbao.injectorEnabled" . -}} +{{- if .injectorEnabled -}} +{{- if and (eq (.Values.injector.leaderElector.enabled | toString) "true") (gt (.Values.injector.replicas | int) 1) }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ template "openbao.fullname" . }}-agent-injector-leader-elector-role + namespace: {{ include "openbao.namespace" . }} + labels: + app.kubernetes.io/name: {{ include "openbao.name" . }}-agent-injector + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +rules: + - apiGroups: [""] + resources: ["secrets", "configmaps"] + verbs: + - "create" + - "get" + - "watch" + - "list" + - "update" + - apiGroups: [""] + resources: ["pods"] + verbs: + - "get" + - "patch" + - "delete" +{{- end }} +{{- end }} \ No newline at end of file diff --git a/packages/system/openbao/charts/openbao/templates/injector-rolebinding.yaml b/packages/system/openbao/charts/openbao/templates/injector-rolebinding.yaml new file mode 100644 index 00000000..8e460c4b --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/injector-rolebinding.yaml @@ -0,0 +1,27 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{- template "openbao.injectorEnabled" . -}} +{{- if .injectorEnabled -}} +{{- if and (eq (.Values.injector.leaderElector.enabled | toString) "true") (gt (.Values.injector.replicas | int) 1) }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ template "openbao.fullname" . }}-agent-injector-leader-elector-binding + namespace: {{ include "openbao.namespace" . }} + labels: + app.kubernetes.io/name: {{ include "openbao.name" . }}-agent-injector + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ template "openbao.fullname" . }}-agent-injector-leader-elector-role +subjects: + - kind: ServiceAccount + name: {{ template "openbao.fullname" . }}-agent-injector + namespace: {{ include "openbao.namespace" . }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/packages/system/openbao/charts/openbao/templates/injector-service.yaml b/packages/system/openbao/charts/openbao/templates/injector-service.yaml new file mode 100644 index 00000000..f9db469f --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/injector-service.yaml @@ -0,0 +1,30 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{- template "openbao.injectorEnabled" . -}} +{{- if .injectorEnabled -}} +apiVersion: v1 +kind: Service +metadata: + name: {{ template "openbao.fullname" . }}-agent-injector-svc + namespace: {{ include "openbao.namespace" . }} + labels: + app.kubernetes.io/name: {{ include "openbao.name" . }}-agent-injector + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + {{- if .Values.injector.service.extraLabels -}} + {{- toYaml .Values.injector.service.extraLabels | nindent 4 -}} + {{- end -}} + {{ template "injector.service.annotations" . }} +spec: + ports: + - name: https + port: 443 + targetPort: {{ .Values.injector.port }} + selector: + app.kubernetes.io/name: {{ include "openbao.name" . }}-agent-injector + app.kubernetes.io/instance: {{ .Release.Name }} + component: webhook +{{- end }} diff --git a/packages/system/openbao/charts/openbao/templates/injector-serviceaccount.yaml b/packages/system/openbao/charts/openbao/templates/injector-serviceaccount.yaml new file mode 100644 index 00000000..a411788c --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/injector-serviceaccount.yaml @@ -0,0 +1,18 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{- template "openbao.injectorEnabled" . -}} +{{- if .injectorEnabled -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ template "openbao.fullname" . }}-agent-injector + namespace: {{ include "openbao.namespace" . }} + labels: + app.kubernetes.io/name: {{ include "openbao.name" . }}-agent-injector + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + {{ template "injector.serviceAccount.annotations" . }} +{{ end }} diff --git a/packages/system/openbao/charts/openbao/templates/prometheus-prometheusrules.yaml b/packages/system/openbao/charts/openbao/templates/prometheus-prometheusrules.yaml new file mode 100644 index 00000000..d371da37 --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/prometheus-prometheusrules.yaml @@ -0,0 +1,32 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{ if and (.Values.serverTelemetry.prometheusRules.rules) + (or (.Values.global.serverTelemetry.prometheusOperator) (.Values.serverTelemetry.prometheusRules.enabled) ) +}} +--- +apiVersion: monitoring.coreos.com/v1 +kind: PrometheusRule +metadata: + name: {{ template "openbao.fullname" . }} + namespace: {{ include "openbao.namespace" . }} + labels: + helm.sh/chart: {{ include "openbao.chart" . }} + app.kubernetes.io/name: {{ include "openbao.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + {{- /* update the selectors docs in values.yaml whenever the defaults below change. */ -}} + {{- $selectors := .Values.serverTelemetry.prometheusRules.selectors }} + {{- if $selectors }} + {{- toYaml $selectors | nindent 4 }} + {{- else }} + release: prometheus + {{- end }} +spec: + groups: + - name: {{ include "openbao.fullname" . }} + rules: + {{- toYaml .Values.serverTelemetry.prometheusRules.rules | nindent 6 }} +{{- end }} diff --git a/packages/system/openbao/charts/openbao/templates/prometheus-servicemonitor.yaml b/packages/system/openbao/charts/openbao/templates/prometheus-servicemonitor.yaml new file mode 100644 index 00000000..950c6194 --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/prometheus-servicemonitor.yaml @@ -0,0 +1,62 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{ template "openbao.mode" . }} +{{ if or (.Values.global.serverTelemetry.prometheusOperator) (.Values.serverTelemetry.serviceMonitor.enabled) }} +--- +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ template "openbao.fullname" . }} + namespace: {{ include "openbao.namespace" . }} + labels: + helm.sh/chart: {{ include "openbao.chart" . }} + app.kubernetes.io/name: {{ include "openbao.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + {{- /* update the selectors docs in values.yaml whenever the defaults below change. */ -}} + {{- $selectors := .Values.serverTelemetry.serviceMonitor.selectors }} + {{- if $selectors }} + {{- toYaml $selectors | nindent 4 }} + {{- else }} + release: prometheus + {{- end }} +spec: + {{- if .Values.serverTelemetry.serviceMonitor.scrapeClass }} + scrapeClass: {{ .Values.serverTelemetry.serviceMonitor.scrapeClass }} + {{- end }} + selector: + matchLabels: + app.kubernetes.io/name: {{ template "openbao.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + {{- if eq .mode "ha" }} + openbao-active: "true" + {{- else }} + openbao-internal: "true" + {{- end }} + endpoints: + - port: {{ .Values.serverTelemetry.serviceMonitor.port | default (include "openbao.scheme" .) }} + interval: {{ .Values.serverTelemetry.serviceMonitor.interval }} + scrapeTimeout: {{ .Values.serverTelemetry.serviceMonitor.scrapeTimeout }} + scheme: {{ .Values.serverTelemetry.serviceMonitor.scheme | default (include "openbao.scheme" .) | lower }} + path: /v1/sys/metrics + params: + format: + - prometheus + {{- with .Values.serverTelemetry.serviceMonitor.tlsConfig }} + tlsConfig: + {{- toYaml . | nindent 6 }} + {{- else }} + tlsConfig: + insecureSkipVerify: true + {{- end }} + {{- with .Values.serverTelemetry.serviceMonitor.authorization }} + authorization: + {{- toYaml . | nindent 6 }} + {{- end }} + namespaceSelector: + matchNames: + - {{ include "openbao.namespace" . }} +{{ end }} diff --git a/packages/system/openbao/charts/openbao/templates/server-backendtlspolicy.yaml b/packages/system/openbao/charts/openbao/templates/server-backendtlspolicy.yaml new file mode 100644 index 00000000..9b492d26 --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/server-backendtlspolicy.yaml @@ -0,0 +1,43 @@ +{{ template "openbao.mode" . }} +{{- if ne .mode "external" }} +{{- if and (not .Values.global.tlsDisable) .Values.server.gateway.tlsPolicy.enabled -}} +{{- $serviceName := include "openbao.fullname" . -}} +{{- template "openbao.serverServiceEnabled" . -}} +{{- if .serverServiceEnabled -}} +{{- if and (eq .mode "ha" ) (eq (.Values.server.gateway.tlsPolicy.activeService | toString) "true") }} +{{- $serviceName = printf "%s-%s" $serviceName "active" -}} +{{- end }} +apiVersion: {{ .Values.server.gateway.tlsPolicy.apiVersion }} +kind: BackendTLSPolicy +metadata: + name: {{ template "openbao.fullname" . }} + namespace: {{ include "openbao.namespace" . }} + labels: + helm.sh/chart: {{ include "openbao.chart" . }} + app.kubernetes.io/name: {{ include "openbao.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + {{- with .Values.server.gateway.tlsPolicy.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- template "openbao.gateway.tlsPolicy.annotations" . }} +spec: + targetRefs: + {{- if .Values.server.gateway.tlsPolicy.targetRefs }} + {{- with .Values.server.gateway.tlsPolicy.targetRefs }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- else }} + - group: '' + kind: Service + name: {{ $serviceName }} + sectionName: {{ include "openbao.scheme" . }} + {{- end }} + validation: + {{- with .Values.server.gateway.tlsPolicy.validation }} + {{- toYaml . | nindent 4 }} + {{- end }} + hostname: {{ include "openbao.fullname" . }} +{{- end }} +{{- end }} +{{- end }} diff --git a/packages/system/openbao/charts/openbao/templates/server-clusterrolebinding.yaml b/packages/system/openbao/charts/openbao/templates/server-clusterrolebinding.yaml new file mode 100644 index 00000000..0f851ec1 --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/server-clusterrolebinding.yaml @@ -0,0 +1,29 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{ template "openbao.serverAuthDelegator" . }} +{{- if .serverAuthDelegator -}} +{{- if .Capabilities.APIVersions.Has "rbac.authorization.k8s.io/v1" -}} +apiVersion: rbac.authorization.k8s.io/v1 +{{- else }} +apiVersion: rbac.authorization.k8s.io/v1beta1 +{{- end }} +kind: ClusterRoleBinding +metadata: + name: {{ template "openbao.fullname" . }}-server-binding + labels: + helm.sh/chart: {{ include "openbao.chart" . }} + app.kubernetes.io/name: {{ include "openbao.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: system:auth-delegator +subjects: +- kind: ServiceAccount + name: {{ template "openbao.serviceAccount.name" . }} + namespace: {{ include "openbao.namespace" . }} +{{ end }} diff --git a/packages/system/openbao/charts/openbao/templates/server-config-configmap.yaml b/packages/system/openbao/charts/openbao/templates/server-config-configmap.yaml new file mode 100644 index 00000000..57c4b0e6 --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/server-config-configmap.yaml @@ -0,0 +1,31 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{ template "openbao.mode" . }} +{{- if ne .mode "external" }} +{{- if .serverEnabled -}} +{{- if ne .mode "dev" -}} +{{ if or (.Values.server.standalone.config) (.Values.server.ha.config) -}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ template "openbao.fullname" . }}-config + namespace: {{ include "openbao.namespace" . }} + labels: + helm.sh/chart: {{ include "openbao.chart" . }} + app.kubernetes.io/name: {{ include "openbao.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- if .Values.server.configAnnotation }} + annotations: + vault.hashicorp.com/config-checksum: {{ include "openbao.config" . | sha256sum }} +{{- end }} +data: + extraconfig-from-values.hcl: |- + {{ template "openbao.config" . }} +{{- end }} +{{- end }} +{{- end }} +{{- end }} diff --git a/packages/system/openbao/charts/openbao/templates/server-discovery-role.yaml b/packages/system/openbao/charts/openbao/templates/server-discovery-role.yaml new file mode 100644 index 00000000..082ff996 --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/server-discovery-role.yaml @@ -0,0 +1,26 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{ template "openbao.mode" . }} +{{- if .serverEnabled -}} +{{- if eq .mode "ha" }} +{{- if eq (.Values.server.serviceAccount.serviceDiscovery.enabled | toString) "true" }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + namespace: {{ include "openbao.namespace" . }} + name: {{ template "openbao.fullname" . }}-discovery-role + labels: + helm.sh/chart: {{ include "openbao.chart" . }} + app.kubernetes.io/name: {{ include "openbao.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +rules: +- apiGroups: [""] + resources: ["pods"] + verbs: ["get", "watch", "list", "update", "patch"] +{{ end }} +{{ end }} +{{ end }} diff --git a/packages/system/openbao/charts/openbao/templates/server-discovery-rolebinding.yaml b/packages/system/openbao/charts/openbao/templates/server-discovery-rolebinding.yaml new file mode 100644 index 00000000..5d3f95e3 --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/server-discovery-rolebinding.yaml @@ -0,0 +1,34 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{ template "openbao.mode" . }} +{{- if .serverEnabled -}} +{{- if eq .mode "ha" }} +{{- if eq (.Values.server.serviceAccount.serviceDiscovery.enabled | toString) "true" }} +{{- if .Capabilities.APIVersions.Has "rbac.authorization.k8s.io/v1" -}} +apiVersion: rbac.authorization.k8s.io/v1 +{{- else }} +apiVersion: rbac.authorization.k8s.io/v1beta1 +{{- end }} +kind: RoleBinding +metadata: + name: {{ template "openbao.fullname" . }}-discovery-rolebinding + namespace: {{ include "openbao.namespace" . }} + labels: + helm.sh/chart: {{ include "openbao.chart" . }} + app.kubernetes.io/name: {{ include "openbao.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ template "openbao.fullname" . }}-discovery-role +subjects: +- kind: ServiceAccount + name: {{ template "openbao.serviceAccount.name" . }} + namespace: {{ include "openbao.namespace" . }} +{{ end }} +{{ end }} +{{ end }} diff --git a/packages/system/openbao/charts/openbao/templates/server-disruptionbudget.yaml b/packages/system/openbao/charts/openbao/templates/server-disruptionbudget.yaml new file mode 100644 index 00000000..7e6660a1 --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/server-disruptionbudget.yaml @@ -0,0 +1,31 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{ template "openbao.mode" . }} +{{- if ne .mode "external" -}} +{{- if .serverEnabled -}} +{{- if and (eq .mode "ha") (eq (.Values.server.ha.disruptionBudget.enabled | toString) "true") -}} +# PodDisruptionBudget to prevent degrading the server cluster through +# voluntary cluster changes. +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ template "openbao.fullname" . }} + namespace: {{ include "openbao.namespace" . }} + labels: + helm.sh/chart: {{ include "openbao.chart" . }} + app.kubernetes.io/name: {{ include "openbao.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +spec: + maxUnavailable: {{ template "openbao.pdb.maxUnavailable" . }} + selector: + matchLabels: + app.kubernetes.io/name: {{ include "openbao.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + component: server +{{- end -}} +{{- end -}} +{{- end -}} diff --git a/packages/system/openbao/charts/openbao/templates/server-ha-active-service.yaml b/packages/system/openbao/charts/openbao/templates/server-ha-active-service.yaml new file mode 100644 index 00000000..19f5afbd --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/server-ha-active-service.yaml @@ -0,0 +1,68 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{ template "openbao.mode" . }} +{{- if ne .mode "external" }} +{{- template "openbao.serverServiceEnabled" . -}} +{{- if .serverServiceEnabled -}} +{{- if eq .mode "ha" }} +{{- if eq (.Values.server.service.active.enabled | toString) "true" }} +# Service for active OpenBao pod +apiVersion: v1 +kind: Service +metadata: + name: {{ template "openbao.fullname" . }}-active + namespace: {{ include "openbao.namespace" . }} + labels: + helm.sh/chart: {{ include "openbao.chart" . }} + app.kubernetes.io/name: {{ include "openbao.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + openbao-active: "true" + {{- if .Values.server.service.active.extraLabels -}} + {{- toYaml .Values.server.service.active.extraLabels | nindent 4 -}} + {{- end -}} +{{- template "openbao.service.active.annotations" . }} +spec: + {{- if .Values.server.service.type}} + type: {{ .Values.server.service.type }} + {{- end}} + {{- if (semverCompare ">= 1.23-0" .Capabilities.KubeVersion.Version) }} + {{- if .Values.server.service.ipFamilyPolicy }} + ipFamilyPolicy: {{ .Values.server.service.ipFamilyPolicy }} + {{- end }} + {{- if .Values.server.service.ipFamilies }} + ipFamilies: {{ .Values.server.service.ipFamilies | toYaml | nindent 2 }} + {{- end }} + {{- end }} + {{- if .Values.server.service.clusterIP }} + clusterIP: {{ .Values.server.service.clusterIP }} + {{- end }} + {{- include "service.externalTrafficPolicy" .Values.server.service }} + publishNotReadyAddresses: {{ .Values.server.service.publishNotReadyAddresses }} + ports: + - name: {{ include "openbao.scheme" . }} + port: {{ .Values.server.service.port }} + targetPort: {{ .Values.server.service.targetPort }} + {{- if and (.Values.server.service.activeNodePort) (eq (.Values.server.service.type | toString) "NodePort") }} + nodePort: {{ .Values.server.service.activeNodePort }} + {{- end }} + - name: https-internal + port: 8201 + targetPort: 8201 + {{- if .Values.server.service.extraPorts -}} + {{ toYaml .Values.server.service.extraPorts | nindent 4}} + {{- end }} + selector: + app.kubernetes.io/name: {{ include "openbao.name" . }} + {{- if eq (.Values.server.service.instanceSelector.enabled | toString) "true" }} + app.kubernetes.io/instance: {{ .Release.Name }} + {{- end }} + component: server + openbao-active: "true" +{{- end }} +{{- end }} +{{- end }} +{{- end }} diff --git a/packages/system/openbao/charts/openbao/templates/server-ha-standby-service.yaml b/packages/system/openbao/charts/openbao/templates/server-ha-standby-service.yaml new file mode 100644 index 00000000..f9ac4d42 --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/server-ha-standby-service.yaml @@ -0,0 +1,67 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{ template "openbao.mode" . }} +{{- if ne .mode "external" }} +{{- template "openbao.serverServiceEnabled" . -}} +{{- if .serverServiceEnabled -}} +{{- if eq .mode "ha" }} +{{- if eq (.Values.server.service.standby.enabled | toString) "true" }} +# Service for standby OpenBao pod +apiVersion: v1 +kind: Service +metadata: + name: {{ template "openbao.fullname" . }}-standby + namespace: {{ include "openbao.namespace" . }} + labels: + helm.sh/chart: {{ include "openbao.chart" . }} + app.kubernetes.io/name: {{ include "openbao.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + {{- if .Values.server.service.standby.extraLabels -}} + {{- toYaml .Values.server.service.standby.extraLabels | nindent 4 -}} + {{- end -}} +{{- template "openbao.service.standby.annotations" . }} +spec: + {{- if .Values.server.service.type}} + type: {{ .Values.server.service.type }} + {{- end}} + {{- if (semverCompare ">= 1.23-0" .Capabilities.KubeVersion.Version) }} + {{- if .Values.server.service.ipFamilyPolicy }} + ipFamilyPolicy: {{ .Values.server.service.ipFamilyPolicy }} + {{- end }} + {{- if .Values.server.service.ipFamilies }} + ipFamilies: {{ .Values.server.service.ipFamilies | toYaml | nindent 2 }} + {{- end }} + {{- end }} + {{- if .Values.server.service.clusterIP }} + clusterIP: {{ .Values.server.service.clusterIP }} + {{- end }} + {{- include "service.externalTrafficPolicy" .Values.server.service }} + publishNotReadyAddresses: {{ .Values.server.service.publishNotReadyAddresses }} + ports: + - name: {{ include "openbao.scheme" . }} + port: {{ .Values.server.service.port }} + targetPort: {{ .Values.server.service.targetPort }} + {{- if and (.Values.server.service.standbyNodePort) (eq (.Values.server.service.type | toString) "NodePort") }} + nodePort: {{ .Values.server.service.standbyNodePort }} + {{- end }} + - name: https-internal + port: 8201 + targetPort: 8201 + {{- if .Values.server.service.extraPorts -}} + {{ toYaml .Values.server.service.extraPorts | nindent 4}} + {{- end }} + selector: + app.kubernetes.io/name: {{ include "openbao.name" . }} + {{- if eq (.Values.server.service.instanceSelector.enabled | toString) "true" }} + app.kubernetes.io/instance: {{ .Release.Name }} + {{- end }} + component: server + openbao-active: "false" +{{- end }} +{{- end }} +{{- end }} +{{- end }} diff --git a/packages/system/openbao/charts/openbao/templates/server-headless-service.yaml b/packages/system/openbao/charts/openbao/templates/server-headless-service.yaml new file mode 100644 index 00000000..b3441c8f --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/server-headless-service.yaml @@ -0,0 +1,46 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{ template "openbao.mode" . }} +{{- if ne .mode "external" }} +{{- template "openbao.serverServiceEnabled" . -}} +{{- if .serverServiceEnabled -}} +# Service for OpenBao cluster +apiVersion: v1 +kind: Service +metadata: + name: {{ template "openbao.fullname" . }}-internal + namespace: {{ include "openbao.namespace" . }} + labels: + helm.sh/chart: {{ include "openbao.chart" . }} + app.kubernetes.io/name: {{ include "openbao.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + openbao-internal: "true" +{{ template "openbao.service.annotations" .}} +spec: + {{- if (semverCompare ">= 1.23-0" .Capabilities.KubeVersion.Version) }} + {{- if .Values.server.service.ipFamilyPolicy }} + ipFamilyPolicy: {{ .Values.server.service.ipFamilyPolicy }} + {{- end }} + {{- if .Values.server.service.ipFamilies }} + ipFamilies: {{ .Values.server.service.ipFamilies | toYaml | nindent 2 }} + {{- end }} + {{- end }} + clusterIP: None + publishNotReadyAddresses: true + ports: + - name: "{{ include "openbao.scheme" . }}" + port: {{ .Values.server.service.port }} + targetPort: {{ .Values.server.service.targetPort }} + - name: https-internal + port: 8201 + targetPort: 8201 + selector: + app.kubernetes.io/name: {{ include "openbao.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + component: server +{{- end }} +{{- end }} diff --git a/packages/system/openbao/charts/openbao/templates/server-httproute.yaml b/packages/system/openbao/charts/openbao/templates/server-httproute.yaml new file mode 100644 index 00000000..ab840b42 --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/server-httproute.yaml @@ -0,0 +1,50 @@ +{{ template "openbao.mode" . }} +{{- if ne .mode "external" }} +{{- if .Values.server.gateway.httpRoute.enabled -}} +{{- $serviceName := include "openbao.fullname" . -}} +{{- template "openbao.serverServiceEnabled" . -}} +{{- if .serverServiceEnabled -}} +{{- if and (eq .mode "ha" ) (eq (.Values.server.gateway.httpRoute.activeService | toString) "true") }} +{{- $serviceName = printf "%s-%s" $serviceName "active" -}} +{{- end }} +{{- $servicePort := .Values.server.service.port -}} +apiVersion: {{ .Values.server.gateway.httpRoute.apiVersion }} +kind: HTTPRoute +metadata: + name: {{ template "openbao.fullname" . }} + namespace: {{ include "openbao.namespace" . }} + labels: + helm.sh/chart: {{ include "openbao.chart" . }} + app.kubernetes.io/name: {{ include "openbao.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + {{- with .Values.server.gateway.httpRoute.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- template "openbao.gateway.httpRoute.annotations" . }} +spec: + hostnames: + {{- range .Values.server.gateway.httpRoute.hosts }} + - {{ . | quote }} + {{- end }} + parentRefs: + {{- with .Values.server.gateway.httpRoute.parentRefs }} + {{- toYaml . | nindent 4 }} + {{- end }} + rules: + - backendRefs: + - kind: Service + name: {{ $serviceName }} + port: {{ $servicePort }} + matches: + {{- with .Values.server.gateway.httpRoute.matches.path }} + - path: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.server.gateway.httpRoute.filters }} + filters: + {{- toYaml . | nindent 6 }} + {{- end }} +{{- end }} +{{- end }} +{{- end }} diff --git a/packages/system/openbao/charts/openbao/templates/server-ingress.yaml b/packages/system/openbao/charts/openbao/templates/server-ingress.yaml new file mode 100644 index 00000000..de33c66a --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/server-ingress.yaml @@ -0,0 +1,67 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{ template "openbao.mode" . }} +{{- if ne .mode "external" }} +{{- if .Values.server.ingress.enabled -}} +{{- $extraPaths := .Values.server.ingress.extraPaths -}} +{{- $serviceName := include "openbao.fullname" . -}} +{{- template "openbao.serverServiceEnabled" . -}} +{{- if .serverServiceEnabled -}} +{{- if and (eq .mode "ha" ) (eq (.Values.server.ingress.activeService | toString) "true") }} +{{- $serviceName = printf "%s-%s" $serviceName "active" -}} +{{- end }} +{{- $servicePort := .Values.server.service.port -}} +{{- $pathType := .Values.server.ingress.pathType -}} +{{- $kubeVersion := .Capabilities.KubeVersion.Version }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ template "openbao.fullname" . }} + namespace: {{ include "openbao.namespace" . }} + labels: + helm.sh/chart: {{ include "openbao.chart" . }} + app.kubernetes.io/name: {{ include "openbao.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + {{- with .Values.server.ingress.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- template "openbao.ingress.annotations" . }} +spec: +{{- if .Values.server.ingress.tls }} + tls: + {{- range .Values.server.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} +{{- end }} +{{- if .Values.server.ingress.ingressClassName }} + ingressClassName: {{ .Values.server.ingress.ingressClassName }} +{{- end }} + rules: + {{- range .Values.server.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: +{{ if $extraPaths }} +{{ toYaml $extraPaths | indent 10 }} +{{- end }} + {{- range (.paths | default (list "/")) }} + - path: {{ . }} + pathType: {{ $pathType }} + backend: + service: + name: {{ $serviceName }} + port: + number: {{ $servicePort }} + {{- end }} + {{- end }} +{{- end }} +{{- end }} +{{- end }} diff --git a/packages/system/openbao/charts/openbao/templates/server-network-policy.yaml b/packages/system/openbao/charts/openbao/templates/server-network-policy.yaml new file mode 100644 index 00000000..0891a508 --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/server-network-policy.yaml @@ -0,0 +1,24 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{- if eq (.Values.server.networkPolicy.enabled | toString) "true" }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ template "openbao.fullname" . }} + labels: + app.kubernetes.io/name: {{ template "openbao.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: {{ template "openbao.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + ingress: {{- toYaml .Values.server.networkPolicy.ingress | nindent 4 }} + {{- if .Values.server.networkPolicy.egress }} + egress: + {{- toYaml .Values.server.networkPolicy.egress | nindent 4 }} + {{ end }} +{{ end }} diff --git a/packages/system/openbao/charts/openbao/templates/server-psp-role.yaml b/packages/system/openbao/charts/openbao/templates/server-psp-role.yaml new file mode 100644 index 00000000..bfb71612 --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/server-psp-role.yaml @@ -0,0 +1,25 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{ template "openbao.mode" . }} +{{- if .serverEnabled -}} +{{- if and (ne .mode "") (eq (.Values.global.psp.enable | toString) "true") }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ template "openbao.fullname" . }}-psp + namespace: {{ include "openbao.namespace" . }} + labels: + app.kubernetes.io/name: {{ include "openbao.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +rules: +- apiGroups: ['policy'] + resources: ['podsecuritypolicies'] + verbs: ['use'] + resourceNames: + - {{ template "openbao.fullname" . }} +{{- end }} +{{- end }} diff --git a/packages/system/openbao/charts/openbao/templates/server-psp-rolebinding.yaml b/packages/system/openbao/charts/openbao/templates/server-psp-rolebinding.yaml new file mode 100644 index 00000000..7f8bb975 --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/server-psp-rolebinding.yaml @@ -0,0 +1,26 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{ template "openbao.mode" . }} +{{- if .serverEnabled -}} +{{- if and (ne .mode "") (eq (.Values.global.psp.enable | toString) "true") }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ template "openbao.fullname" . }}-psp + namespace: {{ include "openbao.namespace" . }} + labels: + app.kubernetes.io/name: {{ include "openbao.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +roleRef: + kind: Role + name: {{ template "openbao.fullname" . }}-psp + apiGroup: rbac.authorization.k8s.io +subjects: + - kind: ServiceAccount + name: {{ template "openbao.fullname" . }} +{{- end }} +{{- end }} diff --git a/packages/system/openbao/charts/openbao/templates/server-psp.yaml b/packages/system/openbao/charts/openbao/templates/server-psp.yaml new file mode 100644 index 00000000..d7c396a7 --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/server-psp.yaml @@ -0,0 +1,54 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{ template "openbao.mode" . }} +{{- if .serverEnabled -}} +{{- if and (ne .mode "") (eq (.Values.global.psp.enable | toString) "true") }} +apiVersion: policy/v1beta1 +kind: PodSecurityPolicy +metadata: + name: {{ template "openbao.fullname" . }} + labels: + app.kubernetes.io/name: {{ include "openbao.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- template "openbao.psp.annotations" . }} +spec: + privileged: false + # Required to prevent escalations to root. + allowPrivilegeEscalation: false + volumes: + - configMap + - emptyDir + - projected + - secret + - downwardAPI + {{- if eq (.Values.server.dataStorage.enabled | toString) "true" }} + - persistentVolumeClaim + {{- end }} + hostNetwork: false + hostIPC: false + hostPID: false + runAsUser: + # Require the container to run without root privileges. + rule: MustRunAsNonRoot + seLinux: + # This policy assumes the nodes are using AppArmor rather than SELinux. + rule: RunAsAny + supplementalGroups: + rule: MustRunAs + ranges: + # Forbid adding the root group. + - min: 1 + max: 65535 + fsGroup: + rule: MustRunAs + ranges: + # Forbid adding the root group. + - min: 1 + max: 65535 + readOnlyRootFilesystem: false +{{- end }} +{{- end }} diff --git a/packages/system/openbao/charts/openbao/templates/server-route.yaml b/packages/system/openbao/charts/openbao/templates/server-route.yaml new file mode 100644 index 00000000..4c350d7d --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/server-route.yaml @@ -0,0 +1,39 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{- if .Values.global.openshift }} +{{- if ne .mode "external" }} +{{- if .Values.server.route.enabled -}} +{{- $serviceName := include "openbao.fullname" . -}} +{{- if and (eq .mode "ha" ) (eq (.Values.server.route.activeService | toString) "true") }} +{{- $serviceName = printf "%s-%s" $serviceName "active" -}} +{{- end }} +kind: Route +apiVersion: route.openshift.io/v1 +metadata: + name: {{ template "openbao.fullname" . }} + namespace: {{ include "openbao.namespace" . }} + labels: + helm.sh/chart: {{ include "openbao.chart" . }} + app.kubernetes.io/name: {{ include "openbao.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + {{- with .Values.server.route.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- template "openbao.route.annotations" . }} +spec: + host: {{ .Values.server.route.host }} + to: + kind: Service + name: {{ $serviceName }} + weight: 100 + port: + targetPort: 8200 + tls: + {{- toYaml .Values.server.route.tls | nindent 4 }} +{{- end }} +{{- end }} +{{- end }} diff --git a/packages/system/openbao/charts/openbao/templates/server-service.yaml b/packages/system/openbao/charts/openbao/templates/server-service.yaml new file mode 100644 index 00000000..7f82db8e --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/server-service.yaml @@ -0,0 +1,64 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{ template "openbao.mode" . }} +{{- if ne .mode "external" }} +{{- template "openbao.serverServiceEnabled" . -}} +{{- if .serverServiceEnabled -}} +# Service for OpenBao cluster +apiVersion: v1 +kind: Service +metadata: + name: {{ template "openbao.fullname" . }} + namespace: {{ include "openbao.namespace" . }} + labels: + helm.sh/chart: {{ include "openbao.chart" . }} + app.kubernetes.io/name: {{ include "openbao.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + {{- if .Values.server.service.extraLabels -}} + {{- toYaml .Values.server.service.extraLabels | nindent 4 -}} + {{- end -}} +{{ template "openbao.service.annotations" .}} +spec: + {{- if .Values.server.service.type}} + type: {{ .Values.server.service.type }} + {{- end}} + {{- if (semverCompare ">= 1.23-0" .Capabilities.KubeVersion.Version) }} + {{- if .Values.server.service.ipFamilyPolicy }} + ipFamilyPolicy: {{ .Values.server.service.ipFamilyPolicy }} + {{- end }} + {{- if .Values.server.service.ipFamilies }} + ipFamilies: {{ .Values.server.service.ipFamilies | toYaml | nindent 2 }} + {{- end }} + {{- end }} + {{- if .Values.server.service.clusterIP }} + clusterIP: {{ .Values.server.service.clusterIP }} + {{- end }} + {{- include "service.externalTrafficPolicy" .Values.server.service }} + # We want the servers to become available even if they're not ready + # since this DNS is also used for join operations. + publishNotReadyAddresses: {{ .Values.server.service.publishNotReadyAddresses }} + ports: + - name: {{ include "openbao.scheme" . }} + port: {{ .Values.server.service.port }} + targetPort: {{ .Values.server.service.targetPort }} + {{- if and (.Values.server.service.nodePort) (eq (.Values.server.service.type | toString) "NodePort") }} + nodePort: {{ .Values.server.service.nodePort }} + {{- end }} + - name: https-internal + port: 8201 + targetPort: 8201 + {{- if .Values.server.service.extraPorts -}} + {{ toYaml .Values.server.service.extraPorts | nindent 4}} + {{- end }} + selector: + app.kubernetes.io/name: {{ include "openbao.name" . }} + {{- if eq (.Values.server.service.instanceSelector.enabled | toString) "true" }} + app.kubernetes.io/instance: {{ .Release.Name }} + {{- end }} + component: server +{{- end }} +{{- end }} diff --git a/packages/system/openbao/charts/openbao/templates/server-serviceaccount-secret.yaml b/packages/system/openbao/charts/openbao/templates/server-serviceaccount-secret.yaml new file mode 100644 index 00000000..e9ab3575 --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/server-serviceaccount-secret.yaml @@ -0,0 +1,21 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{ template "openbao.serverServiceAccountSecretCreationEnabled" . }} +{{- if .serverServiceAccountSecretCreationEnabled -}} +apiVersion: v1 +kind: Secret +metadata: + name: {{ template "openbao.serviceAccount.name" . }}-token + namespace: {{ include "openbao.namespace" . }} + annotations: + kubernetes.io/service-account.name: {{ template "openbao.serviceAccount.name" . }} + labels: + helm.sh/chart: {{ include "openbao.chart" . }} + app.kubernetes.io/name: {{ include "openbao.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +type: kubernetes.io/service-account-token +{{ end }} diff --git a/packages/system/openbao/charts/openbao/templates/server-serviceaccount.yaml b/packages/system/openbao/charts/openbao/templates/server-serviceaccount.yaml new file mode 100644 index 00000000..aa615200 --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/server-serviceaccount.yaml @@ -0,0 +1,22 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{ template "openbao.serverServiceAccountEnabled" . }} +{{- if .serverServiceAccountEnabled -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ template "openbao.serviceAccount.name" . }} + namespace: {{ include "openbao.namespace" . }} + labels: + helm.sh/chart: {{ include "openbao.chart" . }} + app.kubernetes.io/name: {{ include "openbao.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + {{- if .Values.server.serviceAccount.extraLabels -}} + {{- toYaml .Values.server.serviceAccount.extraLabels | nindent 4 -}} + {{- end -}} + {{ template "openbao.serviceAccount.annotations" . }} +{{ end }} diff --git a/packages/system/openbao/charts/openbao/templates/server-statefulset.yaml b/packages/system/openbao/charts/openbao/templates/server-statefulset.yaml new file mode 100644 index 00000000..1628d139 --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/server-statefulset.yaml @@ -0,0 +1,228 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{ template "openbao.mode" . }} +{{- if ne .mode "external" }} +{{- if ne .mode "" }} +{{- if .serverEnabled -}} +# StatefulSet to run the actual openbao server cluster. +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ template "openbao.fullname" . }} + namespace: {{ include "openbao.namespace" . }} + labels: + app.kubernetes.io/name: {{ include "openbao.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + {{- template "openbao.statefulSet.annotations" . }} +spec: + serviceName: {{ template "openbao.fullname" . }}-internal + podManagementPolicy: {{ .Values.server.podManagementPolicy }} + replicas: {{ template "openbao.replicas" . }} + updateStrategy: + type: {{ .Values.server.updateStrategyType }} + {{- if and (semverCompare ">= 1.23-0" .Capabilities.KubeVersion.Version) (.Values.server.persistentVolumeClaimRetentionPolicy) }} + persistentVolumeClaimRetentionPolicy: {{ toYaml .Values.server.persistentVolumeClaimRetentionPolicy | nindent 4 }} + {{- end }} + selector: + matchLabels: + app.kubernetes.io/name: {{ template "openbao.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + component: server + template: + metadata: + labels: + helm.sh/chart: {{ template "openbao.chart" . }} + app.kubernetes.io/name: {{ template "openbao.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + component: server + {{- if .Values.server.extraLabels -}} + {{- toYaml .Values.server.extraLabels | nindent 8 -}} + {{- end -}} + {{ template "openbao.annotations" . }} + spec: + {{ template "openbao.affinity" . }} + {{ template "openbao.topologySpreadConstraints" . }} + {{ template "openbao.tolerations" . }} + {{ template "openbao.nodeselector" . }} + {{- if .Values.server.priorityClassName }} + priorityClassName: {{ .Values.server.priorityClassName }} + {{- end }} + terminationGracePeriodSeconds: {{ .Values.server.terminationGracePeriodSeconds }} + serviceAccountName: {{ template "openbao.serviceAccount.name" . }} + {{ if .Values.server.shareProcessNamespace }} + shareProcessNamespace: true + {{ end }} + {{- template "server.statefulSet.securityContext.pod" . }} + {{- if not .Values.global.openshift }} + hostNetwork: {{ .Values.server.hostNetwork }} + {{- end }} + volumes: + {{ template "openbao.volumes" . }} + - name: home + emptyDir: {} + {{- if .Values.server.hostAliases }} + hostAliases: + {{ toYaml .Values.server.hostAliases | nindent 8}} + {{- end }} + {{- if .Values.server.extraInitContainers }} + initContainers: + {{ toYaml .Values.server.extraInitContainers | nindent 8}} + {{- end }} + containers: + - name: openbao + {{ template "openbao.resources" . }} + image: "{{ .Values.server.image.registry | default "docker.io" }}/{{ .Values.server.image.repository }}:{{ .Values.server.image.tag | default (trimPrefix "v" .Chart.AppVersion) }}" + imagePullPolicy: {{ .Values.server.image.pullPolicy }} + command: + - "/bin/sh" + - "-ec" + args: {{ template "openbao.args" . }} + {{- template "server.statefulSet.securityContext.container" . }} + env: + - name: HOST_IP + valueFrom: + fieldRef: + fieldPath: status.hostIP + - name: POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: BAO_K8S_POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: BAO_K8S_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: BAO_ADDR + value: "{{ include "openbao.scheme" . }}://127.0.0.1:8200" + - name: BAO_API_ADDR + {{- if .Values.server.ha.apiAddr }} + value: {{ .Values.server.ha.apiAddr }} + {{- else }} + value: "{{ include "openbao.scheme" . }}://$(POD_IP):8200" + {{- end }} + - name: SKIP_CHOWN + value: "true" + - name: SKIP_SETCAP + value: "true" + - name: HOSTNAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: BAO_CLUSTER_ADDR + {{- if .Values.server.ha.clusterAddr }} + value: {{ .Values.server.ha.clusterAddr | quote }} + {{- else }} + value: "https://$(HOSTNAME).{{ template "openbao.fullname" . }}-internal:8201" + {{- end }} + {{- if and (eq (.Values.server.ha.raft.enabled | toString) "true") (eq (.Values.server.ha.raft.setNodeId | toString) "true") }} + - name: BAO_RAFT_NODE_ID + valueFrom: + fieldRef: + fieldPath: metadata.name + {{- end }} + - name: HOME + value: "/home/openbao" + {{- if .Values.server.logLevel }} + - name: BAO_LOG_LEVEL + value: "{{ .Values.server.logLevel }}" + {{- end }} + {{- if .Values.server.logFormat }} + - name: BAO_LOG_FORMAT + value: "{{ .Values.server.logFormat }}" + {{- end }} + {{ template "openbao.envs" . }} + {{- include "openbao.extraSecretEnvironmentVars" .Values.server | nindent 12 }} + {{- include "openbao.extraEnvironmentVars" .Values.server | nindent 12 }} + volumeMounts: + {{ template "openbao.mounts" . }} + - name: home + mountPath: /home/openbao + ports: + - containerPort: 8200 + name: {{ include "openbao.scheme" . }} + - containerPort: 8201 + name: https-internal + - containerPort: 8202 + name: {{ include "openbao.scheme" . }}-rep + {{- if .Values.server.extraPorts -}} + {{ toYaml .Values.server.extraPorts | nindent 12}} + {{- end }} + {{- if .Values.server.readinessProbe.enabled }} + readinessProbe: + {{- if .Values.server.readinessProbe.path }} + httpGet: + path: {{ .Values.server.readinessProbe.path | quote }} + port: {{ .Values.server.readinessProbe.port }} + scheme: {{ include "openbao.scheme" . | upper }} + {{- else }} + # Check status; unsealed openbao servers return 0 + # The exit code reflects the seal status: + # 0 - unsealed + # 1 - error + # 2 - sealed + exec: + command: ["/bin/sh", "-ec", "bao status -tls-skip-verify"] + {{- end }} + failureThreshold: {{ .Values.server.readinessProbe.failureThreshold }} + initialDelaySeconds: {{ .Values.server.readinessProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.server.readinessProbe.periodSeconds }} + successThreshold: {{ .Values.server.readinessProbe.successThreshold }} + timeoutSeconds: {{ .Values.server.readinessProbe.timeoutSeconds }} + {{- end }} + {{- if .Values.server.livenessProbe.enabled }} + livenessProbe: + {{- if .Values.server.livenessProbe.execCommand }} + exec: + command: + {{- range (.Values.server.livenessProbe.execCommand) }} + - {{ . | quote }} + {{- end }} + {{- else }} + httpGet: + path: {{ .Values.server.livenessProbe.path | quote }} + port: {{ .Values.server.livenessProbe.port }} + scheme: {{ include "openbao.scheme" . | upper }} + {{- end }} + failureThreshold: {{ .Values.server.livenessProbe.failureThreshold }} + initialDelaySeconds: {{ .Values.server.livenessProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.server.livenessProbe.periodSeconds }} + successThreshold: {{ .Values.server.livenessProbe.successThreshold }} + timeoutSeconds: {{ .Values.server.livenessProbe.timeoutSeconds }} + {{- end }} + lifecycle: + # openbao container doesn't receive SIGTERM from Kubernetes + # and after the grace period ends, Kube sends SIGKILL. This + # causes issues with graceful shutdowns such as deregistering itself + # from Consul (zombie services). + preStop: + exec: + command: [ + "/bin/sh", "-c", + # Adding a sleep here to give the pod eviction a + # chance to propagate, so requests will not be made + # to this pod while it's terminating + "sleep {{ .Values.server.preStopSleepSeconds }} && kill -SIGTERM $(pidof bao)", + ] + {{- if .Values.server.postStart }} + postStart: + exec: + command: + {{- range (.Values.server.postStart) }} + - {{ . | quote }} + {{- end }} + {{- end }} + {{- if .Values.server.extraContainers }} + {{ toYaml .Values.server.extraContainers | nindent 8}} + {{- end }} + {{- include "imagePullSecrets" . | nindent 6 }} + {{ template "openbao.volumeclaims" . }} +{{ end }} +{{ end }} +{{ end }} diff --git a/packages/system/openbao/charts/openbao/templates/server-tlsroute.yaml b/packages/system/openbao/charts/openbao/templates/server-tlsroute.yaml new file mode 100644 index 00000000..14bd0d34 --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/server-tlsroute.yaml @@ -0,0 +1,41 @@ +{{ template "openbao.mode" . }} +{{- if ne .mode "external" }} +{{- if .Values.server.gateway.tlsRoute.enabled -}} +{{- $serviceName := include "openbao.fullname" . -}} +{{- template "openbao.serverServiceEnabled" . -}} +{{- if .serverServiceEnabled -}} +{{- if and (eq .mode "ha" ) (eq (.Values.server.gateway.tlsRoute.activeService | toString) "true") }} +{{- $serviceName = printf "%s-%s" $serviceName "active" -}} +{{- end }} +{{- $servicePort := .Values.server.service.port -}} +{{- $kubeVersion := .Capabilities.KubeVersion.Version }} +apiVersion: {{ .Values.server.gateway.tlsRoute.apiVersion }} +kind: TLSRoute +metadata: + name: {{ template "openbao.fullname" . }} + namespace: {{ include "openbao.namespace" . }} + labels: + helm.sh/chart: {{ include "openbao.chart" . }} + app.kubernetes.io/name: {{ include "openbao.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + {{- with .Values.server.gateway.tlsRoute.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- template "openbao.gateway.tlsRoute.annotations" . }} +spec: + hostnames: + {{- range .Values.server.gateway.tlsRoute.hosts }} + - {{ . | quote }} + {{- end }} + parentRefs: + {{- with .Values.server.gateway.tlsRoute.parentRefs }} + {{- toYaml . | nindent 4 }} + {{- end }} + rules: + - backendRefs: + - name: {{ $serviceName }} + port: {{ $servicePort }} +{{- end }} +{{- end }} +{{- end }} diff --git a/packages/system/openbao/charts/openbao/templates/snapshotagent-configmap.yaml b/packages/system/openbao/charts/openbao/templates/snapshotagent-configmap.yaml new file mode 100644 index 00000000..60be5505 --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/snapshotagent-configmap.yaml @@ -0,0 +1,31 @@ + +--- +{{- if .Values.snapshotAgent.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ template "openbao.fullname" . }}-snapshot + namespace: {{ include "openbao.namespace" . }} + labels: + helm.sh/chart: {{ include "openbao.chart" . }} + app.kubernetes.io/name: {{ include "openbao.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +data: + S3_HOST: {{ .Values.snapshotAgent.config.s3Host }} + S3_BUCKET: {{ .Values.snapshotAgent.config.s3Bucket }} + {{- if .Values.snapshotAgent.config.s3cmdExtraFlag }} + S3CMD_EXTRA_FLAG: {{ .Values.snapshotAgent.config.s3cmdExtraFlag }} + {{- end }} + S3_URI: {{ .Values.snapshotAgent.config.s3Uri }} + S3_EXPIRE_DAYS: {{ .Values.snapshotAgent.config.s3ExpireDays | quote }} + BAO_AUTH_PATH: {{ .Values.snapshotAgent.config.baoAuthPath }} + BAO_ROLE: {{ .Values.snapshotAgent.config.baoRole }} + {{- if include "openbao.externalAddr" . }} + BAO_ADDR: "{{ include "openbao.externalAddr" . }}" + {{- else if and (eq .mode "ha") (eq (.Values.server.service.active.enabled | toString) "true")}} + BAO_ADDR: {{ include "openbao.scheme" . }}://{{ template "openbao.fullname" . }}-active.{{ include "openbao.namespace" . }}.svc:{{ .Values.server.service.port }} + {{- else }} + BAO_ADDR: {{ include "openbao.scheme" . }}://{{ template "openbao.fullname" . }}.{{ include "openbao.namespace" . }}.svc:{{ .Values.server.service.port }} + {{- end }} +{{ end }} diff --git a/packages/system/openbao/charts/openbao/templates/snapshotagent-cronjob.yaml b/packages/system/openbao/charts/openbao/templates/snapshotagent-cronjob.yaml new file mode 100644 index 00000000..e1efadc0 --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/snapshotagent-cronjob.yaml @@ -0,0 +1,66 @@ +{{- if .Values.snapshotAgent.enabled }} +--- +apiVersion: batch/v1 +kind: CronJob +metadata: + labels: + app.kubernetes.io/name: {{ include "openbao.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + {{- template "openbao.snapshotAgent.annotations" . }} + name: {{ template "openbao.fullname" . }}-snapshot + namespace: {{ include "openbao.namespace" . }} +spec: + schedule: {{ .Values.snapshotAgent.schedule | quote }} + jobTemplate: + metadata: + labels: + app.kubernetes.io/name: {{ include "openbao.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + component: snapshot-agent + spec: + template: + metadata: + labels: + app.kubernetes.io/name: {{ include "openbao.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + component: snapshot-agent + spec: + restartPolicy: {{ .Values.snapshotAgent.restartPolicy }} + serviceAccountName: {{ template "openbao.snapshotAgent.serviceAccount.name" . }} + {{ template "snapshotAgent.securityContext.pod" .}} + containers: + - name: bao-snapshot + envFrom: + - configMapRef: + name: {{ template "openbao.fullname" . }}-snapshot + env: + - name: AWS_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + key: AWS_SECRET_ACCESS_KEY + name: {{ .Values.snapshotAgent.s3CredentialsSecret }} + - name: AWS_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + key: AWS_ACCESS_KEY_ID + name: {{ .Values.snapshotAgent.s3CredentialsSecret }} + {{- include "openbao.extraSecretEnvironmentVars" .Values.snapshotAgent | nindent 12 }} + {{- include "openbao.extraEnvironmentVars" .Values.snapshotAgent | nindent 12 }} + image: {{ .Values.snapshotAgent.image.repository }}:{{ .Values.snapshotAgent.image.tag }} + {{ template "openbao.snapshotAgent.resources". }} + {{ template "snapshotAgent.securityContext.container" .}} + volumeMounts: + - name: snapshot-dir + mountPath: /bao-snapshots + {{- with .Values.snapshotAgent.extraVolumeMounts }} + {{- toYaml . | nindent 14 }} + {{- end }} + imagePullPolicy: IfNotPresent + volumes: + - name: snapshot-dir + emptyDir: {} + {{- if .Values.snapshotAgent.extraVolumes }} + {{- toYaml .Values.snapshotAgent.extraVolumes | nindent 10 }} + {{- end }} +{{ end }} diff --git a/packages/system/openbao/charts/openbao/templates/snapshotagent-serviceaccount.yaml b/packages/system/openbao/charts/openbao/templates/snapshotagent-serviceaccount.yaml new file mode 100644 index 00000000..25fa3cb9 --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/snapshotagent-serviceaccount.yaml @@ -0,0 +1,16 @@ +{{- if and .Values.snapshotAgent.enabled .Values.snapshotAgent.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ template "openbao.snapshotAgent.serviceAccount.name" . }} + namespace: {{ include "openbao.namespace" . }} + labels: + helm.sh/chart: {{ include "openbao.chart" . }} + app.kubernetes.io/name: {{ include "openbao.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + {{- if .Values.snapshotAgent.serviceAccount.extraLabels -}} + {{- toYaml .Values.snapshotAgent.serviceAccount.extraLabels | nindent 4 -}} + {{- end -}} + {{ template "openbao.snapshotAgent.serviceAccount.annotations" . }} +{{ end }} diff --git a/packages/system/openbao/charts/openbao/templates/tests/server-test.yaml b/packages/system/openbao/charts/openbao/templates/tests/server-test.yaml new file mode 100644 index 00000000..8c42752e --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/tests/server-test.yaml @@ -0,0 +1,56 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{ template "openbao.mode" . }} +{{- if ne .mode "external" }} +{{- if .serverEnabled -}} +apiVersion: v1 +kind: Pod +metadata: + name: {{ template "openbao.fullname" . }}-server-test + namespace: {{ include "openbao.namespace" . }} + annotations: + "helm.sh/hook": test +spec: + {{- include "imagePullSecrets" . | nindent 2 }} + containers: + - name: {{ .Release.Name }}-server-test + image: {{ .Values.server.image.registry | default "docker.io" }}/{{ .Values.server.image.repository }}:{{ .Values.server.image.tag | default (trimPrefix "v" .Chart.AppVersion) }} + imagePullPolicy: {{ .Values.server.image.pullPolicy }} + env: + - name: VAULT_ADDR + value: {{ include "openbao.scheme" . }}://{{ template "openbao.fullname" . }}.{{ include "openbao.namespace" . }}.svc:{{ .Values.server.service.port }} + {{- include "openbao.extraEnvironmentVars" .Values.server | nindent 8 }} + command: + - /bin/sh + - -c + - | + echo "Checking for sealed info in 'bao status' output" + ATTEMPTS=10 + n=0 + until [ "$n" -ge $ATTEMPTS ] + do + echo "Attempt" $n... + bao status -format yaml | grep -E '^sealed: (true|false)' && break + n=$((n+1)) + sleep 5 + done + if [ $n -ge $ATTEMPTS ]; then + echo "timed out looking for sealed info in 'bao status' output" + exit 1 + fi + + exit 0 + volumeMounts: + {{- if .Values.server.volumeMounts }} + {{- toYaml .Values.server.volumeMounts | nindent 8}} + {{- end }} + volumes: + {{- if .Values.server.volumes }} + {{- toYaml .Values.server.volumes | nindent 4}} + {{- end }} + restartPolicy: Never +{{- end }} +{{- end }} diff --git a/packages/system/openbao/charts/openbao/templates/ui-service.yaml b/packages/system/openbao/charts/openbao/templates/ui-service.yaml new file mode 100644 index 00000000..17ec0012 --- /dev/null +++ b/packages/system/openbao/charts/openbao/templates/ui-service.yaml @@ -0,0 +1,53 @@ +{{/* +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 +*/}} + +{{ template "openbao.mode" . }} +{{- if ne .mode "external" }} +{{- template "openbao.uiEnabled" . -}} +{{- if .uiEnabled -}} + +apiVersion: v1 +kind: Service +metadata: + name: {{ template "openbao.fullname" . }}-ui + namespace: {{ include "openbao.namespace" . }} + labels: + helm.sh/chart: {{ include "openbao.chart" . }} + app.kubernetes.io/name: {{ include "openbao.name" . }}-ui + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + {{- if .Values.ui.extraLabels -}} + {{- toYaml .Values.ui.extraLabels | nindent 4 -}} + {{- end -}} + {{- template "openbao.ui.annotations" . }} +spec: + {{- if (semverCompare ">= 1.23-0" .Capabilities.KubeVersion.Version) }} + {{- if .Values.ui.serviceIPFamilyPolicy }} + ipFamilyPolicy: {{ .Values.ui.serviceIPFamilyPolicy }} + {{- end }} + {{- if .Values.ui.serviceIPFamilies }} + ipFamilies: {{ .Values.ui.serviceIPFamilies | toYaml | nindent 2 }} + {{- end }} + {{- end }} + selector: + app.kubernetes.io/name: {{ include "openbao.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + component: server + {{- if and (.Values.ui.activeOpenbaoPodOnly) (eq .mode "ha") }} + openbao-active: "true" + {{- end }} + publishNotReadyAddresses: {{ .Values.ui.publishNotReadyAddresses }} + ports: + - name: {{ include "openbao.scheme" . }} + port: {{ .Values.ui.externalPort }} + targetPort: {{ .Values.ui.targetPort }} + {{- if .Values.ui.serviceNodePort }} + nodePort: {{ .Values.ui.serviceNodePort }} + {{- end }} + type: {{ .Values.ui.serviceType }} + {{- include "service.externalTrafficPolicy" .Values.ui }} + {{- include "service.loadBalancer" .Values.ui }} +{{- end -}} +{{- end }} diff --git a/packages/system/openbao/charts/openbao/values.openshift.yaml b/packages/system/openbao/charts/openbao/values.openshift.yaml new file mode 100644 index 00000000..964a81b0 --- /dev/null +++ b/packages/system/openbao/charts/openbao/values.openshift.yaml @@ -0,0 +1,30 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +# These overrides are appropriate defaults for deploying this chart on OpenShift + +global: + openshift: true + +injector: + image: + repository: "registry.connect.redhat.com/hashicorp/vault-k8s" + tag: "1.3.1-ubi" + + agentImage: + registry: "quay.io" + repository: "openbao/openbao-ubi" + tag: "2.3.2" + # Use UBI-based OpenBao image for better OpenShift compatibility + # See: https://openbao.org/docs/install/#container-registries + +server: + image: + registry: "quay.io" + repository: "openbao/openbao-ubi" + tag: "2.3.2" + # Use UBI-based OpenBao image for better OpenShift compatibility + # See: https://openbao.org/docs/install/#container-registries + + readinessProbe: + path: "/v1/sys/health?uninitcode=204" diff --git a/packages/system/openbao/charts/openbao/values.schema.json b/packages/system/openbao/charts/openbao/values.schema.json new file mode 100644 index 00000000..75298366 --- /dev/null +++ b/packages/system/openbao/charts/openbao/values.schema.json @@ -0,0 +1,1207 @@ +{ + "$schema": "http://json-schema.org/schema#", + "type": "object", + "properties": { + "csi": { + "type": "object", + "properties": { + "agent": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "extraArgs": { + "type": "array" + }, + "image": { + "type": "object", + "properties": { + "pullPolicy": { + "type": "string" + }, + "repository": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, + "logFormat": { + "type": "string" + }, + "logLevel": { + "type": "string" + }, + "resources": { + "type": "object" + } + } + }, + "daemonSet": { + "type": "object", + "properties": { + "annotations": { + "type": [ + "object", + "string" + ] + }, + "extraLabels": { + "type": "object" + }, + "kubeletRootDir": { + "type": "string" + }, + "providersDir": { + "type": "string" + }, + "securityContext": { + "type": "object", + "properties": { + "container": { + "type": [ + "object", + "string" + ] + }, + "pod": { + "type": [ + "object", + "string" + ] + } + } + }, + "updateStrategy": { + "type": "object", + "properties": { + "maxUnavailable": { + "type": "string" + }, + "type": { + "type": "string" + } + } + } + } + }, + "debug": { + "type": "boolean" + }, + "enabled": { + "type": [ + "boolean", + "string" + ] + }, + "extraArgs": { + "type": "array" + }, + "image": { + "type": "object", + "properties": { + "pullPolicy": { + "type": "string" + }, + "repository": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, + "livenessProbe": { + "type": "object", + "properties": { + "failureThreshold": { + "type": "integer" + }, + "initialDelaySeconds": { + "type": "integer" + }, + "periodSeconds": { + "type": "integer" + }, + "successThreshold": { + "type": "integer" + }, + "timeoutSeconds": { + "type": "integer" + } + } + }, + "pod": { + "type": "object", + "properties": { + "affinity": { + "type": [ + "null", + "object", + "string" + ] + }, + "annotations": { + "type": [ + "object", + "string" + ] + }, + "extraLabels": { + "type": "object" + }, + "nodeSelector": { + "type": [ + "null", + "object", + "string" + ] + }, + "tolerations": { + "type": [ + "null", + "array", + "string" + ] + } + } + }, + "priorityClassName": { + "type": "string" + }, + "readinessProbe": { + "type": "object", + "properties": { + "failureThreshold": { + "type": "integer" + }, + "initialDelaySeconds": { + "type": "integer" + }, + "periodSeconds": { + "type": "integer" + }, + "successThreshold": { + "type": "integer" + }, + "timeoutSeconds": { + "type": "integer" + } + } + }, + "resources": { + "type": "object" + }, + "serviceAccount": { + "type": "object", + "properties": { + "annotations": { + "type": [ + "object", + "string" + ] + }, + "extraLabels": { + "type": "object" + } + } + }, + "volumeMounts": { + "type": [ + "null", + "array" + ] + }, + "volumes": { + "type": [ + "null", + "array" + ] + } + } + }, + "global": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "namespace": { + "type": "string" + }, + "externalVaultAddr": { + "type": "string" + }, + "externalBaoAddr": { + "type": "string" + }, + "imagePullSecrets": { + "type": "array" + }, + "openshift": { + "type": "boolean" + }, + "psp": { + "type": "object", + "properties": { + "annotations": { + "type": [ + "object", + "string" + ] + }, + "enable": { + "type": "boolean" + } + } + }, + "tlsDisable": { + "type": "boolean" + } + } + }, + "injector": { + "type": "object", + "properties": { + "affinity": { + "type": [ + "object", + "string" + ] + }, + "agentDefaults": { + "type": "object", + "properties": { + "cpuLimit": { + "type": "string" + }, + "cpuRequest": { + "type": "string" + }, + "memLimit": { + "type": "string" + }, + "memRequest": { + "type": "string" + }, + "ephemeralLimit": { + "type": "string" + }, + "ephemeralRequest": { + "type": "string" + }, + "template": { + "type": "string" + }, + "templateConfig": { + "type": "object", + "properties": { + "exitOnRetryFailure": { + "type": "boolean" + }, + "staticSecretRenderInterval": { + "type": "string" + } + } + } + } + }, + "agentImage": { + "type": "object", + "properties": { + "repository": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, + "annotations": { + "type": [ + "object", + "string" + ] + }, + "authPath": { + "type": "string" + }, + "certs": { + "type": "object", + "properties": { + "caBundle": { + "type": "string" + }, + "certName": { + "type": "string" + }, + "keyName": { + "type": "string" + }, + "secretName": { + "type": [ + "null", + "string" + ] + } + } + }, + "enabled": { + "type": [ + "boolean", + "string" + ] + }, + "externalVaultAddr": { + "type": "string" + }, + "extraEnvironmentVars": { + "type": "object" + }, + "extraLabels": { + "type": "object" + }, + "failurePolicy": { + "type": "string" + }, + "hostNetwork": { + "type": "boolean" + }, + "image": { + "type": "object", + "properties": { + "pullPolicy": { + "type": "string" + }, + "repository": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, + "leaderElector": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "logFormat": { + "type": "string" + }, + "logLevel": { + "type": "string" + }, + "metrics": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "namespaceSelector": { + "type": "object" + }, + "nodeSelector": { + "type": [ + "null", + "object", + "string" + ] + }, + "objectSelector": { + "type": [ + "object", + "string" + ] + }, + "podDisruptionBudget": { + "type": "object" + }, + "port": { + "type": "integer" + }, + "priorityClassName": { + "type": "string" + }, + "replicas": { + "type": "integer" + }, + "resources": { + "type": "object" + }, + "revokeOnShutdown": { + "type": "boolean" + }, + "securityContext": { + "type": "object", + "properties": { + "container": { + "type": [ + "object", + "string" + ] + }, + "pod": { + "type": [ + "object", + "string" + ] + } + } + }, + "service": { + "type": "object", + "properties": { + "annotations": { + "type": [ + "object", + "string" + ] + } + } + }, + "serviceAccount": { + "type": "object", + "properties": { + "annotations": { + "type": [ + "object", + "string" + ] + } + } + }, + "strategy": { + "type": [ + "object", + "string" + ] + }, + "tolerations": { + "type": [ + "null", + "array", + "string" + ] + }, + "topologySpreadConstraints": { + "type": [ + "null", + "array", + "string" + ] + }, + "webhook": { + "type": "object", + "properties": { + "annotations": { + "type": [ + "object", + "string" + ] + }, + "failurePolicy": { + "type": "string" + }, + "matchPolicy": { + "type": "string" + }, + "namespaceSelector": { + "type": "object" + }, + "objectSelector": { + "type": [ + "object", + "string" + ] + }, + "timeoutSeconds": { + "type": "integer" + } + } + }, + "webhookAnnotations": { + "type": [ + "object", + "string" + ] + } + } + }, + "server": { + "type": "object", + "properties": { + "affinity": { + "type": [ + "object", + "string" + ] + }, + "annotations": { + "type": [ + "object", + "string" + ] + }, + "auditStorage": { + "type": "object", + "properties": { + "accessMode": { + "type": "string" + }, + "annotations": { + "type": [ + "object", + "string" + ] + }, + "labels": { + "type": [ + "object", + "string" + ] + }, + "enabled": { + "type": [ + "boolean", + "string" + ] + }, + "mountPath": { + "type": "string" + }, + "size": { + "type": "string" + }, + "storageClass": { + "type": [ + "null", + "string" + ] + } + } + }, + "authDelegator": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "dataStorage": { + "type": "object", + "properties": { + "accessMode": { + "type": "string" + }, + "annotations": { + "type": [ + "object", + "string" + ] + }, + "labels": { + "type": [ + "object", + "string" + ] + }, + "enabled": { + "type": [ + "boolean", + "string" + ] + }, + "mountPath": { + "type": "string" + }, + "size": { + "type": "string" + }, + "storageClass": { + "type": [ + "null", + "string" + ] + } + } + }, + "persistentVolumeClaimRetentionPolicy": { + "type": "object", + "properties": { + "whenDeleted": { + "type": "string" + }, + "whenScaled": { + "type": "string" + } + } + }, + "dev": { + "type": "object", + "properties": { + "devRootToken": { + "type": "string" + }, + "enabled": { + "type": "boolean" + } + } + }, + "enabled": { + "type": [ + "boolean", + "string" + ] + }, + "extraArgs": { + "type": "string" + }, + "extraPorts": { + "type": [ + "null", + "array" + ] + }, + "extraContainers": { + "type": [ + "null", + "array" + ] + }, + "extraEnvironmentVars": { + "type": "object" + }, + "extraInitContainers": { + "type": [ + "null", + "array" + ] + }, + "extraLabels": { + "type": "object" + }, + "extraSecretEnvironmentVars": { + "type": "array" + }, + "extraVolumes": { + "type": "array" + }, + "ha": { + "type": "object", + "properties": { + "apiAddr": { + "type": [ + "null", + "string" + ] + }, + "clusterAddr": { + "type": [ + "null", + "string" + ] + }, + "config": { + "type": [ + "string", + "object" + ] + }, + "disruptionBudget": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "maxUnavailable": { + "type": [ + "null", + "integer" + ] + } + } + }, + "enabled": { + "type": "boolean" + }, + "raft": { + "type": "object", + "properties": { + "config": { + "type": [ + "string", + "object" + ] + }, + "enabled": { + "type": "boolean" + }, + "setNodeId": { + "type": "boolean" + } + } + }, + "replicas": { + "type": "integer" + } + } + }, + "hostAliases": { + "type": "array" + }, + "image": { + "type": "object", + "properties": { + "pullPolicy": { + "type": "string" + }, + "repository": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, + "ingress": { + "type": "object", + "properties": { + "activeService": { + "type": "boolean" + }, + "annotations": { + "type": [ + "object", + "string" + ] + }, + "enabled": { + "type": "boolean" + }, + "extraPaths": { + "type": "array" + }, + "hosts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "host": { + "type": "string" + }, + "paths": { + "type": "array" + } + } + } + }, + "ingressClassName": { + "type": "string" + }, + "labels": { + "type": "object" + }, + "pathType": { + "type": "string" + }, + "tls": { + "type": "array" + } + } + }, + "livenessProbe": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "failureThreshold": { + "type": "integer" + }, + "initialDelaySeconds": { + "type": "integer" + }, + "path": { + "type": "string" + }, + "port": { + "type": "integer" + }, + "execCommand": { + "type": "array" + }, + "periodSeconds": { + "type": "integer" + }, + "successThreshold": { + "type": "integer" + }, + "timeoutSeconds": { + "type": "integer" + } + } + }, + "logFormat": { + "type": "string" + }, + "logLevel": { + "type": "string" + }, + "networkPolicy": { + "type": "object", + "properties": { + "egress": { + "type": "array" + }, + "enabled": { + "type": "boolean" + }, + "ingress": { + "type": "array" + } + } + }, + "nodeSelector": { + "type": [ + "null", + "object", + "string" + ] + }, + "postStart": { + "type": "array" + }, + "preStopSleepSeconds": { + "type": "integer" + }, + "priorityClassName": { + "type": "string" + }, + "readinessProbe": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "failureThreshold": { + "type": "integer" + }, + "initialDelaySeconds": { + "type": "integer" + }, + "periodSeconds": { + "type": "integer" + }, + "successThreshold": { + "type": "integer" + }, + "timeoutSeconds": { + "type": "integer" + } + } + }, + "resources": { + "type": "object" + }, + "route": { + "type": "object", + "properties": { + "activeService": { + "type": "boolean" + }, + "annotations": { + "type": [ + "object", + "string" + ] + }, + "enabled": { + "type": "boolean" + }, + "host": { + "type": "string" + }, + "labels": { + "type": "object" + }, + "tls": { + "type": "object" + } + } + }, + "service": { + "type": "object", + "properties": { + "active": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "annotations": { + "type": [ + "object", + "string" + ] + } + } + }, + "annotations": { + "type": [ + "object", + "string" + ] + }, + "enabled": { + "type": "boolean" + }, + "externalTrafficPolicy": { + "type": "string" + }, + "instanceSelector": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "port": { + "type": "integer" + }, + "publishNotReadyAddresses": { + "type": "boolean" + }, + "standby": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "annotations": { + "type": [ + "object", + "string" + ] + } + } + }, + "targetPort": { + "type": "integer" + }, + "nodePort": { + "type": "integer" + }, + "activeNodePort": { + "type": "integer" + }, + "standbyNodePort": { + "type": "integer" + }, + "ipFamilyPolicy": { + "type": "string" + }, + "ipFamilies": { + "type": [ + "array" + ] + } + } + }, + "serviceAccount": { + "type": "object", + "properties": { + "annotations": { + "type": [ + "object", + "string" + ] + }, + "create": { + "type": "boolean" + }, + "extraLabels": { + "type": "object" + }, + "createSecret": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "serviceDiscovery": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + } + } + } + } + }, + "shareProcessNamespace": { + "type": "boolean" + }, + "standalone": { + "type": "object", + "properties": { + "config": { + "type": [ + "string", + "object" + ] + }, + "enabled": { + "type": [ + "string", + "boolean" + ] + } + } + }, + "statefulSet": { + "type": "object", + "properties": { + "annotations": { + "type": [ + "object", + "string" + ] + }, + "securityContext": { + "type": "object", + "properties": { + "container": { + "type": [ + "object", + "string" + ] + }, + "pod": { + "type": [ + "object", + "string" + ] + } + } + } + } + }, + "terminationGracePeriodSeconds": { + "type": "integer" + }, + "tolerations": { + "type": [ + "null", + "array", + "string" + ] + }, + "topologySpreadConstraints": { + "type": [ + "null", + "array", + "string" + ] + }, + "updateStrategyType": { + "type": "string" + }, + "volumeMounts": { + "type": [ + "null", + "array" + ] + }, + "volumes": { + "type": [ + "null", + "array" + ] + }, + "hostNetwork": { + "type": "boolean" + } + } + }, + "serverTelemetry": { + "type": "object", + "properties": { + "prometheusRules": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "rules": { + "type": "array" + }, + "selectors": { + "type": "object" + } + } + } + } + }, + "ui": { + "type": "object", + "properties": { + "activeOpenbaoPodOnly": { + "type": "boolean" + }, + "annotations": { + "type": [ + "object", + "string" + ] + }, + "enabled": { + "type": [ + "boolean", + "string" + ] + }, + "externalPort": { + "type": "integer" + }, + "externalTrafficPolicy": { + "type": "string" + }, + "publishNotReadyAddresses": { + "type": "boolean" + }, + "serviceNodePort": { + "type": [ + "null", + "integer" + ] + }, + "serviceType": { + "type": "string" + }, + "targetPort": { + "type": "integer" + }, + "serviceIPFamilyPolicy": { + "type": [ + "string" + ] + }, + "serviceIPFamilies": { + "type": [ + "array" + ] + } + } + } + } +} diff --git a/packages/system/openbao/charts/openbao/values.yaml b/packages/system/openbao/charts/openbao/values.yaml new file mode 100644 index 00000000..7e2f604f --- /dev/null +++ b/packages/system/openbao/charts/openbao/values.yaml @@ -0,0 +1,1583 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +# Available parameters and their default values for the OpenBao chart. + +global: + # -- enabled is the master enabled switch. Setting this to true or false + # will enable or disable all the components within this chart by default. + enabled: true + + # -- The namespace to deploy to. Defaults to the `helm` installation namespace. + namespace: "" + + # -- Image pull secret to use for registry authentication. + # Alternatively, the value may be specified as an array of strings. + imagePullSecrets: [] + # imagePullSecrets: + # - name: image-pull-secret + + # -- TLS for end-to-end encrypted transport + tlsDisable: true + + # -- External openbao server address for the injector and CSI provider to use. + # Setting this will disable deployment of a openbao server. + externalBaoAddr: "" + # -- Deprecated: Please use global.externalBaoAddr instead. + externalVaultAddr: "" + + # -- If deploying to OpenShift + openshift: false + + # -- Create PodSecurityPolicy for pods + psp: + enable: false + # -- Annotation for PodSecurityPolicy. + # This is a multi-line templated string map, and can also be set as YAML. + annotations: | + seccomp.security.alpha.kubernetes.io/allowedProfileNames: docker/default,runtime/default + apparmor.security.beta.kubernetes.io/allowedProfileNames: runtime/default + seccomp.security.alpha.kubernetes.io/defaultProfileName: runtime/default + apparmor.security.beta.kubernetes.io/defaultProfileName: runtime/default + + serverTelemetry: + # -- Enable integration with the Prometheus Operator + # See the top level serverTelemetry section below before enabling this feature. + prometheusOperator: false + +injector: + # -- True if you want to enable openbao agent injection. @default: global.enabled + enabled: "-" + + replicas: 1 + + # -- Configures the port the injector should listen on + port: 8080 + + # -- If multiple replicas are specified, by default a leader will be determined + # so that only one injector attempts to create TLS certificates. + leaderElector: + enabled: true + + # -- If true, will enable a node exporter metrics endpoint at /metrics. + metrics: + enabled: false + + # -- Deprecated: Please use global.externalBaoAddr instead. + externalVaultAddr: "" + + # image sets the repo and tag of the vault-k8s image to use for the injector. + image: + # -- image registry to use for k8s image + registry: "docker.io" + # -- image repo to use for k8s image + repository: "hashicorp/vault-k8s" + # -- image tag to use for k8s image + tag: "1.7.2" + # -- image pull policy to use for k8s image. if tag is "latest", set to "Always" + pullPolicy: IfNotPresent + + # -- agentImage sets the repo and tag of the OpenBao image to use for the OpenBao Agent + # containers. This should be set to the official OpenBao image. OpenBao 1.3.1+ is + # required. + agentImage: + # -- image registry to use for agent image + registry: "quay.io" + # -- image repo to use for agent image + repository: "openbao/openbao" + # -- image tag to use for agent image - defaults to chart appVersion + tag: "" + # -- image pull policy to use for agent image. if tag is "latest", set to "Always" + pullPolicy: IfNotPresent + + # The default values for the injected OpenBao Agent containers. + agentDefaults: + # For more information on configuring resources, see the K8s documentation: + # https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + cpuLimit: "500m" + cpuRequest: "250m" + memLimit: "128Mi" + memRequest: "64Mi" + # ephemeralLimit: "128Mi" + # ephemeralRequest: "64Mi" + + # Default template type for secrets when no custom template is specified. + # Possible values include: "json" and "map". + template: "map" + + # Default values within Agent's template_config stanza. + templateConfig: + exitOnRetryFailure: true + staticSecretRenderInterval: "" + + # Used to define custom livenessProbe settings + livenessProbe: + # -- When a probe fails, Kubernetes will try failureThreshold times before giving up + failureThreshold: 2 + # -- Number of seconds after the container has started before probe initiates + initialDelaySeconds: 5 + # -- How often (in seconds) to perform the probe + periodSeconds: 2 + # -- Minimum consecutive successes for the probe to be considered successful after having failed + successThreshold: 1 + # -- Number of seconds after which the probe times out. + timeoutSeconds: 5 + # Used to define custom readinessProbe settings + readinessProbe: + # -- When a probe fails, Kubernetes will try failureThreshold times before giving up + failureThreshold: 2 + # -- Number of seconds after the container has started before probe initiates + initialDelaySeconds: 5 + # -- How often (in seconds) to perform the probe + periodSeconds: 2 + # -- Minimum consecutive successes for the probe to be considered successful after having failed + successThreshold: 1 + # -- Number of seconds after which the probe times out. + timeoutSeconds: 5 + # Used to define custom startupProbe settings + startupProbe: + # -- When a probe fails, Kubernetes will try failureThreshold times before giving up + failureThreshold: 12 + # -- Number of seconds after the container has started before probe initiates + initialDelaySeconds: 5 + # -- How often (in seconds) to perform the probe + periodSeconds: 5 + # -- Minimum consecutive successes for the probe to be considered successful after having failed + successThreshold: 1 + # -- Number of seconds after which the probe times out. + timeoutSeconds: 5 + + # Mount Path of the OpenBao Kubernetes Auth Method. + authPath: "auth/kubernetes" + + # -- Configures the log verbosity of the injector. + # Supported log levels include: trace, debug, info, warn, error + logLevel: "info" + + # -- Configures the log format of the injector. Supported log formats: "standard", "json". + logFormat: "standard" + + # Configures all OpenBao Agent sidecars to revoke their token when shutting down + revokeOnShutdown: false + + webhook: + # Configures failurePolicy of the webhook. The "unspecified" default behaviour depends on the + # API Version of the WebHook. + # To block pod creation while the webhook is unavailable, set the policy to `Fail` below. + # See https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy + # + failurePolicy: Ignore + + # matchPolicy specifies the approach to accepting changes based on the rules of + # the MutatingWebhookConfiguration. + # See https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy + # for more details. + # + matchPolicy: Exact + + # timeoutSeconds is the amount of seconds before the webhook request will be ignored + # or fails. + # If it is ignored or fails depends on the failurePolicy + # See https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#timeouts + # for more details. + # + timeoutSeconds: 30 + + # namespaceSelector is the selector for restricting the webhook to only + # specific namespaces. + # See https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-namespaceselector + # for more details. + # Example: + # namespaceSelector: + # matchLabels: + # sidecar-injector: enabled + namespaceSelector: {} + + # objectSelector is the selector for restricting the webhook to only + # specific labels. + # See https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector + # for more details. + # Example: + # objectSelector: + # matchLabels: + # vault-sidecar-injector: enabled + objectSelector: | + matchExpressions: + - key: app.kubernetes.io/name + operator: NotIn + values: + - {{ template "openbao.name" . }}-agent-injector + + # Extra annotations to attach to the webhook + annotations: {} + + # Deprecated: please use 'webhook.failurePolicy' instead + # Configures failurePolicy of the webhook. The "unspecified" default behaviour depends on the + # API Version of the WebHook. + # To block pod creation while webhook is unavailable, set the policy to `Fail` below. + # See https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy + # + failurePolicy: Ignore + + # Deprecated: please use 'webhook.namespaceSelector' instead + # namespaceSelector is the selector for restricting the webhook to only + # specific namespaces. + # See https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-namespaceselector + # for more details. + # Example: + # namespaceSelector: + # matchLabels: + # sidecar-injector: enabled + namespaceSelector: {} + + # Deprecated: please use 'webhook.objectSelector' instead + # objectSelector is the selector for restricting the webhook to only + # specific labels. + # See https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector + # for more details. + # Example: + # objectSelector: + # matchLabels: + # vault-sidecar-injector: enabled + objectSelector: {} + + # Deprecated: please use 'webhook.annotations' instead + # Extra annotations to attach to the webhook + webhookAnnotations: {} + + certs: + # secretName is the name of the secret that has the TLS certificate and + # private key to serve the injector webhook. If this is null, then the + # injector will default to its automatic management mode that will assign + # a service account to the injector to generate its own certificates. + secretName: null + + # caBundle is a base64-encoded PEM-encoded certificate bundle for the CA + # that signed the TLS certificate that the webhook serves. This must be set + # if secretName is non-null unless an external service like cert-manager is + # keeping the caBundle updated. + caBundle: "" + + # certName and keyName are the names of the files within the secret for + # the TLS cert and private key, respectively. These have reasonable + # defaults but can be customized if necessary. + certName: tls.crt + keyName: tls.key + + # Security context for the pod template and the injector container + # The default pod securityContext is: + # runAsNonRoot: true + # runAsGroup: {{ .Values.injector.gid | default 1000 }} + # runAsUser: {{ .Values.injector.uid | default 100 }} + # fsGroup: {{ .Values.injector.gid | default 1000 }} + # and for container is + # allowPrivilegeEscalation: false + # capabilities: + # drop: + # - ALL + securityContext: + pod: {} + container: {} + + resources: {} + # resources: + # requests: + # memory: 256Mi + # cpu: 250m + # limits: + # memory: 256Mi + # cpu: 250m + + # extraEnvironmentVars is a list of extra environment variables to set in the + # injector deployment. + extraEnvironmentVars: {} + # KUBERNETES_SERVICE_HOST: kubernetes.default.svc + + # Affinity Settings for injector pods + # This can either be a multi-line string or YAML matching the PodSpec's affinity field. + # Commenting out or setting as empty the affinity variable, will allow + # deployment of multiple replicas to single node services such as Minikube. + affinity: | + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchLabels: + app.kubernetes.io/name: {{ template "openbao.name" . }}-agent-injector + app.kubernetes.io/instance: "{{ .Release.Name }}" + component: webhook + topologyKey: kubernetes.io/hostname + + # Topology settings for injector pods + # ref: https://kubernetes.io/docs/concepts/workloads/pods/pod-topology-spread-constraints/ + # This should be either a multi-line string or YAML matching the topologySpreadConstraints array + # in a PodSpec. + topologySpreadConstraints: [] + + # Toleration Settings for injector pods + # This should be either a multi-line string or YAML matching the Toleration array + # in a PodSpec. + tolerations: [] + + # nodeSelector labels for server pod assignment, formatted as a multi-line string or YAML map. + # ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector + # Example: + # nodeSelector: + # beta.kubernetes.io/arch: amd64 + nodeSelector: {} + + # Priority class for injector pods + priorityClassName: "" + + # Extra annotations to attach to the injector pods + # This can either be YAML or a YAML-formatted multi-line templated string map + # of the annotations to apply to the injector pods + annotations: {} + + # Extra labels to attach to the agent-injector + # This should be a YAML map of the labels to apply to the injector + extraLabels: {} + + # Should the injector pods run on the host network (useful when using + # an alternate CNI in EKS) + hostNetwork: false + + # Injector service specific config + service: + # Extra annotations to attach to the injector service + annotations: {} + # Extra labels for the service definition. + # This should be a YAML map of the labels to apply to the injector service + extraLabels: {} + + # Injector serviceAccount specific config + serviceAccount: + # Extra annotations to attach to the injector serviceAccount + annotations: {} + + # A disruption budget limits the number of pods of a replicated application + # that are down simultaneously from voluntary disruptions + podDisruptionBudget: {} + # podDisruptionBudget: + # maxUnavailable: 1 + + # strategy for updating the deployment. This can be a multi-line string or a + # YAML map. + strategy: {} + # strategy: | + # rollingUpdate: + # maxSurge: 25% + # maxUnavailable: 25% + # type: RollingUpdate + +server: + # If true, or "-" with global.enabled true, OpenBao server will be installed. + # See openbao.mode in _helpers.tpl for implementation details. + enabled: "-" + + # Resource requests, limits, etc. for the server cluster placement. This + # should map directly to the value of the resources field for a PodSpec. + # By default no direct resource request is made. + + image: + # -- image registry to use for server image + registry: "quay.io" + # -- image repo to use for server image + repository: "openbao/openbao" + # -- image tag to use for server image - defaults to chart appVersion + tag: "" + # -- image pull policy to use for server image. if tag is "latest", set to "Always" + pullPolicy: IfNotPresent + + # Configure the Update Strategy Type for the StatefulSet + # See https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#update-strategies + updateStrategyType: "OnDelete" + + # Configure the pod management policy for the StatefulSet + # See https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#pod-management-policies + podManagementPolicy: "OrderedReady" + + # Configure the logging verbosity for the OpenBao server. + # Supported log levels include: trace, debug, info, warn, error + logLevel: "" + + # Configure the logging format for the OpenBao server. + # Supported log formats include: standard, json + logFormat: "" + + resources: {} + # resources: + # requests: + # memory: 256Mi + # cpu: 250m + # limits: + # memory: 256Mi + # cpu: 250m + + # Ingress allows ingress services to be created to allow external access + # from Kubernetes to access OpenBao pods. + # If deployment is on OpenShift, the following block is ignored. + # In order to expose the service, use the route section below + ingress: + enabled: false + labels: {} + # traffic: external + annotations: {} + # | + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + # or + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + + # Optionally use ingressClassName instead of deprecated annotation. + # See: https://kubernetes.io/docs/concepts/services-networking/ingress/#deprecated-annotation + ingressClassName: "" + + # As of Kubernetes 1.19, all Ingress Paths must have a pathType configured. The default value below should be sufficient in most cases. + # See: https://kubernetes.io/docs/concepts/services-networking/ingress/#path-types for other possible values. + pathType: Prefix + + # When HA mode is enabled and K8s service registration is being used, + # configure the ingress to point to the OpenBao active service. + activeService: true + hosts: + - host: chart-example.local + paths: [] + ## Extra paths to prepend to the host configuration. This is useful when working with annotation based services. + extraPaths: [] + # - path: /* + # backend: + # service: + # name: ssl-redirect + # port: + # number: use-annotation + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + + # Gateway consolidates configuration related to the Kubernetes Gateway API + # Currently, only creating a TLSRoute is supported + # See: https://gateway-api.sigs.k8s.io/ + gateway: + # Configures a TLSRoute for the OpenBao server. This can be enabled independently of the ingress configuration to allow for side-by-side scenarios for migration. + tlsRoute: + enabled: false + labels: {} + # traffic: external + annotations: {} + # external-dns.alpha.kubernetes.io/hostname: chart-example.local + + hosts: [] + # - chart-example.local + + # Allows overriding the TLSRoutes apiVersion in case a different version of Gateway API is installed on the cluster. + apiVersion: gateway.networking.k8s.io/v1alpha3 + + # When HA mode is enabled and K8s service registration is being used, + # configure the ingress to point to the OpenBao active service. + activeService: true + + # List of ParentRefs. As the helm chart configures no gateways itself, + # this should be set to at least one gateway with one or more TLS listeners + parentRefs: [] + # - name: my-gw + # namespace: gateway-namespace + # # sectionName is optional to fix to a specific listener + # sectionName: listener-name + + # Configures a HTTPRoute for the OpenBao server. This can be enabled independently of the ingress configuration to allow for side-by-side scenarios for migration. + # WARNING: Terminating TLS before reaching the OpenBao Server is not recommended and may break things like certificate authentication. Prefer usage of `TLSRoute`. + httpRoute: + enabled: false + labels: {} + # traffic: external + annotations: {} + # external-dns.alpha.kubernetes.io/hostname: chart-example.local + + hosts: + - chart-example.local + + # Allows overriding the HTTPRoute apiVersion in case a different version of Gateway API is installed on the cluster. + apiVersion: gateway.networking.k8s.io/v1 + + # When HA mode is enabled and K8s service registration is being used, + # configure the ingress to point to the OpenBao active service. + activeService: true + + # List of ParentRefs. As the helm chart configures no gateways itself, + # this should be set to at least one gateway with one or more HTTP listeners + parentRefs: [] + # - name: my-gw + # namespace: gateway-namespace + # # sectionName is optional to fix to a specific listener + # sectionName: listener-name + + matches: + path: + type: PathPrefix + value: '/' + timeouts: {} + # request: 10s #Maximum time the Gateway waits to complete the full client request and response cycle. + # backendRequest: 10s # Maximum time the Gateway waits for a response from the backend service. + filters: [] + # - type: RequestHeaderModifier + # requestHeaderModifier: + # set: + # - name: X-Forwarded-Proto + # value: https + + # If TLS is enable on server (see global.tlsDisable) the gateway must be configured + # with BackendTLSPolicy to correctly handles TLS connection with server in case TLS termination happens at gateway + tlsPolicy: + enabled: false + labels: {} + # traffic: external + annotations: {} + # external-dns.alpha.kubernetes.io/hostname: chart-example.local + + # Allows overriding the BackendTLSPolicy apiVersion in case a different version of Gateway API is installed on the cluster. + apiVersion: gateway.networking.k8s.io/v1 + + # When HA mode is enabled and K8s service registration is being used, + # configure the ingress to point to the OpenBao active service. + activeService: true + + # Identifies an API object to apply the policy to. + # If no one is specified the default is to target the OpenBao service + targetRefs: [] + + validation: {} + # caCertificateRefs: + # - kind: ConfigMap + # name: vault-ca + + # hostAliases is a list of aliases to be added to /etc/hosts. Specified as a YAML list. + hostAliases: [] + # - ip: 127.0.0.1 + # hostnames: + # - chart-example.local + + # OpenShift only - create a route to expose the service + # By default the created route will be of type passthrough + route: + enabled: false + + # When HA mode is enabled and K8s service registration is being used, + # configure the route to point to the OpenBao active service. + activeService: true + + labels: {} + annotations: {} + host: chart-example.local + # tls will be passed directly to the route's TLS config, which + # can be used to configure other termination methods that terminate + # TLS at the router + tls: + termination: passthrough + + # authDelegator enables a cluster role binding to be attached to the service + # account. This cluster role binding can be used to setup Kubernetes auth + # method. See https://openbao.org/docs/auth/kubernetes + authDelegator: + enabled: true + + # -- extraInitContainers is a list of init containers. Specified as a YAML list. + # This is useful if you need to run a script to provision TLS certificates or + # write out configuration files in a dynamic way. + extraInitContainers: [] + # # This example installs a plugin pulled from github into the /usr/local/libexec/vault/oauthapp folder, + # # which is defined in the volumes value. + # - name: oauthapp + # image: "alpine" + # command: [sh, -c] + # args: + # - cd /tmp && + # wget https://github.com/puppetlabs/vault-plugin-secrets-oauthapp/releases/download/v1.2.0/vault-plugin-secrets-oauthapp-v1.2.0-linux-amd64.tar.xz -O oauthapp.xz && + # tar -xf oauthapp.xz && + # mv vault-plugin-secrets-oauthapp-v1.2.0-linux-amd64 /usr/local/libexec/vault/oauthapp && + # chmod +x /usr/local/libexec/vault/oauthapp + # volumeMounts: + # - name: plugins + # mountPath: /usr/local/libexec/vault + + # extraContainers is a list of sidecar containers. Specified as a YAML list. + extraContainers: null + + # -- shareProcessNamespace enables process namespace sharing between OpenBao and the extraContainers + # This is useful if OpenBao must be signaled, e.g. to send a SIGHUP for a log rotation + shareProcessNamespace: false + + # -- extraArgs is a string containing additional OpenBao server arguments. + extraArgs: "" + + # -- extraPorts is a list of extra ports. Specified as a YAML list. + # This is useful if you need to add additional ports to the statefulset in dynamic way. + extraPorts: [] + # - containerPort: 8300 + # name: http-monitoring + + # Used to define custom readinessProbe settings + readinessProbe: + enabled: true + # If you need to use a http path instead of the default exec + # path: /v1/sys/health?standbyok=true + + # Port number on which readinessProbe will be checked. + port: 8200 + # When a probe fails, Kubernetes will try failureThreshold times before giving up + failureThreshold: 2 + # Number of seconds after the container has started before probe initiates + initialDelaySeconds: 5 + # How often (in seconds) to perform the probe + periodSeconds: 5 + # Minimum consecutive successes for the probe to be considered successful after having failed + successThreshold: 1 + # Number of seconds after which the probe times out. + timeoutSeconds: 3 + # Used to enable a livenessProbe for the pods + livenessProbe: + enabled: false + # Used to define a liveness exec command. If provided, exec is preferred to httpGet (path) as the livenessProbe handler. + execCommand: [] + # - /bin/sh + # - -c + # - /openbao/userconfig/mylivenessscript/run.sh + # Path for the livenessProbe to use httpGet as the livenessProbe handler + path: "/v1/sys/health?standbyok=true" + # Port number on which livenessProbe will be checked if httpGet is used as the livenessProbe handler + port: 8200 + # When a probe fails, Kubernetes will try failureThreshold times before giving up + failureThreshold: 2 + # Number of seconds after the container has started before probe initiates + initialDelaySeconds: 60 + # How often (in seconds) to perform the probe + periodSeconds: 5 + # Minimum consecutive successes for the probe to be considered successful after having failed + successThreshold: 1 + # Number of seconds after which the probe times out. + timeoutSeconds: 3 + + # Optional duration in seconds the pod needs to terminate gracefully. + # See: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/ + terminationGracePeriodSeconds: 10 + + # Used to set the sleep time during the preStop step + preStopSleepSeconds: 5 + + # Used to define commands to run after the pod is ready. + # This can be used to automate processes such as initialization + # or bootstrapping auth methods. + postStart: [] + # - /bin/sh + # - -c + # - /openbao/userconfig/myscript/run.sh + + # extraEnvironmentVars is a list of extra environment variables to set with the stateful set. These could be + # used to include variables required for auto-unseal. + extraEnvironmentVars: {} + # GOOGLE_REGION: global + # GOOGLE_PROJECT: myproject + # GOOGLE_APPLICATION_CREDENTIALS: /openbao/userconfig/myproject/myproject-creds.json + + # extraSecretEnvironmentVars is a list of extra environment variables to set with the stateful set. + # These variables take value from existing Secret objects. + extraSecretEnvironmentVars: [] + # - envName: AWS_SECRET_ACCESS_KEY + # secretName: openbao + # secretKey: AWS_SECRET_ACCESS_KEY + + # Deprecated: please use 'volumes' instead. + # extraVolumes is a list of extra volumes to mount. These will be exposed + # to OpenBao in the path `/openbao/userconfig//`. The value below is + # an array of objects, examples are shown below. + extraVolumes: [] + # - type: secret (or "configMap") + # name: my-secret + # path: null # default is `/openbao/userconfig` + + # volumes is a list of volumes made available to all containers. These are rendered + # via toYaml rather than pre-processed like the extraVolumes value. + # The purpose is to make it easy to share volumes between containers. + volumes: null + # - name: plugins + # emptyDir: {} + + # volumeMounts is a list of volumeMounts for the main server container. These are rendered + # via toYaml rather than pre-processed like the extraVolumes value. + # The purpose is to make it easy to share volumes between containers. + volumeMounts: null + # - mountPath: /usr/local/libexec/vault + # name: plugins + # readOnly: true + + # Affinity Settings + # Commenting out or setting as empty the affinity variable, will allow + # deployment to single node services such as Minikube + # This should be either a multi-line string or YAML matching the PodSpec's affinity field. + affinity: | + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchLabels: + app.kubernetes.io/name: {{ template "openbao.name" . }} + app.kubernetes.io/instance: "{{ .Release.Name }}" + component: server + topologyKey: kubernetes.io/hostname + + # Topology settings for server pods + # ref: https://kubernetes.io/docs/concepts/workloads/pods/pod-topology-spread-constraints/ + # This should be either a multi-line string or YAML matching the topologySpreadConstraints array + # in a PodSpec. + topologySpreadConstraints: [] + + # Toleration Settings for server pods + # This should be either a multi-line string or YAML matching the Toleration array + # in a PodSpec. + tolerations: [] + + # nodeSelector labels for server pod assignment, formatted as a multi-line string or YAML map. + # ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector + # Example: + # nodeSelector: + # beta.kubernetes.io/arch: amd64 + nodeSelector: {} + + # Enables network policy for server pods + networkPolicy: + enabled: false + egress: [] + # egress: + # - to: + # - ipBlock: + # cidr: 10.0.0.0/24 + # ports: + # - protocol: TCP + # port: 443 + ingress: + - from: + - namespaceSelector: {} + ports: + - port: 8200 + protocol: TCP + - port: 8201 + protocol: TCP + + # Priority class for server pods + priorityClassName: "" + + # Extra labels to attach to the server pods + # This should be a YAML map of the labels to apply to the server pods + extraLabels: {} + + # Extra annotations to attach to the server pods + # This can either be YAML or a YAML-formatted multi-line templated string map + # of the annotations to apply to the server pods + annotations: {} + + # Add an annotation to the server configmap and the statefulset pods, + # vaultproject.io/config-checksum, that is a hash of the OpenBao configuration. + # This can be used together with an OnDelete deployment strategy to help + # identify which pods still need to be deleted during a deployment to pick up + # any configuration changes. + configAnnotation: false + + # Enables a headless service to be used by the OpenBao Statefulset + service: + enabled: true + # Enable or disable the openbao-active service, which selects OpenBao pods that + # have labeled themselves as the cluster leader with `openbao-active: "true"`. + active: + enabled: true + # Extra annotations for the service definition. This can either be YAML or a + # YAML-formatted multi-line templated string map of the annotations to apply + # to the active service. + annotations: {} + # Extra labels for the service definition. + # This should be a YAML map of the labels to apply to the active service + extraLabels: {} + # Enable or disable the openbao-standby service, which selects OpenBao pods that + # have labeled themselves as a cluster follower with `openbao-active: "false"`. + standby: + enabled: true + # Extra annotations for the service definition. This can either be YAML or a + # YAML-formatted multi-line templated string map of the annotations to apply + # to the standby service. + annotations: {} + # Extra labels for the service definition. + # This should be a YAML map of the labels to apply to the standby service + extraLabels: {} + # If enabled, the service selectors will include `app.kubernetes.io/instance: {{ .Release.Name }}` + # When disabled, services may select OpenBao pods not deployed from the chart. + # Does not affect the headless openbao-internal service with `ClusterIP: None` + instanceSelector: + enabled: true + # clusterIP controls whether a Cluster IP address is attached to the + # OpenBao service within Kubernetes. By default, the OpenBao service will + # be given a Cluster IP address, set to None to disable. When disabled + # Kubernetes will create a "headless" service. Headless services can be + # used to communicate with pods directly through DNS instead of a round-robin + # load balancer. + # clusterIP: None + + # Configures the service type for the main OpenBao service. Can be ClusterIP + # or NodePort. + # type: ClusterIP + + # The IP family and IP families options are to set the behaviour in a dual-stack environment. + # Omitting these values will let the service fall back to whatever the CNI dictates the defaults + # should be. + # These are only supported for kubernetes versions >=1.23.0 + # + # Configures the service's supported IP family policy, can be either: + # SingleStack: Single-stack service. The control plane allocates a cluster IP for the Service, using the first configured service cluster IP range. + # PreferDualStack: Allocates IPv4 and IPv6 cluster IPs for the Service. + # RequireDualStack: Allocates Service .spec.ClusterIPs from both IPv4 and IPv6 address ranges. + ipFamilyPolicy: "" + + # Sets the families that should be supported and the order in which they should be applied to ClusterIP as well. + # Can be IPv4 and/or IPv6. + ipFamilies: [] + + # Do not wait for pods to be ready before including them in the services' + # targets. Does not apply to the headless service, which is used for + # cluster-internal communication. + publishNotReadyAddresses: true + + # The externalTrafficPolicy can be set to either Cluster or Local + # and is only valid for LoadBalancer and NodePort service types. + # The default value is Cluster. + # ref: https://kubernetes.io/docs/concepts/services-networking/service/#external-traffic-policy + externalTrafficPolicy: Cluster + + # If type is set to "NodePort", a specific nodePort value can be configured, + # will be random if left blank. + # nodePort: 30000 + + # When HA mode is enabled + # If type is set to "NodePort", a specific nodePort value can be configured, + # will be random if left blank. + # activeNodePort: 30001 + + # When HA mode is enabled + # If type is set to "NodePort", a specific nodePort value can be configured, + # will be random if left blank. + # standbyNodePort: 30002 + + # Port on which OpenBao server is listening + port: 8200 + # Target port to which the service should be mapped to + targetPort: 8200 + + # -- extraPorts is a list of extra ports. Specified as a YAML list. + # This is useful if you need to add additional ports to the server service in dynamic way. + extraPorts: [] + # - name: metrics + # port: 9101 + # targetPort: 9101 + + # Extra annotations for the service definition. This can either be YAML or a + # YAML-formatted multi-line templated string map of the annotations to apply + # to the service. + annotations: {} + # Extra labels for the service definition. + # This should be a YAML map of the labels to apply to the service + extraLabels: {} + + # This configures the OpenBao Statefulset to create a PVC for data + # storage when using the file or raft backend storage engines. + # See https://openbao.org/docs/configuration/storage to know more + dataStorage: + enabled: true + # Size of the PVC created + size: 10Gi + # Location where the PVC will be mounted. + mountPath: "/openbao/data" + # Name of the storage class to use. If null it will use the + # configured default Storage Class. + storageClass: null + # Access Mode of the storage device being used for the PVC + accessMode: ReadWriteOnce + # Annotations to apply to the PVC + annotations: {} + # Labels to apply to the PVC + labels: {} + + # Persistent Volume Claim (PVC) retention policy + # ref: https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#persistentvolumeclaim-retention + # Example: + # persistentVolumeClaimRetentionPolicy: + # whenDeleted: Retain + # whenScaled: Retain + persistentVolumeClaimRetentionPolicy: {} + + # This configures the OpenBao Statefulset to create a PVC for audit + # logs. Once OpenBao is deployed, initialized, and unsealed, OpenBao must + # be configured to use this for audit logs. This will be mounted to + # /openbao/audit + # See https://openbao.org/docs/audit to know more + auditStorage: + enabled: false + # Size of the PVC created + size: 10Gi + # Location where the PVC will be mounted. + mountPath: "/openbao/audit" + # Name of the storage class to use. If null it will use the + # configured default Storage Class. + storageClass: null + # Access Mode of the storage device being used for the PVC + accessMode: ReadWriteOnce + # Annotations to apply to the PVC + annotations: {} + # Labels to apply to the PVC + labels: {} + + # Run OpenBao in "dev" mode. This requires no further setup, no state management, + # and no initialization. This is useful for experimenting with OpenBao without + # needing to unseal, store keys, et. al. All data is lost on restart - do not + # use dev mode for anything other than experimenting. + # See https://openbao.org/docs/concepts/dev-server to know more + dev: + enabled: false + + # Set VAULT_DEV_ROOT_TOKEN_ID value + devRootToken: "root" + + # Run OpenBao in "standalone" mode. This is the default mode that will deploy if + # no arguments are given to helm. This requires a PVC for data storage to use + # the "file" backend. This mode is not highly available and should not be scaled + # past a single replica. + standalone: + enabled: "-" + + # config is a raw string of default configuration when using a Stateful + # deployment. Default is to use a PersistentVolumeClaim mounted at /openbao/data + # and store data there. This is only used when using a Replica count of 1, and + # using a stateful set. This should be HCL. + + # Note: Configuration files are stored in ConfigMaps so sensitive data + # such as passwords should be either mounted through extraSecretEnvironmentVars + # or through a Kube secret. For more information see: + # https://openbao.org/docs/platform/k8s/helm/run/#protecting-sensitive-openbao-configurations + config: | + ui = true + + listener "tcp" { + tls_disable = 1 + address = "[::]:8200" + cluster_address = "[::]:8201" + # Enable unauthenticated metrics access (necessary for Prometheus Operator) + #telemetry { + # unauthenticated_metrics_access = "true" + #} + } + storage "file" { + path = "/openbao/data" + } + + # Example configuration for using auto-unseal, using Google Cloud KMS. The + # GKMS keys must already exist, and the cluster must have a service account + # that is authorized to access GCP KMS. + #seal "gcpckms" { + # project = "openbao-helm-dev" + # region = "global" + # key_ring = "openbao-helm-unseal-kr" + # crypto_key = "openbao-helm-unseal-key" + #} + + # Example configuration for enabling Prometheus metrics in your config. + #telemetry { + # prometheus_retention_time = "30s" + # disable_hostname = true + #} + + # Run OpenBao in "HA" mode. There are no storage requirements unless the audit log + # persistence is required. In HA mode OpenBao will configure itself to use Consul + # for its storage backend. The default configuration provided will work the Consul + # Helm project by default. It is possible to manually configure OpenBao to use a + # different HA backend. + ha: + enabled: false + replicas: 3 + + # Set the api_addr configuration for OpenBao HA + # See https://openbao.org/docs/configuration/#high-availability-parameters + # If set to null, this will be set to the Pod IP Address + apiAddr: null + + # Set the cluster_addr configuration for OpenBao HA + # See https://openbao.org/docs/configuration/#high-availability-parameters + # If set to null, this will be set to https://$(HOSTNAME).{{ template "openbao.fullname" . }}-internal:8201 + clusterAddr: null + + # Enables OpenBao's integrated Raft storage. Unlike the typical HA modes where + # OpenBao's persistence is external (such as Consul), enabling Raft mode will create + # persistent volumes for OpenBao to store data according to the configuration under server.dataStorage. + # The OpenBao cluster will coordinate leader elections and failovers internally. + raft: + # Enables Raft integrated storage + enabled: false + # Set the Node Raft ID to the name of the pod + setNodeId: false + + # config is a raw string of default configuration when using a Stateful + # deployment. + # This should be HCL. + + # Note: Configuration files are stored in ConfigMaps so sensitive data + # such as passwords should be either mounted through extraSecretEnvironmentVars + # or through a Kube secret. For more information see: + # https://openbao.org/docs/platform/k8s/helm/run/#protecting-sensitive-openbao-configurations + config: | + ui = true + + listener "tcp" { + tls_disable = 1 + address = "[::]:8200" + cluster_address = "[::]:8201" + # Enable unauthenticated metrics access (necessary for Prometheus Operator) + #telemetry { + # unauthenticated_metrics_access = "true" + #} + } + + storage "raft" { + path = "/openbao/data" + } + + service_registration "kubernetes" {} + + # config is a raw string of default configuration when using a Stateful + # deployment. Default is to use a Consul for its HA storage backend. + # This should be HCL. + + # Note: Configuration files are stored in ConfigMaps so sensitive data + # such as passwords should be either mounted through extraSecretEnvironmentVars + # or through a Kube secret. For more information see: + # https://openbao.org/docs/platform/k8s/helm/run/#protecting-sensitive-openbao-configurations + config: | + ui = true + + listener "tcp" { + tls_disable = 1 + address = "[::]:8200" + cluster_address = "[::]:8201" + } + storage "consul" { + path = "openbao" + address = "HOST_IP:8500" + } + + service_registration "kubernetes" {} + + # Example configuration for using auto-unseal, using Google Cloud KMS. The + # GKMS keys must already exist, and the cluster must have a service account + # that is authorized to access GCP KMS. + #seal "gcpckms" { + # project = "openbao-helm-dev-246514" + # region = "global" + # key_ring = "openbao-helm-unseal-kr" + # crypto_key = "openbao-helm-unseal-key" + #} + + # Example configuration for enabling Prometheus metrics. + # If you are using Prometheus Operator you can enable a ServiceMonitor resource below. + # You may wish to enable unauthenticated metrics in the listener block above. + #telemetry { + # prometheus_retention_time = "30s" + # disable_hostname = true + #} + + # A disruption budget limits the number of pods of a replicated application + # that are down simultaneously from voluntary disruptions + disruptionBudget: + enabled: true + + # maxUnavailable will default to (n/2)-1 where n is the number of + # replicas. If you'd like a custom value, you can specify an override here. + maxUnavailable: null + + # Definition of the serviceAccount used to run Vault. + # These options are also used when using an external OpenBao server to validate + # Kubernetes tokens. + serviceAccount: + # Specifies whether a service account should be created + create: true + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + # Create a Secret API object to store a non-expiring token for the service account. + # Prior to v1.24.0, Kubernetes used to generate this secret for each service account by default. + # Kubernetes now recommends using short-lived tokens from the TokenRequest API or projected volumes instead if possible. + # For more details, see https://kubernetes.io/docs/concepts/configuration/secret/#serviceaccount-token-secrets + # serviceAccount.create must be equal to 'true' in order to use this feature. + createSecret: false + # Extra annotations for the serviceAccount definition. This can either be + # YAML or a YAML-formatted multi-line templated string map of the + # annotations to apply to the serviceAccount. + annotations: {} + # Extra labels to attach to the serviceAccount + # This should be a YAML map of the labels to apply to the serviceAccount + extraLabels: {} + # Enable or disable a service account role binding with the permissions required for + # OpenBao's Kubernetes service_registration config option. + # See https://openbao.org/docs/configuration/service-registration/kubernetes + serviceDiscovery: + enabled: true + + # Settings for the statefulSet used to run OpenBao. + statefulSet: + # Extra annotations for the statefulSet. This can either be YAML or a + # YAML-formatted multi-line templated string map of the annotations to apply + # to the statefulSet. + annotations: {} + + # Set the pod and container security contexts. + # If not set, these will default to, and for *not* OpenShift: + # pod: + # runAsNonRoot: true + # runAsGroup: {{ .Values.server.gid | default 1000 }} + # runAsUser: {{ .Values.server.uid | default 100 }} + # fsGroup: {{ .Values.server.gid | default 1000 }} + # container: + # allowPrivilegeEscalation: false + # + # If not set, these will default to, and for OpenShift: + # pod: {} + # container: {} + securityContext: + pod: {} + container: {} + + # Should the server pods run on the host network + hostNetwork: false + +# OpenBao UI +ui: + # True if you want to create a Service entry for the OpenBao UI. + # + # serviceType can be used to control the type of service created. For + # example, setting this to "LoadBalancer" will create an external load + # balancer (for supported K8S installations) to access the UI. + enabled: false + publishNotReadyAddresses: true + # The service should only contain selectors for active OpenBao pod + activeOpenbaoPodOnly: false + serviceType: "ClusterIP" + serviceNodePort: null + externalPort: 8200 + targetPort: 8200 + + # The IP family and IP families options are to set the behaviour in a dual-stack environment. + # Omitting these values will let the service fall back to whatever the CNI dictates the defaults + # should be. + # These are only supported for kubernetes versions >=1.23.0 + # + # Configures the service's supported IP family, can be either: + # SingleStack: Single-stack service. The control plane allocates a cluster IP for the Service, using the first configured service cluster IP range. + # PreferDualStack: Allocates IPv4 and IPv6 cluster IPs for the Service. + # RequireDualStack: Allocates Service .spec.ClusterIPs from both IPv4 and IPv6 address ranges. + serviceIPFamilyPolicy: "" + + # Sets the families that should be supported and the order in which they should be applied to ClusterIP as well + # Can be IPv4 and/or IPv6. + serviceIPFamilies: [] + + # The externalTrafficPolicy can be set to either Cluster or Local + # and is only valid for LoadBalancer and NodePort service types. + # The default value is Cluster. + # ref: https://kubernetes.io/docs/concepts/services-networking/service/#external-traffic-policy + externalTrafficPolicy: Cluster + + # loadBalancerSourceRanges: + # - 10.0.0.0/16 + # - 1.78.23.3/32 + + # loadBalancerIP: + + # Extra annotations to attach to the ui service + # This can either be YAML or a YAML-formatted multi-line templated string map + # of the annotations to apply to the ui service + annotations: {} + + # Extra labels for the service definition. + # This should be a YAML map of the labels to apply to the ui service + extraLabels: {} + +# openbao-csi-provider +csi: + # -- True if you want to install a openbao-csi-provider daemonset. + # + # Requires installing the secrets-store-csi-driver separately, see: + # https://secrets-store-csi-driver.sigs.k8s.io/getting-started/installation + # + # With the driver and provider installed, you can mount OpenBao secrets into volumes + # similar to the OpenBao Agent injector, and you can also sync those secrets into + # Kubernetes secrets. + enabled: false + + image: + # -- image registry to use for csi image + registry: "quay.io" + # -- image repo to use for csi image + repository: "openbao/openbao-csi-provider" + # -- image tag to use for csi image + tag: "2.0.0" + # -- image pull policy to use for csi image. if tag is "latest", set to "Always" + pullPolicy: IfNotPresent + + # -- volumes is a list of volumes made available to all containers. These are rendered + # via toYaml rather than pre-processed like the extraVolumes value. + # The purpose is to make it easy to share volumes between containers. + volumes: [] + # - name: tls + # secret: + # secretName: openbao-tls + + # -- volumeMounts is a list of volumeMounts for the main server container. These are rendered + # via toYaml rather than pre-processed like the extraVolumes value. + # The purpose is to make it easy to share volumes between containers. + volumeMounts: [] + # - name: tls + # mountPath: "/openbao/tls" + # readOnly: true + + resources: {} + # resources: + # requests: + # cpu: 50m + # memory: 128Mi + # limits: + # cpu: 50m + # memory: 128Mi + + # Override the default secret name for the CSI Provider's HMAC key used for + # generating secret versions. + hmacSecretName: "" + + # Settings for the daemonSet used to run the provider. + daemonSet: + updateStrategy: + type: RollingUpdate + maxUnavailable: "" + # Extra annotations for the daemonSet. This can either be YAML or a + # YAML-formatted multi-line templated string map of the annotations to apply + # to the daemonSet. + annotations: {} + # Provider host path (must match the CSI provider's path) + providersDir: "/etc/kubernetes/secrets-store-csi-providers" + # Kubelet host path + kubeletRootDir: "/var/lib/kubelet" + # endpoint path for the provider + endpoint: "/provider/openbao.sock" + + # Extra labels to attach to the openbao-csi-provider daemonSet + # This should be a YAML map of the labels to apply to the csi provider daemonSet + extraLabels: {} + # security context for the pod template and container in the csi provider daemonSet + securityContext: + pod: {} + container: {} + + pod: + # Extra annotations for the provider pods. This can either be YAML or a + # YAML-formatted multi-line templated string map of the annotations to apply + # to the pod. + annotations: {} + + # Toleration Settings for provider pods + # This should be either a multi-line string or YAML matching the Toleration array + # in a PodSpec. + tolerations: [] + + # nodeSelector labels for csi pod assignment, formatted as a multi-line string or YAML map. + # ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector + # Example: + # nodeSelector: + # beta.kubernetes.io/arch: amd64 + nodeSelector: {} + + # Affinity Settings + # This should be either a multi-line string or YAML matching the PodSpec's affinity field. + affinity: {} + + # Extra labels to attach to the openbao-csi-provider pod + # This should be a YAML map of the labels to apply to the csi provider pod + extraLabels: {} + + agent: + enabled: true + extraArgs: [] + + image: + # -- image registry to use for agent image + registry: "quay.io" + # -- image repo to use for agent image + repository: "openbao/openbao" + # -- image tag to use for agent image - defaults to chart appVersion + tag: "" + # -- image pull policy to use for agent image. if tag is "latest", set to "Always" + pullPolicy: IfNotPresent + + logFormat: standard + logLevel: info + + resources: {} + # resources: + # requests: + # memory: 256Mi + # cpu: 250m + # limits: + # memory: 256Mi + # cpu: 250m + + # Priority class for csi pods + priorityClassName: "" + + serviceAccount: + # Extra annotations for the serviceAccount definition. This can either be + # YAML or a YAML-formatted multi-line templated string map of the + # annotations to apply to the serviceAccount. + annotations: {} + + # Extra labels to attach to the openbao-csi-provider serviceAccount + # This should be a YAML map of the labels to apply to the csi provider serviceAccount + extraLabels: {} + + # Used to configure readinessProbe for the pods. + readinessProbe: + # When a probe fails, Kubernetes will try failureThreshold times before giving up + failureThreshold: 2 + # Number of seconds after the container has started before probe initiates + initialDelaySeconds: 5 + # How often (in seconds) to perform the probe + periodSeconds: 5 + # Minimum consecutive successes for the probe to be considered successful after having failed + successThreshold: 1 + # Number of seconds after which the probe times out. + timeoutSeconds: 3 + # Used to configure livenessProbe for the pods. + livenessProbe: + # When a probe fails, Kubernetes will try failureThreshold times before giving up + failureThreshold: 2 + # Number of seconds after the container has started before probe initiates + initialDelaySeconds: 5 + # How often (in seconds) to perform the probe + periodSeconds: 5 + # Minimum consecutive successes for the probe to be considered successful after having failed + successThreshold: 1 + # Number of seconds after which the probe times out. + timeoutSeconds: 3 + + # Enables debug logging. + debug: false + + # Pass arbitrary additional arguments to openbao-csi-provider. + # See https://openbao.org/docs/platform/k8s/csi/configurations + # for the available command line flags. + extraArgs: [] + +# OpenBao is able to collect and publish various runtime metrics. +# Enabling this feature requires setting adding `telemetry{}` stanza to +# the OpenBao configuration. There are a few examples included in the `config` sections above. +# +# For more information see: +# https://openbao.org/docs/configuration/telemetry +# https://openbao.org/docs/internals/telemetry +serverTelemetry: + # Enable support for the Prometheus Operator. If authorization is not required for + # OpenBao's metrics endpoint, the following OpenBao server `telemetry{}` config must be included + # in the `listener "tcp"{}` stanza + # telemetry { + # unauthenticated_metrics_access = "true" + # } + # + # See the `standalone.config` for a more complete example of this. + # + # In addition, a top level `telemetry{}` stanza must also be included in the OpenBao configuration: + # + # example: + # telemetry { + # prometheus_retention_time = "30s" + # disable_hostname = true + # } + # + # Configuration for monitoring the OpenBao server. + serviceMonitor: + # The Prometheus operator *must* be installed before enabling this feature, + # if not the chart will fail to install due to missing CustomResourceDefinitions + # provided by the operator. + # + # Instructions on how to install the Helm chart can be found here: + # https://github.com/prometheus-community/helm-charts/tree/main/charts/kube-prometheus-stack + # More information can be found here: + # https://github.com/prometheus-operator/prometheus-operator + # https://github.com/prometheus-operator/kube-prometheus + + # Enable deployment of the OpenBao Server ServiceMonitor CustomResource. + enabled: false + + # Selector labels to add to the ServiceMonitor. + # When empty, defaults to: + # release: prometheus + selectors: {} + + # -- Port which Prometheus uses when scraping metrics. If empty will use `openbao.scheme` helper for its value + port: "" + + # -- scheme to use when Prometheus scrapes metrics. If empty will use `openbao.scheme` helper for its value + scheme: "" + + # Interval at which Prometheus scrapes metrics + interval: 30s + + # Timeout for Prometheus scrapes + scrapeTimeout: 10s + + # tlsConfig used for scraping the Vault metrics API. + tlsConfig: {} + + # authorization used for scraping the Vault metrics API. + authorization: {} + + # scrapeClass to be used by the serviceMonitor + scrapeClass: "" + + prometheusRules: + # The Prometheus operator *must* be installed before enabling this feature, + # if not the chart will fail to install due to missing CustomResourceDefinitions + # provided by the operator. + + # Deploy the PrometheusRule custom resource for AlertManager based alerts. + # Requires that AlertManager is properly deployed. + enabled: false + + # Selector labels to add to the PrometheusRules. + # When empty, defaults to: + # release: prometheus + selectors: {} + + # Some example rules. + rules: [] + # - alert: vault-HighResponseTime + # annotations: + # message: The response time of OpenBao is over 500ms on average over the last 5 minutes. + # expr: vault_core_handle_request{quantile="0.5", namespace="mynamespace"} > 500 + # for: 5m + # labels: + # severity: warning + # - alert: vault-HighResponseTime + # annotations: + # message: The response time of OpenBao is over 1s on average over the last 10 minutes. + # expr: vault_core_handle_request{quantile="0.5", namespace="mynamespace"} > 1000 + # for: 10m + # labels: + # severity: critical + + grafanaDashboard: + # Enable deployment of the OpenBao Grafana dashboard. + # https://grafana.com/grafana/dashboards/23725-openbao + enabled: false + + # Add `grafana_dashboard: "1"` default label + defaultLabel: true + + # Extra labels for dashboard ConfigMap + extraLabel: {} + + # Extra annotations for dashboard ConfigMap + extraAnnotations: {} + +# extraObjects allows you to add any extra Kubernetes manifests to this chart +extraObjects: [] +# Examples: +# Defining as a Structured YAML Object Example: +# extraObjects: +# - apiVersion: v1 +# kind: ConfigMap +# metadata: +# name: cert-manager-configmap-{{ .Release.Name }} +# +# Using a String for Advanced Templating Example: +# extraObjects: +# - | +# apiVersion: v1 +# kind: ConfigMap +# metadata: +# name: cert-manager-configmap-{{ include "some-other-template" }} + +# Snapshot Agent Configuration +snapshotAgent: + # whether or not to enable the snapshot agent cronjob + enabled: false + # extra Annotations for the job + annotations: {} + # schedule of the cronjob + schedule: "*/15 * * * *" + restartPolicy: OnFailure + # service account settings for the snapshot agent + serviceAccount: + create: true + name: "" + annotations: {} + extraLabels: {} + # The image settings for the snapshot agent + image: + repository: ghcr.io/openbao/openbao-snapshot-agent + tag: 0.2.4 + + # -- List of extraVolumes made available to the snapshot cronjob container. + extraVolumes: [] + # - name: openbao-tls + # secret: + # defaultMode: 420 + # secretName: openbao-tls + + # -- List of additional volumeMounts for the snapshot cronjob container. + extraVolumeMounts: [] + # - mountPath: /openbao/tls/ca.crt + # name: openbao-tls + # readOnly: true + # subPath: ca.crt + + # s3CredentialsSecret to use + s3CredentialsSecret: "my-s3-credentials" + + # configuration for the snapshot agent + config: + s3Host: "s3.eu-east-1.amazonaws.com" + s3Bucket: "openbao-snapshots" + s3Uri: "s3://openbao-snapshots" + s3ExpireDays: "14" + s3cmdExtraFlag: "-v" + baoAuthPath: "kubernetes" + baoRole: "snapshot" + + # configuration of the CronJobs resources + resources: {} + + # -- Map of extra environment variables to set in the snapshot-agent cronjob + extraEnvironmentVars: {} + # BAO_CACERT: /openbao/tls/ca.crt + + # -- List of extra environment variables to set in the snapshot-agent cronjob + # These variables take value from existing Secret objects. + extraSecretEnvironmentVars: [] + # - envName: AWS_SECRET_ACCESS_KEY + # secretName: openbao + # secretKey: AWS_SECRET_ACCESS_KEY + + # Security context for the pod template and the snapshotAgent container + # The default pod securityContext is: + # runAsNonRoot: true + # runAsGroup: {{ .Values.snapshotAgent.gid | default 1000 }} + # runAsUser: {{ .Values.snapshotAgent.uid | default 100 }} + # fsGroup: {{ .Values.snapshotAgent.gid | default 1000 }} + # and for container is + # allowPrivilegeEscalation: false + # capabilities: + # drop: + # - ALL + securityContext: + pod: {} + container: {} diff --git a/packages/system/openbao/values.yaml b/packages/system/openbao/values.yaml new file mode 100644 index 00000000..957f37a0 --- /dev/null +++ b/packages/system/openbao/values.yaml @@ -0,0 +1,5 @@ +openbao: + injector: + enabled: false + csi: + enabled: false From da59efec21b479bc541e88274eac8c4a9a62d411 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Tue, 17 Feb 2026 20:03:01 +0300 Subject: [PATCH 063/666] feat(openbao): add application chart with standalone and HA modes Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- packages/apps/openbao/.helmignore | 3 + packages/apps/openbao/Chart.yaml | 7 ++ packages/apps/openbao/Makefile | 5 + packages/apps/openbao/README.md | 27 +++++ packages/apps/openbao/charts/cozy-lib | 1 + packages/apps/openbao/logos/openbao.svg | 11 +++ .../apps/openbao/templates/_resources.tpl | 49 +++++++++ .../templates/dashboard-resourcemap.yaml | 31 ++++++ packages/apps/openbao/templates/openbao.yaml | 99 +++++++++++++++++++ .../openbao/templates/workloadmonitor.yaml | 13 +++ packages/apps/openbao/values.schema.json | 87 ++++++++++++++++ packages/apps/openbao/values.yaml | 41 ++++++++ 12 files changed, 374 insertions(+) create mode 100644 packages/apps/openbao/.helmignore create mode 100644 packages/apps/openbao/Chart.yaml create mode 100644 packages/apps/openbao/Makefile create mode 100644 packages/apps/openbao/README.md create mode 120000 packages/apps/openbao/charts/cozy-lib create mode 100644 packages/apps/openbao/logos/openbao.svg create mode 100644 packages/apps/openbao/templates/_resources.tpl create mode 100644 packages/apps/openbao/templates/dashboard-resourcemap.yaml create mode 100644 packages/apps/openbao/templates/openbao.yaml create mode 100644 packages/apps/openbao/templates/workloadmonitor.yaml create mode 100644 packages/apps/openbao/values.schema.json create mode 100644 packages/apps/openbao/values.yaml diff --git a/packages/apps/openbao/.helmignore b/packages/apps/openbao/.helmignore new file mode 100644 index 00000000..1ea0ae84 --- /dev/null +++ b/packages/apps/openbao/.helmignore @@ -0,0 +1,3 @@ +.helmignore +/logos +/Makefile diff --git a/packages/apps/openbao/Chart.yaml b/packages/apps/openbao/Chart.yaml new file mode 100644 index 00000000..f33fc756 --- /dev/null +++ b/packages/apps/openbao/Chart.yaml @@ -0,0 +1,7 @@ +apiVersion: v2 +name: openbao +description: Managed OpenBAO secrets management service +icon: /logos/openbao.svg +type: application +version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process +appVersion: "2.5.0" diff --git a/packages/apps/openbao/Makefile b/packages/apps/openbao/Makefile new file mode 100644 index 00000000..d1cfda8e --- /dev/null +++ b/packages/apps/openbao/Makefile @@ -0,0 +1,5 @@ +include ../../../hack/package.mk + +generate: + cozyvalues-gen -v values.yaml -s values.schema.json -r README.md + ../../../hack/update-crd.sh diff --git a/packages/apps/openbao/README.md b/packages/apps/openbao/README.md new file mode 100644 index 00000000..c53c9d28 --- /dev/null +++ b/packages/apps/openbao/README.md @@ -0,0 +1,27 @@ +# Managed OpenBAO Service + +OpenBAO is an open-source secrets management solution forked from HashiCorp Vault. +It provides identity-based secrets and encryption management for cloud infrastructure. + +## Parameters + +### Common parameters + +| Name | Description | Type | Value | +| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | ------- | +| `replicas` | Number of OpenBAO replicas. HA with Raft is automatically enabled when replicas > 1. Switching between standalone (file storage) and HA (Raft storage) modes requires data migration. | `int` | `1` | +| `resources` | Explicit CPU and memory configuration for each OpenBAO replica. When omitted, the preset defined in `resourcesPreset` is applied. | `object` | `{}` | +| `resources.cpu` | CPU available to each replica. | `quantity` | `""` | +| `resources.memory` | Memory (RAM) available to each replica. | `quantity` | `""` | +| `resourcesPreset` | Default sizing preset used when `resources` is omitted. | `string` | `small` | +| `size` | Persistent Volume Claim size for data storage. | `quantity` | `10Gi` | +| `storageClass` | StorageClass used to store the data. | `string` | `""` | +| `external` | Enable external access from outside the cluster. | `bool` | `false` | + + +### Application-specific parameters + +| Name | Description | Type | Value | +| ---- | -------------------------- | ------ | ------ | +| `ui` | Enable the OpenBAO web UI. | `bool` | `true` | + diff --git a/packages/apps/openbao/charts/cozy-lib b/packages/apps/openbao/charts/cozy-lib new file mode 120000 index 00000000..e1813509 --- /dev/null +++ b/packages/apps/openbao/charts/cozy-lib @@ -0,0 +1 @@ +../../../library/cozy-lib \ No newline at end of file diff --git a/packages/apps/openbao/logos/openbao.svg b/packages/apps/openbao/logos/openbao.svg new file mode 100644 index 00000000..5ce73b79 --- /dev/null +++ b/packages/apps/openbao/logos/openbao.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/packages/apps/openbao/templates/_resources.tpl b/packages/apps/openbao/templates/_resources.tpl new file mode 100644 index 00000000..7aeb976e --- /dev/null +++ b/packages/apps/openbao/templates/_resources.tpl @@ -0,0 +1,49 @@ +{{/* +Copyright Broadcom, Inc. All Rights Reserved. +SPDX-License-Identifier: APACHE-2.0 +*/}} + +{{/* vim: set filetype=mustache: */}} + +{{/* +Return a resource request/limit object based on a given preset. +These presets are for basic testing and not meant to be used in production +{{ include "resources.preset" (dict "type" "nano") -}} +*/}} +{{- define "resources.preset" -}} +{{- $presets := dict + "nano" (dict + "requests" (dict "cpu" "100m" "memory" "128Mi" "ephemeral-storage" "50Mi") + "limits" (dict "memory" "128Mi" "ephemeral-storage" "2Gi") + ) + "micro" (dict + "requests" (dict "cpu" "250m" "memory" "256Mi" "ephemeral-storage" "50Mi") + "limits" (dict "memory" "256Mi" "ephemeral-storage" "2Gi") + ) + "small" (dict + "requests" (dict "cpu" "500m" "memory" "512Mi" "ephemeral-storage" "50Mi") + "limits" (dict "memory" "512Mi" "ephemeral-storage" "2Gi") + ) + "medium" (dict + "requests" (dict "cpu" "500m" "memory" "1Gi" "ephemeral-storage" "50Mi") + "limits" (dict "memory" "1Gi" "ephemeral-storage" "2Gi") + ) + "large" (dict + "requests" (dict "cpu" "1" "memory" "2Gi" "ephemeral-storage" "50Mi") + "limits" (dict "memory" "2Gi" "ephemeral-storage" "2Gi") + ) + "xlarge" (dict + "requests" (dict "cpu" "2" "memory" "4Gi" "ephemeral-storage" "50Mi") + "limits" (dict "memory" "4Gi" "ephemeral-storage" "2Gi") + ) + "2xlarge" (dict + "requests" (dict "cpu" "4" "memory" "8Gi" "ephemeral-storage" "50Mi") + "limits" (dict "memory" "8Gi" "ephemeral-storage" "2Gi") + ) + }} +{{- if hasKey $presets .type -}} +{{- index $presets .type | toYaml -}} +{{- else -}} +{{- printf "ERROR: Preset key '%s' invalid. Allowed values are %s" .type (join "," (keys $presets)) | fail -}} +{{- end -}} +{{- end -}} diff --git a/packages/apps/openbao/templates/dashboard-resourcemap.yaml b/packages/apps/openbao/templates/dashboard-resourcemap.yaml new file mode 100644 index 00000000..452a07db --- /dev/null +++ b/packages/apps/openbao/templates/dashboard-resourcemap.yaml @@ -0,0 +1,31 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ .Release.Name }}-dashboard-resources +rules: +- apiGroups: + - "" + resources: + - services + resourceNames: + - {{ .Release.Name }} + - {{ .Release.Name }}-internal + verbs: ["get", "list", "watch"] +- apiGroups: + - cozystack.io + resources: + - workloadmonitors + resourceNames: + - {{ .Release.Name }} + verbs: ["get", "list", "watch"] +--- +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: {{ .Release.Name }}-dashboard-resources +subjects: +{{ include "cozy-lib.rbac.subjectsForTenantAndAccessLevel" (list "use" .Release.Namespace) }} +roleRef: + kind: Role + name: {{ .Release.Name }}-dashboard-resources + apiGroup: rbac.authorization.k8s.io diff --git a/packages/apps/openbao/templates/openbao.yaml b/packages/apps/openbao/templates/openbao.yaml new file mode 100644 index 00000000..daa6f9c4 --- /dev/null +++ b/packages/apps/openbao/templates/openbao.yaml @@ -0,0 +1,99 @@ +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: {{ .Release.Name }}-system + labels: + sharding.fluxcd.io/key: tenants +spec: + chartRef: + kind: ExternalArtifact + name: cozystack-openbao-application-default-openbao-system + namespace: cozy-system + interval: 5m + timeout: 10m + install: + remediation: + retries: -1 + upgrade: + force: true + remediation: + retries: -1 + valuesFrom: + - kind: Secret + name: cozystack-values + values: + openbao: + fullnameOverride: {{ .Release.Name }} + global: + tlsDisable: true + server: + podManagementPolicy: Parallel + resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.resourcesPreset .Values.resources $) | nindent 10 }} + dataStorage: + enabled: true + size: {{ .Values.size }} + {{- with .Values.storageClass }} + storageClass: {{ . }} + {{- end }} + {{- if gt (int .Values.replicas) 1 }} + standalone: + enabled: false + ha: + enabled: true + replicas: {{ .Values.replicas }} + raft: + enabled: true + setNodeId: true + config: | + ui = {{ .Values.ui }} + + listener "tcp" { + address = "[::]:8200" + cluster_address = "[::]:8201" + tls_disable = true + } + + storage "raft" { + path = "/openbao/data" + {{- range $i := until (int $.Values.replicas) }} + retry_join { + leader_api_addr = "http://{{ $.Release.Name }}-{{ $i }}.{{ $.Release.Name }}-internal:8200" + } + {{- end }} + } + + service_registration "kubernetes" {} + {{- else }} + standalone: + enabled: true + config: | + ui = {{ .Values.ui }} + + listener "tcp" { + address = "[::]:8200" + cluster_address = "[::]:8201" + tls_disable = true + } + + storage "file" { + path = "/openbao/data" + } + # Note: service_registration "kubernetes" {} is intentionally omitted + # in standalone mode — it requires an HA-capable storage backend and + # causes a fatal error with storage "file". + ha: + enabled: false + {{- end }} + {{- if .Values.external }} + service: + type: LoadBalancer + {{- end }} + ui: + enabled: {{ .Values.ui }} + {{- if .Values.external }} + serviceType: LoadBalancer + {{- end }} + injector: + enabled: false + csi: + enabled: false diff --git a/packages/apps/openbao/templates/workloadmonitor.yaml b/packages/apps/openbao/templates/workloadmonitor.yaml new file mode 100644 index 00000000..0a9acf76 --- /dev/null +++ b/packages/apps/openbao/templates/workloadmonitor.yaml @@ -0,0 +1,13 @@ +--- +apiVersion: cozystack.io/v1alpha1 +kind: WorkloadMonitor +metadata: + name: {{ $.Release.Name }} +spec: + replicas: {{ .Values.replicas }} + minReplicas: 1 + kind: openbao + type: openbao + selector: + app.kubernetes.io/instance: {{ $.Release.Name }}-system + version: {{ $.Chart.Version }} diff --git a/packages/apps/openbao/values.schema.json b/packages/apps/openbao/values.schema.json new file mode 100644 index 00000000..b48f28a6 --- /dev/null +++ b/packages/apps/openbao/values.schema.json @@ -0,0 +1,87 @@ +{ + "title": "Chart Values", + "type": "object", + "properties": { + "external": { + "description": "Enable external access from outside the cluster.", + "type": "boolean", + "default": false + }, + "replicas": { + "description": "Number of OpenBAO replicas. HA with Raft is automatically enabled when replicas \u003e 1. Switching between standalone (file storage) and HA (Raft storage) modes requires data migration.", + "type": "integer", + "default": 1 + }, + "resources": { + "description": "Explicit CPU and memory configuration for each OpenBAO replica. When omitted, 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.", + "type": "string", + "default": "small", + "enum": [ + "nano", + "micro", + "small", + "medium", + "large", + "xlarge", + "2xlarge" + ] + }, + "size": { + "description": "Persistent Volume Claim size for data storage.", + "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", + "default": "" + }, + "ui": { + "description": "Enable the OpenBAO web UI.", + "type": "boolean", + "default": true + } + } +} \ No newline at end of file diff --git a/packages/apps/openbao/values.yaml b/packages/apps/openbao/values.yaml new file mode 100644 index 00000000..fc8b75cc --- /dev/null +++ b/packages/apps/openbao/values.yaml @@ -0,0 +1,41 @@ +## +## @section Common parameters +## + +## @typedef {struct} Resources - Explicit CPU and memory configuration for each OpenBAO replica. +## @field {quantity} [cpu] - CPU available to each replica. +## @field {quantity} [memory] - Memory (RAM) available to each replica. + +## @enum {string} ResourcesPreset - Default sizing preset. +## @value nano +## @value micro +## @value small +## @value medium +## @value large +## @value xlarge +## @value 2xlarge + +## @param {int} replicas - Number of OpenBAO replicas. HA with Raft is automatically enabled when replicas > 1. Switching between standalone (file storage) and HA (Raft storage) modes requires data migration. +replicas: 1 + +## @param {Resources} [resources] - Explicit CPU and memory configuration for each OpenBAO replica. When omitted, the preset defined in `resourcesPreset` is applied. +resources: {} + +## @param {ResourcesPreset} resourcesPreset="small" - Default sizing preset used when `resources` is omitted. +resourcesPreset: "small" + +## @param {quantity} size - Persistent Volume Claim size for data storage. +size: 10Gi + +## @param {string} storageClass - StorageClass used to store the data. +storageClass: "" + +## @param {bool} external - Enable external access from outside the cluster. +external: false + +## +## @section Application-specific parameters +## + +## @param {bool} ui - Enable the OpenBAO web UI. +ui: true From 088bc0ffe2285bdbf783862d602baa9498fd15b8 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Tue, 17 Feb 2026 20:03:02 +0300 Subject: [PATCH 064/666] feat(openbao): add resource definition, PackageSource, and PaaS bundle entry Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- .../platform/sources/openbao-application.yaml | 29 +++++++++++++++ .../core/platform/templates/bundles/paas.yaml | 1 + packages/system/openbao-rd/Chart.yaml | 3 ++ packages/system/openbao-rd/Makefile | 4 ++ .../system/openbao-rd/cozyrds/openbao.yaml | 37 +++++++++++++++++++ .../system/openbao-rd/templates/cozyrd.yaml | 4 ++ packages/system/openbao-rd/values.yaml | 1 + 7 files changed, 79 insertions(+) create mode 100644 packages/core/platform/sources/openbao-application.yaml create mode 100644 packages/system/openbao-rd/Chart.yaml create mode 100644 packages/system/openbao-rd/Makefile create mode 100644 packages/system/openbao-rd/cozyrds/openbao.yaml create mode 100644 packages/system/openbao-rd/templates/cozyrd.yaml create mode 100644 packages/system/openbao-rd/values.yaml diff --git a/packages/core/platform/sources/openbao-application.yaml b/packages/core/platform/sources/openbao-application.yaml new file mode 100644 index 00000000..438148af --- /dev/null +++ b/packages/core/platform/sources/openbao-application.yaml @@ -0,0 +1,29 @@ +--- +apiVersion: cozystack.io/v1alpha1 +kind: PackageSource +metadata: + name: cozystack.openbao-application +spec: + sourceRef: + kind: OCIRepository + name: cozystack-packages + namespace: cozy-system + path: / + variants: + - name: default + dependsOn: + - cozystack.networking + libraries: + - name: cozy-lib + path: library/cozy-lib + components: + - name: openbao-system + path: system/openbao + - name: openbao + path: apps/openbao + libraries: ["cozy-lib"] + - name: openbao-rd + path: system/openbao-rd + install: + namespace: cozy-system + releaseName: openbao-rd diff --git a/packages/core/platform/templates/bundles/paas.yaml b/packages/core/platform/templates/bundles/paas.yaml index 5a478f9d..80eb9824 100644 --- a/packages/core/platform/templates/bundles/paas.yaml +++ b/packages/core/platform/templates/bundles/paas.yaml @@ -15,6 +15,7 @@ {{include "cozystack.platform.package.default" (list "cozystack.mariadb-application" $) }} {{include "cozystack.platform.package.default" (list "cozystack.mongodb-application" $) }} {{include "cozystack.platform.package.default" (list "cozystack.nats-application" $) }} +{{include "cozystack.platform.package.default" (list "cozystack.openbao-application" $) }} {{include "cozystack.platform.package.default" (list "cozystack.postgres-application" $) }} {{include "cozystack.platform.package.default" (list "cozystack.qdrant-application" $) }} {{include "cozystack.platform.package.default" (list "cozystack.rabbitmq-application" $) }} diff --git a/packages/system/openbao-rd/Chart.yaml b/packages/system/openbao-rd/Chart.yaml new file mode 100644 index 00000000..f93484c3 --- /dev/null +++ b/packages/system/openbao-rd/Chart.yaml @@ -0,0 +1,3 @@ +apiVersion: v2 +name: openbao-rd +version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process diff --git a/packages/system/openbao-rd/Makefile b/packages/system/openbao-rd/Makefile new file mode 100644 index 00000000..3ad0f7ba --- /dev/null +++ b/packages/system/openbao-rd/Makefile @@ -0,0 +1,4 @@ +export NAME=openbao-rd +export NAMESPACE=cozy-system + +include ../../../hack/package.mk diff --git a/packages/system/openbao-rd/cozyrds/openbao.yaml b/packages/system/openbao-rd/cozyrds/openbao.yaml new file mode 100644 index 00000000..9c20776e --- /dev/null +++ b/packages/system/openbao-rd/cozyrds/openbao.yaml @@ -0,0 +1,37 @@ +apiVersion: cozystack.io/v1alpha1 +kind: ApplicationDefinition +metadata: + name: openbao +spec: + application: + kind: OpenBAO + plural: openbaos + singular: openbao + openAPISchema: |- + {"title":"Chart Values","type":"object","properties":{"external":{"description":"Enable external access from outside the cluster.","type":"boolean","default":false},"replicas":{"description":"Number of OpenBAO replicas. HA with Raft is automatically enabled when replicas > 1. Switching between standalone (file storage) and HA (Raft storage) modes requires data migration.","type":"integer","default":1},"resources":{"description":"Explicit CPU and memory configuration for each OpenBAO replica. When omitted, 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.","type":"string","default":"small","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]},"size":{"description":"Persistent Volume Claim size for data storage.","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","default":""},"ui":{"description":"Enable the OpenBAO web UI.","type":"boolean","default":true}}} + release: + prefix: openbao- + labels: + sharding.fluxcd.io/key: tenants + chartRef: + kind: ExternalArtifact + name: cozystack-openbao-application-default-openbao + namespace: cozy-system + dashboard: + category: PaaS + singular: OpenBAO + plural: OpenBAO + description: Managed OpenBAO secrets management service + tags: + - secrets + icon: PHN2ZyB3aWR0aD0iMTQ0IiBoZWlnaHQ9IjE0NCIgdmlld0JveD0iMCAwIDE0NCAxNDQiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIxNDQiIGhlaWdodD0iMTQ0IiByeD0iMjQiIGZpbGw9InVybCgjcGFpbnQwX2xpbmVhcikiLz4KPHJlY3Qgd2lkdGg9IjE0NCIgaGVpZ2h0PSIxNDQiIHJ4PSIyNCIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC4zIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNzIgMzBDNTMuMjIyIDMwIDM4IDQ1LjIyMiAzOCA2NHY4Yy0zLjMxNCAwLTYgMi42ODYtNiA2djMwYzAgMy4zMTQgMi42ODYgNiA2IDZoNjhjMy4zMTQgMCA2LTIuNjg2IDYtNlY3OGMwLTMuMzE0LTIuNjg2LTYtNi02di04QzEwNiA0NS4yMjIgOTAuNzc4IDMwIDcyIDMwem0tOCA0MnYtOGMwLTQuNDE4IDMuNTgyLTggOC04czggMy41ODIgOCA4djhINjR6bTI2IDB2LThjMC04LjgzNy03LjE2My0xNi0xNi0xNnMtMTYgNy4xNjMtMTYgMTZ2OGgtMnYyOGg2MFY3Mkg5MHptLTIyIDE0YTQgNCAwIDExOCAwIDQgNCAwIDAxLTggMHptNC04YTggOCAwIDEwMCAxNiA4IDggMCAwMDAtMTZ6IiBmaWxsPSJ3aGl0ZSIvPgo8ZGVmcz4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDBfbGluZWFyIiB4MT0iMTAiIHkxPSIxNS41IiB4Mj0iMTQ0IiB5Mj0iMTMxLjUiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KPHN0b3Agc3RvcC1jb2xvcj0iIzg3ZDZiZSIvPgo8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiM3OWMwYWIiLz4KPC9saW5lYXJHcmFkaWVudD4KPC9kZWZzPgo8L3N2Zz4K + keysOrder: [["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata", "name"], ["spec", "replicas"], ["spec", "resources"], ["spec", "resourcesPreset"], ["spec", "size"], ["spec", "storageClass"], ["spec", "external"], ["spec", "ui"]] + secrets: + exclude: [] + include: [] + services: + exclude: [] + include: + - resourceNames: + - openbao-{{ .name }} + - openbao-{{ .name }}-internal diff --git a/packages/system/openbao-rd/templates/cozyrd.yaml b/packages/system/openbao-rd/templates/cozyrd.yaml new file mode 100644 index 00000000..e079ddcf --- /dev/null +++ b/packages/system/openbao-rd/templates/cozyrd.yaml @@ -0,0 +1,4 @@ +{{- range $path, $_ := .Files.Glob "cozyrds/*" }} +--- +{{ $.Files.Get $path }} +{{- end }} diff --git a/packages/system/openbao-rd/values.yaml b/packages/system/openbao-rd/values.yaml new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/packages/system/openbao-rd/values.yaml @@ -0,0 +1 @@ +{} From dd4723386fcbb4041d6bb6bef144c5c8597e55ac Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Tue, 17 Feb 2026 21:23:27 +0300 Subject: [PATCH 065/666] test(openbao): add E2E test for standalone mode Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- hack/e2e-apps/openbao.bats | 59 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 hack/e2e-apps/openbao.bats diff --git a/hack/e2e-apps/openbao.bats b/hack/e2e-apps/openbao.bats new file mode 100644 index 00000000..cd9d47d3 --- /dev/null +++ b/hack/e2e-apps/openbao.bats @@ -0,0 +1,59 @@ +#!/usr/bin/env bats + +@test "Create OpenBAO (standalone)" { + name='test' + kubectl apply -f- </dev/null | grep -q true; do sleep 5; done"; then + echo "=== DEBUG: Container did not start in time ===" >&2 + kubectl -n tenant-test describe pod openbao-$name-0 >&2 || true + kubectl -n tenant-test logs openbao-$name-0 --previous >&2 || true + kubectl -n tenant-test logs openbao-$name-0 >&2 || true + return 1 + fi + + # Wait for OpenBAO API to accept connections + # bao status exit codes: 0 = unsealed, 1 = error/not ready, 2 = sealed but responsive + if ! timeout 60 sh -ec "until kubectl -n tenant-test exec openbao-$name-0 -- bao status >/dev/null 2>&1; rc=\$?; test \$rc -eq 0 -o \$rc -eq 2; do sleep 3; done"; then + echo "=== DEBUG: OpenBAO API did not become responsive ===" >&2 + kubectl -n tenant-test describe pod openbao-$name-0 >&2 || true + kubectl -n tenant-test logs openbao-$name-0 --previous >&2 || true + kubectl -n tenant-test logs openbao-$name-0 >&2 || true + return 1 + fi + + # Initialize OpenBAO (single key share for testing simplicity) + init_output=$(kubectl -n tenant-test exec openbao-$name-0 -- bao operator init -key-shares=1 -key-threshold=1 -format=json) + unseal_key=$(echo "$init_output" | jq -r '.unseal_keys_b64[0]') + if [ -z "$unseal_key" ] || [ "$unseal_key" = "null" ]; then + echo "Failed to extract unseal key. Init output: $init_output" >&2 + return 1 + fi + + # Unseal OpenBAO + kubectl -n tenant-test exec openbao-$name-0 -- bao operator unseal "$unseal_key" + + # Now wait for pod to become ready (readiness probe checks seal status) + kubectl -n tenant-test wait sts openbao-$name --timeout=90s --for=jsonpath='{.status.readyReplicas}'=1 + kubectl -n tenant-test wait pvc data-openbao-$name-0 --timeout=50s --for=jsonpath='{.status.phase}'=Bound + kubectl -n tenant-test delete openbao.apps.cozystack.io $name + kubectl -n tenant-test delete pvc data-openbao-$name-0 --ignore-not-found +} From 8cc8e52d157f9b9116b2f4c84650000b77f9904b Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Tue, 17 Feb 2026 19:31:57 +0100 Subject: [PATCH 066/666] chore(platform): remove standalone kilo PackageSource Kilo is now integrated into the cilium-kilo networking variant instead of being a separate package that users install manually. Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- packages/core/platform/sources/kilo.yaml | 35 ------------------------ 1 file changed, 35 deletions(-) delete mode 100644 packages/core/platform/sources/kilo.yaml diff --git a/packages/core/platform/sources/kilo.yaml b/packages/core/platform/sources/kilo.yaml deleted file mode 100644 index 95770667..00000000 --- a/packages/core/platform/sources/kilo.yaml +++ /dev/null @@ -1,35 +0,0 @@ ---- -apiVersion: cozystack.io/v1alpha1 -kind: PackageSource -metadata: - name: cozystack.kilo -spec: - sourceRef: - kind: OCIRepository - name: cozystack-packages - namespace: cozy-system - path: / - variants: - - name: default - dependsOn: - - cozystack.networking - components: - - name: kilo - path: system/kilo - install: - privileged: true - namespace: cozy-kilo - releaseName: kilo - - name: cilium - dependsOn: - - cozystack.networking - components: - - name: kilo - path: system/kilo - valuesFiles: - - values.yaml - - values-cilium.yaml - install: - privileged: true - namespace: cozy-kilo - releaseName: kilo From 536766cffcf4fcebad5ef28e4ad21991ff5c2c3d Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Tue, 17 Feb 2026 19:32:19 +0100 Subject: [PATCH 067/666] feat(platform): add cilium-kilo networking variant Add a new networking variant that integrates Kilo with Cilium pre-configured. Cilium is deployed with host firewall disabled and enable-ipip-termination enabled, which are required for correct IPIP encapsulation through Cilium's overlay. Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- .../core/platform/sources/networking.yaml | 25 +++++++++++++++++++ packages/system/cilium/values-kilo.yaml | 5 ++++ 2 files changed, 30 insertions(+) create mode 100644 packages/system/cilium/values-kilo.yaml diff --git a/packages/core/platform/sources/networking.yaml b/packages/core/platform/sources/networking.yaml index 9694dcc3..8e8485b3 100644 --- a/packages/core/platform/sources/networking.yaml +++ b/packages/core/platform/sources/networking.yaml @@ -33,6 +33,31 @@ spec: releaseName: cilium-networkpolicy dependsOn: - cilium + - name: cilium-kilo + dependsOn: [] + components: + - name: cilium + path: system/cilium + valuesFiles: + - values.yaml + - values-talos.yaml + - values-kilo.yaml + install: + privileged: true + namespace: cozy-cilium + releaseName: cilium + dependsOn: [] + - name: kilo + path: system/kilo + valuesFiles: + - values.yaml + - values-cilium.yaml + install: + privileged: true + namespace: cozy-kilo + releaseName: kilo + dependsOn: + - cilium # Generic Cilium variant for non-Talos clusters (kubeadm, k3s, RKE2, etc.) - name: cilium-generic dependsOn: [] diff --git a/packages/system/cilium/values-kilo.yaml b/packages/system/cilium/values-kilo.yaml new file mode 100644 index 00000000..c7e98d2a --- /dev/null +++ b/packages/system/cilium/values-kilo.yaml @@ -0,0 +1,5 @@ +cilium: + hostFirewall: + enabled: false + extraConfig: + enable-ipip-termination: "true" From 96ba3b9ca5df595d3dbc2f499e29ef763db4f2ef Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Tue, 17 Feb 2026 20:14:34 +0100 Subject: [PATCH 068/666] fix(kilo): remove service-cidr option from chart The --service-cidr flag prevents masquerading for service IPs, but service CIDRs are cluster-local and not useful for multi-location mesh routing. Remove the serviceCIDR value and its template usage. Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- packages/system/kilo/templates/kilo.yaml | 3 --- packages/system/kilo/values.yaml | 5 ++--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/system/kilo/templates/kilo.yaml b/packages/system/kilo/templates/kilo.yaml index fef7a718..a0af620a 100644 --- a/packages/system/kilo/templates/kilo.yaml +++ b/packages/system/kilo/templates/kilo.yaml @@ -38,9 +38,6 @@ spec: {{- with .Values.kilo.podCIDR }} - --service-cidr={{ . }} {{- end }} - {{- with .Values.kilo.serviceCIDR }} - - --service-cidr={{ . }} - {{- end }} {{- with .Values.kilo.transitCIDR }} - --subnet={{ . }} {{- end }} diff --git a/packages/system/kilo/values.yaml b/packages/system/kilo/values.yaml index 4057c284..939fd222 100644 --- a/packages/system/kilo/values.yaml +++ b/packages/system/kilo/values.yaml @@ -1,10 +1,9 @@ kilo: image: pullPolicy: IfNotPresent - tag: v0.8.1@sha256:56602fa796eccea0d0e005c29e5c7094ae46d15bd5306b02b829672b178dcd5c - repository: ghcr.io/cozystack/cozystack/kilo + tag: kilo-require-cilium-tunl + repository: ghcr.io/kvaps/test podCIDR: 10.244.0.0/16 - serviceCIDR: 10.96.0.0/16 transitCIDR: 100.66.0.0/16 meshGranularity: location cleanUpInterface: false From bf1e49d34bad47afeb6d25ff9350472aa1fbd6e6 Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Tue, 17 Feb 2026 20:16:01 +0100 Subject: [PATCH 069/666] fix(kilo): remove podCIDR passed as --service-cidr The podCIDR was incorrectly passed as --service-cidr to prevent masquerading on pod traffic. This is unnecessary for multi-location mesh and was a leftover from single-cluster assumptions. Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- packages/system/kilo/templates/kilo.yaml | 3 --- packages/system/kilo/values.yaml | 1 - 2 files changed, 4 deletions(-) diff --git a/packages/system/kilo/templates/kilo.yaml b/packages/system/kilo/templates/kilo.yaml index a0af620a..ceac643d 100644 --- a/packages/system/kilo/templates/kilo.yaml +++ b/packages/system/kilo/templates/kilo.yaml @@ -35,9 +35,6 @@ spec: - --encapsulate=crosssubnet - --local=false - --mesh-granularity={{ .Values.kilo.meshGranularity | default "location" }} - {{- with .Values.kilo.podCIDR }} - - --service-cidr={{ . }} - {{- end }} {{- with .Values.kilo.transitCIDR }} - --subnet={{ . }} {{- end }} diff --git a/packages/system/kilo/values.yaml b/packages/system/kilo/values.yaml index 939fd222..5ffd5da0 100644 --- a/packages/system/kilo/values.yaml +++ b/packages/system/kilo/values.yaml @@ -3,7 +3,6 @@ kilo: pullPolicy: IfNotPresent tag: kilo-require-cilium-tunl repository: ghcr.io/kvaps/test - podCIDR: 10.244.0.0/16 transitCIDR: 100.66.0.0/16 meshGranularity: location cleanUpInterface: false From b3b73071057d3dce856f21b60eca44e9c1b12123 Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Tue, 17 Feb 2026 22:14:52 +0100 Subject: [PATCH 070/666] fix(kilo): use official kilo image and clean up cilium-kilo config Switch kilo image to official ghcr.io/cozystack/cozystack/kilo:v0.8.2, remove unnecessary enable-ipip-termination from cilium-kilo values, and update platform source digest. Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- packages/system/cilium/values-kilo.yaml | 2 -- packages/system/kilo/values.yaml | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/system/cilium/values-kilo.yaml b/packages/system/cilium/values-kilo.yaml index c7e98d2a..c303ef0a 100644 --- a/packages/system/cilium/values-kilo.yaml +++ b/packages/system/cilium/values-kilo.yaml @@ -1,5 +1,3 @@ cilium: hostFirewall: enabled: false - extraConfig: - enable-ipip-termination: "true" diff --git a/packages/system/kilo/values.yaml b/packages/system/kilo/values.yaml index 5ffd5da0..d7c00af4 100644 --- a/packages/system/kilo/values.yaml +++ b/packages/system/kilo/values.yaml @@ -1,8 +1,8 @@ kilo: image: pullPolicy: IfNotPresent - tag: kilo-require-cilium-tunl - repository: ghcr.io/kvaps/test + tag: v0.8.2@sha256:9f27000ffd0bbf9adf3aefd017b7835dfae57b6ea38bf7488fb636536c2467da + repository: ghcr.io/cozystack/cozystack/kilo transitCIDR: 100.66.0.0/16 meshGranularity: location cleanUpInterface: false From 153d2c48ae7215eb3b793b5ef7b2027f39edc5a9 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Mon, 16 Feb 2026 22:24:29 +0300 Subject: [PATCH 071/666] refactor(e2e): use helm install instead of kubectl apply for cozystack installation Replace pre-rendered static YAML application with direct helm chart installation in e2e tests. The chart directory with correct values is already present in the sandbox after pr.patch application. - Remove CRD/operator artifact upload/download from CI workflow - Remove copy-installer-manifest target from testing Makefile - Use helm upgrade --install from local chart in e2e-install-cozystack.bats Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- .github/workflows/pull-requests.yaml | 40 +--------------------------- hack/e2e-install-cozystack.bats | 25 ++++++++--------- packages/core/testing/Makefile | 8 ++---- 3 files changed, 14 insertions(+), 59 deletions(-) diff --git a/.github/workflows/pull-requests.yaml b/.github/workflows/pull-requests.yaml index edc8b923..b9b7b0ac 100644 --- a/.github/workflows/pull-requests.yaml +++ b/.github/workflows/pull-requests.yaml @@ -71,18 +71,6 @@ jobs: name: pr-patch path: _out/assets/pr.patch - - name: Upload CRDs - uses: actions/upload-artifact@v4 - with: - name: cozystack-crds - path: _out/assets/cozystack-crds.yaml - - - name: Upload operator - uses: actions/upload-artifact@v4 - with: - name: cozystack-operator - path: _out/assets/cozystack-operator-talos.yaml - - name: Upload Talos image uses: actions/upload-artifact@v4 with: @@ -94,8 +82,6 @@ jobs: runs-on: ubuntu-latest if: contains(github.event.pull_request.labels.*.name, 'release') outputs: - crds_id: ${{ steps.fetch_assets.outputs.crds_id }} - operator_id: ${{ steps.fetch_assets.outputs.operator_id }} disk_id: ${{ steps.fetch_assets.outputs.disk_id }} steps: @@ -139,15 +125,11 @@ jobs: return; } const find = (n) => draft.assets.find(a => a.name === n)?.id; - const crdsId = find('cozystack-crds.yaml'); - const operatorId = find('cozystack-operator-talos.yaml'); const diskId = find('nocloud-amd64.raw.xz'); - if (!crdsId || !operatorId || !diskId) { + if (!diskId) { core.setFailed('Required assets missing in draft release'); return; } - core.setOutput('crds_id', crdsId); - core.setOutput('operator_id', operatorId); core.setOutput('disk_id', diskId); @@ -174,20 +156,6 @@ jobs: name: talos-image path: _out/assets - - name: "Download CRDs (regular PR)" - if: "!contains(github.event.pull_request.labels.*.name, 'release')" - uses: actions/download-artifact@v4 - with: - name: cozystack-crds - path: _out/assets - - - name: "Download operator (regular PR)" - if: "!contains(github.event.pull_request.labels.*.name, 'release')" - uses: actions/download-artifact@v4 - with: - name: cozystack-operator - path: _out/assets - - name: Download PR patch if: "!contains(github.event.pull_request.labels.*.name, 'release')" uses: actions/download-artifact@v4 @@ -208,12 +176,6 @@ jobs: curl -sSL -H "Authorization: token ${GH_PAT}" -H "Accept: application/octet-stream" \ -o _out/assets/nocloud-amd64.raw.xz \ "https://api.github.com/repos/${GITHUB_REPOSITORY}/releases/assets/${{ needs.resolve_assets.outputs.disk_id }}" - curl -sSL -H "Authorization: token ${GH_PAT}" -H "Accept: application/octet-stream" \ - -o _out/assets/cozystack-crds.yaml \ - "https://api.github.com/repos/${GITHUB_REPOSITORY}/releases/assets/${{ needs.resolve_assets.outputs.crds_id }}" - curl -sSL -H "Authorization: token ${GH_PAT}" -H "Accept: application/octet-stream" \ - -o _out/assets/cozystack-operator-talos.yaml \ - "https://api.github.com/repos/${GITHUB_REPOSITORY}/releases/assets/${{ needs.resolve_assets.outputs.operator_id }}" env: GH_PAT: ${{ secrets.GH_PAT }} diff --git a/hack/e2e-install-cozystack.bats b/hack/e2e-install-cozystack.bats index 1abb691f..36329e93 100644 --- a/hack/e2e-install-cozystack.bats +++ b/hack/e2e-install-cozystack.bats @@ -1,25 +1,22 @@ #!/usr/bin/env bats -@test "Required installer assets exist" { - if [ ! -f _out/assets/cozystack-crds.yaml ]; then - echo "Missing: _out/assets/cozystack-crds.yaml" >&2 - exit 1 - fi - if [ ! -f _out/assets/cozystack-operator-talos.yaml ]; then - echo "Missing: _out/assets/cozystack-operator-talos.yaml" >&2 +@test "Required installer chart exists" { + if [ ! -f packages/core/installer/Chart.yaml ]; then + echo "Missing: packages/core/installer/Chart.yaml" >&2 exit 1 fi } @test "Install Cozystack" { - # Create namespace - kubectl create namespace cozy-system --dry-run=client -o yaml | kubectl apply -f - + # Install cozy-installer chart + helm upgrade installer packages/core/installer \ + --install \ + --namespace cozy-system \ + --create-namespace \ + --wait \ + --timeout 2m - # Apply installer manifests (CRDs + operator) - kubectl apply -f _out/assets/cozystack-crds.yaml - kubectl apply -f _out/assets/cozystack-operator-talos.yaml - - # Wait for the operator deployment to become available + # Verify the operator deployment is available kubectl wait deployment/cozystack-operator -n cozy-system --timeout=1m --for=condition=Available # Create platform Package with isp-full variant diff --git a/packages/core/testing/Makefile b/packages/core/testing/Makefile index 2fb41eba..35897909 100644 --- a/packages/core/testing/Makefile +++ b/packages/core/testing/Makefile @@ -30,17 +30,13 @@ test: test-cluster test-openapi test-apps ## Run the end-to-end tests in existin copy-nocloud-image: docker cp ../../../_out/assets/nocloud-amd64.raw.xz "${SANDBOX_NAME}":/workspace/_out/assets/nocloud-amd64.raw.xz -copy-installer-manifest: - docker cp ../../../_out/assets/cozystack-crds.yaml "${SANDBOX_NAME}":/workspace/_out/assets/cozystack-crds.yaml - docker cp ../../../_out/assets/cozystack-operator-talos.yaml "${SANDBOX_NAME}":/workspace/_out/assets/cozystack-operator-talos.yaml - prepare-cluster: copy-nocloud-image docker exec "${SANDBOX_NAME}" sh -c 'cd /workspace && hack/cozytest.sh hack/e2e-prepare-cluster.bats' -install-cozystack: copy-installer-manifest +install-cozystack: docker exec "${SANDBOX_NAME}" sh -c 'cd /workspace && hack/cozytest.sh hack/e2e-install-cozystack.bats' -test-cluster: copy-nocloud-image copy-installer-manifest ## Run the end-to-end for creating a cluster +test-cluster: copy-nocloud-image ## Run the end-to-end for creating a cluster docker exec "${SANDBOX_NAME}" sh -c 'cd /workspace && hack/cozytest.sh hack/e2e-cluster.bats' test-openapi: From 58dfc972019ee9db5593eb8ed3161733b8d3775f Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Mon, 16 Feb 2026 22:52:17 +0300 Subject: [PATCH 072/666] fix(e2e): apply CRDs before helm install to resolve dependency ordering Helm cannot validate PackageSource CR during install because the CRD is part of the same chart. Pre-apply CRDs via helm template + kubectl apply --server-side before running helm upgrade --install. Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- hack/e2e-install-cozystack.bats | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/hack/e2e-install-cozystack.bats b/hack/e2e-install-cozystack.bats index 36329e93..f98c1266 100644 --- a/hack/e2e-install-cozystack.bats +++ b/hack/e2e-install-cozystack.bats @@ -8,6 +8,12 @@ } @test "Install Cozystack" { + # Install CRDs first (PackageSource CR in the chart depends on them) + helm template installer packages/core/installer \ + --namespace cozy-system \ + --show-only templates/crds.yaml \ + | kubectl apply --server-side -f - + # Install cozy-installer chart helm upgrade installer packages/core/installer \ --install \ From 55cd8fc0e1ddc945d0d2523956646688c6804ecb Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Mon, 16 Feb 2026 23:04:03 +0300 Subject: [PATCH 073/666] refactor(installer): move CRDs to crds/ directory for proper Helm install ordering Helm installs crds/ contents before processing templates, resolving the chicken-and-egg problem where PackageSource CR validation fails because its CRD hasn't been registered yet. - Move definitions/ to crds/ in the installer chart - Remove templates/crds.yaml (Helm auto-installs from crds/) - Update codegen script to write CRDs to crds/ - Replace helm template with cat for static CRD manifest generation - Remove pre-apply CRD workaround from e2e test Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- Makefile | 4 +--- hack/e2e-install-cozystack.bats | 8 +------- hack/update-codegen.sh | 2 +- packages/core/installer/.helmignore | 2 -- .../core/installer/{definitions => crds}/.gitattributes | 0 .../{definitions => crds}/cozystack.io_packages.yaml | 0 .../cozystack.io_packagesources.yaml | 0 packages/core/installer/templates/crds.yaml | 3 --- 8 files changed, 3 insertions(+), 16 deletions(-) rename packages/core/installer/{definitions => crds}/.gitattributes (100%) rename packages/core/installer/{definitions => crds}/cozystack.io_packages.yaml (100%) rename packages/core/installer/{definitions => crds}/cozystack.io_packagesources.yaml (100%) delete mode 100644 packages/core/installer/templates/crds.yaml diff --git a/Makefile b/Makefile index 12554861..dd6a249d 100644 --- a/Makefile +++ b/Makefile @@ -38,9 +38,7 @@ build: build-deps manifests: mkdir -p _out/assets - helm template installer packages/core/installer -n cozy-system \ - -s templates/crds.yaml \ - > _out/assets/cozystack-crds.yaml + cat packages/core/installer/crds/*.yaml > _out/assets/cozystack-crds.yaml # Talos variant (default) helm template installer packages/core/installer -n cozy-system \ -s templates/cozystack-operator.yaml \ diff --git a/hack/e2e-install-cozystack.bats b/hack/e2e-install-cozystack.bats index f98c1266..5e518cad 100644 --- a/hack/e2e-install-cozystack.bats +++ b/hack/e2e-install-cozystack.bats @@ -8,13 +8,7 @@ } @test "Install Cozystack" { - # Install CRDs first (PackageSource CR in the chart depends on them) - helm template installer packages/core/installer \ - --namespace cozy-system \ - --show-only templates/crds.yaml \ - | kubectl apply --server-side -f - - - # Install cozy-installer chart + # Install cozy-installer chart (CRDs from crds/ are applied automatically) helm upgrade installer packages/core/installer \ --install \ --namespace cozy-system \ diff --git a/hack/update-codegen.sh b/hack/update-codegen.sh index 293f229b..d0711cad 100755 --- a/hack/update-codegen.sh +++ b/hack/update-codegen.sh @@ -24,7 +24,7 @@ API_KNOWN_VIOLATIONS_DIR="${API_KNOWN_VIOLATIONS_DIR:-"${SCRIPT_ROOT}/api/api-ru UPDATE_API_KNOWN_VIOLATIONS="${UPDATE_API_KNOWN_VIOLATIONS:-true}" CONTROLLER_GEN="go run sigs.k8s.io/controller-tools/cmd/controller-gen@v0.16.4" TMPDIR=$(mktemp -d) -OPERATOR_CRDDIR=packages/core/installer/definitions +OPERATOR_CRDDIR=packages/core/installer/crds COZY_CONTROLLER_CRDDIR=packages/system/cozystack-controller/definitions COZY_RD_CRDDIR=packages/system/application-definition-crd/definition BACKUPS_CORE_CRDDIR=packages/system/backup-controller/definitions diff --git a/packages/core/installer/.helmignore b/packages/core/installer/.helmignore index ba88ffe8..6901ff3d 100644 --- a/packages/core/installer/.helmignore +++ b/packages/core/installer/.helmignore @@ -7,5 +7,3 @@ Makefile images/ example/ *.tgz - -# NOTE: definitions/ is intentionally NOT excluded — needed by crds.yaml via .Files.Glob diff --git a/packages/core/installer/definitions/.gitattributes b/packages/core/installer/crds/.gitattributes similarity index 100% rename from packages/core/installer/definitions/.gitattributes rename to packages/core/installer/crds/.gitattributes diff --git a/packages/core/installer/definitions/cozystack.io_packages.yaml b/packages/core/installer/crds/cozystack.io_packages.yaml similarity index 100% rename from packages/core/installer/definitions/cozystack.io_packages.yaml rename to packages/core/installer/crds/cozystack.io_packages.yaml diff --git a/packages/core/installer/definitions/cozystack.io_packagesources.yaml b/packages/core/installer/crds/cozystack.io_packagesources.yaml similarity index 100% rename from packages/core/installer/definitions/cozystack.io_packagesources.yaml rename to packages/core/installer/crds/cozystack.io_packagesources.yaml diff --git a/packages/core/installer/templates/crds.yaml b/packages/core/installer/templates/crds.yaml deleted file mode 100644 index 7c7ea584..00000000 --- a/packages/core/installer/templates/crds.yaml +++ /dev/null @@ -1,3 +0,0 @@ -{{- range $path, $_ := .Files.Glob "definitions/*.yaml" }} -{{ $.Files.Get $path }} -{{- end }} From d8dd5adbe020e5e35da3ab5c6950c8ad03e3701e Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Mon, 16 Feb 2026 23:13:33 +0300 Subject: [PATCH 074/666] fix(testing): remove broken test-cluster target The test-cluster target references non-existent hack/e2e-cluster.bats file. Remove it and its dependency from the test target. Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- packages/core/testing/Makefile | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/core/testing/Makefile b/packages/core/testing/Makefile index 35897909..ce21a8e1 100644 --- a/packages/core/testing/Makefile +++ b/packages/core/testing/Makefile @@ -25,7 +25,7 @@ image-e2e-sandbox: yq -i '.e2e.image = strenv(IMAGE)' values.yaml rm -f images/e2e-sandbox.json -test: test-cluster test-openapi test-apps ## Run the end-to-end tests in existing sandbox +test: test-openapi test-apps ## Run the end-to-end tests in existing sandbox copy-nocloud-image: docker cp ../../../_out/assets/nocloud-amd64.raw.xz "${SANDBOX_NAME}":/workspace/_out/assets/nocloud-amd64.raw.xz @@ -36,9 +36,6 @@ prepare-cluster: copy-nocloud-image install-cozystack: docker exec "${SANDBOX_NAME}" sh -c 'cd /workspace && hack/cozytest.sh hack/e2e-install-cozystack.bats' -test-cluster: copy-nocloud-image ## Run the end-to-end for creating a cluster - docker exec "${SANDBOX_NAME}" sh -c 'cd /workspace && hack/cozytest.sh hack/e2e-cluster.bats' - test-openapi: docker exec "${SANDBOX_NAME}" sh -c 'cd /workspace && hack/cozytest.sh hack/e2e-test-openapi.bats' From 1ddbe68bc2c28c3990af8d8d22d6309731437130 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Tue, 17 Feb 2026 23:23:10 +0300 Subject: [PATCH 075/666] feat(operator): add --install-crds flag with embedded CRD manifests Embed Package and PackageSource CRDs in the operator binary using Go embed, following the same pattern as --install-flux. The operator applies CRDs at startup using server-side apply, ensuring they are updated on every operator restart/upgrade. This addresses the CRD lifecycle concern: Helm crds/ directory handles initial install, while the operator manages updates on subsequent deployments. Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- cmd/cozystack-operator/main.go | 16 ++ internal/crdinstall/install.go | 144 ++++++++++ internal/crdinstall/manifests.embed.go | 50 ++++ internal/crdinstall/manifests/.gitattributes | 1 + .../manifests/cozystack.io_packages.yaml | 171 ++++++++++++ .../cozystack.io_packagesources.yaml | 250 ++++++++++++++++++ 6 files changed, 632 insertions(+) create mode 100644 internal/crdinstall/install.go create mode 100644 internal/crdinstall/manifests.embed.go create mode 100644 internal/crdinstall/manifests/.gitattributes create mode 100644 internal/crdinstall/manifests/cozystack.io_packages.yaml create mode 100644 internal/crdinstall/manifests/cozystack.io_packagesources.yaml diff --git a/cmd/cozystack-operator/main.go b/cmd/cozystack-operator/main.go index 188ce160..f365a076 100644 --- a/cmd/cozystack-operator/main.go +++ b/cmd/cozystack-operator/main.go @@ -50,6 +50,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" "github.com/cozystack/cozystack/internal/cozyvaluesreplicator" + "github.com/cozystack/cozystack/internal/crdinstall" "github.com/cozystack/cozystack/internal/fluxinstall" "github.com/cozystack/cozystack/internal/operator" "github.com/cozystack/cozystack/internal/telemetry" @@ -77,6 +78,7 @@ func main() { var probeAddr string var secureMetrics bool var enableHTTP2 bool + var installCRDs bool var installFlux bool var disableTelemetry bool var telemetryEndpoint string @@ -97,6 +99,7 @@ func main() { "If set the metrics endpoint is served securely") flag.BoolVar(&enableHTTP2, "enable-http2", false, "If set, HTTP/2 will be enabled for the metrics and webhook servers") + flag.BoolVar(&installCRDs, "install-crds", false, "Install Cozystack CRDs before starting reconcile loop") flag.BoolVar(&installFlux, "install-flux", false, "Install Flux components before starting reconcile loop") flag.BoolVar(&disableTelemetry, "disable-telemetry", false, "Disable telemetry collection") @@ -177,6 +180,19 @@ func main() { os.Exit(1) } + // Install Cozystack CRDs before starting reconcile loop + if installCRDs { + setupLog.Info("Installing Cozystack CRDs before starting reconcile loop") + installCtx, installCancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer installCancel() + + if err := crdinstall.Install(installCtx, directClient, crdinstall.WriteEmbeddedManifests); err != nil { + setupLog.Error(err, "failed to install CRDs") + os.Exit(1) + } + setupLog.Info("CRD installation completed successfully") + } + // Install Flux before starting reconcile loop if installFlux { setupLog.Info("Installing Flux components before starting reconcile loop") diff --git a/internal/crdinstall/install.go b/internal/crdinstall/install.go new file mode 100644 index 00000000..fb6e4588 --- /dev/null +++ b/internal/crdinstall/install.go @@ -0,0 +1,144 @@ +/* +Copyright 2025 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 crdinstall + +import ( + "bufio" + "bytes" + "context" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + k8syaml "k8s.io/apimachinery/pkg/util/yaml" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +// Install applies Cozystack CRDs using embedded manifests. +// It extracts the manifests and applies them to the cluster using server-side apply. +func Install(ctx context.Context, k8sClient client.Client, writeEmbeddedManifests func(string) error) error { + logger := log.FromContext(ctx) + + tmpDir, err := os.MkdirTemp("", "crd-install-*") + if err != nil { + return fmt.Errorf("failed to create temp directory: %w", err) + } + defer os.RemoveAll(tmpDir) + + manifestsDir := filepath.Join(tmpDir, "manifests") + if err := os.MkdirAll(manifestsDir, 0755); err != nil { + return fmt.Errorf("failed to create manifests directory: %w", err) + } + + if err := writeEmbeddedManifests(manifestsDir); err != nil { + return fmt.Errorf("failed to extract embedded manifests: %w", err) + } + + entries, err := os.ReadDir(manifestsDir) + if err != nil { + return fmt.Errorf("failed to read manifests directory: %w", err) + } + + var manifestFiles []string + for _, entry := range entries { + if strings.HasSuffix(entry.Name(), ".yaml") { + manifestFiles = append(manifestFiles, filepath.Join(manifestsDir, entry.Name())) + } + } + + if len(manifestFiles) == 0 { + return fmt.Errorf("no YAML manifest files found in directory") + } + + var objects []*unstructured.Unstructured + for _, manifestPath := range manifestFiles { + objs, err := parseManifests(manifestPath) + if err != nil { + return fmt.Errorf("failed to parse manifests from %s: %w", manifestPath, err) + } + objects = append(objects, objs...) + } + + if len(objects) == 0 { + return fmt.Errorf("no objects found in manifests") + } + + logger.Info("Applying Cozystack CRDs", "count", len(objects)) + for _, obj := range objects { + patchOptions := &client.PatchOptions{ + FieldManager: "cozystack-operator", + Force: func() *bool { b := true; return &b }(), + } + + if err := k8sClient.Patch(ctx, obj, client.Apply, patchOptions); err != nil { + return fmt.Errorf("failed to apply CRD %s: %w", obj.GetName(), err) + } + logger.Info("Applied CRD", "name", obj.GetName()) + } + + logger.Info("CRD installation completed successfully") + return nil +} + +func parseManifests(manifestPath string) ([]*unstructured.Unstructured, error) { + data, err := os.ReadFile(manifestPath) + if err != nil { + return nil, fmt.Errorf("failed to read manifest file: %w", err) + } + + return readYAMLObjects(bytes.NewReader(data)) +} + +func readYAMLObjects(reader io.Reader) ([]*unstructured.Unstructured, error) { + var objects []*unstructured.Unstructured + yamlReader := k8syaml.NewYAMLReader(bufio.NewReader(reader)) + + for { + doc, err := yamlReader.Read() + if err != nil { + if err == io.EOF { + break + } + return nil, fmt.Errorf("failed to read YAML document: %w", err) + } + + if len(bytes.TrimSpace(doc)) == 0 { + continue + } + + obj := &unstructured.Unstructured{} + decoder := k8syaml.NewYAMLOrJSONDecoder(bytes.NewReader(doc), len(doc)) + if err := decoder.Decode(obj); err != nil { + if err == io.EOF { + continue + } + return nil, fmt.Errorf("failed to decode YAML document: %w", err) + } + + if obj.GetKind() == "" { + continue + } + + objects = append(objects, obj) + } + + return objects, nil +} diff --git a/internal/crdinstall/manifests.embed.go b/internal/crdinstall/manifests.embed.go new file mode 100644 index 00000000..dd578db6 --- /dev/null +++ b/internal/crdinstall/manifests.embed.go @@ -0,0 +1,50 @@ +/* +Copyright 2025 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 crdinstall + +import ( + "embed" + "fmt" + "io/fs" + "os" + "path" +) + +//go:embed manifests/*.yaml +var embeddedCRDManifests embed.FS + +// WriteEmbeddedManifests extracts embedded CRD manifests to a directory. +func WriteEmbeddedManifests(dir string) error { + manifests, err := fs.ReadDir(embeddedCRDManifests, "manifests") + if err != nil { + return fmt.Errorf("failed to read embedded manifests: %w", err) + } + + for _, manifest := range manifests { + data, err := fs.ReadFile(embeddedCRDManifests, path.Join("manifests", manifest.Name())) + if err != nil { + return fmt.Errorf("failed to read file %s: %w", manifest.Name(), err) + } + + outputPath := path.Join(dir, manifest.Name()) + if err := os.WriteFile(outputPath, data, 0666); err != nil { + return fmt.Errorf("failed to write file %s: %w", outputPath, err) + } + } + + return nil +} diff --git a/internal/crdinstall/manifests/.gitattributes b/internal/crdinstall/manifests/.gitattributes new file mode 100644 index 00000000..f581feca --- /dev/null +++ b/internal/crdinstall/manifests/.gitattributes @@ -0,0 +1 @@ +*.yaml linguist-generated diff --git a/internal/crdinstall/manifests/cozystack.io_packages.yaml b/internal/crdinstall/manifests/cozystack.io_packages.yaml new file mode 100644 index 00000000..cb7e2763 --- /dev/null +++ b/internal/crdinstall/manifests/cozystack.io_packages.yaml @@ -0,0 +1,171 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.4 + name: packages.cozystack.io +spec: + group: cozystack.io + names: + kind: Package + listKind: PackageList + plural: packages + shortNames: + - pkg + - pkgs + singular: package + scope: Cluster + versions: + - additionalPrinterColumns: + - description: Selected variant + jsonPath: .spec.variant + name: Variant + type: string + - description: Ready status + jsonPath: .status.conditions[?(@.type=='Ready')].status + name: Ready + type: string + - description: Ready message + jsonPath: .status.conditions[?(@.type=='Ready')].message + name: Status + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: Package is the Schema for the packages API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: PackageSpec defines the desired state of Package + properties: + components: + additionalProperties: + description: PackageComponent defines overrides for a specific component + properties: + enabled: + description: |- + Enabled indicates whether this component should be installed + If false, the component will be disabled even if it's defined in the PackageSource + type: boolean + values: + description: |- + Values contains Helm chart values as a JSON object + These values will be merged with the default values from the PackageSource + x-kubernetes-preserve-unknown-fields: true + type: object + description: |- + Components is a map of release name to component overrides + Allows overriding values and enabling/disabling specific components from the PackageSource + type: object + ignoreDependencies: + description: |- + IgnoreDependencies is a list of package source dependencies to ignore + Dependencies listed here will not be installed even if they are specified in the PackageSource + items: + type: string + type: array + variant: + description: |- + Variant is the name of the variant to use from the PackageSource + If not specified, defaults to "default" + type: string + type: object + status: + description: PackageStatus defines the observed state of Package + properties: + conditions: + description: Conditions represents the latest available observations + of a Package's state + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + dependencies: + additionalProperties: + description: DependencyStatus represents the readiness status of + a dependency + properties: + ready: + description: Ready indicates whether the dependency is ready + type: boolean + required: + - ready + type: object + description: |- + Dependencies tracks the readiness status of each dependency + Key is the dependency package name, value indicates if the dependency is ready + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/internal/crdinstall/manifests/cozystack.io_packagesources.yaml b/internal/crdinstall/manifests/cozystack.io_packagesources.yaml new file mode 100644 index 00000000..0acfcdd2 --- /dev/null +++ b/internal/crdinstall/manifests/cozystack.io_packagesources.yaml @@ -0,0 +1,250 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.4 + name: packagesources.cozystack.io +spec: + group: cozystack.io + names: + kind: PackageSource + listKind: PackageSourceList + plural: packagesources + shortNames: + - pks + singular: packagesource + scope: Cluster + versions: + - additionalPrinterColumns: + - description: Package variants (comma-separated) + jsonPath: .status.variants + name: Variants + type: string + - description: Ready status + jsonPath: .status.conditions[?(@.type=='Ready')].status + name: Ready + type: string + - description: Ready message + jsonPath: .status.conditions[?(@.type=='Ready')].message + name: Status + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: PackageSource is the Schema for the packagesources API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: PackageSourceSpec defines the desired state of PackageSource + properties: + sourceRef: + description: SourceRef is the source reference for the package source + charts + properties: + kind: + description: Kind of the source reference + enum: + - GitRepository + - OCIRepository + type: string + name: + description: Name of the source reference + type: string + namespace: + description: Namespace of the source reference + type: string + path: + description: |- + Path is the base path where packages are located in the source. + For GitRepository, defaults to "packages" if not specified. + For OCIRepository, defaults to empty string (root) if not specified. + type: string + required: + - kind + - name + - namespace + type: object + variants: + description: |- + Variants is a list of package source variants + Each variant defines components, applications, dependencies, and libraries for a specific configuration + items: + description: Variant defines a single variant configuration + properties: + components: + description: Components is a list of Helm releases to be installed + as part of this variant + items: + description: Component defines a single Helm release component + within a package source + properties: + install: + description: Install defines installation parameters for + this component + properties: + dependsOn: + description: DependsOn is a list of component names + that must be installed before this component + items: + type: string + type: array + namespace: + description: Namespace is the Kubernetes namespace + where the release will be installed + type: string + privileged: + description: Privileged indicates whether this release + requires privileged access + type: boolean + releaseName: + description: |- + ReleaseName is the name of the HelmRelease resource that will be created + If not specified, defaults to the component Name field + type: string + type: object + libraries: + description: |- + Libraries is a list of library names that this component depends on + These libraries must be defined at the variant level + items: + type: string + type: array + name: + description: Name is the unique identifier for this component + within the package source + type: string + path: + description: Path is the path to the Helm chart directory + type: string + valuesFiles: + description: ValuesFiles is a list of values file names + to use + items: + type: string + type: array + required: + - name + - path + type: object + type: array + dependsOn: + description: |- + DependsOn is a list of package source dependencies + For example: "cozystack.networking" + items: + type: string + type: array + libraries: + description: Libraries is a list of Helm library charts used + by components in this variant + items: + description: Library defines a Helm library chart + properties: + name: + description: Name is the optional name for library placed + in charts + type: string + path: + description: Path is the path to the library chart directory + type: string + required: + - path + type: object + type: array + name: + description: Name is the unique identifier for this variant + type: string + required: + - name + type: object + type: array + type: object + status: + description: PackageSourceStatus defines the observed state of PackageSource + properties: + conditions: + description: Conditions represents the latest available observations + of a PackageSource's state + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + variants: + description: |- + Variants is a comma-separated list of package variant names + This field is populated by the controller based on spec.variants keys + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} From 879b10b777d90ba5a7335d77c095eb0896846f87 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Tue, 17 Feb 2026 23:24:36 +0300 Subject: [PATCH 076/666] feat(installer): enable --install-crds in operator deployment Add --install-crds=true to cozystack-operator container args so the operator applies embedded CRD manifests on startup via server-side apply. Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- packages/core/installer/templates/cozystack-operator.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/installer/templates/cozystack-operator.yaml b/packages/core/installer/templates/cozystack-operator.yaml index 4568e18b..359fb40c 100644 --- a/packages/core/installer/templates/cozystack-operator.yaml +++ b/packages/core/installer/templates/cozystack-operator.yaml @@ -53,6 +53,7 @@ spec: args: - --leader-elect=true - --install-flux=true + - --install-crds=true - --metrics-bind-address=0 - --health-probe-bind-address= {{- if .Values.cozystackOperator.disableTelemetry }} From 1558fb428aad0cfaef0cffbbd60d30834e52586a Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Tue, 17 Feb 2026 23:24:51 +0300 Subject: [PATCH 077/666] build(codegen): sync CRDs to operator embed directory After generating CRDs to packages/core/installer/crds/, copy them to internal/crdinstall/manifests/ so the operator binary embeds the latest CRD definitions. Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- hack/update-codegen.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/hack/update-codegen.sh b/hack/update-codegen.sh index d0711cad..b7f96691 100755 --- a/hack/update-codegen.sh +++ b/hack/update-codegen.sh @@ -25,6 +25,7 @@ UPDATE_API_KNOWN_VIOLATIONS="${UPDATE_API_KNOWN_VIOLATIONS:-true}" CONTROLLER_GEN="go run sigs.k8s.io/controller-tools/cmd/controller-gen@v0.16.4" TMPDIR=$(mktemp -d) OPERATOR_CRDDIR=packages/core/installer/crds +OPERATOR_EMBEDDIR=internal/crdinstall/manifests COZY_CONTROLLER_CRDDIR=packages/system/cozystack-controller/definitions COZY_RD_CRDDIR=packages/system/application-definition-crd/definition BACKUPS_CORE_CRDDIR=packages/system/backup-controller/definitions @@ -73,6 +74,9 @@ $CONTROLLER_GEN rbac:roleName=manager-role crd paths="./api/..." output:crd:arti mv ${TMPDIR}/cozystack.io_packages.yaml ${OPERATOR_CRDDIR}/cozystack.io_packages.yaml mv ${TMPDIR}/cozystack.io_packagesources.yaml ${OPERATOR_CRDDIR}/cozystack.io_packagesources.yaml +cp ${OPERATOR_CRDDIR}/cozystack.io_packages.yaml ${OPERATOR_EMBEDDIR}/cozystack.io_packages.yaml +cp ${OPERATOR_CRDDIR}/cozystack.io_packagesources.yaml ${OPERATOR_EMBEDDIR}/cozystack.io_packagesources.yaml + mv ${TMPDIR}/cozystack.io_applicationdefinitions.yaml \ ${COZY_RD_CRDDIR}/cozystack.io_applicationdefinitions.yaml From 4187b5ed9460d48860736004bf07969c9e46bed3 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Tue, 17 Feb 2026 23:31:29 +0300 Subject: [PATCH 078/666] fix(crdinstall): use filepath for OS paths, restrict permissions, add tests - Use filepath.Join instead of path.Join for OS file paths - Restrict extracted manifest permissions from 0666 to 0600 - Add unit tests for readYAMLObjects, parseManifests, and WriteEmbeddedManifests including permission verification Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- internal/crdinstall/install_test.go | 242 +++++++++++++++++++++++++ internal/crdinstall/manifests.embed.go | 5 +- 2 files changed, 245 insertions(+), 2 deletions(-) create mode 100644 internal/crdinstall/install_test.go diff --git a/internal/crdinstall/install_test.go b/internal/crdinstall/install_test.go new file mode 100644 index 00000000..fc1f1fd9 --- /dev/null +++ b/internal/crdinstall/install_test.go @@ -0,0 +1,242 @@ +/* +Copyright 2025 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 crdinstall + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestReadYAMLObjects(t *testing.T) { + tests := []struct { + name string + input string + wantCount int + wantErr bool + }{ + { + name: "single document", + input: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test +`, + wantCount: 1, + }, + { + name: "multiple documents", + input: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test1 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test2 +`, + wantCount: 2, + }, + { + name: "empty input", + input: "", + wantCount: 0, + }, + { + name: "document without kind returns error", + input: `apiVersion: v1 +metadata: + name: test +`, + wantErr: true, + }, + { + name: "whitespace-only document between separators is skipped", + input: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test1 +--- + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test2 +`, + wantCount: 2, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + objects, err := readYAMLObjects(strings.NewReader(tt.input)) + if (err != nil) != tt.wantErr { + t.Errorf("readYAMLObjects() error = %v, wantErr %v", err, tt.wantErr) + return + } + if len(objects) != tt.wantCount { + t.Errorf("readYAMLObjects() returned %d objects, want %d", len(objects), tt.wantCount) + } + }) + } +} + +func TestReadYAMLObjects_preservesFields(t *testing.T) { + input := `apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: packages.cozystack.io +spec: + group: cozystack.io +` + objects, err := readYAMLObjects(strings.NewReader(input)) + if err != nil { + t.Fatalf("readYAMLObjects() error = %v", err) + } + if len(objects) != 1 { + t.Fatalf("expected 1 object, got %d", len(objects)) + } + + obj := objects[0] + if obj.GetKind() != "CustomResourceDefinition" { + t.Errorf("kind = %q, want %q", obj.GetKind(), "CustomResourceDefinition") + } + if obj.GetName() != "packages.cozystack.io" { + t.Errorf("name = %q, want %q", obj.GetName(), "packages.cozystack.io") + } + if obj.GetAPIVersion() != "apiextensions.k8s.io/v1" { + t.Errorf("apiVersion = %q, want %q", obj.GetAPIVersion(), "apiextensions.k8s.io/v1") + } +} + +func TestParseManifests(t *testing.T) { + tmpDir := t.TempDir() + manifestPath := filepath.Join(tmpDir, "test.yaml") + + content := `apiVersion: v1 +kind: ConfigMap +metadata: + name: cm1 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: cm2 +` + if err := os.WriteFile(manifestPath, []byte(content), 0600); err != nil { + t.Fatalf("failed to write test manifest: %v", err) + } + + objects, err := parseManifests(manifestPath) + if err != nil { + t.Fatalf("parseManifests() error = %v", err) + } + if len(objects) != 2 { + t.Errorf("parseManifests() returned %d objects, want 2", len(objects)) + } +} + +func TestParseManifests_fileNotFound(t *testing.T) { + _, err := parseManifests("/nonexistent/path/test.yaml") + if err == nil { + t.Error("parseManifests() expected error for nonexistent file, got nil") + } +} + +func TestWriteEmbeddedManifests(t *testing.T) { + tmpDir := t.TempDir() + + if err := WriteEmbeddedManifests(tmpDir); err != nil { + t.Fatalf("WriteEmbeddedManifests() error = %v", err) + } + + entries, err := os.ReadDir(tmpDir) + if err != nil { + t.Fatalf("failed to read output dir: %v", err) + } + + var yamlFiles []string + for _, e := range entries { + if strings.HasSuffix(e.Name(), ".yaml") { + yamlFiles = append(yamlFiles, e.Name()) + } + } + + if len(yamlFiles) == 0 { + t.Error("WriteEmbeddedManifests() produced no YAML files") + } + + expectedFiles := []string{ + "cozystack.io_packages.yaml", + "cozystack.io_packagesources.yaml", + } + for _, expected := range expectedFiles { + found := false + for _, actual := range yamlFiles { + if actual == expected { + found = true + break + } + } + if !found { + t.Errorf("expected file %q not found in output, got %v", expected, yamlFiles) + } + } + + // Verify files are non-empty and contain valid YAML + for _, f := range yamlFiles { + data, err := os.ReadFile(filepath.Join(tmpDir, f)) + if err != nil { + t.Errorf("failed to read %s: %v", f, err) + continue + } + if len(data) == 0 { + t.Errorf("file %s is empty", f) + } + } +} + +func TestWriteEmbeddedManifests_filePermissions(t *testing.T) { + tmpDir := t.TempDir() + + if err := WriteEmbeddedManifests(tmpDir); err != nil { + t.Fatalf("WriteEmbeddedManifests() error = %v", err) + } + + entries, err := os.ReadDir(tmpDir) + if err != nil { + t.Fatalf("failed to read output dir: %v", err) + } + + for _, e := range entries { + if !strings.HasSuffix(e.Name(), ".yaml") { + continue + } + info, err := e.Info() + if err != nil { + t.Errorf("failed to get info for %s: %v", e.Name(), err) + continue + } + perm := info.Mode().Perm() + if perm&0o077 != 0 { + t.Errorf("file %s has overly permissive mode %o, expected no group/other access", e.Name(), perm) + } + } +} diff --git a/internal/crdinstall/manifests.embed.go b/internal/crdinstall/manifests.embed.go index dd578db6..7464e54e 100644 --- a/internal/crdinstall/manifests.embed.go +++ b/internal/crdinstall/manifests.embed.go @@ -22,6 +22,7 @@ import ( "io/fs" "os" "path" + "path/filepath" ) //go:embed manifests/*.yaml @@ -40,8 +41,8 @@ func WriteEmbeddedManifests(dir string) error { return fmt.Errorf("failed to read file %s: %w", manifest.Name(), err) } - outputPath := path.Join(dir, manifest.Name()) - if err := os.WriteFile(outputPath, data, 0666); err != nil { + outputPath := filepath.Join(dir, manifest.Name()) + if err := os.WriteFile(outputPath, data, 0600); err != nil { return fmt.Errorf("failed to write file %s: %w", outputPath, err) } } From 20d122445dfc34aea556bb2f9e33d899cf5413f6 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Tue, 17 Feb 2026 23:42:16 +0300 Subject: [PATCH 079/666] fix(crdinstall): add CRD readiness check, Install tests, fix fluxinstall - Wait for CRDs to have Established condition after server-side apply, instead of returning immediately - Add TestInstall with fake client and interceptor to simulate CRD establishment - Add TestInstall_noManifests and TestInstall_writeManifestsFails for error paths - Fix fluxinstall/manifests.embed.go: use filepath.Join for OS paths and restrict permissions from 0666 to 0600 (same fix as crdinstall) Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- internal/crdinstall/install.go | 68 ++++++++++++ internal/crdinstall/install_test.go | 141 ++++++++++++++++++++++++ internal/fluxinstall/manifests.embed.go | 5 +- 3 files changed, 212 insertions(+), 2 deletions(-) diff --git a/internal/crdinstall/install.go b/internal/crdinstall/install.go index fb6e4588..62615f68 100644 --- a/internal/crdinstall/install.go +++ b/internal/crdinstall/install.go @@ -25,8 +25,10 @@ import ( "os" "path/filepath" "strings" + "time" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/types" k8syaml "k8s.io/apimachinery/pkg/util/yaml" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" @@ -94,10 +96,76 @@ func Install(ctx context.Context, k8sClient client.Client, writeEmbeddedManifest logger.Info("Applied CRD", "name", obj.GetName()) } + if err := waitForCRDsEstablished(ctx, k8sClient, objects, logger); err != nil { + return fmt.Errorf("CRDs not established after apply: %w", err) + } + logger.Info("CRD installation completed successfully") return nil } +// waitForCRDsEstablished polls applied CRDs until all have the Established condition. +func waitForCRDsEstablished(ctx context.Context, k8sClient client.Client, objects []*unstructured.Unstructured, logger interface{ Info(string, ...interface{}) }) error { + var crdNames []string + for _, obj := range objects { + if obj.GetKind() == "CustomResourceDefinition" { + crdNames = append(crdNames, obj.GetName()) + } + } + + if len(crdNames) == 0 { + return nil + } + + ticker := time.NewTicker(500 * time.Millisecond) + defer ticker.Stop() + + for { + allEstablished := true + for _, name := range crdNames { + crd := &unstructured.Unstructured{} + crd.SetGroupVersionKind(objects[0].GroupVersionKind()) + if err := k8sClient.Get(ctx, types.NamespacedName{Name: name}, crd); err != nil { + allEstablished = false + break + } + + conditions, found, err := unstructured.NestedSlice(crd.Object, "status", "conditions") + if err != nil || !found { + allEstablished = false + break + } + + established := false + for _, c := range conditions { + cond, ok := c.(map[string]interface{}) + if !ok { + continue + } + if cond["type"] == "Established" && cond["status"] == "True" { + established = true + break + } + } + if !established { + allEstablished = false + break + } + } + + if allEstablished { + logger.Info("All CRDs established", "count", len(crdNames)) + return nil + } + + select { + case <-ctx.Done(): + return fmt.Errorf("context cancelled while waiting for CRDs to be established: %w", ctx.Err()) + case <-ticker.C: + } + } +} + func parseManifests(manifestPath string) ([]*unstructured.Unstructured, error) { data, err := os.ReadFile(manifestPath) if err != nil { diff --git a/internal/crdinstall/install_test.go b/internal/crdinstall/install_test.go index fc1f1fd9..ca49db51 100644 --- a/internal/crdinstall/install_test.go +++ b/internal/crdinstall/install_test.go @@ -17,10 +17,21 @@ limitations under the License. package crdinstall import ( + "context" "os" "path/filepath" "strings" "testing" + "time" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/client/interceptor" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" ) func TestReadYAMLObjects(t *testing.T) { @@ -213,6 +224,136 @@ func TestWriteEmbeddedManifests(t *testing.T) { } } +func TestInstall_appliesAllCRDs(t *testing.T) { + log.SetLogger(zap.New(zap.UseDevMode(true))) + + scheme := runtime.NewScheme() + if err := apiextensionsv1.AddToScheme(scheme); err != nil { + t.Fatalf("failed to add apiextensions to scheme: %v", err) + } + + // Intercept Get calls to simulate CRDs becoming Established + fakeClient := fake.NewClientBuilder(). + WithScheme(scheme). + WithInterceptorFuncs(interceptor.Funcs{ + Get: func(ctx context.Context, c client.WithWatch, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { + if err := c.Get(ctx, key, obj, opts...); err != nil { + return err + } + u, ok := obj.(*unstructured.Unstructured) + if !ok { + return nil + } + if u.GetKind() == "CustomResourceDefinition" { + _ = unstructured.SetNestedSlice(u.Object, []interface{}{ + map[string]interface{}{ + "type": "Established", + "status": "True", + }, + }, "status", "conditions") + } + return nil + }, + }). + Build() + + // Write two CRD manifests + writeManifests := func(dir string) error { + crd1 := `apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: packages.cozystack.io +spec: + group: cozystack.io + names: + kind: Package + plural: packages + scope: Namespaced + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object +` + crd2 := `apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: packagesources.cozystack.io +spec: + group: cozystack.io + names: + kind: PackageSource + plural: packagesources + scope: Namespaced + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object +` + if err := os.WriteFile(filepath.Join(dir, "crd1.yaml"), []byte(crd1), 0600); err != nil { + return err + } + return os.WriteFile(filepath.Join(dir, "crd2.yaml"), []byte(crd2), 0600) + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + ctx = log.IntoContext(ctx, log.FromContext(context.Background())) + + err := Install(ctx, fakeClient, writeManifests) + if err != nil { + t.Fatalf("Install() error = %v", err) + } +} + +func TestInstall_noManifests(t *testing.T) { + log.SetLogger(zap.New(zap.UseDevMode(true))) + + scheme := runtime.NewScheme() + fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build() + + writeEmpty := func(dir string) error { + return nil + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + ctx = log.IntoContext(ctx, log.FromContext(context.Background())) + + err := Install(ctx, fakeClient, writeEmpty) + if err == nil { + t.Error("Install() expected error for empty manifests, got nil") + } + if !strings.Contains(err.Error(), "no YAML manifest files found") { + t.Errorf("Install() error = %v, want error containing 'no YAML manifest files found'", err) + } +} + +func TestInstall_writeManifestsFails(t *testing.T) { + log.SetLogger(zap.New(zap.UseDevMode(true))) + + scheme := runtime.NewScheme() + fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build() + + writeFail := func(dir string) error { + return os.ErrPermission + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + ctx = log.IntoContext(ctx, log.FromContext(context.Background())) + + err := Install(ctx, fakeClient, writeFail) + if err == nil { + t.Error("Install() expected error when writeManifests fails, got nil") + } +} + func TestWriteEmbeddedManifests_filePermissions(t *testing.T) { tmpDir := t.TempDir() diff --git a/internal/fluxinstall/manifests.embed.go b/internal/fluxinstall/manifests.embed.go index e6a13a99..6ff48d2d 100644 --- a/internal/fluxinstall/manifests.embed.go +++ b/internal/fluxinstall/manifests.embed.go @@ -22,6 +22,7 @@ import ( "io/fs" "os" "path" + "path/filepath" ) //go:embed manifests/*.yaml @@ -40,8 +41,8 @@ func WriteEmbeddedManifests(dir string) error { return fmt.Errorf("failed to read file %s: %w", manifest.Name(), err) } - outputPath := path.Join(dir, manifest.Name()) - if err := os.WriteFile(outputPath, data, 0666); err != nil { + outputPath := filepath.Join(dir, manifest.Name()) + if err := os.WriteFile(outputPath, data, 0600); err != nil { return fmt.Errorf("failed to write file %s: %w", outputPath, err) } } From 962f8e96f4f458f27641ca13248683b8d0c5f4f7 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Tue, 17 Feb 2026 23:42:59 +0300 Subject: [PATCH 080/666] ci(makefile): add CRD sync verification between Helm crds/ and operator embed Add verify-crds target that diffs packages/core/installer/crds/ and internal/crdinstall/manifests/ to catch accidental divergence. Include it in unit-tests target so it runs in CI. Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- Makefile | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index dd6a249d..85b48659 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: manifests assets unit-tests helm-unit-tests +.PHONY: manifests assets unit-tests helm-unit-tests verify-crds include hack/common-envs.mk @@ -80,7 +80,11 @@ test: make -C packages/core/testing apply make -C packages/core/testing test -unit-tests: helm-unit-tests +verify-crds: + @diff packages/core/installer/crds/ internal/crdinstall/manifests/ --exclude=.gitattributes \ + || (echo "ERROR: CRD manifests out of sync. Run 'make generate' to fix." && exit 1) + +unit-tests: helm-unit-tests verify-crds helm-unit-tests: hack/helm-unit-tests.sh From b3fe6a8c4a2af1d92e580bc17fcd136c3ba4e87e Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Tue, 17 Feb 2026 23:49:20 +0300 Subject: [PATCH 081/666] fix(crdinstall): hardcode CRD GVK, add timeout test, document dual install - Use explicit apiextensions.k8s.io/v1 CRD GVK in waitForCRDsEstablished instead of fragile objects[0].GroupVersionKind() - Add TestInstall_crdNotEstablished for context timeout path - Add --recursive to diff in verify-crds Makefile target - Document why both crds/ and --install-crds exist in deployment template Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- Makefile | 2 +- internal/crdinstall/install.go | 7 ++- internal/crdinstall/install_test.go | 46 +++++++++++++++++++ .../templates/cozystack-operator.yaml | 3 ++ 4 files changed, 56 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 85b48659..9707ade4 100644 --- a/Makefile +++ b/Makefile @@ -81,7 +81,7 @@ test: make -C packages/core/testing test verify-crds: - @diff packages/core/installer/crds/ internal/crdinstall/manifests/ --exclude=.gitattributes \ + @diff --recursive packages/core/installer/crds/ internal/crdinstall/manifests/ --exclude=.gitattributes \ || (echo "ERROR: CRD manifests out of sync. Run 'make generate' to fix." && exit 1) unit-tests: helm-unit-tests verify-crds diff --git a/internal/crdinstall/install.go b/internal/crdinstall/install.go index 62615f68..315de612 100644 --- a/internal/crdinstall/install.go +++ b/internal/crdinstall/install.go @@ -28,6 +28,7 @@ import ( "time" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" k8syaml "k8s.io/apimachinery/pkg/util/yaml" "sigs.k8s.io/controller-runtime/pkg/client" @@ -124,7 +125,11 @@ func waitForCRDsEstablished(ctx context.Context, k8sClient client.Client, object allEstablished := true for _, name := range crdNames { crd := &unstructured.Unstructured{} - crd.SetGroupVersionKind(objects[0].GroupVersionKind()) + crd.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "apiextensions.k8s.io", + Version: "v1", + Kind: "CustomResourceDefinition", + }) if err := k8sClient.Get(ctx, types.NamespacedName{Name: name}, crd); err != nil { allEstablished = false break diff --git a/internal/crdinstall/install_test.go b/internal/crdinstall/install_test.go index ca49db51..2fd200bc 100644 --- a/internal/crdinstall/install_test.go +++ b/internal/crdinstall/install_test.go @@ -354,6 +354,52 @@ func TestInstall_writeManifestsFails(t *testing.T) { } } +func TestInstall_crdNotEstablished(t *testing.T) { + log.SetLogger(zap.New(zap.UseDevMode(true))) + + scheme := runtime.NewScheme() + if err := apiextensionsv1.AddToScheme(scheme); err != nil { + t.Fatalf("failed to add apiextensions to scheme: %v", err) + } + + // No interceptor: CRDs will never get Established condition + fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build() + + writeManifests := func(dir string) error { + crd := `apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: packages.cozystack.io +spec: + group: cozystack.io + names: + kind: Package + plural: packages + scope: Namespaced + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object +` + return os.WriteFile(filepath.Join(dir, "crd.yaml"), []byte(crd), 0600) + } + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + ctx = log.IntoContext(ctx, log.FromContext(context.Background())) + + err := Install(ctx, fakeClient, writeManifests) + if err == nil { + t.Fatal("Install() expected error when CRDs never become established, got nil") + } + if !strings.Contains(err.Error(), "CRDs not established") { + t.Errorf("Install() error = %v, want error containing 'CRDs not established'", err) + } +} + func TestWriteEmbeddedManifests_filePermissions(t *testing.T) { tmpDir := t.TempDir() diff --git a/packages/core/installer/templates/cozystack-operator.yaml b/packages/core/installer/templates/cozystack-operator.yaml index 359fb40c..1153ec4d 100644 --- a/packages/core/installer/templates/cozystack-operator.yaml +++ b/packages/core/installer/templates/cozystack-operator.yaml @@ -53,6 +53,9 @@ spec: args: - --leader-elect=true - --install-flux=true + # CRDs are also in crds/ for initial helm install, but Helm never updates + # them on upgrade. The operator applies embedded CRDs via server-side apply + # on every startup, ensuring they stay up to date. - --install-crds=true - --metrics-bind-address=0 - --health-probe-bind-address= From 3fbce0dba5dbe2a41e1672f6386a07f41a3e59a8 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Wed, 18 Feb 2026 00:01:46 +0300 Subject: [PATCH 082/666] refactor(operator): extract shared manifest utils from crdinstall and fluxinstall Move duplicated YAML parsing (ReadYAMLObjects, ParseManifestFile) and CRD readiness check (WaitForCRDsEstablished, CollectCRDNames) into a shared internal/manifestutil package. Both crdinstall and fluxinstall now import from manifestutil instead of maintaining identical copies. Replace fluxinstall's time.Sleep(2s) after CRD apply with proper WaitForCRDsEstablished polling, matching the crdinstall behavior. Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- internal/crdinstall/install.go | 128 +------- internal/crdinstall/install_test.go | 473 ++++++++++------------------ internal/fluxinstall/install.go | 68 +--- internal/manifestutil/crd.go | 116 +++++++ internal/manifestutil/crd_test.go | 190 +++++++++++ internal/manifestutil/parse.go | 76 +++++ internal/manifestutil/parse_test.go | 161 ++++++++++ 7 files changed, 717 insertions(+), 495 deletions(-) create mode 100644 internal/manifestutil/crd.go create mode 100644 internal/manifestutil/crd_test.go create mode 100644 internal/manifestutil/parse.go create mode 100644 internal/manifestutil/parse_test.go diff --git a/internal/crdinstall/install.go b/internal/crdinstall/install.go index 315de612..4cd93597 100644 --- a/internal/crdinstall/install.go +++ b/internal/crdinstall/install.go @@ -17,26 +17,22 @@ limitations under the License. package crdinstall import ( - "bufio" - "bytes" "context" "fmt" - "io" "os" "path/filepath" "strings" - "time" + + "github.com/cozystack/cozystack/internal/manifestutil" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/types" - k8syaml "k8s.io/apimachinery/pkg/util/yaml" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" ) // Install applies Cozystack CRDs using embedded manifests. -// It extracts the manifests and applies them to the cluster using server-side apply. +// It extracts the manifests and applies them to the cluster using server-side apply, +// then waits for all CRDs to have the Established condition. func Install(ctx context.Context, k8sClient client.Client, writeEmbeddedManifests func(string) error) error { logger := log.FromContext(ctx) @@ -73,7 +69,7 @@ func Install(ctx context.Context, k8sClient client.Client, writeEmbeddedManifest var objects []*unstructured.Unstructured for _, manifestPath := range manifestFiles { - objs, err := parseManifests(manifestPath) + objs, err := manifestutil.ParseManifestFile(manifestPath) if err != nil { return fmt.Errorf("failed to parse manifests from %s: %w", manifestPath, err) } @@ -97,121 +93,11 @@ func Install(ctx context.Context, k8sClient client.Client, writeEmbeddedManifest logger.Info("Applied CRD", "name", obj.GetName()) } - if err := waitForCRDsEstablished(ctx, k8sClient, objects, logger); err != nil { + crdNames := manifestutil.CollectCRDNames(objects) + if err := manifestutil.WaitForCRDsEstablished(ctx, k8sClient, crdNames); err != nil { return fmt.Errorf("CRDs not established after apply: %w", err) } logger.Info("CRD installation completed successfully") return nil } - -// waitForCRDsEstablished polls applied CRDs until all have the Established condition. -func waitForCRDsEstablished(ctx context.Context, k8sClient client.Client, objects []*unstructured.Unstructured, logger interface{ Info(string, ...interface{}) }) error { - var crdNames []string - for _, obj := range objects { - if obj.GetKind() == "CustomResourceDefinition" { - crdNames = append(crdNames, obj.GetName()) - } - } - - if len(crdNames) == 0 { - return nil - } - - ticker := time.NewTicker(500 * time.Millisecond) - defer ticker.Stop() - - for { - allEstablished := true - for _, name := range crdNames { - crd := &unstructured.Unstructured{} - crd.SetGroupVersionKind(schema.GroupVersionKind{ - Group: "apiextensions.k8s.io", - Version: "v1", - Kind: "CustomResourceDefinition", - }) - if err := k8sClient.Get(ctx, types.NamespacedName{Name: name}, crd); err != nil { - allEstablished = false - break - } - - conditions, found, err := unstructured.NestedSlice(crd.Object, "status", "conditions") - if err != nil || !found { - allEstablished = false - break - } - - established := false - for _, c := range conditions { - cond, ok := c.(map[string]interface{}) - if !ok { - continue - } - if cond["type"] == "Established" && cond["status"] == "True" { - established = true - break - } - } - if !established { - allEstablished = false - break - } - } - - if allEstablished { - logger.Info("All CRDs established", "count", len(crdNames)) - return nil - } - - select { - case <-ctx.Done(): - return fmt.Errorf("context cancelled while waiting for CRDs to be established: %w", ctx.Err()) - case <-ticker.C: - } - } -} - -func parseManifests(manifestPath string) ([]*unstructured.Unstructured, error) { - data, err := os.ReadFile(manifestPath) - if err != nil { - return nil, fmt.Errorf("failed to read manifest file: %w", err) - } - - return readYAMLObjects(bytes.NewReader(data)) -} - -func readYAMLObjects(reader io.Reader) ([]*unstructured.Unstructured, error) { - var objects []*unstructured.Unstructured - yamlReader := k8syaml.NewYAMLReader(bufio.NewReader(reader)) - - for { - doc, err := yamlReader.Read() - if err != nil { - if err == io.EOF { - break - } - return nil, fmt.Errorf("failed to read YAML document: %w", err) - } - - if len(bytes.TrimSpace(doc)) == 0 { - continue - } - - obj := &unstructured.Unstructured{} - decoder := k8syaml.NewYAMLOrJSONDecoder(bytes.NewReader(doc), len(doc)) - if err := decoder.Decode(obj); err != nil { - if err == io.EOF { - continue - } - return nil, fmt.Errorf("failed to decode YAML document: %w", err) - } - - if obj.GetKind() == "" { - continue - } - - objects = append(objects, obj) - } - - return objects, nil -} diff --git a/internal/crdinstall/install_test.go b/internal/crdinstall/install_test.go index 2fd200bc..a4de0da2 100644 --- a/internal/crdinstall/install_test.go +++ b/internal/crdinstall/install_test.go @@ -18,6 +18,7 @@ package crdinstall import ( "context" + "fmt" "os" "path/filepath" "strings" @@ -34,143 +35,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log/zap" ) -func TestReadYAMLObjects(t *testing.T) { - tests := []struct { - name string - input string - wantCount int - wantErr bool - }{ - { - name: "single document", - input: `apiVersion: v1 -kind: ConfigMap -metadata: - name: test -`, - wantCount: 1, - }, - { - name: "multiple documents", - input: `apiVersion: v1 -kind: ConfigMap -metadata: - name: test1 ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: test2 -`, - wantCount: 2, - }, - { - name: "empty input", - input: "", - wantCount: 0, - }, - { - name: "document without kind returns error", - input: `apiVersion: v1 -metadata: - name: test -`, - wantErr: true, - }, - { - name: "whitespace-only document between separators is skipped", - input: `apiVersion: v1 -kind: ConfigMap -metadata: - name: test1 ---- - ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: test2 -`, - wantCount: 2, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - objects, err := readYAMLObjects(strings.NewReader(tt.input)) - if (err != nil) != tt.wantErr { - t.Errorf("readYAMLObjects() error = %v, wantErr %v", err, tt.wantErr) - return - } - if len(objects) != tt.wantCount { - t.Errorf("readYAMLObjects() returned %d objects, want %d", len(objects), tt.wantCount) - } - }) - } -} - -func TestReadYAMLObjects_preservesFields(t *testing.T) { - input := `apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: packages.cozystack.io -spec: - group: cozystack.io -` - objects, err := readYAMLObjects(strings.NewReader(input)) - if err != nil { - t.Fatalf("readYAMLObjects() error = %v", err) - } - if len(objects) != 1 { - t.Fatalf("expected 1 object, got %d", len(objects)) - } - - obj := objects[0] - if obj.GetKind() != "CustomResourceDefinition" { - t.Errorf("kind = %q, want %q", obj.GetKind(), "CustomResourceDefinition") - } - if obj.GetName() != "packages.cozystack.io" { - t.Errorf("name = %q, want %q", obj.GetName(), "packages.cozystack.io") - } - if obj.GetAPIVersion() != "apiextensions.k8s.io/v1" { - t.Errorf("apiVersion = %q, want %q", obj.GetAPIVersion(), "apiextensions.k8s.io/v1") - } -} - -func TestParseManifests(t *testing.T) { - tmpDir := t.TempDir() - manifestPath := filepath.Join(tmpDir, "test.yaml") - - content := `apiVersion: v1 -kind: ConfigMap -metadata: - name: cm1 ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: cm2 -` - if err := os.WriteFile(manifestPath, []byte(content), 0600); err != nil { - t.Fatalf("failed to write test manifest: %v", err) - } - - objects, err := parseManifests(manifestPath) - if err != nil { - t.Fatalf("parseManifests() error = %v", err) - } - if len(objects) != 2 { - t.Errorf("parseManifests() returned %d objects, want 2", len(objects)) - } -} - -func TestParseManifests_fileNotFound(t *testing.T) { - _, err := parseManifests("/nonexistent/path/test.yaml") - if err == nil { - t.Error("parseManifests() expected error for nonexistent file, got nil") - } -} - func TestWriteEmbeddedManifests(t *testing.T) { tmpDir := t.TempDir() @@ -211,7 +75,7 @@ func TestWriteEmbeddedManifests(t *testing.T) { } } - // Verify files are non-empty and contain valid YAML + // Verify files are non-empty for _, f := range yamlFiles { data, err := os.ReadFile(filepath.Join(tmpDir, f)) if err != nil { @@ -224,182 +88,6 @@ func TestWriteEmbeddedManifests(t *testing.T) { } } -func TestInstall_appliesAllCRDs(t *testing.T) { - log.SetLogger(zap.New(zap.UseDevMode(true))) - - scheme := runtime.NewScheme() - if err := apiextensionsv1.AddToScheme(scheme); err != nil { - t.Fatalf("failed to add apiextensions to scheme: %v", err) - } - - // Intercept Get calls to simulate CRDs becoming Established - fakeClient := fake.NewClientBuilder(). - WithScheme(scheme). - WithInterceptorFuncs(interceptor.Funcs{ - Get: func(ctx context.Context, c client.WithWatch, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { - if err := c.Get(ctx, key, obj, opts...); err != nil { - return err - } - u, ok := obj.(*unstructured.Unstructured) - if !ok { - return nil - } - if u.GetKind() == "CustomResourceDefinition" { - _ = unstructured.SetNestedSlice(u.Object, []interface{}{ - map[string]interface{}{ - "type": "Established", - "status": "True", - }, - }, "status", "conditions") - } - return nil - }, - }). - Build() - - // Write two CRD manifests - writeManifests := func(dir string) error { - crd1 := `apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: packages.cozystack.io -spec: - group: cozystack.io - names: - kind: Package - plural: packages - scope: Namespaced - versions: - - name: v1alpha1 - served: true - storage: true - schema: - openAPIV3Schema: - type: object -` - crd2 := `apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: packagesources.cozystack.io -spec: - group: cozystack.io - names: - kind: PackageSource - plural: packagesources - scope: Namespaced - versions: - - name: v1alpha1 - served: true - storage: true - schema: - openAPIV3Schema: - type: object -` - if err := os.WriteFile(filepath.Join(dir, "crd1.yaml"), []byte(crd1), 0600); err != nil { - return err - } - return os.WriteFile(filepath.Join(dir, "crd2.yaml"), []byte(crd2), 0600) - } - - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - ctx = log.IntoContext(ctx, log.FromContext(context.Background())) - - err := Install(ctx, fakeClient, writeManifests) - if err != nil { - t.Fatalf("Install() error = %v", err) - } -} - -func TestInstall_noManifests(t *testing.T) { - log.SetLogger(zap.New(zap.UseDevMode(true))) - - scheme := runtime.NewScheme() - fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build() - - writeEmpty := func(dir string) error { - return nil - } - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - ctx = log.IntoContext(ctx, log.FromContext(context.Background())) - - err := Install(ctx, fakeClient, writeEmpty) - if err == nil { - t.Error("Install() expected error for empty manifests, got nil") - } - if !strings.Contains(err.Error(), "no YAML manifest files found") { - t.Errorf("Install() error = %v, want error containing 'no YAML manifest files found'", err) - } -} - -func TestInstall_writeManifestsFails(t *testing.T) { - log.SetLogger(zap.New(zap.UseDevMode(true))) - - scheme := runtime.NewScheme() - fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build() - - writeFail := func(dir string) error { - return os.ErrPermission - } - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - ctx = log.IntoContext(ctx, log.FromContext(context.Background())) - - err := Install(ctx, fakeClient, writeFail) - if err == nil { - t.Error("Install() expected error when writeManifests fails, got nil") - } -} - -func TestInstall_crdNotEstablished(t *testing.T) { - log.SetLogger(zap.New(zap.UseDevMode(true))) - - scheme := runtime.NewScheme() - if err := apiextensionsv1.AddToScheme(scheme); err != nil { - t.Fatalf("failed to add apiextensions to scheme: %v", err) - } - - // No interceptor: CRDs will never get Established condition - fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build() - - writeManifests := func(dir string) error { - crd := `apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: packages.cozystack.io -spec: - group: cozystack.io - names: - kind: Package - plural: packages - scope: Namespaced - versions: - - name: v1alpha1 - served: true - storage: true - schema: - openAPIV3Schema: - type: object -` - return os.WriteFile(filepath.Join(dir, "crd.yaml"), []byte(crd), 0600) - } - - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) - defer cancel() - ctx = log.IntoContext(ctx, log.FromContext(context.Background())) - - err := Install(ctx, fakeClient, writeManifests) - if err == nil { - t.Fatal("Install() expected error when CRDs never become established, got nil") - } - if !strings.Contains(err.Error(), "CRDs not established") { - t.Errorf("Install() error = %v, want error containing 'CRDs not established'", err) - } -} - func TestWriteEmbeddedManifests_filePermissions(t *testing.T) { tmpDir := t.TempDir() @@ -427,3 +115,160 @@ func TestWriteEmbeddedManifests_filePermissions(t *testing.T) { } } } + +// newCRDManifestWriter returns a function that writes test CRD YAML files. +func newCRDManifestWriter(crds ...string) func(string) error { + return func(dir string) error { + for i, crd := range crds { + filename := filepath.Join(dir, fmt.Sprintf("crd%d.yaml", i+1)) + if err := os.WriteFile(filename, []byte(crd), 0600); err != nil { + return err + } + } + return nil + } +} + +var testCRD1 = `apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: packages.cozystack.io +spec: + group: cozystack.io + names: + kind: Package + plural: packages + scope: Namespaced + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object +` + +var testCRD2 = `apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: packagesources.cozystack.io +spec: + group: cozystack.io + names: + kind: PackageSource + plural: packagesources + scope: Namespaced + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object +` + +// establishedInterceptor simulates CRDs becoming Established in the API server. +func establishedInterceptor() interceptor.Funcs { + return interceptor.Funcs{ + Get: func(ctx context.Context, c client.WithWatch, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { + if err := c.Get(ctx, key, obj, opts...); err != nil { + return err + } + u, ok := obj.(*unstructured.Unstructured) + if !ok { + return nil + } + if u.GetKind() == "CustomResourceDefinition" { + _ = unstructured.SetNestedSlice(u.Object, []interface{}{ + map[string]interface{}{ + "type": "Established", + "status": "True", + }, + }, "status", "conditions") + } + return nil + }, + } +} + +func TestInstall_appliesAllCRDs(t *testing.T) { + log.SetLogger(zap.New(zap.UseDevMode(true))) + + scheme := runtime.NewScheme() + if err := apiextensionsv1.AddToScheme(scheme); err != nil { + t.Fatalf("failed to add apiextensions to scheme: %v", err) + } + + fakeClient := fake.NewClientBuilder(). + WithScheme(scheme). + WithInterceptorFuncs(establishedInterceptor()). + Build() + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + ctx = log.IntoContext(ctx, log.FromContext(context.Background())) + + err := Install(ctx, fakeClient, newCRDManifestWriter(testCRD1, testCRD2)) + if err != nil { + t.Fatalf("Install() error = %v", err) + } +} + +func TestInstall_noManifests(t *testing.T) { + log.SetLogger(zap.New(zap.UseDevMode(true))) + + scheme := runtime.NewScheme() + fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + ctx = log.IntoContext(ctx, log.FromContext(context.Background())) + + err := Install(ctx, fakeClient, func(string) error { return nil }) + if err == nil { + t.Error("Install() expected error for empty manifests, got nil") + } + if !strings.Contains(err.Error(), "no YAML manifest files found") { + t.Errorf("Install() error = %v, want error containing 'no YAML manifest files found'", err) + } +} + +func TestInstall_writeManifestsFails(t *testing.T) { + log.SetLogger(zap.New(zap.UseDevMode(true))) + + scheme := runtime.NewScheme() + fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + ctx = log.IntoContext(ctx, log.FromContext(context.Background())) + + err := Install(ctx, fakeClient, func(string) error { return os.ErrPermission }) + if err == nil { + t.Error("Install() expected error when writeManifests fails, got nil") + } +} + +func TestInstall_crdNotEstablished(t *testing.T) { + log.SetLogger(zap.New(zap.UseDevMode(true))) + + scheme := runtime.NewScheme() + if err := apiextensionsv1.AddToScheme(scheme); err != nil { + t.Fatalf("failed to add apiextensions to scheme: %v", err) + } + + // No interceptor: CRDs will never get Established condition + fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build() + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + ctx = log.IntoContext(ctx, log.FromContext(context.Background())) + + err := Install(ctx, fakeClient, newCRDManifestWriter(testCRD1)) + if err == nil { + t.Fatal("Install() expected error when CRDs never become established, got nil") + } + if !strings.Contains(err.Error(), "CRDs not established") { + t.Errorf("Install() error = %v, want error containing 'CRDs not established'", err) + } +} diff --git a/internal/fluxinstall/install.go b/internal/fluxinstall/install.go index 2097ecfb..4ae6f4cb 100644 --- a/internal/fluxinstall/install.go +++ b/internal/fluxinstall/install.go @@ -17,18 +17,15 @@ limitations under the License. package fluxinstall import ( - "bufio" - "bytes" "context" "fmt" - "io" "os" "path/filepath" "strings" - "time" + + "github.com/cozystack/cozystack/internal/manifestutil" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - k8syaml "k8s.io/apimachinery/pkg/util/yaml" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" ) @@ -76,7 +73,7 @@ func Install(ctx context.Context, k8sClient client.Client, writeEmbeddedManifest // Parse all manifest files var objects []*unstructured.Unstructured for _, manifestPath := range manifestFiles { - objs, err := parseManifests(manifestPath) + objs, err := manifestutil.ParseManifestFile(manifestPath) if err != nil { return fmt.Errorf("failed to parse manifests from %s: %w", manifestPath, err) } @@ -110,56 +107,6 @@ func Install(ctx context.Context, k8sClient client.Client, writeEmbeddedManifest return nil } -// parseManifests parses YAML manifests into unstructured objects. -func parseManifests(manifestPath string) ([]*unstructured.Unstructured, error) { - data, err := os.ReadFile(manifestPath) - if err != nil { - return nil, fmt.Errorf("failed to read manifest file: %w", err) - } - - return readYAMLObjects(bytes.NewReader(data)) -} - -// readYAMLObjects parses multi-document YAML into unstructured objects. -func readYAMLObjects(reader io.Reader) ([]*unstructured.Unstructured, error) { - var objects []*unstructured.Unstructured - yamlReader := k8syaml.NewYAMLReader(bufio.NewReader(reader)) - - for { - doc, err := yamlReader.Read() - if err != nil { - if err == io.EOF { - break - } - return nil, fmt.Errorf("failed to read YAML document: %w", err) - } - - // Skip empty documents - if len(bytes.TrimSpace(doc)) == 0 { - continue - } - - obj := &unstructured.Unstructured{} - decoder := k8syaml.NewYAMLOrJSONDecoder(bytes.NewReader(doc), len(doc)) - if err := decoder.Decode(obj); err != nil { - // Skip documents that can't be decoded (might be comments or empty) - if err == io.EOF { - continue - } - return nil, fmt.Errorf("failed to decode YAML document: %w", err) - } - - // Skip empty objects (no kind) - if obj.GetKind() == "" { - continue - } - - objects = append(objects, obj) - } - - return objects, nil -} - // applyManifests applies Kubernetes objects using server-side apply. func applyManifests(ctx context.Context, k8sClient client.Client, objects []*unstructured.Unstructured) error { logger := log.FromContext(ctx) @@ -183,8 +130,11 @@ func applyManifests(ctx context.Context, k8sClient client.Client, objects []*uns return fmt.Errorf("failed to apply cluster definitions: %w", err) } - // Wait a bit for CRDs to be registered - time.Sleep(2 * time.Second) + // Wait for CRDs to be established before applying dependent resources + crdNames := manifestutil.CollectCRDNames(stageOne) + if err := manifestutil.WaitForCRDsEstablished(ctx, k8sClient, crdNames); err != nil { + return fmt.Errorf("CRDs not established after apply: %w", err) + } } // Apply stage two (everything else) @@ -215,7 +165,6 @@ func applyObjects(ctx context.Context, k8sClient client.Client, objects []*unstr return nil } - // extractNamespace extracts the namespace name from the Namespace object in the manifests. func extractNamespace(objects []*unstructured.Unstructured) (string, error) { for _, obj := range objects { @@ -386,4 +335,3 @@ func setEnvVar(env []interface{}, name, value string) []interface{} { return env } - diff --git a/internal/manifestutil/crd.go b/internal/manifestutil/crd.go new file mode 100644 index 00000000..00421c4c --- /dev/null +++ b/internal/manifestutil/crd.go @@ -0,0 +1,116 @@ +/* +Copyright 2025 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 manifestutil + +import ( + "context" + "fmt" + "time" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +var crdGVK = schema.GroupVersionKind{ + Group: "apiextensions.k8s.io", + Version: "v1", + Kind: "CustomResourceDefinition", +} + +// WaitForCRDsEstablished polls the API server until all named CRDs have the +// Established condition set to True, or the context is cancelled. +func WaitForCRDsEstablished(ctx context.Context, k8sClient client.Client, crdNames []string) error { + if len(crdNames) == 0 { + return nil + } + + logger := log.FromContext(ctx) + ticker := time.NewTicker(500 * time.Millisecond) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return fmt.Errorf("context cancelled while waiting for CRDs to be established: %w", ctx.Err()) + default: + } + + allEstablished := true + var pendingCRD string + for _, name := range crdNames { + crd := &unstructured.Unstructured{} + crd.SetGroupVersionKind(crdGVK) + if err := k8sClient.Get(ctx, types.NamespacedName{Name: name}, crd); err != nil { + allEstablished = false + pendingCRD = name + break + } + + conditions, found, err := unstructured.NestedSlice(crd.Object, "status", "conditions") + if err != nil || !found { + allEstablished = false + pendingCRD = name + break + } + + established := false + for _, c := range conditions { + cond, ok := c.(map[string]interface{}) + if !ok { + continue + } + if cond["type"] == "Established" && cond["status"] == "True" { + established = true + break + } + } + if !established { + allEstablished = false + pendingCRD = name + break + } + } + + if allEstablished { + logger.Info("All CRDs established", "count", len(crdNames)) + return nil + } + + logger.V(1).Info("Waiting for CRD to be established", "crd", pendingCRD) + + select { + case <-ctx.Done(): + return fmt.Errorf("context cancelled while waiting for CRD %q to be established: %w", pendingCRD, ctx.Err()) + case <-ticker.C: + } + } +} + +// CollectCRDNames returns the names of all CustomResourceDefinition objects +// from the given list of unstructured objects. +func CollectCRDNames(objects []*unstructured.Unstructured) []string { + var names []string + for _, obj := range objects { + if obj.GetKind() == "CustomResourceDefinition" { + names = append(names, obj.GetName()) + } + } + return names +} diff --git a/internal/manifestutil/crd_test.go b/internal/manifestutil/crd_test.go new file mode 100644 index 00000000..874377b9 --- /dev/null +++ b/internal/manifestutil/crd_test.go @@ -0,0 +1,190 @@ +/* +Copyright 2025 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 manifestutil + +import ( + "context" + "testing" + "time" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/client/interceptor" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" +) + +func TestCollectCRDNames(t *testing.T) { + objects := []*unstructured.Unstructured{ + {Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": map[string]interface{}{"name": "test-ns"}, + }}, + {Object: map[string]interface{}{ + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "CustomResourceDefinition", + "metadata": map[string]interface{}{"name": "packages.cozystack.io"}, + }}, + {Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{"name": "test-deploy"}, + }}, + {Object: map[string]interface{}{ + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "CustomResourceDefinition", + "metadata": map[string]interface{}{"name": "packagesources.cozystack.io"}, + }}, + } + + names := CollectCRDNames(objects) + if len(names) != 2 { + t.Fatalf("CollectCRDNames() returned %d names, want 2", len(names)) + } + if names[0] != "packages.cozystack.io" { + t.Errorf("names[0] = %q, want %q", names[0], "packages.cozystack.io") + } + if names[1] != "packagesources.cozystack.io" { + t.Errorf("names[1] = %q, want %q", names[1], "packagesources.cozystack.io") + } +} + +func TestCollectCRDNames_noCRDs(t *testing.T) { + objects := []*unstructured.Unstructured{ + {Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": map[string]interface{}{"name": "test"}, + }}, + } + + names := CollectCRDNames(objects) + if len(names) != 0 { + t.Errorf("CollectCRDNames() returned %d names, want 0", len(names)) + } +} + +func TestWaitForCRDsEstablished_success(t *testing.T) { + log.SetLogger(zap.New(zap.UseDevMode(true))) + + scheme := runtime.NewScheme() + if err := apiextensionsv1.AddToScheme(scheme); err != nil { + t.Fatalf("failed to add apiextensions to scheme: %v", err) + } + + // Create a CRD object in the fake client + crd := &unstructured.Unstructured{Object: map[string]interface{}{ + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "CustomResourceDefinition", + "metadata": map[string]interface{}{"name": "packages.cozystack.io"}, + }} + + fakeClient := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(crd). + WithInterceptorFuncs(interceptor.Funcs{ + Get: func(ctx context.Context, c client.WithWatch, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { + if err := c.Get(ctx, key, obj, opts...); err != nil { + return err + } + u, ok := obj.(*unstructured.Unstructured) + if !ok { + return nil + } + if u.GetKind() == "CustomResourceDefinition" { + _ = unstructured.SetNestedSlice(u.Object, []interface{}{ + map[string]interface{}{ + "type": "Established", + "status": "True", + }, + }, "status", "conditions") + } + return nil + }, + }). + Build() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + ctx = log.IntoContext(ctx, log.FromContext(context.Background())) + + err := WaitForCRDsEstablished(ctx, fakeClient, []string{"packages.cozystack.io"}) + if err != nil { + t.Fatalf("WaitForCRDsEstablished() error = %v", err) + } +} + +func TestWaitForCRDsEstablished_timeout(t *testing.T) { + log.SetLogger(zap.New(zap.UseDevMode(true))) + + scheme := runtime.NewScheme() + if err := apiextensionsv1.AddToScheme(scheme); err != nil { + t.Fatalf("failed to add apiextensions to scheme: %v", err) + } + + // CRD exists but never gets Established condition + crd := &unstructured.Unstructured{Object: map[string]interface{}{ + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "CustomResourceDefinition", + "metadata": map[string]interface{}{"name": "packages.cozystack.io"}, + }} + + fakeClient := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(crd). + Build() + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + ctx = log.IntoContext(ctx, log.FromContext(context.Background())) + + err := WaitForCRDsEstablished(ctx, fakeClient, []string{"packages.cozystack.io"}) + if err == nil { + t.Fatal("WaitForCRDsEstablished() expected error on timeout, got nil") + } + if !contains(err.Error(), "packages.cozystack.io") { + t.Errorf("error should mention stuck CRD name, got: %v", err) + } +} + +func TestWaitForCRDsEstablished_empty(t *testing.T) { + scheme := runtime.NewScheme() + fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build() + + ctx := context.Background() + err := WaitForCRDsEstablished(ctx, fakeClient, nil) + if err != nil { + t.Fatalf("WaitForCRDsEstablished() with empty names should return nil, got: %v", err) + } +} + +func contains(s, substr string) bool { + return len(s) >= len(substr) && searchString(s, substr) +} + +func searchString(s, substr string) bool { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} diff --git a/internal/manifestutil/parse.go b/internal/manifestutil/parse.go new file mode 100644 index 00000000..009e4a96 --- /dev/null +++ b/internal/manifestutil/parse.go @@ -0,0 +1,76 @@ +/* +Copyright 2025 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 manifestutil + +import ( + "bufio" + "bytes" + "fmt" + "io" + "os" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + k8syaml "k8s.io/apimachinery/pkg/util/yaml" +) + +// ParseManifestFile reads a YAML file and parses it into unstructured objects. +func ParseManifestFile(manifestPath string) ([]*unstructured.Unstructured, error) { + data, err := os.ReadFile(manifestPath) + if err != nil { + return nil, fmt.Errorf("failed to read manifest file: %w", err) + } + + return ReadYAMLObjects(bytes.NewReader(data)) +} + +// ReadYAMLObjects parses multi-document YAML from a reader into unstructured objects. +// Empty documents and documents without a kind are skipped. +func ReadYAMLObjects(reader io.Reader) ([]*unstructured.Unstructured, error) { + var objects []*unstructured.Unstructured + yamlReader := k8syaml.NewYAMLReader(bufio.NewReader(reader)) + + for { + doc, err := yamlReader.Read() + if err != nil { + if err == io.EOF { + break + } + return nil, fmt.Errorf("failed to read YAML document: %w", err) + } + + if len(bytes.TrimSpace(doc)) == 0 { + continue + } + + obj := &unstructured.Unstructured{} + decoder := k8syaml.NewYAMLOrJSONDecoder(bytes.NewReader(doc), len(doc)) + if err := decoder.Decode(obj); err != nil { + if err == io.EOF { + continue + } + return nil, fmt.Errorf("failed to decode YAML document: %w", err) + } + + if obj.GetKind() == "" { + continue + } + + objects = append(objects, obj) + } + + return objects, nil +} diff --git a/internal/manifestutil/parse_test.go b/internal/manifestutil/parse_test.go new file mode 100644 index 00000000..30bf7030 --- /dev/null +++ b/internal/manifestutil/parse_test.go @@ -0,0 +1,161 @@ +/* +Copyright 2025 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 manifestutil + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestReadYAMLObjects(t *testing.T) { + tests := []struct { + name string + input string + wantCount int + wantErr bool + }{ + { + name: "single document", + input: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test +`, + wantCount: 1, + }, + { + name: "multiple documents", + input: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test1 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test2 +`, + wantCount: 2, + }, + { + name: "empty input", + input: "", + wantCount: 0, + }, + { + name: "document without kind returns error", + input: `apiVersion: v1 +metadata: + name: test +`, + wantErr: true, + }, + { + name: "whitespace-only document between separators is skipped", + input: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test1 +--- + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test2 +`, + wantCount: 2, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + objects, err := ReadYAMLObjects(strings.NewReader(tt.input)) + if (err != nil) != tt.wantErr { + t.Errorf("ReadYAMLObjects() error = %v, wantErr %v", err, tt.wantErr) + return + } + if len(objects) != tt.wantCount { + t.Errorf("ReadYAMLObjects() returned %d objects, want %d", len(objects), tt.wantCount) + } + }) + } +} + +func TestReadYAMLObjects_preservesFields(t *testing.T) { + input := `apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: packages.cozystack.io +spec: + group: cozystack.io +` + objects, err := ReadYAMLObjects(strings.NewReader(input)) + if err != nil { + t.Fatalf("ReadYAMLObjects() error = %v", err) + } + if len(objects) != 1 { + t.Fatalf("expected 1 object, got %d", len(objects)) + } + + obj := objects[0] + if obj.GetKind() != "CustomResourceDefinition" { + t.Errorf("kind = %q, want %q", obj.GetKind(), "CustomResourceDefinition") + } + if obj.GetName() != "packages.cozystack.io" { + t.Errorf("name = %q, want %q", obj.GetName(), "packages.cozystack.io") + } + if obj.GetAPIVersion() != "apiextensions.k8s.io/v1" { + t.Errorf("apiVersion = %q, want %q", obj.GetAPIVersion(), "apiextensions.k8s.io/v1") + } +} + +func TestParseManifestFile(t *testing.T) { + tmpDir := t.TempDir() + manifestPath := filepath.Join(tmpDir, "test.yaml") + + content := `apiVersion: v1 +kind: ConfigMap +metadata: + name: cm1 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: cm2 +` + if err := os.WriteFile(manifestPath, []byte(content), 0600); err != nil { + t.Fatalf("failed to write test manifest: %v", err) + } + + objects, err := ParseManifestFile(manifestPath) + if err != nil { + t.Fatalf("ParseManifestFile() error = %v", err) + } + if len(objects) != 2 { + t.Errorf("ParseManifestFile() returned %d objects, want 2", len(objects)) + } +} + +func TestParseManifestFile_notFound(t *testing.T) { + _, err := ParseManifestFile("/nonexistent/path/test.yaml") + if err == nil { + t.Error("ParseManifestFile() expected error for nonexistent file, got nil") + } +} From abd644122f3ed93e8efc84a43a868c0383bd64ce Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Wed, 18 Feb 2026 00:07:09 +0300 Subject: [PATCH 083/666] fix(crdinstall): reject non-CRD objects in embedded manifests Validate that all parsed objects are CustomResourceDefinition before applying with force server-side apply. This prevents accidental application of arbitrary resources if a non-CRD file is placed in the manifests directory. Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- internal/crdinstall/install.go | 9 +++++++++ internal/crdinstall/install_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/internal/crdinstall/install.go b/internal/crdinstall/install.go index 4cd93597..f65abf63 100644 --- a/internal/crdinstall/install.go +++ b/internal/crdinstall/install.go @@ -80,6 +80,15 @@ func Install(ctx context.Context, k8sClient client.Client, writeEmbeddedManifest return fmt.Errorf("no objects found in manifests") } + // Validate all objects are CRDs — reject anything else to prevent + // accidental force-apply of arbitrary resources. + for _, obj := range objects { + if obj.GetKind() != "CustomResourceDefinition" { + return fmt.Errorf("unexpected object %s/%s in CRD manifests, only CustomResourceDefinition is allowed", + obj.GetKind(), obj.GetName()) + } + } + logger.Info("Applying Cozystack CRDs", "count", len(objects)) for _, obj := range objects { patchOptions := &client.PatchOptions{ diff --git a/internal/crdinstall/install_test.go b/internal/crdinstall/install_test.go index a4de0da2..870a146c 100644 --- a/internal/crdinstall/install_test.go +++ b/internal/crdinstall/install_test.go @@ -249,6 +249,34 @@ func TestInstall_writeManifestsFails(t *testing.T) { } } +func TestInstall_rejectsNonCRDObjects(t *testing.T) { + log.SetLogger(zap.New(zap.UseDevMode(true))) + + scheme := runtime.NewScheme() + if err := apiextensionsv1.AddToScheme(scheme); err != nil { + t.Fatalf("failed to add apiextensions to scheme: %v", err) + } + + fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build() + + nonCRD := `apiVersion: v1 +kind: Namespace +metadata: + name: should-not-be-applied +` + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + ctx = log.IntoContext(ctx, log.FromContext(context.Background())) + + err := Install(ctx, fakeClient, newCRDManifestWriter(nonCRD)) + if err == nil { + t.Fatal("Install() expected error for non-CRD object, got nil") + } + if !strings.Contains(err.Error(), "unexpected object") { + t.Errorf("Install() error = %v, want error containing 'unexpected object'", err) + } +} + func TestInstall_crdNotEstablished(t *testing.T) { log.SetLogger(zap.New(zap.UseDevMode(true))) From cecc5861af1e8eb7aab966b11de835fa4be3b0c6 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Wed, 18 Feb 2026 00:13:25 +0300 Subject: [PATCH 084/666] fix(operator): validate CRD apiVersion, respect SIGTERM during install - Check both apiVersion and kind when validating embedded CRD manifests to prevent applying objects with wrong API group - Move ctrl.SetupSignalHandler() before install phases so CRD and Flux installs respect SIGTERM instead of blocking for up to 2 minutes - Replace custom contains/searchString helpers with strings.Contains Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- cmd/cozystack-operator/main.go | 10 ++++++---- internal/crdinstall/install.go | 6 +++--- internal/manifestutil/crd_test.go | 15 ++------------- 3 files changed, 11 insertions(+), 20 deletions(-) diff --git a/cmd/cozystack-operator/main.go b/cmd/cozystack-operator/main.go index f365a076..a0efdd84 100644 --- a/cmd/cozystack-operator/main.go +++ b/cmd/cozystack-operator/main.go @@ -180,10 +180,13 @@ func main() { os.Exit(1) } + // Set up signal handler early so install phases respect SIGTERM + mgrCtx := ctrl.SetupSignalHandler() + // Install Cozystack CRDs before starting reconcile loop if installCRDs { setupLog.Info("Installing Cozystack CRDs before starting reconcile loop") - installCtx, installCancel := context.WithTimeout(context.Background(), 2*time.Minute) + installCtx, installCancel := context.WithTimeout(mgrCtx, 2*time.Minute) defer installCancel() if err := crdinstall.Install(installCtx, directClient, crdinstall.WriteEmbeddedManifests); err != nil { @@ -196,7 +199,7 @@ func main() { // Install Flux before starting reconcile loop if installFlux { setupLog.Info("Installing Flux components before starting reconcile loop") - installCtx, installCancel := context.WithTimeout(context.Background(), 5*time.Minute) + installCtx, installCancel := context.WithTimeout(mgrCtx, 5*time.Minute) defer installCancel() // Use direct client for pre-start operations (cache is not ready yet) @@ -210,7 +213,7 @@ func main() { // Generate and install platform source resource if specified if platformSourceURL != "" { setupLog.Info("Generating platform source resource", "url", platformSourceURL, "name", platformSourceName, "ref", platformSourceRef) - installCtx, installCancel := context.WithTimeout(context.Background(), 2*time.Minute) + installCtx, installCancel := context.WithTimeout(mgrCtx, 2*time.Minute) defer installCancel() // Use direct client for pre-start operations (cache is not ready yet) @@ -292,7 +295,6 @@ func main() { } setupLog.Info("Starting controller manager") - mgrCtx := ctrl.SetupSignalHandler() if err := mgr.Start(mgrCtx); err != nil { setupLog.Error(err, "problem running manager") os.Exit(1) diff --git a/internal/crdinstall/install.go b/internal/crdinstall/install.go index f65abf63..5143f2e6 100644 --- a/internal/crdinstall/install.go +++ b/internal/crdinstall/install.go @@ -83,9 +83,9 @@ func Install(ctx context.Context, k8sClient client.Client, writeEmbeddedManifest // Validate all objects are CRDs — reject anything else to prevent // accidental force-apply of arbitrary resources. for _, obj := range objects { - if obj.GetKind() != "CustomResourceDefinition" { - return fmt.Errorf("unexpected object %s/%s in CRD manifests, only CustomResourceDefinition is allowed", - obj.GetKind(), obj.GetName()) + if obj.GetAPIVersion() != "apiextensions.k8s.io/v1" || obj.GetKind() != "CustomResourceDefinition" { + return fmt.Errorf("unexpected object %s %s/%s in CRD manifests, only apiextensions.k8s.io/v1 CustomResourceDefinition is allowed", + obj.GetAPIVersion(), obj.GetKind(), obj.GetName()) } } diff --git a/internal/manifestutil/crd_test.go b/internal/manifestutil/crd_test.go index 874377b9..c67b2efa 100644 --- a/internal/manifestutil/crd_test.go +++ b/internal/manifestutil/crd_test.go @@ -18,6 +18,7 @@ package manifestutil import ( "context" + "strings" "testing" "time" @@ -160,7 +161,7 @@ func TestWaitForCRDsEstablished_timeout(t *testing.T) { if err == nil { t.Fatal("WaitForCRDsEstablished() expected error on timeout, got nil") } - if !contains(err.Error(), "packages.cozystack.io") { + if !strings.Contains(err.Error(), "packages.cozystack.io") { t.Errorf("error should mention stuck CRD name, got: %v", err) } } @@ -176,15 +177,3 @@ func TestWaitForCRDsEstablished_empty(t *testing.T) { } } -func contains(s, substr string) bool { - return len(s) >= len(substr) && searchString(s, substr) -} - -func searchString(s, substr string) bool { - for i := 0; i <= len(s)-len(substr); i++ { - if s[i:i+len(substr)] == substr { - return true - } - } - return false -} From 9eb13fdafe50f9970899a9f0a1e59664be2d034a Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Wed, 18 Feb 2026 00:15:08 +0300 Subject: [PATCH 085/666] fix(controller): update workload test to use current label name The workload reconciler was refactored to use the label workloads.cozystack.io/monitor but the test still used the old workloadmonitor.cozystack.io/name label, causing the reconciler to delete the workload instead of keeping it. Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- internal/controller/workload_controller_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/controller/workload_controller_test.go b/internal/controller/workload_controller_test.go index b454adc3..194a20d3 100644 --- a/internal/controller/workload_controller_test.go +++ b/internal/controller/workload_controller_test.go @@ -43,7 +43,7 @@ func TestWorkloadReconciler_DeletesOnMissingMonitor(t *testing.T) { Name: "pod-foo", Namespace: "default", Labels: map[string]string{ - "workloadmonitor.cozystack.io/name": "missing-monitor", + "workloads.cozystack.io/monitor": "missing-monitor", }, }, } @@ -89,7 +89,7 @@ func TestWorkloadReconciler_KeepsWhenAllExist(t *testing.T) { Name: "pod-foo", Namespace: "default", Labels: map[string]string{ - "workloadmonitor.cozystack.io/name": "mon", + "workloads.cozystack.io/monitor": "mon", }, }, } From 92d261fc1e64a87b3b0fee5a87c5d010f86cb1fb Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Wed, 18 Feb 2026 00:25:06 +0300 Subject: [PATCH 086/666] fix: address review findings in operator and tests - Remove duplicate "Starting controller manager" log before install phases, keep only the one before mgr.Start() - Rename misleading test "document without kind returns error" to "decoder rejects document without kind" to match actual behavior - Document Helm uninstall CRD behavior in deployment template comment - Use --health-probe-bind-address=0 consistently with metrics-bind - Exclude all dotfiles in verify-crds diff, not just .gitattributes Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- Makefile | 2 +- cmd/cozystack-operator/main.go | 3 +-- internal/manifestutil/parse_test.go | 2 +- packages/core/installer/templates/cozystack-operator.yaml | 7 ++++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 9707ade4..73bd0bea 100644 --- a/Makefile +++ b/Makefile @@ -81,7 +81,7 @@ test: make -C packages/core/testing test verify-crds: - @diff --recursive packages/core/installer/crds/ internal/crdinstall/manifests/ --exclude=.gitattributes \ + @diff --recursive packages/core/installer/crds/ internal/crdinstall/manifests/ --exclude='.*' \ || (echo "ERROR: CRD manifests out of sync. Run 'make generate' to fix." && exit 1) unit-tests: helm-unit-tests verify-crds diff --git a/cmd/cozystack-operator/main.go b/cmd/cozystack-operator/main.go index a0efdd84..d3c40f7e 100644 --- a/cmd/cozystack-operator/main.go +++ b/cmd/cozystack-operator/main.go @@ -137,8 +137,7 @@ func main() { os.Exit(1) } - // Start the controller manager - setupLog.Info("Starting controller manager") + // Initialize the controller manager mgr, err := ctrl.NewManager(config, ctrl.Options{ Scheme: scheme, Cache: cache.Options{ diff --git a/internal/manifestutil/parse_test.go b/internal/manifestutil/parse_test.go index 30bf7030..860405c7 100644 --- a/internal/manifestutil/parse_test.go +++ b/internal/manifestutil/parse_test.go @@ -59,7 +59,7 @@ metadata: wantCount: 0, }, { - name: "document without kind returns error", + name: "decoder rejects document without kind", input: `apiVersion: v1 metadata: name: test diff --git a/packages/core/installer/templates/cozystack-operator.yaml b/packages/core/installer/templates/cozystack-operator.yaml index 1153ec4d..5cd471cf 100644 --- a/packages/core/installer/templates/cozystack-operator.yaml +++ b/packages/core/installer/templates/cozystack-operator.yaml @@ -54,11 +54,12 @@ spec: - --leader-elect=true - --install-flux=true # CRDs are also in crds/ for initial helm install, but Helm never updates - # them on upgrade. The operator applies embedded CRDs via server-side apply - # on every startup, ensuring they stay up to date. + # them on upgrade and never deletes them on uninstall. The operator applies + # embedded CRDs via server-side apply on every startup, ensuring they stay + # up to date. To fully remove CRDs, delete them manually after helm uninstall. - --install-crds=true - --metrics-bind-address=0 - - --health-probe-bind-address= + - --health-probe-bind-address=0 {{- if .Values.cozystackOperator.disableTelemetry }} - --disable-telemetry {{- end }} From 09805ff382a69ca5891566f7c0f6916bd78a908f Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Wed, 18 Feb 2026 00:29:52 +0300 Subject: [PATCH 087/666] fix(manifestutil): check apiVersion in CollectCRDNames for consistent GVK matching CollectCRDNames now requires both apiVersion "apiextensions.k8s.io/v1" and kind "CustomResourceDefinition", consistent with the validation in crdinstall.Install. Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- internal/manifestutil/crd.go | 6 ++++-- internal/manifestutil/crd_test.go | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/internal/manifestutil/crd.go b/internal/manifestutil/crd.go index 00421c4c..08468d1b 100644 --- a/internal/manifestutil/crd.go +++ b/internal/manifestutil/crd.go @@ -104,11 +104,13 @@ func WaitForCRDsEstablished(ctx context.Context, k8sClient client.Client, crdNam } // CollectCRDNames returns the names of all CustomResourceDefinition objects -// from the given list of unstructured objects. +// from the given list of unstructured objects. Only objects with +// apiVersion "apiextensions.k8s.io/v1" and kind "CustomResourceDefinition" +// are matched. func CollectCRDNames(objects []*unstructured.Unstructured) []string { var names []string for _, obj := range objects { - if obj.GetKind() == "CustomResourceDefinition" { + if obj.GetAPIVersion() == "apiextensions.k8s.io/v1" && obj.GetKind() == "CustomResourceDefinition" { names = append(names, obj.GetName()) } } diff --git a/internal/manifestutil/crd_test.go b/internal/manifestutil/crd_test.go index c67b2efa..5d046353 100644 --- a/internal/manifestutil/crd_test.go +++ b/internal/manifestutil/crd_test.go @@ -68,6 +68,29 @@ func TestCollectCRDNames(t *testing.T) { } } +func TestCollectCRDNames_ignoresWrongAPIVersion(t *testing.T) { + objects := []*unstructured.Unstructured{ + {Object: map[string]interface{}{ + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "CustomResourceDefinition", + "metadata": map[string]interface{}{"name": "real.crd.io"}, + }}, + {Object: map[string]interface{}{ + "apiVersion": "apiextensions.k8s.io/v1beta1", + "kind": "CustomResourceDefinition", + "metadata": map[string]interface{}{"name": "legacy.crd.io"}, + }}, + } + + names := CollectCRDNames(objects) + if len(names) != 1 { + t.Fatalf("CollectCRDNames() returned %d names, want 1", len(names)) + } + if names[0] != "real.crd.io" { + t.Errorf("names[0] = %q, want %q", names[0], "real.crd.io") + } +} + func TestCollectCRDNames_noCRDs(t *testing.T) { objects := []*unstructured.Unstructured{ {Object: map[string]interface{}{ From 543ce6e5fd040a33bdf1825b29c79cad9953a2b6 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Mon, 16 Feb 2026 19:48:32 +0300 Subject: [PATCH 088/666] [harbor] Add managed Harbor container registry application Add Harbor v2.14.2 as a tenant-level managed service with per-component resource configuration, ingress with TLS termination, and internal PostgreSQL/Redis. Includes: - extra/harbor wrapper chart with HelmRelease, WorkloadMonitors, Ingress - system/harbor with vendored upstream chart (helm.goharbor.io v1.18.2) - harbor-rd ApplicationDefinition for dynamic CRD registration - PackageSource and system.yaml bundle entry - E2E test with Secret and Service verification Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- hack/e2e-apps/harbor.bats | 48 + .../platform/sources/harbor-application.yaml | 29 + .../platform/templates/bundles/system.yaml | 1 + packages/extra/harbor/Chart.yaml | 7 + packages/extra/harbor/Makefile | 7 + packages/extra/harbor/README.md | 54 + packages/extra/harbor/charts/cozy-lib | 1 + packages/extra/harbor/logos/harbor.svg | 1 + .../templates/dashboard-resourcemap.yaml | 46 + packages/extra/harbor/templates/harbor.yaml | 153 +++ packages/extra/harbor/templates/ingress.yaml | 33 + packages/extra/harbor/values.schema.json | 414 +++++++ packages/extra/harbor/values.yaml | 90 ++ packages/system/harbor-rd/Chart.yaml | 3 + packages/system/harbor-rd/Makefile | 4 + packages/system/harbor-rd/cozyrds/harbor.yaml | 47 + .../system/harbor-rd/templates/cozyrd.yaml | 4 + packages/system/harbor-rd/values.yaml | 1 + packages/system/harbor/Chart.yaml | 3 + packages/system/harbor/Makefile | 10 + .../system/harbor/charts/harbor/.helmignore | 6 + .../system/harbor/charts/harbor/Chart.yaml | 22 + packages/system/harbor/charts/harbor/LICENSE | 201 +++ .../system/harbor/charts/harbor/README.md | 775 ++++++++++++ .../harbor/charts/harbor/templates/NOTES.txt | 3 + .../charts/harbor/templates/_helpers.tpl | 606 +++++++++ .../charts/harbor/templates/core/core-cm.yaml | 92 ++ .../harbor/templates/core/core-dpl.yaml | 258 ++++ .../templates/core/core-pre-upgrade-job.yaml | 78 ++ .../harbor/templates/core/core-secret.yaml | 37 + .../harbor/templates/core/core-svc.yaml | 32 + .../harbor/templates/core/core-tls.yaml | 16 + .../templates/database/database-secret.yaml | 12 + .../templates/database/database-ss.yaml | 165 +++ .../templates/database/database-svc.yaml | 21 + .../templates/exporter/exporter-cm-env.yaml | 36 + .../templates/exporter/exporter-dpl.yaml | 146 +++ .../templates/exporter/exporter-secret.yaml | 17 + .../templates/exporter/exporter-svc.yaml | 22 + .../harbor/templates/gateway-apis/route.yaml | 55 + .../harbor/templates/ingress/ingress.yaml | 132 ++ .../harbor/templates/ingress/secret.yaml | 18 + .../harbor/templates/internal/auto-tls.yaml | 86 ++ .../jobservice/jobservice-cm-env.yaml | 37 + .../templates/jobservice/jobservice-cm.yaml | 58 + .../templates/jobservice/jobservice-dpl.yaml | 183 +++ .../templates/jobservice/jobservice-pvc.yaml | 32 + .../jobservice/jobservice-secrets.yaml | 17 + .../templates/jobservice/jobservice-svc.yaml | 25 + .../templates/jobservice/jobservice-tls.yaml | 16 + .../templates/metrics/metrics-svcmon.yaml | 29 + .../templates/nginx/configmap-http.yaml | 136 ++ .../templates/nginx/configmap-https.yaml | 173 +++ .../harbor/templates/nginx/deployment.yaml | 133 ++ .../charts/harbor/templates/nginx/secret.yaml | 26 + .../harbor/templates/nginx/service.yaml | 101 ++ .../harbor/templates/portal/configmap.yaml | 68 + .../harbor/templates/portal/deployment.yaml | 124 ++ .../harbor/templates/portal/service.yaml | 27 + .../charts/harbor/templates/portal/tls.yaml | 16 + .../harbor/templates/redis/service.yaml | 21 + .../harbor/templates/redis/statefulset.yaml | 128 ++ .../templates/registry/registry-cm.yaml | 248 ++++ .../templates/registry/registry-dpl.yaml | 431 +++++++ .../templates/registry/registry-pvc.yaml | 34 + .../templates/registry/registry-secret.yaml | 57 + .../templates/registry/registry-svc.yaml | 27 + .../templates/registry/registry-tls.yaml | 16 + .../templates/registry/registryctl-cm.yaml | 9 + .../registry/registryctl-secret.yaml | 10 + .../harbor/templates/trivy/trivy-secret.yaml | 13 + .../harbor/templates/trivy/trivy-sts.yaml | 237 ++++ .../harbor/templates/trivy/trivy-svc.yaml | 23 + .../harbor/templates/trivy/trivy-tls.yaml | 16 + .../system/harbor/charts/harbor/values.yaml | 1095 +++++++++++++++++ packages/system/harbor/values.yaml | 1 + 76 files changed, 7359 insertions(+) create mode 100644 hack/e2e-apps/harbor.bats create mode 100644 packages/core/platform/sources/harbor-application.yaml create mode 100644 packages/extra/harbor/Chart.yaml create mode 100644 packages/extra/harbor/Makefile create mode 100644 packages/extra/harbor/README.md create mode 120000 packages/extra/harbor/charts/cozy-lib create mode 100644 packages/extra/harbor/logos/harbor.svg create mode 100644 packages/extra/harbor/templates/dashboard-resourcemap.yaml create mode 100644 packages/extra/harbor/templates/harbor.yaml create mode 100644 packages/extra/harbor/templates/ingress.yaml create mode 100644 packages/extra/harbor/values.schema.json create mode 100644 packages/extra/harbor/values.yaml create mode 100644 packages/system/harbor-rd/Chart.yaml create mode 100644 packages/system/harbor-rd/Makefile create mode 100644 packages/system/harbor-rd/cozyrds/harbor.yaml create mode 100644 packages/system/harbor-rd/templates/cozyrd.yaml create mode 100644 packages/system/harbor-rd/values.yaml create mode 100644 packages/system/harbor/Chart.yaml create mode 100644 packages/system/harbor/Makefile create mode 100644 packages/system/harbor/charts/harbor/.helmignore create mode 100644 packages/system/harbor/charts/harbor/Chart.yaml create mode 100644 packages/system/harbor/charts/harbor/LICENSE create mode 100644 packages/system/harbor/charts/harbor/README.md create mode 100644 packages/system/harbor/charts/harbor/templates/NOTES.txt create mode 100644 packages/system/harbor/charts/harbor/templates/_helpers.tpl create mode 100644 packages/system/harbor/charts/harbor/templates/core/core-cm.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/core/core-dpl.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/core/core-pre-upgrade-job.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/core/core-secret.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/core/core-svc.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/core/core-tls.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/database/database-secret.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/database/database-ss.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/database/database-svc.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/exporter/exporter-cm-env.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/exporter/exporter-dpl.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/exporter/exporter-secret.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/exporter/exporter-svc.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/gateway-apis/route.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/ingress/ingress.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/ingress/secret.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/internal/auto-tls.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/jobservice/jobservice-cm-env.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/jobservice/jobservice-cm.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/jobservice/jobservice-dpl.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/jobservice/jobservice-pvc.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/jobservice/jobservice-secrets.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/jobservice/jobservice-svc.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/jobservice/jobservice-tls.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/metrics/metrics-svcmon.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/nginx/configmap-http.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/nginx/configmap-https.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/nginx/deployment.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/nginx/secret.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/nginx/service.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/portal/configmap.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/portal/deployment.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/portal/service.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/portal/tls.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/redis/service.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/redis/statefulset.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/registry/registry-cm.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/registry/registry-dpl.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/registry/registry-pvc.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/registry/registry-secret.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/registry/registry-svc.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/registry/registry-tls.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/registry/registryctl-cm.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/registry/registryctl-secret.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/trivy/trivy-secret.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/trivy/trivy-sts.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/trivy/trivy-svc.yaml create mode 100644 packages/system/harbor/charts/harbor/templates/trivy/trivy-tls.yaml create mode 100644 packages/system/harbor/charts/harbor/values.yaml create mode 100644 packages/system/harbor/values.yaml diff --git a/hack/e2e-apps/harbor.bats b/hack/e2e-apps/harbor.bats new file mode 100644 index 00000000..8d1daecb --- /dev/null +++ b/hack/e2e-apps/harbor.bats @@ -0,0 +1,48 @@ +#!/usr/bin/env bats + +@test "Create Harbor" { + name='harbor' + kubectl apply -f- < \ No newline at end of file diff --git a/packages/extra/harbor/templates/dashboard-resourcemap.yaml b/packages/extra/harbor/templates/dashboard-resourcemap.yaml new file mode 100644 index 00000000..f8d91a61 --- /dev/null +++ b/packages/extra/harbor/templates/dashboard-resourcemap.yaml @@ -0,0 +1,46 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ .Release.Name }}-dashboard-resources +rules: +- apiGroups: + - "" + resources: + - services + resourceNames: + - {{ .Release.Name }} + verbs: ["get", "list", "watch"] +- apiGroups: + - "" + resources: + - secrets + resourceNames: + - {{ .Release.Name }}-credentials + verbs: ["get", "list", "watch"] +- apiGroups: + - networking.k8s.io + resources: + - ingresses + resourceNames: + - {{ .Release.Name }}-ingress + verbs: ["get", "list", "watch"] +- apiGroups: + - cozystack.io + resources: + - workloadmonitors + resourceNames: + - {{ .Release.Name }}-core + - {{ .Release.Name }}-registry + - {{ .Release.Name }}-portal + verbs: ["get", "list", "watch"] +--- +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: {{ .Release.Name }}-dashboard-resources +subjects: +{{ include "cozy-lib.rbac.subjectsForTenantAndAccessLevel" (list "super-admin" .Release.Namespace) }} +roleRef: + kind: Role + name: {{ .Release.Name }}-dashboard-resources + apiGroup: rbac.authorization.k8s.io diff --git a/packages/extra/harbor/templates/harbor.yaml b/packages/extra/harbor/templates/harbor.yaml new file mode 100644 index 00000000..23c06641 --- /dev/null +++ b/packages/extra/harbor/templates/harbor.yaml @@ -0,0 +1,153 @@ +{{- $ingress := .Values._namespace.ingress }} +{{- $host := .Values._namespace.host }} +{{- $harborHost := .Values.host | default (printf "harbor.%s" $host) }} + +{{- $existingSecret := lookup "v1" "Secret" .Release.Namespace (printf "%s-credentials" .Release.Name) }} +{{- $adminPassword := randAlphaNum 16 }} +{{- if $existingSecret }} + {{- $adminPassword = index $existingSecret.data "admin-password" | b64dec }} +{{- end }} + +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Release.Name }}-credentials +stringData: + admin-password: {{ $adminPassword | quote }} + url: https://{{ $harborHost }} + +--- + +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: {{ .Release.Name }}-system + labels: + sharding.fluxcd.io/key: tenants +spec: + chartRef: + kind: ExternalArtifact + name: cozystack-harbor-application-default-harbor-system + namespace: cozy-system + interval: 5m + timeout: 15m + install: + remediation: + retries: -1 + upgrade: + force: true + remediation: + retries: -1 + valuesFrom: + - kind: Secret + name: cozystack-values + values: + harbor: + fullnameOverride: {{ .Release.Name }} + harborAdminPassword: {{ $adminPassword | quote }} + externalURL: https://{{ $harborHost }} + expose: + type: clusterIP + clusterIP: + name: {{ .Release.Name }} + tls: + enabled: false + persistence: + enabled: true + resourcePolicy: "keep" + persistentVolumeClaim: + registry: + size: {{ .Values.registry.size }} + {{- with .Values.storageClass }} + storageClass: {{ . }} + {{- end }} + database: + size: {{ .Values.database.size }} + {{- with .Values.storageClass }} + storageClass: {{ . }} + {{- end }} + redis: + size: {{ .Values.redis.size }} + {{- with .Values.storageClass }} + storageClass: {{ . }} + {{- end }} + {{- if .Values.trivy.enabled }} + trivy: + size: {{ .Values.trivy.size }} + {{- with .Values.storageClass }} + storageClass: {{ . }} + {{- end }} + {{- end }} + portal: + resources: {{- include "cozy-lib.resources.defaultingSanitize" (list "nano" (dict) $) | nindent 10 }} + core: + resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.core.resourcesPreset .Values.core.resources $) | nindent 10 }} + registry: + resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.registry.resourcesPreset .Values.registry.resources $) | nindent 10 }} + jobservice: + resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.jobservice.resourcesPreset .Values.jobservice.resources $) | nindent 10 }} + trivy: + enabled: {{ .Values.trivy.enabled }} + {{- if .Values.trivy.enabled }} + resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.trivy.resourcesPreset .Values.trivy.resources $) | nindent 10 }} + {{- end }} + database: + type: internal + internal: + resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.database.resourcesPreset .Values.database.resources $) | nindent 12 }} + redis: + type: internal + internal: + resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.redis.resourcesPreset .Values.redis.resources $) | nindent 12 }} + metrics: + enabled: true + serviceMonitor: + enabled: true + +--- + +apiVersion: cozystack.io/v1alpha1 +kind: WorkloadMonitor +metadata: + name: {{ $.Release.Name }}-core +spec: + replicas: 1 + minReplicas: 1 + kind: harbor + type: core + selector: + release: {{ $.Release.Name }}-system + component: core + version: {{ $.Chart.Version }} + +--- + +apiVersion: cozystack.io/v1alpha1 +kind: WorkloadMonitor +metadata: + name: {{ $.Release.Name }}-registry +spec: + replicas: 1 + minReplicas: 1 + kind: harbor + type: registry + selector: + release: {{ $.Release.Name }}-system + component: registry + version: {{ $.Chart.Version }} + +--- + +apiVersion: cozystack.io/v1alpha1 +kind: WorkloadMonitor +metadata: + name: {{ $.Release.Name }}-portal +spec: + replicas: 1 + minReplicas: 1 + kind: harbor + type: portal + selector: + release: {{ $.Release.Name }}-system + component: portal + version: {{ $.Chart.Version }} diff --git a/packages/extra/harbor/templates/ingress.yaml b/packages/extra/harbor/templates/ingress.yaml new file mode 100644 index 00000000..785b4a79 --- /dev/null +++ b/packages/extra/harbor/templates/ingress.yaml @@ -0,0 +1,33 @@ +{{- $ingress := .Values._namespace.ingress }} +{{- $host := .Values._namespace.host }} +{{- $harborHost := .Values.host | default (printf "harbor.%s" $host) }} +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ .Release.Name }}-ingress + annotations: + nginx.ingress.kubernetes.io/proxy-body-size: "0" + nginx.ingress.kubernetes.io/proxy-read-timeout: "900" + nginx.ingress.kubernetes.io/proxy-send-timeout: "900" + nginx.ingress.kubernetes.io/ssl-redirect: "true" + nginx.ingress.kubernetes.io/backend-protocol: "HTTP" + acme.cert-manager.io/http01-ingress-class: {{ $ingress }} + cert-manager.io/cluster-issuer: letsencrypt-prod +spec: + ingressClassName: {{ $ingress }} + tls: + - hosts: + - {{ $harborHost }} + secretName: {{ .Release.Name }}-ingress-tls + rules: + - host: {{ $harborHost }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: {{ .Release.Name }} + port: + number: 80 diff --git a/packages/extra/harbor/values.schema.json b/packages/extra/harbor/values.schema.json new file mode 100644 index 00000000..a4632de7 --- /dev/null +++ b/packages/extra/harbor/values.schema.json @@ -0,0 +1,414 @@ +{ + "title": "Chart Values", + "type": "object", + "properties": { + "core": { + "description": "Core API server configuration.", + "type": "object", + "default": {}, + "properties": { + "resources": { + "description": "Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.", + "type": "object", + "default": {}, + "properties": { + "cpu": { + "description": "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": "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.", + "type": "string", + "default": "small", + "enum": [ + "nano", + "micro", + "small", + "medium", + "large", + "xlarge", + "2xlarge" + ] + } + } + }, + "database": { + "description": "Internal PostgreSQL database configuration.", + "type": "object", + "default": {}, + "required": [ + "size" + ], + "properties": { + "resources": { + "description": "Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.", + "type": "object", + "default": {}, + "properties": { + "cpu": { + "description": "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": "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.", + "type": "string", + "default": "nano", + "enum": [ + "nano", + "micro", + "small", + "medium", + "large", + "xlarge", + "2xlarge" + ] + }, + "size": { + "description": "Persistent Volume size for database storage.", + "default": "5Gi", + "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 + } + } + }, + "host": { + "description": "Hostname for external access to Harbor (defaults to 'harbor' subdomain for the tenant host).", + "type": "string", + "default": "" + }, + "jobservice": { + "description": "Background job service configuration.", + "type": "object", + "default": {}, + "properties": { + "resources": { + "description": "Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.", + "type": "object", + "default": {}, + "properties": { + "cpu": { + "description": "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": "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.", + "type": "string", + "default": "nano", + "enum": [ + "nano", + "micro", + "small", + "medium", + "large", + "xlarge", + "2xlarge" + ] + } + } + }, + "redis": { + "description": "Internal Redis cache configuration.", + "type": "object", + "default": {}, + "required": [ + "size" + ], + "properties": { + "resources": { + "description": "Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.", + "type": "object", + "default": {}, + "properties": { + "cpu": { + "description": "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": "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.", + "type": "string", + "default": "nano", + "enum": [ + "nano", + "micro", + "small", + "medium", + "large", + "xlarge", + "2xlarge" + ] + }, + "size": { + "description": "Persistent Volume size for cache storage.", + "default": "1Gi", + "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 + } + } + }, + "registry": { + "description": "Container image registry configuration.", + "type": "object", + "default": {}, + "required": [ + "size" + ], + "properties": { + "resources": { + "description": "Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.", + "type": "object", + "default": {}, + "properties": { + "cpu": { + "description": "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": "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.", + "type": "string", + "default": "small", + "enum": [ + "nano", + "micro", + "small", + "medium", + "large", + "xlarge", + "2xlarge" + ] + }, + "size": { + "description": "Persistent Volume size for container image storage.", + "default": "50Gi", + "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", + "default": "" + }, + "trivy": { + "description": "Trivy vulnerability scanner configuration.", + "type": "object", + "default": {}, + "required": [ + "enabled", + "size" + ], + "properties": { + "enabled": { + "description": "Enable or disable the vulnerability scanner.", + "type": "boolean", + "default": true + }, + "resources": { + "description": "Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.", + "type": "object", + "default": {}, + "properties": { + "cpu": { + "description": "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": "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.", + "type": "string", + "default": "nano", + "enum": [ + "nano", + "micro", + "small", + "medium", + "large", + "xlarge", + "2xlarge" + ] + }, + "size": { + "description": "Persistent Volume size for vulnerability database cache.", + "default": "5Gi", + "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 + } + } + } + } +} \ No newline at end of file diff --git a/packages/extra/harbor/values.yaml b/packages/extra/harbor/values.yaml new file mode 100644 index 00000000..50d2ff5f --- /dev/null +++ b/packages/extra/harbor/values.yaml @@ -0,0 +1,90 @@ +## +## @section Common parameters +## + +## @param {string} [host] - Hostname for external access to Harbor (defaults to 'harbor' subdomain for the tenant host). +host: "" + +## @param {string} storageClass - StorageClass used to store the data. +storageClass: "" + +## +## @section Component configuration +## + +## @typedef {struct} Resources - Resource configuration. +## @field {quantity} [cpu] - Number of CPU cores allocated. +## @field {quantity} [memory] - Amount of memory allocated. + +## @enum {string} ResourcesPreset - Default sizing preset. +## @value nano +## @value micro +## @value small +## @value medium +## @value large +## @value xlarge +## @value 2xlarge + +## @typedef {struct} Core - Core API server configuration. +## @field {Resources} [resources] - Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied. +## @field {ResourcesPreset} [resourcesPreset] - Default sizing preset used when `resources` is omitted. + +## @param {Core} core - Core API server configuration. +core: + resources: {} + resourcesPreset: "small" + +## @typedef {struct} Registry - Container image registry configuration. +## @field {quantity} size - Persistent Volume size for container image storage. +## @field {Resources} [resources] - Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied. +## @field {ResourcesPreset} [resourcesPreset] - Default sizing preset used when `resources` is omitted. + +## @param {Registry} registry - Container image registry configuration. +registry: + size: 50Gi + resources: {} + resourcesPreset: "small" + +## @typedef {struct} Jobservice - Background job service configuration. +## @field {Resources} [resources] - Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied. +## @field {ResourcesPreset} [resourcesPreset] - Default sizing preset used when `resources` is omitted. + +## @param {Jobservice} jobservice - Background job service configuration. +jobservice: + resources: {} + resourcesPreset: "nano" + +## @typedef {struct} Trivy - Trivy vulnerability scanner configuration. +## @field {bool} enabled - Enable or disable the vulnerability scanner. +## @field {quantity} size - Persistent Volume size for vulnerability database cache. +## @field {Resources} [resources] - Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied. +## @field {ResourcesPreset} [resourcesPreset] - Default sizing preset used when `resources` is omitted. + +## @param {Trivy} trivy - Trivy vulnerability scanner configuration. +trivy: + enabled: true + size: 5Gi + resources: {} + resourcesPreset: "nano" + +## @typedef {struct} Database - Internal PostgreSQL database configuration. +## @field {quantity} size - Persistent Volume size for database storage. +## @field {Resources} [resources] - Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied. +## @field {ResourcesPreset} [resourcesPreset] - Default sizing preset used when `resources` is omitted. + +## @param {Database} database - Internal PostgreSQL database configuration. +database: + size: 5Gi + resources: {} + resourcesPreset: "nano" + +## @typedef {struct} Redis - Internal Redis cache configuration. +## @field {quantity} size - Persistent Volume size for cache storage. +## @field {Resources} [resources] - Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied. +## @field {ResourcesPreset} [resourcesPreset] - Default sizing preset used when `resources` is omitted. + +## @param {Redis} redis - Internal Redis cache configuration. +redis: + size: 1Gi + resources: {} + resourcesPreset: "nano" diff --git a/packages/system/harbor-rd/Chart.yaml b/packages/system/harbor-rd/Chart.yaml new file mode 100644 index 00000000..c27f19d0 --- /dev/null +++ b/packages/system/harbor-rd/Chart.yaml @@ -0,0 +1,3 @@ +apiVersion: v2 +name: harbor-rd +version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process diff --git a/packages/system/harbor-rd/Makefile b/packages/system/harbor-rd/Makefile new file mode 100644 index 00000000..ed36644a --- /dev/null +++ b/packages/system/harbor-rd/Makefile @@ -0,0 +1,4 @@ +export NAME=harbor-rd +export NAMESPACE=cozy-system + +include ../../../hack/package.mk diff --git a/packages/system/harbor-rd/cozyrds/harbor.yaml b/packages/system/harbor-rd/cozyrds/harbor.yaml new file mode 100644 index 00000000..da8cfb62 --- /dev/null +++ b/packages/system/harbor-rd/cozyrds/harbor.yaml @@ -0,0 +1,47 @@ +apiVersion: cozystack.io/v1alpha1 +kind: ApplicationDefinition +metadata: + name: harbor +spec: + application: + kind: Harbor + singular: harbor + plural: harbors + openAPISchema: |- + {"title":"Chart Values","type":"object","properties":{"core":{"description":"Core API server configuration.","type":"object","default":{},"properties":{"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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.","type":"string","default":"small","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]}}},"database":{"description":"Internal PostgreSQL database configuration.","type":"object","default":{},"required":["size"],"properties":{"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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.","type":"string","default":"nano","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]},"size":{"description":"Persistent Volume size for database storage.","default":"5Gi","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}}},"host":{"description":"Hostname for external access to Harbor (defaults to 'harbor' subdomain for the tenant host).","type":"string","default":""},"jobservice":{"description":"Background job service configuration.","type":"object","default":{},"properties":{"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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.","type":"string","default":"nano","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]}}},"redis":{"description":"Internal Redis cache configuration.","type":"object","default":{},"required":["size"],"properties":{"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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.","type":"string","default":"nano","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]},"size":{"description":"Persistent Volume size for cache storage.","default":"1Gi","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}}},"registry":{"description":"Container image registry configuration.","type":"object","default":{},"required":["size"],"properties":{"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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.","type":"string","default":"small","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]},"size":{"description":"Persistent Volume size for container image storage.","default":"50Gi","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","default":""},"trivy":{"description":"Trivy vulnerability scanner configuration.","type":"object","default":{},"required":["enabled","size"],"properties":{"enabled":{"description":"Enable or disable the vulnerability scanner.","type":"boolean","default":true},"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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.","type":"string","default":"nano","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]},"size":{"description":"Persistent Volume size for vulnerability database cache.","default":"5Gi","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}}}}} + release: + prefix: "" + labels: + sharding.fluxcd.io/key: tenants + internal.cozystack.io/tenantmodule: "true" + chartRef: + kind: ExternalArtifact + name: cozystack-harbor-application-default-harbor + namespace: cozy-system + dashboard: + category: PaaS + singular: Harbor + plural: Harbor + name: harbor + description: Managed Harbor container registry + module: true + tags: + - registry + - container + icon: "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHJvbGU9ImltZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHZpZXdCb3g9IjEuMDUgLTEuOTUgMzU5LjQxIDM2MS42NiI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXItZ3JhZGllbnQiIHgxPSIyNjQuNzkiIHgyPSIyNjcuMjciIHkxPSI5NTIuMzkiIHkyPSI5NTIuMzkiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMzAuNDMgMCAwIC0zMC40MyAtNzk1NS4yMiAyOTI4NS43NSkiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiM2MGI5MzIiLz48c3RvcCBvZmZzZXQ9Ii4yOCIgc3RvcC1jb2xvcj0iIzYwYjkzMiIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzM2N2MzNCIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXItZ3JhZGllbnQtMiIgeDE9IjI2My43NyIgeDI9IjI2Ni4yNiIgeTE9Ijk1NS42NSIgeTI9Ijk1NS42NSIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgyNy4yMSAwIDAgLTI3LjIxIC03MDczLjg1IDI2MTY5LjQxKSIgeGxpbms6aHJlZj0iI2xpbmVhci1ncmFkaWVudCIvPjxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyLWdyYWRpZW50LTMiIHgxPSIyNjMuMjgiIHgyPSIyNjUuNzYiIHkxPSI5NTMuNzQiIHkyPSI5NTMuNzQiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMjUuNzUgMCAwIC0yNS43NSAtNjY3MS4xMyAyNDgxMi4yMykiIHhsaW5rOmhyZWY9IiNsaW5lYXItZ3JhZGllbnQiLz48bGluZWFyR3JhZGllbnQgaWQ9ImxpbmVhci1ncmFkaWVudC00IiB4MT0iMjYzLjc3IiB4Mj0iMjY2LjI1IiB5MT0iOTUzLjIiIHkyPSI5NTMuMiIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgyNy4xIDAgMCAtMjcuMSAtNzA0MC45IDI2MTAyLjQ5KSIgeGxpbms6aHJlZj0iI2xpbmVhci1ncmFkaWVudCIvPjxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyLWdyYWRpZW50LTUiIHgxPSIyNjIuNzMiIHgyPSIyNjUuMjEiIHkxPSI5NTQuMzQiIHkyPSI5NTQuMzQiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMjQuNCAwIDAgLTI0LjQgLTYzMDEuMzYgMjM1MjEuOTcpIiB4bGluazpocmVmPSIjbGluZWFyLWdyYWRpZW50Ii8+PGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXItZ3JhZGllbnQtNiIgeDE9IjI3Mi4xNCIgeDI9IjI3NC42MiIgeTE9Ijk1NS4xNSIgeTI9Ijk1NS4xNSIgZ3JhZGllbnRUcmFuc2Zvcm09InNjYWxlKDY2LjA5IC02Ni4wOSkgcm90YXRlKDM2LjUyIDE1ODguMTUzIDY4LjE0OCkiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiM0NTk2ZDgiLz48c3RvcCBvZmZzZXQ9Ii4yIiBzdG9wLWNvbG9yPSIjNDU5NmQ4Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjZmZmIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9ImxpbmVhci1ncmFkaWVudC03IiB4MT0iMjcwLjY1IiB4Mj0iMjczLjEzIiB5MT0iOTUyLjM4IiB5Mj0iOTUyLjM4IiBncmFkaWVudFRyYW5zZm9ybT0ic2NhbGUoNzcuOCAtNzcuOCkgcm90YXRlKC0xMS41NCAtNDU4Ny4yMDkgMTgwMy4zMjMpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjNDE5NGQ3Ii8+PHN0b3Agb2Zmc2V0PSIuMiIgc3RvcC1jb2xvcj0iIzQxOTRkNyIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI2ZmZiIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXItZ3JhZGllbnQtOCIgeDE9IjI3MC45NyIgeDI9IjI3My40NSIgeTE9Ijk1My43NSIgeTI9Ijk1My43NSIgZ3JhZGllbnRUcmFuc2Zvcm09InNjYWxlKDcxLjM1IC03MS4zNSkgcm90YXRlKDEwLjIzIDU0NzcuMzcgLTEwMjQuNjAyKSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzQxOTRkNyIvPjxzdG9wIG9mZnNldD0iLjMzIiBzdG9wLWNvbG9yPSIjNDQ5NmQ4Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjZmZmIi8+PC9saW5lYXJHcmFkaWVudD48Y2xpcFBhdGggaWQ9ImNsaXAtcGF0aCI+PHBhdGggZD0iTTE3NC40IDMwMi41MmwtNjguNjUtMzAuMjYtMy44MyA0My4yMSA3NS41IDIzLjk4LTMuMDItMzYuOTN6IiBjbGFzcz0iY2xzLTEiLz48L2NsaXBQYXRoPjxjbGlwUGF0aCBpZD0iY2xpcC1wYXRoLTIiPjxwYXRoIGQ9Ik0xMTkuMTkgMTM1LjM4aDM4LjU1djMyLjg5aC05LjE1di0xNGExMC4xMyAxMC4xMyAwIDEgMC0yMC4yNiAwdjE0aC05LjE1em0tNy43IDcybDU3LjIgMjUuMjEtMy45NC00OC4yNGg3LjQ5di0xNi4wOGgtNS41NXYtMzIuODloNS41NXYtOS40NWwtMzAuODYtMzAuMTl2LTIuMTJhMi45MSAyLjkxIDAgMCAwLTUuODIgMHYyLjEybC0zMC44NiAzMC4xOXY5LjQ1aDUuNTZ2MzIuODloLTUuNTZ2MTYuMTJoOC44MnoiIGNsYXNzPSJjbHMtMSIvPjwvY2xpcFBhdGg+PGNsaXBQYXRoIGlkPSJjbGlwLXBhdGgtMyI+PHBhdGggZD0iTTEwOC4xNCAyNDUuMjhsNjMuODggMjguMTYtLjk2LTExLjczLTYxLjk2LTI3LjMtLjk2IDEwLjg3eiIgY2xhc3M9ImNscy0xIi8+PC9jbGlwUGF0aD48Y2xpcFBhdGggaWQ9ImNsaXAtcGF0aC00Ij48cGF0aCBkPSJNMTA2LjQ2IDI2NC4yMWw2Ny4yMyAyOS42My0uOTYtMTEuNzItNjUuMzEtMjguNzgtLjk2IDEwLjg3eiIgY2xhc3M9ImNscy0xIi8+PC9jbGlwUGF0aD48Y2xpcFBhdGggaWQ9ImNsaXAtcGF0aC01Ij48cGF0aCBkPSJNMTEwLjc3IDIxNS40OGwtLjk2IDEwLjg3IDYwLjU0IDI2LjY4LS45NS0xMS43Mi01OC42My0yNS44M3oiIGNsYXNzPSJjbHMtMSIvPjwvY2xpcFBhdGg+PGNsaXBQYXRoIGlkPSJjbGlwLXBhdGgtNiI+PHBhdGggZD0iTTMxMy4xMyA2Ny41OWExNzUuMzEgMTc1LjMxIDAgMCAwLTI5Ljc1LTI4LjEzYy0xLjU3LTEuMTctMy4xOC0yLjMtNC43OS0zLjQyTDI1NiA1OS41bC04Mi4zNCA4NS42MyAxMTMuNDEtNTguNjggMjkuMDctMTVjLTEuMDEtMS4zMS0xLjk4LTIuNjItMy4wMS0zLjg2eiIgY2xhc3M9ImNscy0xIi8+PC9jbGlwUGF0aD48Y2xpcFBhdGggaWQ9ImNsaXAtcGF0aC03Ij48cGF0aCBkPSJNMzUzLjU5IDE3Ny42MWMwLTItLjE0LTQtLjIyLTUuOTNsLTMyLjIxLTIuMzEtMTQ3LjQ3LTEwLjU4TDMxOC4zNiAyMDlsMzAuNDEgMTAuNTVjLjA5LS4zNi4xOS0uNzEuMjgtMS4wOGExNzMuNjUgMTczLjY1IDAgMCAwIDQuNTctMzkuNDd2LTEuMzd6IiBjbGFzcz0iY2xzLTEiLz48L2NsaXBQYXRoPjxjbGlwUGF0aCBpZD0iY2xpcC1wYXRoLTgiPjxwYXRoIGQ9Ik0zNDguODQgMTM4LjYxYTE3Mi41NSAxNzIuNTUgMCAwIDAtMTMuODQtMzcuN2wtMzAuOTQgOS45NEwxNzUuOTIgMTUybDE0Mi00LjgzIDMyLjUtMS4xMWMtLjUtMi40OC0uOTktNC45OC0xLjU4LTcuNDV6IiBjbGFzcz0iY2xzLTEiLz48L2NsaXBQYXRoPjxzdHlsZT4uY2xzLTF7ZmlsbDpub25lfS5jbHMtMTN7ZmlsbDojNjk2NTY2fTwvc3R5bGU+PC9kZWZzPjxnIGlkPSJnMTIiPjxwYXRoIGlkPSJwYXRoMTQiIGZpbGw9IiNmZmYiIGQ9Ik0zMC44OSAxNzlhMTQ4Ljg3IDE0OC44NyAwIDEgMSAxNDguODcgMTQ4Ljg1QTE0OC44NyAxNDguODcgMCAwIDEgMzAuODkgMTc5Ii8+PGcgaWQ9ImczMCIgY2xpcC1wYXRoPSJ1cmwoI2NsaXAtcGF0aCkiPjxnIGlkPSJnMzIiPjxwYXRoIGlkPSJwYXRoNDYiIGZpbGw9InVybCgjbGluZWFyLWdyYWRpZW50KSIgZD0iTTE3NC40IDMwMi41MmwtNjguNjUtMzAuMjUtMy44MiA0My4yIDc1LjUgMjQtMy0zNi45MyIvPjwvZz48L2c+PGcgaWQ9Imc0OCIgY2xpcC1wYXRoPSJ1cmwoI2NsaXAtcGF0aC0yKSI+PGcgaWQ9Imc1MCI+PHBhdGggaWQ9InBhdGg2NCIgZmlsbD0idXJsKCNsaW5lYXItZ3JhZGllbnQtMikiIGQ9Ik0xMTkuMTkgMTM1LjM4aDM4LjU1djMyLjg5aC05LjE1di0xNGExMC4xMyAxMC4xMyAwIDEgMC0yMC4yNiAwdjE0aC05LjE1em0tNy43IDcybDU3LjIgMjUuMjEtMy45NC00OC4yNGg3LjQ5di0xNi4wOGgtNS41NXYtMzIuODloNS41NXYtOS40NWwtMzAuODYtMzAuMTl2LTIuMTJhMi45MSAyLjkxIDAgMCAwLTUuODIgMHYyLjEybC0zMC44NiAzMC4xOXY5LjQ1aDUuNTZ2MzIuODloLTUuNTZ2MTYuMTJoOC44MmwtMiAyMyIvPjwvZz48L2c+PGcgaWQ9Imc2NiIgY2xpcC1wYXRoPSJ1cmwoI2NsaXAtcGF0aC0zKSI+PGcgaWQ9Imc2OCI+PHBhdGggaWQ9InBhdGg4MiIgZmlsbD0idXJsKCNsaW5lYXItZ3JhZGllbnQtMykiIGQ9Ik0xMDguMTMgMjQ1LjI4TDE3MiAyNzMuNDRsLTEtMTEuNzItNjItMjcuMzEtMSAxMC44NyIvPjwvZz48L2c+PGcgaWQ9Imc4NCIgY2xpcC1wYXRoPSJ1cmwoI2NsaXAtcGF0aC00KSI+PGcgaWQ9Imc4NiI+PHBhdGggaWQ9InBhdGgxMDAiIGZpbGw9InVybCgjbGluZWFyLWdyYWRpZW50LTQpIiBkPSJNMTA2LjQ2IDI2NC4yMWw2Ny4yMyAyOS42My0xLTExLjcyLTY1LjMxLTI4Ljc4LTEgMTAuODciLz48L2c+PC9nPjxnIGlkPSJnMTAyIiBjbGlwLXBhdGg9InVybCgjY2xpcC1wYXRoLTUpIj48ZyBpZD0iZzEwNCI+PHBhdGggaWQ9InBhdGgxMTgiIGZpbGw9InVybCgjbGluZWFyLWdyYWRpZW50LTUpIiBkPSJNMTEwLjc3IDIxNS40OGwtMSAxMC44OEwxNzAuMzUgMjUzbC0xLTExLjcyLTU4LjYyLTI1LjgzIi8+PC9nPjwvZz48cGF0aCBpZD0icGF0aDEyMCIgZD0iTTMwNC4wNyAxMTAuODVsMzAuOTMtOS45NGMtLjExLS4yMi0uMjEtLjQ1LS4zMi0uNjZhMTc0LjQxIDE3NC40MSAwIDAgMC0xOC41NS0yOC44M2wtMjkuMDcgMTVhMTQyLjcxIDE0Mi43MSAwIDAgMSAxNi43MyAyMy44N2MuMS4xNy4xOC4zNS4yNy41MyIgY2xhc3M9ImNscy0xMyIvPjxwYXRoIGlkPSJwYXRoMTIyIiBkPSJNMzIxLjE1IDE2OS4zN2wzMi4yMSAyLjMxYTE3Mi44NiAxNzIuODYgMCAwIDAtMy0yNS41OWwtMzIuNSAxLjExYTE0MSAxNDEgMCAwIDEgMy4yNSAyMi4xNyIgY2xhc3M9ImNscy0xMyIvPjxwYXRoIGlkPSJwYXRoMTI0IiBkPSJNMTgyIDMyMC44MWMtNzguMiAwLTE0MS44My02My42Mi0xNDEuODMtMTQxLjgyUzEwMy44MiAzNy4xNiAxODIgMzcuMTZhMTQwLjkzIDE0MC45MyAwIDAgMSA3Ni4zIDIyLjM0TDI4MC44NSAzNkExNzIuODYgMTcyLjg2IDAgMCAwIDE4MiA1LjExQzg2LjE1IDUuMTEgOC4xNSA4My4xMSA4LjE1IDE3OXM3OCAxNzMuODUgMTczLjg1IDE3My44NWM4MS45IDAgMTUwLjY5LTU3IDE2OS0xMzMuMzJMMzIwLjYyIDIwOWMtMTMuOCA2My44NC03MC42OSAxMTEuODMtMTM4LjYgMTExLjgzIiBjbGFzcz0iY2xzLTEzIi8+PGcgaWQ9ImcxMjYiIGNsaXAtcGF0aD0idXJsKCNjbGlwLXBhdGgtNikiPjxnIGlkPSJnMTI4Ij48cGF0aCBpZD0icGF0aDE0MiIgZmlsbD0idXJsKCNsaW5lYXItZ3JhZGllbnQtNikiIGQ9Ik0zMTMuMTMgNjcuNTlhMTc1LjMxIDE3NS4zMSAwIDAgMC0yOS43NS0yOC4xM2MtMS41Ny0xLjE3LTMuMTgtMi4zLTQuNzktMy40MkwyNTYgNTkuNWwtODIuMzQgODUuNjMgMTEzLjQxLTU4LjY4IDI5LjA3LTE1Yy0xLTEuMjctMi0yLjU4LTMtMy44MiIvPjwvZz48L2c+PGcgaWQ9ImcxNDQiIGNsaXAtcGF0aD0idXJsKCNjbGlwLXBhdGgtNykiPjxnIGlkPSJnMTQ2Ij48cGF0aCBpZD0icGF0aDE2MCIgZmlsbD0idXJsKCNsaW5lYXItZ3JhZGllbnQtNykiIGQ9Ik0zNTMuNTkgMTc3LjYxYzAtMi0uMTQtNC0uMjItNS45M2wtMzIuMjEtMi4zMS0xNDcuNDctMTAuNThMMzE4LjM2IDIwOWwzMC40MSAxMC41NWMuMDktLjM2LjE5LS43MS4yOC0xLjA4YTE3My42NSAxNzMuNjUgMCAwIDAgNC41Ny0zOS40N3YtMS4zNyIvPjwvZz48L2c+PGcgaWQ9ImcxNjIiIGNsaXAtcGF0aD0idXJsKCNjbGlwLXBhdGgtOCkiPjxnIGlkPSJnMTY0Ij48cGF0aCBpZD0icGF0aDE3OCIgZmlsbD0idXJsKCNsaW5lYXItZ3JhZGllbnQtOCkiIGQ9Ik0zNDguODQgMTM4LjYxYTE3Mi41NSAxNzIuNTUgMCAwIDAtMTMuODQtMzcuN2wtMzAuOTQgOS45NEwxNzUuOTIgMTUybDE0Mi00LjgzIDMyLjUtMS4xMWMtLjQ4LTIuNTEtMS01LTEuNTYtNy40OCIvPjwvZz48L2c+PC9nPjwvc3ZnPg==" + keysOrder: [["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata", "name"], ["spec", "host"], ["spec", "storageClass"], ["spec", "core"], ["spec", "core", "resources"], ["spec", "core", "resourcesPreset"], ["spec", "registry"], ["spec", "registry", "size"], ["spec", "registry", "resources"], ["spec", "registry", "resourcesPreset"], ["spec", "jobservice"], ["spec", "jobservice", "resources"], ["spec", "jobservice", "resourcesPreset"], ["spec", "trivy"], ["spec", "trivy", "enabled"], ["spec", "trivy", "size"], ["spec", "trivy", "resources"], ["spec", "trivy", "resourcesPreset"], ["spec", "database"], ["spec", "database", "size"], ["spec", "database", "resources"], ["spec", "database", "resourcesPreset"], ["spec", "redis"], ["spec", "redis", "size"], ["spec", "redis", "resources"], ["spec", "redis", "resourcesPreset"]] + secrets: + exclude: [] + include: + - resourceNames: + - {{ .name }}-credentials + services: + exclude: [] + include: + - resourceNames: + - {{ .name }} + ingresses: + exclude: [] + include: + - resourceNames: + - {{ .name }}-ingress diff --git a/packages/system/harbor-rd/templates/cozyrd.yaml b/packages/system/harbor-rd/templates/cozyrd.yaml new file mode 100644 index 00000000..e079ddcf --- /dev/null +++ b/packages/system/harbor-rd/templates/cozyrd.yaml @@ -0,0 +1,4 @@ +{{- range $path, $_ := .Files.Glob "cozyrds/*" }} +--- +{{ $.Files.Get $path }} +{{- end }} diff --git a/packages/system/harbor-rd/values.yaml b/packages/system/harbor-rd/values.yaml new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/packages/system/harbor-rd/values.yaml @@ -0,0 +1 @@ +{} diff --git a/packages/system/harbor/Chart.yaml b/packages/system/harbor/Chart.yaml new file mode 100644 index 00000000..5e7007b1 --- /dev/null +++ b/packages/system/harbor/Chart.yaml @@ -0,0 +1,3 @@ +apiVersion: v2 +name: cozy-harbor +version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process diff --git a/packages/system/harbor/Makefile b/packages/system/harbor/Makefile new file mode 100644 index 00000000..5cae60ed --- /dev/null +++ b/packages/system/harbor/Makefile @@ -0,0 +1,10 @@ +export NAME=harbor-system +export NAMESPACE=cozy-system + +include ../../../hack/package.mk + +update: + rm -rf charts + helm repo add harbor https://helm.goharbor.io + helm repo update harbor + helm pull harbor/harbor --untar --untardir charts diff --git a/packages/system/harbor/charts/harbor/.helmignore b/packages/system/harbor/charts/harbor/.helmignore new file mode 100644 index 00000000..b4424fd5 --- /dev/null +++ b/packages/system/harbor/charts/harbor/.helmignore @@ -0,0 +1,6 @@ +.github/* +docs/* +.git/* +.gitignore +CONTRIBUTING.md +test/* \ No newline at end of file diff --git a/packages/system/harbor/charts/harbor/Chart.yaml b/packages/system/harbor/charts/harbor/Chart.yaml new file mode 100644 index 00000000..e30a1b58 --- /dev/null +++ b/packages/system/harbor/charts/harbor/Chart.yaml @@ -0,0 +1,22 @@ +apiVersion: v1 +appVersion: 2.14.2 +description: An open source trusted cloud native registry that stores, signs, and + scans content +home: https://goharbor.io +icon: https://raw.githubusercontent.com/goharbor/website/main/static/img/logos/harbor-icon-color.png +keywords: +- docker +- registry +- harbor +maintainers: +- email: yan-yw.wang@broadcom.com + name: Yan Wang +- email: stone.zhang@broadcom.com + name: Stone Zhang +- email: miner.yang@broadcom.com + name: Miner Yang +name: harbor +sources: +- https://github.com/goharbor/harbor +- https://github.com/goharbor/harbor-helm +version: 1.18.2 diff --git a/packages/system/harbor/charts/harbor/LICENSE b/packages/system/harbor/charts/harbor/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/packages/system/harbor/charts/harbor/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/packages/system/harbor/charts/harbor/README.md b/packages/system/harbor/charts/harbor/README.md new file mode 100644 index 00000000..658150d6 --- /dev/null +++ b/packages/system/harbor/charts/harbor/README.md @@ -0,0 +1,775 @@ +# Helm Chart for Harbor + +**Notes:** The master branch is in heavy development, please use the other stable versions instead. A highly available solution for Harbor based on chart can be found [here](docs/High%20Availability.md). And refer to the [guide](docs/Upgrade.md) to upgrade the existing deployment. + +This repository, including the issues, focuses on deploying Harbor chart via helm. For functionality issues or Harbor questions, please open issues on [goharbor/harbor](https://github.com/goharbor/harbor) + +## Introduction + +This [Helm](https://github.com/kubernetes/helm) chart installs [Harbor](https://github.com/goharbor/harbor) in a Kubernetes cluster. Welcome to [contribute](CONTRIBUTING.md) to Helm Chart for Harbor. + +## Prerequisites + +- Kubernetes cluster 1.20+ +- Helm v3.2.0+ + +## Installation + +### Add Helm repository + +```bash +helm repo add harbor https://helm.goharbor.io +``` + +### Configure the chart + +The following items can be set via `--set` flag during installation or configured by editing the `values.yaml` directly (need to download the chart first). + +#### Configure how to expose Harbor service + +- **Ingress**: The ingress controller must be installed in the Kubernetes cluster. + **Notes:** if TLS is disabled, the port must be included in the command when pulling/pushing images. Refer to issue [#5291](https://github.com/goharbor/harbor/issues/5291) for details. +- **ClusterIP**: Exposes the service on a cluster-internal IP. Choosing this value makes the service only reachable from within the cluster. +- **NodePort**: Exposes the service on each Node’s IP at a static port (the NodePort). You’ll be able to contact the NodePort service, from outside the cluster, by requesting `NodeIP:NodePort`. +- **LoadBalancer**: Exposes the service externally using a cloud provider’s load balancer. +- **Gateway APIs**: Exposes the service using gateway-api CRDs using HTTPRoute. Requires v1.0.0+ + +#### Configure the external URL + +The external URL for Harbor core service is used to: + +1. populate the docker/helm commands showed on portal +2. populate the token service URL returned to docker client + +Format: `protocol://domain[:port]`. Usually: + +- if service exposed via `Ingress`, the `domain` should be the value of `expose.ingress.hosts.core` +- if service exposed via `ClusterIP`, the `domain` should be the value of `expose.clusterIP.name` +- if service exposed via `NodePort`, the `domain` should be the IP address of one Kubernetes node +- if service exposed via `LoadBalancer`, set the `domain` as your own domain name and add a CNAME record to map the domain name to the one you got from the cloud provider + +If Harbor is deployed behind the proxy, set it as the URL of proxy. + +#### Configure how to persist data + +- **Disable**: The data does not survive the termination of a pod. +- **Persistent Volume Claim(default)**: A default `StorageClass` is needed in the Kubernetes cluster to dynamically provision the volumes. Specify another StorageClass in the `storageClass` or set `existingClaim` if you already have existing persistent volumes to use. +- **External Storage(only for images and charts)**: For images and charts, the external storages are supported: `azure`, `gcs`, `s3` `swift` and `oss`. + +#### Configure the other items listed in [configuration](#configuration) section + +### Install the chart + +Install the Harbor helm chart with a release name `my-release`: + +```bash +helm install my-release harbor/harbor +``` + +## Uninstallation + +To uninstall/delete the `my-release` deployment: + +```bash +helm uninstall my-release +``` + +## Configuration + +The following table lists the configurable parameters of the Harbor chart and the default values. + + +| Parameter | Description | Default | +|----------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------| +| **Expose** | | | +| `expose.type` | How to expose the service: `ingress`, `clusterIP`, `nodePort`, `loadBalancer` or `route` other values will be ignored and the creation of service will be skipped. | `ingress` | +| `expose.tls.enabled` | Enable TLS or not. Delete the `ssl-redirect` annotations in `expose.ingress.annotations` when TLS is disabled and `expose.type` is `ingress`. Note: if the `expose.type` is `ingress` and TLS is disabled, the port must be included in the command when pulling/pushing images. Refer to https://github.com/goharbor/harbor/issues/5291 for details. | `true` | +| `expose.tls.certSource` | The source of the TLS certificate. Set as `auto`, `secret` or `none` and fill the information in the corresponding section: 1) auto: generate the TLS certificate automatically 2) secret: read the TLS certificate from the specified secret. The TLS certificate can be generated manually or by cert manager 3) none: configure no TLS certificate for the ingress. If the default TLS certificate is configured in the ingress controller, choose this option | `auto` | +| `expose.tls.auto.commonName` | The common name used to generate the certificate, it's necessary when the type isn't `ingress` | | +| `expose.tls.secret.secretName` | The name of secret which contains keys named: `tls.crt` - the certificate; `tls.key` - the private key | | +| `expose.ingress.hosts.core` | The host of Harbor core service in ingress rule | `core.harbor.domain` | +| `expose.ingress.controller` | The ingress controller type. Currently supports `default`, `gce`, `alb`, `f5-bigip` and `ncp` | `default` | +| `expose.ingress.kubeVersionOverride` | Allows the ability to override the kubernetes version used while templating the ingress | | +| `expose.ingress.className` | Specify the `ingressClassName` used to implement the Ingress (Kubernetes 1.18+) | | +| `expose.ingress.annotations` | The annotations used commonly for ingresses | | +| `expose.ingress.labels` | The labels specific to ingress | {} | +| `expose.clusterIP.name` | The name of ClusterIP service | `harbor` | +| `expose.clusterIP.annotations` | The annotations attached to the ClusterIP service | {} | +| `expose.clusterIP.ports.httpPort` | The service port Harbor listens on when serving HTTP | `80` | +| `expose.clusterIP.ports.httpsPort` | The service port Harbor listens on when serving HTTPS | `443` | +| `expose.clusterIP.annotations` | The annotations used commonly for clusterIP | | +| `expose.clusterIP.labels` | The labels specific to clusterIP | {} | +| `expose.nodePort.name` | The name of NodePort service | `harbor` | +| `expose.nodePort.ports.http.port` | The service port Harbor listens on when serving HTTP | `80` | +| `expose.nodePort.ports.http.nodePort` | The node port Harbor listens on when serving HTTP | `30002` | +| `expose.nodePort.ports.https.port` | The service port Harbor listens on when serving HTTPS | `443` | +| `expose.nodePort.ports.https.nodePort` | The node port Harbor listens on when serving HTTPS | `30003` | +| `expose.nodePort.annotations` | The annotations used commonly for nodePort | | +| `expose.nodePort.labels` | The labels specific to nodePort | {} | +| `expose.loadBalancer.name` | The name of service | `harbor` | +| `expose.loadBalancer.IP` | The IP of the loadBalancer. It only works when loadBalancer supports assigning IP | `""` | +| `expose.loadBalancer.ports.httpPort` | The service port Harbor listens on when serving HTTP | `80` | +| `expose.loadBalancer.ports.httpsPort` | The service port Harbor listens on when serving HTTPS | `30002` | +| `expose.loadBalancer.annotations` | The annotations attached to the loadBalancer service | {} | +| `expose.loadBalancer.labels` | The labels specific to loadBalancer | {} | +| `expose.loadBalancer.sourceRanges` | List of IP address ranges to assign to loadBalancerSourceRanges | [] | +| `expose.route.labels` | The labels to attach to the HTTPRoute | `{}` | +| `expose.route.annotations` | The annotations to attach to the HTTPRoute | `{}` | +| `expose.route.hosts` | The hosts that the HTTPRoute will request to the Gateway | `[]` | +| `expose.route.parentRefs` | The Gateways to attach to the HTTPRoute | `{}` | +| **Internal TLS** | | | +| `internalTLS.enabled` | Enable TLS for the components (core, jobservice, portal, registry, trivy) | `false` | +| `internalTLS.strong_ssl_ciphers` | Enable strong ssl ciphers for nginx and portal | `false` | +| `internalTLS.certSource` | Method to provide TLS for the components, options are `auto`, `manual`, `secret`. | `auto` | +| `internalTLS.trustCa` | The content of trust CA, only available when `certSource` is `manual`. **Note**: all the internal certificates of the components must be issued by this CA | | +| `internalTLS.core.secretName` | The secret name for core component, only available when `certSource` is `secret`. The secret must contain keys named: `ca.crt` - the CA certificate which is used to issue internal key and crt pair for components and all Harbor components must be issued by the same CA, `tls.crt` - the content of the TLS cert file, `tls.key` - the content of the TLS key file. | | +| `internalTLS.core.crt` | Content of core's TLS cert file, only available when `certSource` is `manual` | | +| `internalTLS.core.key` | Content of core's TLS key file, only available when `certSource` is `manual` | | +| `internalTLS.jobservice.secretName` | The secret name for jobservice component, only available when `certSource` is `secret`. The secret must contain keys named: `ca.crt` - the CA certificate which is used to issue internal key and crt pair for components and all Harbor components must be issued by the same CA, `tls.crt` - the content of the TLS cert file, `tls.key` - the content of the TLS key file. | | +| `internalTLS.jobservice.crt` | Content of jobservice's TLS cert file, only available when `certSource` is `manual` | | +| `internalTLS.jobservice.key` | Content of jobservice's TLS key file, only available when `certSource` is `manual` | | +| `internalTLS.registry.secretName` | The secret name for registry component, only available when `certSource` is `secret`. The secret must contain keys named: `ca.crt` - the CA certificate which is used to issue internal key and crt pair for components and all Harbor components must be issued by the same CA, `tls.crt` - the content of the TLS cert file, `tls.key` - the content of the TLS key file. | | +| `internalTLS.registry.crt` | Content of registry's TLS cert file, only available when `certSource` is `manual` | | +| `internalTLS.registry.key` | Content of registry's TLS key file, only available when `certSource` is `manual` | | +| `internalTLS.portal.secretName` | The secret name for portal component, only available when `certSource` is `secret`. The secret must contain keys named: `ca.crt` - the CA certificate which is used to issue internal key and crt pair for components and all Harbor components must be issued by the same CA, `tls.crt` - the content of the TLS cert file, `tls.key` - the content of the TLS key file. | | +| `internalTLS.portal.crt` | Content of portal's TLS cert file, only available when `certSource` is `manual` | | +| `internalTLS.portal.key` | Content of portal's TLS key file, only available when `certSource` is `manual` | | +| `internalTLS.trivy.secretName` | The secret name for trivy component, only available when `certSource` is `secret`. The secret must contain keys named: `ca.crt` - the CA certificate which is used to issue internal key and crt pair for components and all Harbor components must be issued by the same CA, `tls.crt` - the content of the TLS cert file, `tls.key` - the content of the TLS key file. | | +| `internalTLS.trivy.crt` | Content of trivy's TLS cert file, only available when `certSource` is `manual` | | +| `internalTLS.trivy.key` | Content of trivy's TLS key file, only available when `certSource` is `manual` | | +| **IPFamily** | | | +| `ipFamily.ipv4.enabled` | if cluster is ipv4 enabled, all ipv4 related configs will set correspondingly, but currently it only affects the nginx related components | `true` | +| `ipFamily.ipv6.enabled` | if cluster is ipv6 enabled, all ipv6 related configs will set correspondingly, but currently it only affects the nginx related components | `true` | +| `ipFamily.policy` | Sets the IP family policy for services to be able to configure dual-stack; see [Configure dual-stack](https://kubernetes.io/docs/concepts/services-networking/dual-stack/#services). | `""` | +| `ipFamily.families` | A list of IP families for services that should be supported, in the order in which they should be applied. Can be "IPv4" and/or "IPv6". | [] | +| **Persistence** | | | +| `persistence.enabled` | Enable the data persistence or not | `true` | +| `persistence.resourcePolicy` | Setting it to `keep` to avoid removing PVCs during a helm delete operation. Leaving it empty will delete PVCs after the chart deleted. Does not affect PVCs created for internal database and redis components. | `keep` | +| `persistence.persistentVolumeClaim.registry.existingClaim` | Use the existing PVC which must be created manually before bound, and specify the `subPath` if the PVC is shared with other components | | +| `persistence.persistentVolumeClaim.registry.storageClass` | Specify the `storageClass` used to provision the volume. Or the default StorageClass will be used (the default). Set it to `-` to disable dynamic provisioning | | +| `persistence.persistentVolumeClaim.registry.subPath` | The sub path used in the volume | | +| `persistence.persistentVolumeClaim.registry.accessMode` | The access mode of the volume | `ReadWriteOnce` | +| `persistence.persistentVolumeClaim.registry.size` | The size of the volume | `5Gi` | +| `persistence.persistentVolumeClaim.registry.annotations` | The annotations of the volume | | +| `persistence.persistentVolumeClaim.jobservice.jobLog.existingClaim` | Use the existing PVC which must be created manually before bound, and specify the `subPath` if the PVC is shared with other components. | | +| `persistence.persistentVolumeClaim.jobservice.jobLog.storageClass` | Specify the `storageClass` used to provision the volume. Or the default StorageClass will be used (the default). Set it to `-` to disable dynamic provisioning | | +| `persistence.persistentVolumeClaim.jobservice.jobLog.subPath` | The sub path used in the volume | | +| `persistence.persistentVolumeClaim.jobservice.jobLog.accessMode` | The access mode of the volume | `ReadWriteOnce` | +| `persistence.persistentVolumeClaim.jobservice.jobLog.size` | The size of the volume | `1Gi` | +| `persistence.persistentVolumeClaim.jobservice.jobLog.annotations` | The annotations of the volume | | +| `persistence.persistentVolumeClaim.database.existingClaim` | Use the existing PVC which must be created manually before bound, and specify the `subPath` if the PVC is shared with other components. If external database is used, the setting will be ignored | | +| `persistence.persistentVolumeClaim.database.storageClass` | Specify the `storageClass` used to provision the volume. Or the default StorageClass will be used (the default). Set it to `-` to disable dynamic provisioning. If external database is used, the setting will be ignored | | +| `persistence.persistentVolumeClaim.database.subPath` | The sub path used in the volume. If external database is used, the setting will be ignored | | +| `persistence.persistentVolumeClaim.database.accessMode` | The access mode of the volume. If external database is used, the setting will be ignored | `ReadWriteOnce` | +| `persistence.persistentVolumeClaim.database.size` | The size of the volume. If external database is used, the setting will be ignored | `1Gi` | +| `persistence.persistentVolumeClaim.database.annotations` | The annotations of the volume | | +| `persistence.persistentVolumeClaim.redis.existingClaim` | Use the existing PVC which must be created manually before bound, and specify the `subPath` if the PVC is shared with other components. If external Redis is used, the setting will be ignored | | +| `persistence.persistentVolumeClaim.redis.storageClass` | Specify the `storageClass` used to provision the volume. Or the default StorageClass will be used (the default). Set it to `-` to disable dynamic provisioning. If external Redis is used, the setting will be ignored | | +| `persistence.persistentVolumeClaim.redis.subPath` | The sub path used in the volume. If external Redis is used, the setting will be ignored | | +| `persistence.persistentVolumeClaim.redis.accessMode` | The access mode of the volume. If external Redis is used, the setting will be ignored | `ReadWriteOnce` | +| `persistence.persistentVolumeClaim.redis.size` | The size of the volume. If external Redis is used, the setting will be ignored | `1Gi` | +| `persistence.persistentVolumeClaim.redis.annotations` | The annotations of the volume | | +| `persistence.persistentVolumeClaim.trivy.existingClaim` | Use the existing PVC which must be created manually before bound, and specify the `subPath` if the PVC is shared with other components | | +| `persistence.persistentVolumeClaim.trivy.storageClass` | Specify the `storageClass` used to provision the volume. Or the default StorageClass will be used (the default). Set it to `-` to disable dynamic provisioning | | +| `persistence.persistentVolumeClaim.trivy.subPath` | The sub path used in the volume | | +| `persistence.persistentVolumeClaim.trivy.accessMode` | The access mode of the volume | `ReadWriteOnce` | +| `persistence.persistentVolumeClaim.trivy.size` | The size of the volume | `1Gi` | +| `persistence.persistentVolumeClaim.trivy.annotations` | The annotations of the volume | | +| `persistence.imageChartStorage.disableredirect` | The configuration for managing redirects from content backends. For backends which not supported it (such as using minio for `s3` storage type), please set it to `true` to disable redirects. Refer to the [guide](https://github.com/docker/distribution/blob/master/docs/configuration.md#redirect) for more details | `false` | +| `persistence.imageChartStorage.caBundleSecretName` | Specify the `caBundleSecretName` if the storage service uses a self-signed certificate. The secret must contain keys named `ca.crt` which will be injected into the trust store of registry's and containers. | | +| `persistence.imageChartStorage.type` | The type of storage for images and charts: `filesystem`, `azure`, `gcs`, `s3`, `swift` or `oss`. The type must be `filesystem` if you want to use persistent volumes for registry. Refer to the [guide](https://github.com/docker/distribution/blob/master/docs/configuration.md#storage) for more details | `filesystem` | +| `persistence.imageChartStorage.gcs.existingSecret` | An existing secret containing the gcs service account json key. The key must be gcs-key.json. | `""` | +| `persistence.imageChartStorage.gcs.useWorkloadIdentity` | A boolean to allow the use of workloadidentity in a GKE cluster. To use it, create a kubernetes service account and set the name in the key `serviceAccountName` of each component, then allow automounting the service account. | `false` | +| **General** | | | +| `externalURL` | The external URL for Harbor core service | `https://core.harbor.domain` | +| `caBundleSecretName` | The custom CA bundle secret name, the secret must contain key named "ca.crt" which will be injected into the trust store for core, jobservice, registry, trivy components. | | +| `uaaSecretName` | If using external UAA auth which has a self signed cert, you can provide a pre-created secret containing it under the key `ca.crt`. | | +| `imagePullPolicy` | The image pull policy | | +| `imagePullSecrets` | The imagePullSecrets names for all deployments | | +| `updateStrategy.type` | The update strategy for deployments with persistent volumes(jobservice, registry): `RollingUpdate` or `Recreate`. Set it as `Recreate` when `RWM` for volumes isn't supported | `RollingUpdate` | +| `logLevel` | The log level: `debug`, `info`, `warning`, `error` or `fatal` | `info` | +| `harborAdminPassword` | The initial password of Harbor admin. Change it from portal after launching Harbor | `Harbor12345` | +| `existingSecretAdminPassword` | The name of secret where admin password can be found. | | +| `existingSecretAdminPasswordKey` | The name of the key in the secret where to find harbor admin password Harbor | `HARBOR_ADMIN_PASSWORD` | +| `caSecretName` | The name of the secret which contains key named `ca.crt`. Setting this enables the download link on portal to download the CA certificate when the certificate isn't generated automatically | | +| `secretKey` | The key used for encryption. Must be a string of 16 chars | `not-a-secure-key` | +| `existingSecretSecretKey` | An existing secret containing the encoding secretKey | `""` | +| `proxy.httpProxy` | The URL of the HTTP proxy server | | +| `proxy.httpsProxy` | The URL of the HTTPS proxy server | | +| `proxy.noProxy` | The URLs that the proxy settings not apply to | 127.0.0.1,localhost,.local,.internal | +| `proxy.components` | The component list that the proxy settings apply to | core, jobservice, trivy | +| `enableMigrateHelmHook` | Run the migration job via helm hook, if it is true, the database migration will be separated from harbor-core, run with a preupgrade job migration-job | `false` | +| **Nginx** (if service exposed via `ingress`, Nginx will not be used) | | | +| `nginx.image.repository` | Image repository | `goharbor/nginx-photon` | +| `nginx.image.tag` | Image tag | `dev` | +| `nginx.replicas` | The replica count | `1` | +| `nginx.revisionHistoryLimit` | The revision history limit | `10` | +| `nginx.resources` | The [resources] to allocate for container | undefined | +| `nginx.automountServiceAccountToken` | Mount serviceAccountToken? | `false` | +| `nginx.nodeSelector` | Node labels for pod assignment | `{}` | +| `nginx.tolerations` | Tolerations for pod assignment | `[]` | +| `nginx.affinity` | Node/Pod affinities | `{}` | +| `nginx.topologySpreadConstraints` | Constraints that define how Pods are spread across failure-domains like regions or availability zones | `[]` | +| `nginx.podAnnotations` | Annotations to add to the nginx pod | `{}` | +| `nginx.priorityClassName` | The priority class to run the pod as | | +| **Portal** | | | +| `portal.image.repository` | Repository for portal image | `goharbor/harbor-portal` | +| `portal.image.tag` | Tag for portal image | `dev` | +| `portal.replicas` | The replica count | `1` | +| `portal.revisionHistoryLimit` | The revision history limit | `10` | +| `portal.resources` | The [resources] to allocate for container | undefined | +| `portal.automountServiceAccountToken` | Mount serviceAccountToken? | `false` | +| `portal.nodeSelector` | Node labels for pod assignment | `{}` | +| `portal.tolerations` | Tolerations for pod assignment | `[]` | +| `portal.affinity` | Node/Pod affinities | `{}` | +| `portal.topologySpreadConstraints` | Constraints that define how Pods are spread across failure-domains like regions or availability zones | `[]` | +| `portal.podAnnotations` | Annotations to add to the portal pod | `{}` | +| `portal.serviceAnnotations` | Annotations to add to the portal service | `{}` | +| `portal.priorityClassName` | The priority class to run the pod as | | +| `portal.initContainers` | Init containers to be run before the controller's container starts. | `[]` | +| **Core** | | | +| `core.image.repository` | Repository for Harbor core image | `goharbor/harbor-core` | +| `core.image.tag` | Tag for Harbor core image | `dev` | +| `core.replicas` | The replica count | `1` | +| `core.revisionHistoryLimit` | The revision history limit | `10` | +| `core.startupProbe.initialDelaySeconds` | The initial delay in seconds for the startup probe | `10` | +| `core.resources` | The [resources] to allocate for container | undefined | +| `core.automountServiceAccountToken` | Mount serviceAccountToken? | `false` | +| `core.nodeSelector` | Node labels for pod assignment | `{}` | +| `core.tolerations` | Tolerations for pod assignment | `[]` | +| `core.affinity` | Node/Pod affinities | `{}` | +| `core.topologySpreadConstraints` | Constraints that define how Pods are spread across failure-domains like regions or availability zones | `[]` | +| `core.podAnnotations` | Annotations to add to the core pod | `{}` | +| `core.serviceAnnotations` | Annotations to add to the core service | `{}` | +| `core.configureUserSettings` | A JSON string to set in the environment variable `CONFIG_OVERWRITE_JSON` to configure user settings. See the [official docs](https://goharbor.io/docs/latest/install-config/configure-user-settings-cli/#configure-users-settings-using-an-environment-variable). | | +| `core.quotaUpdateProvider` | The provider for updating project quota(usage), there are 2 options, redis or db. By default it is implemented by db but you can configure it to redis which can improve the performance of high concurrent pushing to the same project, and reduce the database connections spike and occupies. Using redis will bring up some delay for quota usage updation for display, so only suggest switch provider to redis if you were ran into the db connections spike around the scenario of high concurrent pushing to same project, no improvment for other scenes. | `db` | +| `core.secret` | Secret is used when core server communicates with other components. If a secret key is not specified, Helm will generate one. Must be a string of 16 chars. | | +| `core.secretName` | Fill the name of a kubernetes secret if you want to use your own TLS certificate and private key for token encryption/decryption. The secret must contain keys named: `tls.crt` - the certificate and `tls.key` - the private key. The default key pair will be used if it isn't set | | +| `core.tokenKey` | PEM-formatted RSA private key used to sign service tokens. Only used if `core.secretName` is unset. If set, `core.tokenCert` MUST also be set. | | +| `core.tokenCert` | PEM-formatted certificate signed by `core.tokenKey` used to validate service tokens. Only used if `core.secretName` is unset. If set, `core.tokenKey` MUST also be set. | | +| `core.xsrfKey` | The XSRF key. Will be generated automatically if it isn't specified | | +| `core.priorityClassName` | The priority class to run the pod as | | +| `core.artifactPullAsyncFlushDuration` | The time duration for async update artifact pull_time and repository pull_count | | +| `core.gdpr.deleteUser` | Enable GDPR compliant user delete | `false` | +| `core.gdpr.auditLogsCompliant` | Enable GDPR compliant for audit logs by changing username to its CRC32 value if that user was deleted from the system | `false` | +| `core.initContainers` | Init containers to be run before the controller's container starts. | `[]` | +| **Jobservice** | | | +| `jobservice.image.repository` | Repository for jobservice image | `goharbor/harbor-jobservice` | +| `jobservice.image.tag` | Tag for jobservice image | `dev` | +| `jobservice.replicas` | The replica count | `1` | +| `jobservice.revisionHistoryLimit` | The revision history limit | `10` | +| `jobservice.maxJobWorkers` | The max job workers | `10` | +| `jobservice.jobLoggers` | The loggers for jobs: `file`, `database` or `stdout` | `[file]` | +| `jobservice.loggerSweeperDuration` | The jobLogger sweeper duration in days (ignored if `jobLoggers` is set to `stdout`) | `14` | +| `jobservice.notification.webhook_job_max_retry` | The maximum retry of webhook sending notifications | `3` | +| `jobservice.notification.webhook_job_http_client_timeout` | The http client timeout value of webhook sending notifications | `3` | +| `jobservice.reaper.max_update_hours` | the max time to wait for a task to finish, if unfinished after max_update_hours, the task will be mark as error, but the task will continue to run, default value is 24 | `24` | +| `jobservice.reaper.max_dangling_hours` | the max time for execution in running state without new task created | `168` | +| `jobservice.resources` | The [resources] to allocate for container | undefined | +| `jobservice.automountServiceAccountToken` | Mount serviceAccountToken? | `false` | +| `jobservice.nodeSelector` | Node labels for pod assignment | `{}` | +| `jobservice.tolerations` | Tolerations for pod assignment | `[]` | +| `jobservice.affinity` | Node/Pod affinities | `{}` | +| `jobservice.topologySpreadConstraints` | Constraints that define how Pods are spread across failure-domains like regions or availability zones | `[]` | +| `jobservice.podAnnotations` | Annotations to add to the jobservice pod | `{}` | +| `jobservice.priorityClassName` | The priority class to run the pod as | | +| `jobservice.secret` | Secret is used when job service communicates with other components. If a secret key is not specified, Helm will generate one. Must be a string of 16 chars. | | +| `jobservice.initContainers` | Init containers to be run before the controller's container starts. | `[]` | +| **Registry** | | | +| `registry.registry.image.repository` | Repository for registry image | `goharbor/registry-photon` | +| `registry.registry.image.tag` | Tag for registry image | `dev` | +| `registry.registry.resources` | The [resources] to allocate for container | undefined | +| `registry.controller.image.repository` | Repository for registry controller image | `goharbor/harbor-registryctl` | +| `registry.controller.image.tag` | Tag for registry controller image | `dev` | +| `registry.controller.resources` | The [resources] to allocate for container | undefined | +| `registry.replicas` | The replica count | `1` | +| `registry.revisionHistoryLimit` | The revision history limit | `10` | +| `registry.nodeSelector` | Node labels for pod assignment | `{}` | +| `registry.automountServiceAccountToken` | Mount serviceAccountToken? | `false` | +| `registry.tolerations` | Tolerations for pod assignment | `[]` | +| `registry.affinity` | Node/Pod affinities | `{}` | +| `registry.topologySpreadConstraints` | Constraints that define how Pods are spread across failure-domains like regions or availability zones | `[]` | +| `registry.middleware` | Middleware is used to add support for a CDN between backend storage and `docker pull` recipient. See [official docs](https://github.com/docker/distribution/blob/master/docs/configuration.md#middleware). | | +| `registry.podAnnotations` | Annotations to add to the registry pod | `{}` | +| `registry.priorityClassName` | The priority class to run the pod as | | +| `registry.secret` | Secret is used to secure the upload state from client and registry storage backend. See [official docs](https://github.com/docker/distribution/blob/master/docs/configuration.md#http). If a secret key is not specified, Helm will generate one. Must be a string of 16 chars. | | +| `registry.credentials.username` | The username that harbor core uses internally to access the registry instance. Together with the `registry.credentials.password`, a htpasswd is created. This is an alternative to providing `registry.credentials.htpasswdString`. For more details see [official docs](https://github.com/docker/distribution/blob/master/docs/configuration.md#htpasswd). | `harbor_registry_user` | +| `registry.credentials.password` | The password that harbor core uses internally to access the registry instance. Together with the `registry.credentials.username`, a htpasswd is created. This is an alternative to providing `registry.credentials.htpasswdString`. For more details see [official docs](https://github.com/docker/distribution/blob/master/docs/configuration.md#htpasswd). It is suggested you update this value before installation. | `harbor_registry_password` | +| `registry.credentials.existingSecret` | An existing secret containing the password for accessing the registry instance, which is hosted by htpasswd auth mode. More details see [official docs](https://github.com/docker/distribution/blob/master/docs/configuration.md#htpasswd). The key must be `REGISTRY_PASSWD` | `""` | +| `registry.credentials.htpasswdString` | Login and password in htpasswd string format. Excludes `registry.credentials.username` and `registry.credentials.password`. May come in handy when integrating with tools like argocd or flux. This allows the same line to be generated each time the template is rendered, instead of the `htpasswd` function from helm, which generates different lines each time because of the salt. | undefined | +| `registry.relativeurls` | If true, the registry returns relative URLs in Location headers. The client is responsible for resolving the correct URL. Needed if harbor is behind a reverse proxy | `false` | +| `registry.upload_purging.enabled` | If true, enable purge _upload directories | `true` | +| `registry.upload_purging.age` | Remove files in _upload directories which exist for a period of time, default is one week. | `168h` | +| `registry.upload_purging.interval` | The interval of the purge operations | `24h` | +| `registry.upload_purging.dryrun` | If true, enable dryrun for purging _upload, default false | `false` | +| `registry.initContainers` | Init containers to be run before the controller's container starts. | `[]` | +| **[Trivy][trivy]** | | | +| `trivy.enabled` | The flag to enable Trivy scanner | `true` | +| `trivy.image.repository` | Repository for Trivy adapter image | `goharbor/trivy-adapter-photon` | +| `trivy.image.tag` | Tag for Trivy adapter image | `dev` | +| `trivy.resources` | The [resources] to allocate for Trivy adapter container | | +| `trivy.automountServiceAccountToken` | Mount serviceAccountToken? | `false` | +| `trivy.replicas` | The number of Pod replicas | `1` | +| `trivy.debugMode` | The flag to enable Trivy debug mode | `false` | +| `trivy.vulnType` | Comma-separated list of vulnerability types. Possible values `os` and `library`. | `os,library` | +| `trivy.severity` | Comma-separated list of severities to be checked | `UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL` | +| `trivy.ignoreUnfixed` | The flag to display only fixed vulnerabilities | `false` | +| `trivy.insecure` | The flag to skip verifying registry certificate | `false` | +| `trivy.skipUpdate` | The flag to disable [Trivy DB][trivy-db] downloads from GitHub | `false` | +| `trivy.skipJavaDBUpdate` | If the flag is enabled you have to manually download the `trivy-java.db` file [Trivy Java DB][trivy-java-db] and mount it in the `/home/scanner/.cache/trivy/java-db/trivy-java.db` path | `false` | +| `trivy.offlineScan` | The flag prevents Trivy from sending API requests to identify dependencies. | `false` | +| `trivy.securityCheck` | Comma-separated list of what security issues to detect. | `vuln` | +| `trivy.timeout` | The duration to wait for scan completion | `5m0s` | +| `trivy.gitHubToken` | The GitHub access token to download [Trivy DB][trivy-db] (see [GitHub rate limiting][trivy-rate-limiting]) | | +| `trivy.priorityClassName` | The priority class to run the pod as | | +| `trivy.topologySpreadConstraints` | The priority class to run the pod as | | +| `trivy.initContainers` | Init containers to be run before the controller's container starts. | `[]` | +| **Database** | | | +| `database.type` | If external database is used, set it to `external` | `internal` | +| `database.internal.image.repository` | Repository for database image | `goharbor/harbor-db` | +| `database.internal.image.tag` | Tag for database image | `dev` | +| `database.internal.password` | The password for database | `changeit` | +| `database.internal.shmSizeLimit` | The limit for the size of shared memory for internal PostgreSQL, conventionally it's around 50% of the memory limit of the container | `512Mi` | +| `database.internal.resources` | The [resources] to allocate for container | undefined | +| `database.internal.automountServiceAccountToken` | Mount serviceAccountToken? | `false` | +| `database.internal.initContainer.migrator.resources` | The [resources] to allocate for the database migrator initContainer | undefined | +| `database.internal.initContainer.permissions.resources` | The [resources] to allocate for the database permissions initContainer | undefined | +| `database.internal.nodeSelector` | Node labels for pod assignment | `{}` | +| `database.internal.tolerations` | Tolerations for pod assignment | `[]` | +| `database.internal.affinity` | Node/Pod affinities | `{}` | +| `database.internal.priorityClassName` | The priority class to run the pod as | | +| `database.internal.livenessProbe.timeoutSeconds` | The timeout used in liveness probe; 1 to 5 seconds | 1 | +| `database.internal.readinessProbe.timeoutSeconds` | The timeout used in readiness probe; 1 to 5 seconds | 1 | +| `database.internal.extrInitContainers` | Extra init containers to be run before the database's container starts. | `[]` | +| `database.external.host` | The hostname of external database | `192.168.0.1` | +| `database.external.port` | The port of external database | `5432` | +| `database.external.username` | The username of external database | `user` | +| `database.external.password` | The password of external database | `password` | +| `database.external.coreDatabase` | The database used by core service | `registry` | +| `database.external.existingSecret` | An existing password containing the database password. the key must be `password`. | `""` | +| `database.external.sslmode` | Connection method of external database (require, verify-full, verify-ca, disable) | `disable` | +| `database.maxIdleConns` | The maximum number of connections in the idle connection pool. If it <=0, no idle connections are retained. | `50` | +| `database.maxOpenConns` | The maximum number of open connections to the database. If it <= 0, then there is no limit on the number of open connections. | `100` | +| `database.podAnnotations` | Annotations to add to the database pod | `{}` | +| **Redis** | | | +| `redis.type` | If external redis is used, set it to `external` | `internal` | +| `redis.internal.image.repository` | Repository for redis image | `goharbor/redis-photon` | +| `redis.internal.image.tag` | Tag for redis image | `dev` | +| `redis.internal.resources` | The [resources] to allocate for container | undefined | +| `redis.internal.automountServiceAccountToken` | Mount serviceAccountToken? | `false` | +| `redis.internal.nodeSelector` | Node labels for pod assignment | `{}` | +| `redis.internal.tolerations` | Tolerations for pod assignment | `[]` | +| `redis.internal.affinity` | Node/Pod affinities | `{}` | +| `redis.internal.priorityClassName` | The priority class to run the pod as | | +| `redis.internal.jobserviceDatabaseIndex` | The database index for jobservice | `1` | +| `redis.internal.registryDatabaseIndex` | The database index for registry | `2` | +| `redis.internal.trivyAdapterIndex` | The database index for trivy adapter | `5` | +| `redis.internal.harborDatabaseIndex` | The database index for harbor miscellaneous business logic | `0` | +| `redis.internal.cacheLayerDatabaseIndex` | The database index for harbor cache layer | `0` | +| `redis.internal.initContainers` | Init containers to be run before the redis's container starts. | `[]` | +| `redis.external.addr` | The addr of external Redis: :. When using sentinel, it should be :,:,: | `192.168.0.2:6379` | +| `redis.external.sentinelMasterSet` | The name of the set of Redis instances to monitor | | +| `redis.external.coreDatabaseIndex` | The database index for core | `0` | +| `redis.external.jobserviceDatabaseIndex` | The database index for jobservice | `1` | +| `redis.external.registryDatabaseIndex` | The database index for registry | `2` | +| `redis.external.trivyAdapterIndex` | The database index for trivy adapter | `5` | +| `redis.external.harborDatabaseIndex` | The database index for harbor miscellaneous business logic | `0` | +| `redis.external.cacheLayerDatabaseIndex` | The database index for harbor cache layer | `0` | +| `redis.external.username` | The username of external Redis | | +| `redis.external.password` | The password of external Redis | | +| `redis.external.existingSecret` | Use an existing secret to connect to redis. The key must be `REDIS_PASSWORD`. | `""` | +| `redis.podAnnotations` | Annotations to add to the redis pod | `{}` | +| **Exporter** | | | +| `exporter.replicas` | The replica count | `1` | +| `exporter.revisionHistoryLimit` | The revision history limit | `10` | +| `exporter.podAnnotations` | Annotations to add to the exporter pod | `{}` | +| `exporter.image.repository` | Repository for redis image | `goharbor/harbor-exporter` | +| `exporter.image.tag` | Tag for exporter image | `dev` | +| `exporter.nodeSelector` | Node labels for pod assignment | `{}` | +| `exporter.tolerations` | Tolerations for pod assignment | `[]` | +| `exporter.affinity` | Node/Pod affinities | `{}` | +| `exporter.topologySpreadConstraints` | Constraints that define how Pods are spread across failure-domains like regions or availability zones | `[]` | +| `exporter.automountServiceAccountToken` | Mount serviceAccountToken? | `false` | +| `exporter.cacheDuration` | the cache duration for information that exporter collected from Harbor | `30` | +| `exporter.cacheCleanInterval` | cache clean interval for information that exporter collected from Harbor | `14400` | +| `exporter.priorityClassName` | The priority class to run the pod as | | +| **Metrics** | | | +| `metrics.enabled` | if enable harbor metrics | `false` | +| `metrics.core.path` | the url path for core metrics | `/metrics` | +| `metrics.core.port` | the port for core metrics | `8001` | +| `metrics.registry.path` | the url path for registry metrics | `/metrics` | +| `metrics.registry.port` | the port for registry metrics | `8001` | +| `metrics.exporter.path` | the url path for exporter metrics | `/metrics` | +| `metrics.exporter.port` | the port for exporter metrics | `8001` | +| `metrics.serviceMonitor.enabled` | create prometheus serviceMonitor. Requires prometheus CRD's | `false` | +| `metrics.serviceMonitor.additionalLabels` | additional labels to upsert to the manifest | `""` | +| `metrics.serviceMonitor.interval` | scrape period for harbor metrics | `""` | +| `metrics.serviceMonitor.metricRelabelings` | metrics relabel to add/mod/del before ingestion | `[]` | +| `metrics.serviceMonitor.relabelings` | relabels to add/mod/del to sample before scrape | `[]` | +| **Trace** | | | +| `trace.enabled` | Enable tracing or not | `false` | +| `trace.provider` | The tracing provider: `jaeger` or `otel`. `jaeger` should be 1.26+ | `jaeger` | +| `trace.sample_rate` | Set `sample_rate` to 1 if you want sampling 100% of trace data; set 0.5 if you want sampling 50% of trace data, and so forth | `1` | +| `trace.namespace` | Namespace used to differentiate different harbor services | | +| `trace.attributes` | `attributes` is a key value dict contains user defined attributes used to initialize trace provider | | +| `trace.jaeger.endpoint` | The endpoint of jaeger | `http://hostname:14268/api/traces` | +| `trace.jaeger.username` | The username of jaeger | | +| `trace.jaeger.password` | The password of jaeger | | +| `trace.jaeger.agent_host` | The agent host of jaeger | | +| `trace.jaeger.agent_port` | The agent port of jaeger | `6831` | +| `trace.otel.endpoint` | The endpoint of otel | `hostname:4318` | +| `trace.otel.url_path` | The URL path of otel | `/v1/traces` | +| `trace.otel.compression` | Whether enable compression or not for otel | `false` | +| `trace.otel.insecure` | Whether establish insecure connection or not for otel | `true` | +| `trace.otel.timeout` | The timeout in seconds of otel | `10` | +| **Cache** | | | +| `cache.enabled` | Enable cache layer or not | `false` | +| `cache.expireHours` | The expire hours of cache layer | `24` | +| Parameter | Description | Default | +|----------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------| +| **Expose** | | | +| `expose.type` | How to expose the service: `ingress`, `clusterIP`, `nodePort` or `loadBalancer`, other values will be ignored and the creation of service will be skipped. | `ingress` | +| `expose.tls.enabled` | Enable TLS or not. Delete the `ssl-redirect` annotations in `expose.ingress.annotations` when TLS is disabled and `expose.type` is `ingress`. Note: if the `expose.type` is `ingress` and TLS is disabled, the port must be included in the command when pulling/pushing images. Refer to https://github.com/goharbor/harbor/issues/5291 for details. | `true` | +| `expose.tls.certSource` | The source of the TLS certificate. Set as `auto`, `secret` or `none` and fill the information in the corresponding section: 1) auto: generate the TLS certificate automatically 2) secret: read the TLS certificate from the specified secret. The TLS certificate can be generated manually or by cert manager 3) none: configure no TLS certificate for the ingress. If the default TLS certificate is configured in the ingress controller, choose this option | `auto` | +| `expose.tls.auto.commonName` | The common name used to generate the certificate, it's necessary when the type isn't `ingress` | | +| `expose.tls.secret.secretName` | The name of secret which contains keys named: `tls.crt` - the certificate; `tls.key` - the private key | | +| `expose.ingress.hosts.core` | The host of Harbor core service in ingress rule | `core.harbor.domain` | +| `expose.ingress.controller` | The ingress controller type. Currently supports `default`, `gce`, `alb`, `f5-bigip` and `ncp` | `default` | +| `expose.ingress.kubeVersionOverride` | Allows the ability to override the kubernetes version used while templating the ingress | | +| `expose.ingress.className` | Specify the `ingressClassName` used to implement the Ingress (Kubernetes 1.18+) | | +| `expose.ingress.annotations` | The annotations used commonly for ingresses | | +| `expose.ingress.labels` | The labels specific to ingress | {} | +| `expose.clusterIP.name` | The name of ClusterIP service | `harbor` | +| `expose.clusterIP.annotations` | The annotations attached to the ClusterIP service | {} | +| `expose.clusterIP.ports.httpPort` | The service port Harbor listens on when serving HTTP | `80` | +| `expose.clusterIP.ports.httpsPort` | The service port Harbor listens on when serving HTTPS | `443` | +| `expose.clusterIP.annotations` | The annotations used commonly for clusterIP | | +| `expose.clusterIP.labels` | The labels specific to clusterIP | {} | +| `expose.nodePort.name` | The name of NodePort service | `harbor` | +| `expose.nodePort.ports.http.port` | The service port Harbor listens on when serving HTTP | `80` | +| `expose.nodePort.ports.http.nodePort` | The node port Harbor listens on when serving HTTP | `30002` | +| `expose.nodePort.ports.https.port` | The service port Harbor listens on when serving HTTPS | `443` | +| `expose.nodePort.ports.https.nodePort` | The node port Harbor listens on when serving HTTPS | `30003` | +| `expose.nodePort.annotations` | The annotations used commonly for nodePort | | +| `expose.nodePort.labels` | The labels specific to nodePort | {} | +| `expose.loadBalancer.name` | The name of service | `harbor` | +| `expose.loadBalancer.IP` | The IP of the loadBalancer. It only works when loadBalancer supports assigning IP | `""` | +| `expose.loadBalancer.ports.httpPort` | The service port Harbor listens on when serving HTTP | `80` | +| `expose.loadBalancer.ports.httpsPort` | The service port Harbor listens on when serving HTTPS | `30002` | +| `expose.loadBalancer.annotations` | The annotations attached to the loadBalancer service | {} | +| `expose.loadBalancer.labels` | The labels specific to loadBalancer | {} | +| `expose.loadBalancer.sourceRanges` | List of IP address ranges to assign to loadBalancerSourceRanges | [] | +| **Internal TLS** | | | +| `internalTLS.enabled` | Enable TLS for the components (core, jobservice, portal, registry, trivy) | `false` | +| `internalTLS.strong_ssl_ciphers` | Enable strong ssl ciphers for nginx and portal | `false` | +| `internalTLS.certSource` | Method to provide TLS for the components, options are `auto`, `manual`, `secret`. | `auto` | +| `internalTLS.trustCa` | The content of trust CA, only available when `certSource` is `manual`. **Note**: all the internal certificates of the components must be issued by this CA | | +| `internalTLS.core.secretName` | The secret name for core component, only available when `certSource` is `secret`. The secret must contain keys named: `ca.crt` - the CA certificate which is used to issue internal key and crt pair for components and all Harbor components must be issued by the same CA, `tls.crt` - the content of the TLS cert file, `tls.key` - the content of the TLS key file. | | +| `internalTLS.core.crt` | Content of core's TLS cert file, only available when `certSource` is `manual` | | +| `internalTLS.core.key` | Content of core's TLS key file, only available when `certSource` is `manual` | | +| `internalTLS.jobservice.secretName` | The secret name for jobservice component, only available when `certSource` is `secret`. The secret must contain keys named: `ca.crt` - the CA certificate which is used to issue internal key and crt pair for components and all Harbor components must be issued by the same CA, `tls.crt` - the content of the TLS cert file, `tls.key` - the content of the TLS key file. | | +| `internalTLS.jobservice.crt` | Content of jobservice's TLS cert file, only available when `certSource` is `manual` | | +| `internalTLS.jobservice.key` | Content of jobservice's TLS key file, only available when `certSource` is `manual` | | +| `internalTLS.registry.secretName` | The secret name for registry component, only available when `certSource` is `secret`. The secret must contain keys named: `ca.crt` - the CA certificate which is used to issue internal key and crt pair for components and all Harbor components must be issued by the same CA, `tls.crt` - the content of the TLS cert file, `tls.key` - the content of the TLS key file. | | +| `internalTLS.registry.crt` | Content of registry's TLS cert file, only available when `certSource` is `manual` | | +| `internalTLS.registry.key` | Content of registry's TLS key file, only available when `certSource` is `manual` | | +| `internalTLS.portal.secretName` | The secret name for portal component, only available when `certSource` is `secret`. The secret must contain keys named: `ca.crt` - the CA certificate which is used to issue internal key and crt pair for components and all Harbor components must be issued by the same CA, `tls.crt` - the content of the TLS cert file, `tls.key` - the content of the TLS key file. | | +| `internalTLS.portal.crt` | Content of portal's TLS cert file, only available when `certSource` is `manual` | | +| `internalTLS.portal.key` | Content of portal's TLS key file, only available when `certSource` is `manual` | | +| `internalTLS.trivy.secretName` | The secret name for trivy component, only available when `certSource` is `secret`. The secret must contain keys named: `ca.crt` - the CA certificate which is used to issue internal key and crt pair for components and all Harbor components must be issued by the same CA, `tls.crt` - the content of the TLS cert file, `tls.key` - the content of the TLS key file. | | +| `internalTLS.trivy.crt` | Content of trivy's TLS cert file, only available when `certSource` is `manual` | | +| `internalTLS.trivy.key` | Content of trivy's TLS key file, only available when `certSource` is `manual` | | +| **IPFamily** | | | +| `ipFamily.ipv4.enabled` | if cluster is ipv4 enabled, all ipv4 related configs will set correspondingly, but currently it only affects the nginx related components | `true` | +| `ipFamily.ipv6.enabled` | if cluster is ipv6 enabled, all ipv6 related configs will set correspondingly, but currently it only affects the nginx related components | `true` | +| **Persistence** | | | +| `persistence.enabled` | Enable the data persistence or not | `true` | +| `persistence.resourcePolicy` | Setting it to `keep` to avoid removing PVCs during a helm delete operation. Leaving it empty will delete PVCs after the chart deleted. Does not affect PVCs created for internal database and redis components. | `keep` | +| `persistence.persistentVolumeClaim.registry.existingClaim` | Use the existing PVC which must be created manually before bound, and specify the `subPath` if the PVC is shared with other components | | +| `persistence.persistentVolumeClaim.registry.storageClass` | Specify the `storageClass` used to provision the volume. Or the default StorageClass will be used (the default). Set it to `-` to disable dynamic provisioning | | +| `persistence.persistentVolumeClaim.registry.subPath` | The sub path used in the volume | | +| `persistence.persistentVolumeClaim.registry.accessMode` | The access mode of the volume | `ReadWriteOnce` | +| `persistence.persistentVolumeClaim.registry.size` | The size of the volume | `5Gi` | +| `persistence.persistentVolumeClaim.registry.annotations` | The annotations of the volume | | +| `persistence.persistentVolumeClaim.jobservice.jobLog.existingClaim` | Use the existing PVC which must be created manually before bound, and specify the `subPath` if the PVC is shared with other components. | | +| `persistence.persistentVolumeClaim.jobservice.jobLog.storageClass` | Specify the `storageClass` used to provision the volume. Or the default StorageClass will be used (the default). Set it to `-` to disable dynamic provisioning | | +| `persistence.persistentVolumeClaim.jobservice.jobLog.subPath` | The sub path used in the volume | | +| `persistence.persistentVolumeClaim.jobservice.jobLog.accessMode` | The access mode of the volume | `ReadWriteOnce` | +| `persistence.persistentVolumeClaim.jobservice.jobLog.size` | The size of the volume | `1Gi` | +| `persistence.persistentVolumeClaim.jobservice.jobLog.annotations` | The annotations of the volume | | +| `persistence.persistentVolumeClaim.database.existingClaim` | Use the existing PVC which must be created manually before bound, and specify the `subPath` if the PVC is shared with other components. If external database is used, the setting will be ignored | | +| `persistence.persistentVolumeClaim.database.storageClass` | Specify the `storageClass` used to provision the volume. Or the default StorageClass will be used (the default). Set it to `-` to disable dynamic provisioning. If external database is used, the setting will be ignored | | +| `persistence.persistentVolumeClaim.database.subPath` | The sub path used in the volume. If external database is used, the setting will be ignored | | +| `persistence.persistentVolumeClaim.database.accessMode` | The access mode of the volume. If external database is used, the setting will be ignored | `ReadWriteOnce` | +| `persistence.persistentVolumeClaim.database.size` | The size of the volume. If external database is used, the setting will be ignored | `1Gi` | +| `persistence.persistentVolumeClaim.database.annotations` | The annotations of the volume | | +| `persistence.persistentVolumeClaim.redis.existingClaim` | Use the existing PVC which must be created manually before bound, and specify the `subPath` if the PVC is shared with other components. If external Redis is used, the setting will be ignored | | +| `persistence.persistentVolumeClaim.redis.storageClass` | Specify the `storageClass` used to provision the volume. Or the default StorageClass will be used (the default). Set it to `-` to disable dynamic provisioning. If external Redis is used, the setting will be ignored | | +| `persistence.persistentVolumeClaim.redis.subPath` | The sub path used in the volume. If external Redis is used, the setting will be ignored | | +| `persistence.persistentVolumeClaim.redis.accessMode` | The access mode of the volume. If external Redis is used, the setting will be ignored | `ReadWriteOnce` | +| `persistence.persistentVolumeClaim.redis.size` | The size of the volume. If external Redis is used, the setting will be ignored | `1Gi` | +| `persistence.persistentVolumeClaim.redis.annotations` | The annotations of the volume | | +| `persistence.persistentVolumeClaim.trivy.existingClaim` | Use the existing PVC which must be created manually before bound, and specify the `subPath` if the PVC is shared with other components | | +| `persistence.persistentVolumeClaim.trivy.storageClass` | Specify the `storageClass` used to provision the volume. Or the default StorageClass will be used (the default). Set it to `-` to disable dynamic provisioning | | +| `persistence.persistentVolumeClaim.trivy.subPath` | The sub path used in the volume | | +| `persistence.persistentVolumeClaim.trivy.accessMode` | The access mode of the volume | `ReadWriteOnce` | +| `persistence.persistentVolumeClaim.trivy.size` | The size of the volume | `1Gi` | +| `persistence.persistentVolumeClaim.trivy.annotations` | The annotations of the volume | | +| `persistence.imageChartStorage.disableredirect` | The configuration for managing redirects from content backends. For backends which not supported it (such as using minio for `s3` storage type), please set it to `true` to disable redirects. Refer to the [guide](https://github.com/docker/distribution/blob/master/docs/configuration.md#redirect) for more details | `false` | +| `persistence.imageChartStorage.caBundleSecretName` | Specify the `caBundleSecretName` if the storage service uses a self-signed certificate. The secret must contain keys named `ca.crt` which will be injected into the trust store of registry's and containers. | | +| `persistence.imageChartStorage.type` | The type of storage for images and charts: `filesystem`, `azure`, `gcs`, `s3`, `swift` or `oss`. The type must be `filesystem` if you want to use persistent volumes for registry. Refer to the [guide](https://github.com/docker/distribution/blob/master/docs/configuration.md#storage) for more details | `filesystem` | +| `persistence.imageChartStorage.gcs.existingSecret` | An existing secret containing the gcs service account json key. The key must be gcs-key.json. | `""` | +| `persistence.imageChartStorage.gcs.useWorkloadIdentity` | A boolean to allow the use of workloadidentity in a GKE cluster. To use it, create a kubernetes service account and set the name in the key `serviceAccountName` of each component, then allow automounting the service account. | `false` | +| **General** | | | +| `externalURL` | The external URL for Harbor core service | `https://core.harbor.domain` | +| `caBundleSecretName` | The custom CA bundle secret name, the secret must contain key named "ca.crt" which will be injected into the trust store for core, jobservice, registry, trivy components. | | +| `uaaSecretName` | If using external UAA auth which has a self signed cert, you can provide a pre-created secret containing it under the key `ca.crt`. | | +| `imagePullPolicy` | The image pull policy | | +| `imagePullSecrets` | The imagePullSecrets names for all deployments | | +| `updateStrategy.type` | The update strategy for deployments with persistent volumes(jobservice, registry): `RollingUpdate` or `Recreate`. Set it as `Recreate` when `RWM` for volumes isn't supported | `RollingUpdate` | +| `logLevel` | The log level: `debug`, `info`, `warning`, `error` or `fatal` | `info` | +| `harborAdminPassword` | The initial password of Harbor admin. Change it from portal after launching Harbor | `Harbor12345` | +| `existingSecretAdminPassword` | The name of secret where admin password can be found. | | +| `existingSecretAdminPasswordKey` | The name of the key in the secret where to find harbor admin password Harbor | `HARBOR_ADMIN_PASSWORD` | +| `caSecretName` | The name of the secret which contains key named `ca.crt`. Setting this enables the download link on portal to download the CA certificate when the certificate isn't generated automatically | | +| `secretKey` | The key used for encryption. Must be a string of 16 chars | `not-a-secure-key` | +| `existingSecretSecretKey` | An existing secret containing the encoding secretKey | `""` | +| `proxy.httpProxy` | The URL of the HTTP proxy server | | +| `proxy.httpsProxy` | The URL of the HTTPS proxy server | | +| `proxy.noProxy` | The URLs that the proxy settings not apply to | 127.0.0.1,localhost,.local,.internal | +| `proxy.components` | The component list that the proxy settings apply to | core, jobservice, trivy | +| `enableMigrateHelmHook` | Run the migration job via helm hook, if it is true, the database migration will be separated from harbor-core, run with a preupgrade job migration-job | `false` | +| **Nginx** (if service exposed via `ingress`, Nginx will not be used) | | | +| `nginx.image.repository` | Image repository | `goharbor/nginx-photon` | +| `nginx.image.tag` | Image tag | `dev` | +| `nginx.replicas` | The replica count | `1` | +| `nginx.revisionHistoryLimit` | The revision history limit | `10` | +| `nginx.resources` | The [resources] to allocate for container | undefined | +| `nginx.automountServiceAccountToken` | Mount serviceAccountToken? | `false` | +| `nginx.nodeSelector` | Node labels for pod assignment | `{}` | +| `nginx.tolerations` | Tolerations for pod assignment | `[]` | +| `nginx.affinity` | Node/Pod affinities | `{}` | +| `nginx.topologySpreadConstraints` | Constraints that define how Pods are spread across failure-domains like regions or availability zones | `[]` | +| `nginx.podAnnotations` | Annotations to add to the nginx pod | `{}` | +| `nginx.priorityClassName` | The priority class to run the pod as | | +| **Portal** | | | +| `portal.image.repository` | Repository for portal image | `goharbor/harbor-portal` | +| `portal.image.tag` | Tag for portal image | `dev` | +| `portal.replicas` | The replica count | `1` | +| `portal.revisionHistoryLimit` | The revision history limit | `10` | +| `portal.resources` | The [resources] to allocate for container | undefined | +| `portal.automountServiceAccountToken` | Mount serviceAccountToken? | `false` | +| `portal.nodeSelector` | Node labels for pod assignment | `{}` | +| `portal.tolerations` | Tolerations for pod assignment | `[]` | +| `portal.affinity` | Node/Pod affinities | `{}` | +| `portal.topologySpreadConstraints` | Constraints that define how Pods are spread across failure-domains like regions or availability zones | `[]` | +| `portal.podAnnotations` | Annotations to add to the portal pod | `{}` | +| `portal.serviceAnnotations` | Annotations to add to the portal service | `{}` | +| `portal.priorityClassName` | The priority class to run the pod as | | +| `portal.initContainers` | Init containers to be run before the controller's container starts. | `[]` | +| **Core** | | | +| `core.image.repository` | Repository for Harbor core image | `goharbor/harbor-core` | +| `core.image.tag` | Tag for Harbor core image | `dev` | +| `core.replicas` | The replica count | `1` | +| `core.revisionHistoryLimit` | The revision history limit | `10` | +| `core.startupProbe.initialDelaySeconds` | The initial delay in seconds for the startup probe | `10` | +| `core.resources` | The [resources] to allocate for container | undefined | +| `core.automountServiceAccountToken` | Mount serviceAccountToken? | `false` | +| `core.nodeSelector` | Node labels for pod assignment | `{}` | +| `core.tolerations` | Tolerations for pod assignment | `[]` | +| `core.affinity` | Node/Pod affinities | `{}` | +| `core.topologySpreadConstraints` | Constraints that define how Pods are spread across failure-domains like regions or availability zones | `[]` | +| `core.podAnnotations` | Annotations to add to the core pod | `{}` | +| `core.serviceAnnotations` | Annotations to add to the core service | `{}` | +| `core.configureUserSettings` | A JSON string to set in the environment variable `CONFIG_OVERWRITE_JSON` to configure user settings. See the [official docs](https://goharbor.io/docs/latest/install-config/configure-user-settings-cli/#configure-users-settings-using-an-environment-variable). | | +| `core.quotaUpdateProvider` | The provider for updating project quota(usage), there are 2 options, `redis` or `db`. You can set it to be implemented by `redis` which can improve the performance of high concurrent pushing to the same project, and reduce database connection spikes and occupies. Using redis will bring up some delay for quota usage update for display, so only suggest switch provider to redis if you ran into the db connections spike around the scenario of high concurrent pushing to same project, no improvement for other scenes. | `db` | +| `core.secret` | Secret is used when core server communicates with other components. If a secret key is not specified, Helm will generate one. Must be a string of 16 chars. | | +| `core.secretName` | Fill the name of a kubernetes secret if you want to use your own TLS certificate and private key for token encryption/decryption. The secret must contain keys named: `tls.crt` - the certificate and `tls.key` - the private key. The default key pair will be used if it isn't set | | +| `core.tokenKey` | PEM-formatted RSA private key used to sign service tokens. Only used if `core.secretName` is unset. If set, `core.tokenCert` MUST also be set. | | +| `core.tokenCert` | PEM-formatted certificate signed by `core.tokenKey` used to validate service tokens. Only used if `core.secretName` is unset. If set, `core.tokenKey` MUST also be set. | | +| `core.xsrfKey` | The XSRF key. Will be generated automatically if it isn't specified | | +| `core.priorityClassName` | The priority class to run the pod as | | +| `core.artifactPullAsyncFlushDuration` | The time duration for async update artifact pull_time and repository pull_count | | +| `core.gdpr.deleteUser` | Enable GDPR compliant user delete | `false` | +| `core.gdpr.auditLogsCompliant` | Enable GDPR compliant for audit logs by changing username to its CRC32 value if that user was deleted from the system | `false` | +| `core.initContainers` | Init containers to be run before the controller's container starts. | `[]` | +| **Jobservice** | | | +| `jobservice.image.repository` | Repository for jobservice image | `goharbor/harbor-jobservice` | +| `jobservice.image.tag` | Tag for jobservice image | `dev` | +| `jobservice.replicas` | The replica count | `1` | +| `jobservice.revisionHistoryLimit` | The revision history limit | `10` | +| `jobservice.maxJobWorkers` | The max job workers | `10` | +| `jobservice.jobLoggers` | The loggers for jobs: `file`, `database` or `stdout` | `[file]` | +| `jobservice.loggerSweeperDuration` | The jobLogger sweeper duration in days (ignored if `jobLoggers` is set to `stdout`) | `14` | +| `jobservice.notification.webhook_job_max_retry` | The maximum retry of webhook sending notifications | `3` | +| `jobservice.notification.webhook_job_http_client_timeout` | The http client timeout value of webhook sending notifications | `3` | +| `jobservice.reaper.max_update_hours` | the max time to wait for a task to finish, if unfinished after max_update_hours, the task will be mark as error, but the task will continue to run | `24` | +| `jobservice.reaper.max_dangling_hours` | the max time for execution in running state without new task created | `168` | +| `jobservice.resources` | The [resources] to allocate for container | undefined | +| `jobservice.automountServiceAccountToken` | Mount serviceAccountToken? | `false` | +| `jobservice.nodeSelector` | Node labels for pod assignment | `{}` | +| `jobservice.tolerations` | Tolerations for pod assignment | `[]` | +| `jobservice.affinity` | Node/Pod affinities | `{}` | +| `jobservice.topologySpreadConstraints` | Constraints that define how Pods are spread across failure-domains like regions or availability zones | `[]` | +| `jobservice.podAnnotations` | Annotations to add to the jobservice pod | `{}` | +| `jobservice.priorityClassName` | The priority class to run the pod as | | +| `jobservice.secret` | Secret is used when job service communicates with other components. If a secret key is not specified, Helm will generate one. Must be a string of 16 chars. | | +| `jobservice.initContainers` | Init containers to be run before the controller's container starts. | `[]` | +| **Registry** | | | +| `registry.registry.image.repository` | Repository for registry image | `goharbor/registry-photon` | +| `registry.registry.image.tag` | Tag for registry image | `dev` | +| `registry.registry.resources` | The [resources] to allocate for container | undefined | +| `registry.controller.image.repository` | Repository for registry controller image | `goharbor/harbor-registryctl` | +| `registry.controller.image.tag` | Tag for registry controller image | `dev` | +| `registry.controller.resources` | The [resources] to allocate for container | undefined | +| `registry.replicas` | The replica count | `1` | +| `registry.revisionHistoryLimit` | The revision history limit | `10` | +| `registry.nodeSelector` | Node labels for pod assignment | `{}` | +| `registry.automountServiceAccountToken` | Mount serviceAccountToken? | `false` | +| `registry.tolerations` | Tolerations for pod assignment | `[]` | +| `registry.affinity` | Node/Pod affinities | `{}` | +| `registry.topologySpreadConstraints` | Constraints that define how Pods are spread across failure-domains like regions or availability zones | `[]` | +| `registry.middleware` | Middleware is used to add support for a CDN between backend storage and `docker pull` recipient. See [official docs](https://github.com/docker/distribution/blob/master/docs/configuration.md#middleware). | | +| `registry.podAnnotations` | Annotations to add to the registry pod | `{}` | +| `registry.priorityClassName` | The priority class to run the pod as | | +| `registry.secret` | Secret is used to secure the upload state from client and registry storage backend. See [official docs](https://github.com/docker/distribution/blob/master/docs/configuration.md#http). If a secret key is not specified, Helm will generate one. Must be a string of 16 chars. | | +| `registry.credentials.username` | The username that harbor core uses internally to access the registry instance. Together with the `registry.credentials.password`, a htpasswd is created. This is an alternative to providing `registry.credentials.htpasswdString`. For more details see [official docs](https://github.com/docker/distribution/blob/master/docs/configuration.md#htpasswd). | `harbor_registry_user` | +| `registry.credentials.password` | The password that harbor core uses internally to access the registry instance. Together with the `registry.credentials.username`, a htpasswd is created. This is an alternative to providing `registry.credentials.htpasswdString`. For more details see [official docs](https://github.com/docker/distribution/blob/master/docs/configuration.md#htpasswd). It is suggested you update this value before installation. | `harbor_registry_password` | +| `registry.credentials.existingSecret` | An existing secret containing the password for accessing the registry instance, which is hosted by htpasswd auth mode. More details see [official docs](https://github.com/docker/distribution/blob/master/docs/configuration.md#htpasswd). The key must be `REGISTRY_PASSWD` | `""` | +| `registry.credentials.htpasswdString` | Login and password in htpasswd string format. Excludes `registry.credentials.username` and `registry.credentials.password`. May come in handy when integrating with tools like argocd or flux. This allows the same line to be generated each time the template is rendered, instead of the `htpasswd` function from helm, which generates different lines each time because of the salt. | undefined | +| `registry.relativeurls` | If true, the registry returns relative URLs in Location headers. The client is responsible for resolving the correct URL. Needed if harbor is behind a reverse proxy | `false` | +| `registry.upload_purging.enabled` | If true, enable purge _upload directories | `true` | +| `registry.upload_purging.age` | Remove files in _upload directories which exist for a period of time, default is one week. | `168h` | +| `registry.upload_purging.interval` | The interval of the purge operations | `24h` | +| `registry.upload_purging.dryrun` | If true, enable dryrun for purging _upload | `false` | +| `registry.initContainers` | Init containers to be run before the controller's container starts. | `[]` | +| **[Trivy][trivy]** | | | +| `trivy.enabled` | The flag to enable Trivy scanner | `true` | +| `trivy.image.repository` | Repository for Trivy adapter image | `goharbor/trivy-adapter-photon` | +| `trivy.image.tag` | Tag for Trivy adapter image | `dev` | +| `trivy.resources` | The [resources] to allocate for Trivy adapter container | | +| `trivy.automountServiceAccountToken` | Mount serviceAccountToken? | `false` | +| `trivy.replicas` | The number of Pod replicas | `1` | +| `trivy.debugMode` | The flag to enable Trivy debug mode | `false` | +| `trivy.vulnType` | Comma-separated list of vulnerability types. Possible values `os` and `library`. | `os,library` | +| `trivy.severity` | Comma-separated list of severities to be checked | `UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL` | +| `trivy.ignoreUnfixed` | The flag to display only fixed vulnerabilities | `false` | +| `trivy.insecure` | The flag to skip verifying registry certificate | `false` | +| `trivy.skipUpdate` | The flag to disable [Trivy DB][trivy-db] downloads from GitHub | `false` | +| `trivy.skipJavaDBUpdate` | If the flag is enabled you have to manually download the `trivy-java.db` file [Trivy Java DB][trivy-java-db] and mount it in the `/home/scanner/.cache/trivy/java-db/trivy-java.db` path | `false` | +| `trivy.dbRepository` | OCI repository(ies) to retrieve the trivy vulnerability database in order of priority | `mirror.gcr.io/aquasec/trivy-db,ghcr.io/aquasecurity/trivy-db` | +| `trivy.javaDBRepository` | OCI repository(ies) to retrieve the Java trivy vulnerability database in order of priority | `mirror.gcr.io/aquasec/trivy-java-db,ghcr.io/aquasecurity/trivy-java-db` | +| `trivy.offlineScan` | The flag prevents Trivy from sending API requests to identify dependencies. | `false` | +| `trivy.securityCheck` | Comma-separated list of what security issues to detect. | `vuln` | +| `trivy.timeout` | The duration to wait for scan completion | `5m0s` | +| `trivy.gitHubToken` | The GitHub access token to download [Trivy DB][trivy-db] (see [GitHub rate limiting][trivy-rate-limiting]) | | +| `trivy.priorityClassName` | The priority class to run the pod as | | +| `trivy.topologySpreadConstraints` | The priority class to run the pod as | | +| `trivy.initContainers` | Init containers to be run before the controller's container starts. | `[]` | +| **Database** | | | +| `database.type` | If external database is used, set it to `external` | `internal` | +| `database.internal.image.repository` | Repository for database image | `goharbor/harbor-db` | +| `database.internal.image.tag` | Tag for database image | `dev` | +| `database.internal.password` | The password for database | `changeit` | +| `database.internal.shmSizeLimit` | The limit for the size of shared memory for internal PostgreSQL, conventionally it's around 50% of the memory limit of the container | `512Mi` | +| `database.internal.resources` | The [resources] to allocate for container | undefined | +| `database.internal.automountServiceAccountToken` | Mount serviceAccountToken? | `false` | +| `database.internal.initContainer.migrator.resources` | The [resources] to allocate for the database migrator initContainer | undefined | +| `database.internal.initContainer.permissions.resources` | The [resources] to allocate for the database permissions initContainer | undefined | +| `database.internal.nodeSelector` | Node labels for pod assignment | `{}` | +| `database.internal.tolerations` | Tolerations for pod assignment | `[]` | +| `database.internal.affinity` | Node/Pod affinities | `{}` | +| `database.internal.priorityClassName` | The priority class to run the pod as | | +| `database.internal.livenessProbe.timeoutSeconds` | The timeout used in liveness probe; 1 to 5 seconds | 1 | +| `database.internal.readinessProbe.timeoutSeconds` | The timeout used in readiness probe; 1 to 5 seconds | 1 | +| `database.internal.extrInitContainers` | Extra init containers to be run before the database's container starts. | `[]` | +| `database.external.host` | The hostname of external database | `192.168.0.1` | +| `database.external.port` | The port of external database | `5432` | +| `database.external.username` | The username of external database | `user` | +| `database.external.password` | The password of external database | `password` | +| `database.external.coreDatabase` | The database used by core service | `registry` | +| `database.external.existingSecret` | An existing password containing the database password. the key must be `password`. | `""` | +| `database.external.sslmode` | Connection method of external database (require, verify-full, verify-ca, disable) | `disable` | +| `database.maxIdleConns` | The maximum number of connections in the idle connection pool. If it <=0, no idle connections are retained. | `50` | +| `database.maxOpenConns` | The maximum number of open connections to the database. If it <= 0, then there is no limit on the number of open connections. | `100` | +| `database.podAnnotations` | Annotations to add to the database pod | `{}` | +| **Redis** | | | +| `redis.type` | If external redis is used, set it to `external` | `internal` | +| `redis.internal.image.repository` | Repository for redis image | `goharbor/redis-photon` | +| `redis.internal.image.tag` | Tag for redis image | `dev` | +| `redis.internal.resources` | The [resources] to allocate for container | undefined | +| `redis.internal.automountServiceAccountToken` | Mount serviceAccountToken? | `false` | +| `redis.internal.nodeSelector` | Node labels for pod assignment | `{}` | +| `redis.internal.tolerations` | Tolerations for pod assignment | `[]` | +| `redis.internal.affinity` | Node/Pod affinities | `{}` | +| `redis.internal.priorityClassName` | The priority class to run the pod as | | +| `redis.internal.jobserviceDatabaseIndex` | The database index for jobservice | `1` | +| `redis.internal.registryDatabaseIndex` | The database index for registry | `2` | +| `redis.internal.trivyAdapterIndex` | The database index for trivy adapter | `5` | +| `redis.internal.harborDatabaseIndex` | The database index for harbor miscellaneous business logic | `0` | +| `redis.internal.cacheLayerDatabaseIndex` | The database index for harbor cache layer | `0` | +| `redis.internal.initContainers` | Init containers to be run before the redis's container starts. | `[]` | +| `redis.external.addr` | The addr of external Redis: :. When using sentinel, it should be :,:,: | `192.168.0.2:6379` | +| `redis.external.sentinelMasterSet` | The name of the set of Redis instances to monitor | | +| `redis.external.coreDatabaseIndex` | The database index for core | `0` | +| `redis.external.jobserviceDatabaseIndex` | The database index for jobservice | `1` | +| `redis.external.registryDatabaseIndex` | The database index for registry | `2` | +| `redis.external.trivyAdapterIndex` | The database index for trivy adapter | `5` | +| `redis.external.harborDatabaseIndex` | The database index for harbor miscellaneous business logic | `0` | +| `redis.external.cacheLayerDatabaseIndex` | The database index for harbor cache layer | `0` | +| `redis.external.username` | The username of external Redis | | +| `redis.external.password` | The password of external Redis | | +| `redis.external.existingSecret` | Use an existing secret to connect to redis. The key must be `REDIS_PASSWORD`. | `""` | +| `redis.podAnnotations` | Annotations to add to the redis pod | `{}` | +| **Exporter** | | | +| `exporter.replicas` | The replica count | `1` | +| `exporter.revisionHistoryLimit` | The revision history limit | `10` | +| `exporter.podAnnotations` | Annotations to add to the exporter pod | `{}` | +| `exporter.image.repository` | Repository for redis image | `goharbor/harbor-exporter` | +| `exporter.image.tag` | Tag for exporter image | `dev` | +| `exporter.nodeSelector` | Node labels for pod assignment | `{}` | +| `exporter.tolerations` | Tolerations for pod assignment | `[]` | +| `exporter.affinity` | Node/Pod affinities | `{}` | +| `exporter.topologySpreadConstraints` | Constraints that define how Pods are spread across failure-domains like regions or availability zones | `[]` | +| `exporter.automountServiceAccountToken` | Mount serviceAccountToken? | `false` | +| `exporter.cacheDuration` | the cache duration for information that exporter collected from Harbor | `30` | +| `exporter.cacheCleanInterval` | cache clean interval for information that exporter collected from Harbor | `14400` | +| `exporter.priorityClassName` | The priority class to run the pod as | | +| **Metrics** | | | +| `metrics.enabled` | if enable harbor metrics | `false` | +| `metrics.core.path` | the url path for core metrics | `/metrics` | +| `metrics.core.port` | the port for core metrics | `8001` | +| `metrics.registry.path` | the url path for registry metrics | `/metrics` | +| `metrics.registry.port` | the port for registry metrics | `8001` | +| `metrics.exporter.path` | the url path for exporter metrics | `/metrics` | +| `metrics.exporter.port` | the port for exporter metrics | `8001` | +| `metrics.serviceMonitor.enabled` | create prometheus serviceMonitor. Requires prometheus CRD's | `false` | +| `metrics.serviceMonitor.additionalLabels` | additional labels to upsert to the manifest | `""` | +| `metrics.serviceMonitor.interval` | scrape period for harbor metrics | `""` | +| `metrics.serviceMonitor.metricRelabelings` | metrics relabel to add/mod/del before ingestion | `[]` | +| `metrics.serviceMonitor.relabelings` | relabels to add/mod/del to sample before scrape | `[]` | +| **Trace** | | | +| `trace.enabled` | Enable tracing or not | `false` | +| `trace.provider` | The tracing provider: `jaeger` or `otel`. `jaeger` should be 1.26+ | `jaeger` | +| `trace.sample_rate` | Set `sample_rate` to 1 if you want sampling 100% of trace data; set 0.5 if you want sampling 50% of trace data, and so forth | `1` | +| `trace.namespace` | Namespace used to differentiate different harbor services | | +| `trace.attributes` | `attributes` is a key value dict contains user defined attributes used to initialize trace provider | | +| `trace.jaeger.endpoint` | The endpoint of jaeger | `http://hostname:14268/api/traces` | +| `trace.jaeger.username` | The username of jaeger | | +| `trace.jaeger.password` | The password of jaeger | | +| `trace.jaeger.agent_host` | The agent host of jaeger | | +| `trace.jaeger.agent_port` | The agent port of jaeger | `6831` | +| `trace.otel.endpoint` | The endpoint of otel | `hostname:4318` | +| `trace.otel.url_path` | The URL path of otel | `/v1/traces` | +| `trace.otel.compression` | Whether enable compression or not for otel | `false` | +| `trace.otel.insecure` | Whether establish insecure connection or not for otel | `true` | +| `trace.otel.timeout` | The timeout in seconds of otel | `10` | +| **Cache** | | | +| `cache.enabled` | Enable cache layer or not | `false` | +| `cache.expireHours` | The expire hours of cache layer | `24` | + +[resources]: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/ +[trivy]: https://github.com/aquasecurity/trivy +[trivy-db]: https://github.com/aquasecurity/trivy-db +[trivy-java-db]: https://github.com/aquasecurity/trivy-java-db +[trivy-rate-limiting]: https://github.com/aquasecurity/trivy#github-rate-limiting diff --git a/packages/system/harbor/charts/harbor/templates/NOTES.txt b/packages/system/harbor/charts/harbor/templates/NOTES.txt new file mode 100644 index 00000000..0980c08a --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/NOTES.txt @@ -0,0 +1,3 @@ +Please wait for several minutes for Harbor deployment to complete. +Then you should be able to visit the Harbor portal at {{ .Values.externalURL }} +For more details, please visit https://github.com/goharbor/harbor diff --git a/packages/system/harbor/charts/harbor/templates/_helpers.tpl b/packages/system/harbor/charts/harbor/templates/_helpers.tpl new file mode 100644 index 00000000..95643c49 --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/_helpers.tpl @@ -0,0 +1,606 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +*/}} +{{- define "harbor.name" -}} +{{- default "harbor" .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "harbor.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default "harbor" .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* Helm required labels: legacy */}} +{{- define "harbor.legacy.labels" -}} +heritage: {{ .Release.Service }} +release: {{ .Release.Name }} +chart: {{ .Chart.Name }} +app: "{{ template "harbor.name" . }}" +{{- end -}} + +{{/* Helm required labels */}} +{{- define "harbor.labels" -}} +heritage: {{ .Release.Service }} +release: {{ .Release.Name }} +chart: {{ .Chart.Name }} +app: "{{ template "harbor.name" . }}" +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/name: {{ include "harbor.name" . }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +app.kubernetes.io/part-of: {{ include "harbor.name" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +{{- end -}} + +{{/* matchLabels */}} +{{- define "harbor.matchLabels" -}} +release: {{ .Release.Name }} +app: "{{ template "harbor.name" . }}" +{{- end -}} + +{{/* Helper for printing values from existing secrets*/}} +{{- define "harbor.secretKeyHelper" -}} + {{- if and (not (empty .data)) (hasKey .data .key) }} + {{- index .data .key | b64dec -}} + {{- end -}} +{{- end -}} + +{{- define "harbor.autoGenCert" -}} + {{- if and .Values.expose.tls.enabled (eq .Values.expose.tls.certSource "auto") -}} + {{- printf "true" -}} + {{- else -}} + {{- printf "false" -}} + {{- end -}} +{{- end -}} + +{{- define "harbor.autoGenCertForIngress" -}} + {{- if and (eq (include "harbor.autoGenCert" .) "true") (eq .Values.expose.type "ingress") -}} + {{- printf "true" -}} + {{- else -}} + {{- printf "false" -}} + {{- end -}} +{{- end -}} + +{{- define "harbor.autoGenCertForNginx" -}} + {{- if and (eq (include "harbor.autoGenCert" .) "true") (ne .Values.expose.type "ingress") -}} + {{- printf "true" -}} + {{- else -}} + {{- printf "false" -}} + {{- end -}} +{{- end -}} + +{{- define "harbor.database.host" -}} + {{- if eq .Values.database.type "internal" -}} + {{- template "harbor.database" . }} + {{- else -}} + {{- .Values.database.external.host -}} + {{- end -}} +{{- end -}} + +{{- define "harbor.database.port" -}} + {{- if eq .Values.database.type "internal" -}} + {{- printf "%s" "5432" -}} + {{- else -}} + {{- .Values.database.external.port -}} + {{- end -}} +{{- end -}} + +{{- define "harbor.database.username" -}} + {{- if eq .Values.database.type "internal" -}} + {{- printf "%s" "postgres" -}} + {{- else -}} + {{- .Values.database.external.username -}} + {{- end -}} +{{- end -}} + +{{- define "harbor.database.rawPassword" -}} + {{- if eq .Values.database.type "internal" -}} + {{- $existingSecret := lookup "v1" "Secret" .Release.Namespace (include "harbor.database" .) -}} + {{- if and (not (empty $existingSecret)) (hasKey $existingSecret.data "POSTGRES_PASSWORD") -}} + {{- .Values.database.internal.password | default (index $existingSecret.data "POSTGRES_PASSWORD" | b64dec) -}} + {{- else -}} + {{- .Values.database.internal.password -}} + {{- end -}} + {{- else -}} + {{- .Values.database.external.password -}} + {{- end -}} +{{- end -}} + +{{- define "harbor.database.escapedRawPassword" -}} + {{- include "harbor.database.rawPassword" . | urlquery | replace "+" "%20" -}} +{{- end -}} + +{{- define "harbor.database.encryptedPassword" -}} + {{- include "harbor.database.rawPassword" . | b64enc | quote -}} +{{- end -}} + +{{- define "harbor.database.coreDatabase" -}} + {{- if eq .Values.database.type "internal" -}} + {{- printf "%s" "registry" -}} + {{- else -}} + {{- .Values.database.external.coreDatabase -}} + {{- end -}} +{{- end -}} + +{{- define "harbor.database.sslmode" -}} + {{- if eq .Values.database.type "internal" -}} + {{- printf "%s" "disable" -}} + {{- else -}} + {{- .Values.database.external.sslmode -}} + {{- end -}} +{{- end -}} + +{{- define "harbor.redis.scheme" -}} + {{- with .Values.redis }} + {{- if eq .type "external" -}} + {{- if not (not .external.sentinelMasterSet) -}} + {{- ternary "rediss+sentinel" "redis+sentinel" (.external.tlsOptions.enable) }} + {{- else -}} + {{- ternary "rediss" "redis" (.external.tlsOptions.enable) }} + {{- end -}} + {{- else -}} + {{ print "redis" }} + {{- end -}} + {{- end }} +{{- end -}} + +{{- define "harbor.redis.enableTLS" -}} + {{- with .Values.redis }} + {{- ternary "true" "false" (and ( eq .type "external") (.external.tlsOptions.enable)) }} + {{- end }} +{{- end -}} + +/*host:port*/ +{{- define "harbor.redis.addr" -}} + {{- with .Values.redis }} + {{- ternary (printf "%s:6379" (include "harbor.redis" $ )) .external.addr (eq .type "internal") }} + {{- end }} +{{- end -}} + +{{- define "harbor.redis.masterSet" -}} + {{- with .Values.redis }} + {{- ternary .external.sentinelMasterSet "" (contains "+sentinel" (include "harbor.redis.scheme" $)) }} + {{- end }} +{{- end -}} + +{{- define "harbor.redis.password" -}} + {{- with .Values.redis }} + {{- ternary "" .external.password (eq .type "internal") }} + {{- end }} +{{- end -}} + + +{{- define "harbor.redis.usernamefromsecret" -}} + {{- $existingSecret := (lookup "v1" "Secret" .Release.Namespace (.Values.redis.external.existingSecret)) -}} + {{- if and (not (empty $existingSecret)) (hasKey $existingSecret.data "REDIS_USERNAME") -}} + {{- printf "%s" ($existingSecret.data.REDIS_USERNAME | b64dec | trim ) }} + {{- end -}} +{{- end -}} + +{{- define "harbor.redis.pwdfromsecret" -}} + {{- (lookup "v1" "Secret" .Release.Namespace (.Values.redis.external.existingSecret)).data.REDIS_PASSWORD | b64dec }} +{{- end -}} + +{{- define "harbor.redis.cred" -}} + {{- with .Values.redis }} + {{- if (and (eq .type "external" ) (.external.existingSecret)) }} + {{- printf "%s:%s@" (include "harbor.redis.usernamefromsecret" $) (include "harbor.redis.pwdfromsecret" $) -}} + {{- else }} + {{- ternary (printf "%s:%s@" (.external.username | urlquery) (.external.password | urlquery)) "" (and (eq .type "external" ) (not (not .external.password))) }} + {{- end }} + {{- end }} +{{- end -}} + +/*scheme://[:password@]host:port[/master_set]*/ +{{- define "harbor.redis.url" -}} + {{- with .Values.redis }} + {{- $path := ternary "" (printf "/%s" (include "harbor.redis.masterSet" $)) (not (include "harbor.redis.masterSet" $)) }} + {{- printf "%s://%s%s%s" (include "harbor.redis.scheme" $) (include "harbor.redis.cred" $) (include "harbor.redis.addr" $) $path -}} + {{- end }} +{{- end -}} + +/*scheme://[:password@]addr/db_index?idle_timeout_seconds=30*/ +{{- define "harbor.redis.urlForCore" -}} + {{- with .Values.redis }} + {{- $index := ternary "0" .external.coreDatabaseIndex (eq .type "internal") }} + {{- printf "%s/%s?idle_timeout_seconds=30" (include "harbor.redis.url" $) $index -}} + {{- end }} +{{- end -}} + +/*scheme://[:password@]addr/db_index*/ +{{- define "harbor.redis.urlForJobservice" -}} + {{- with .Values.redis }} + {{- $index := ternary .internal.jobserviceDatabaseIndex .external.jobserviceDatabaseIndex (eq .type "internal") }} + {{- printf "%s/%s" (include "harbor.redis.url" $) $index -}} + {{- end }} +{{- end -}} + +/*scheme://[:password@]addr/db_index?idle_timeout_seconds=30*/ +{{- define "harbor.redis.urlForRegistry" -}} + {{- with .Values.redis }} + {{- $index := ternary .internal.registryDatabaseIndex .external.registryDatabaseIndex (eq .type "internal") }} + {{- printf "%s/%s?idle_timeout_seconds=30" (include "harbor.redis.url" $) $index -}} + {{- end }} +{{- end -}} + +/*scheme://[:password@]addr/db_index?idle_timeout_seconds=30*/ +{{- define "harbor.redis.urlForTrivy" -}} + {{- with .Values.redis }} + {{- $index := ternary .internal.trivyAdapterIndex .external.trivyAdapterIndex (eq .type "internal") }} + {{- printf "%s/%s?idle_timeout_seconds=30" (include "harbor.redis.url" $) $index -}} + {{- end }} +{{- end -}} + +/*scheme://[:password@]addr/db_index?idle_timeout_seconds=30*/ +{{- define "harbor.redis.urlForHarbor" -}} + {{- with .Values.redis }} + {{- $index := ternary .internal.harborDatabaseIndex .external.harborDatabaseIndex (eq .type "internal") }} + {{- printf "%s/%s?idle_timeout_seconds=30" (include "harbor.redis.url" $) $index -}} + {{- end }} +{{- end -}} + +/*scheme://[:password@]addr/db_index?idle_timeout_seconds=30*/ +{{- define "harbor.redis.urlForCache" -}} + {{- with .Values.redis }} + {{- $index := ternary .internal.cacheLayerDatabaseIndex .external.cacheLayerDatabaseIndex (eq .type "internal") }} + {{- printf "%s/%s?idle_timeout_seconds=30" (include "harbor.redis.url" $) $index -}} + {{- end }} +{{- end -}} + +{{- define "harbor.redis.dbForRegistry" -}} + {{- with .Values.redis }} + {{- ternary .internal.registryDatabaseIndex .external.registryDatabaseIndex (eq .type "internal") }} + {{- end }} +{{- end -}} + +{{- define "harbor.portal" -}} + {{- printf "%s-portal" (include "harbor.fullname" .) -}} +{{- end -}} + +{{- define "harbor.core" -}} + {{- printf "%s-core" (include "harbor.fullname" .) -}} +{{- end -}} + +{{- define "harbor.redis" -}} + {{- printf "%s-redis" (include "harbor.fullname" .) -}} +{{- end -}} + +{{- define "harbor.jobservice" -}} + {{- printf "%s-jobservice" (include "harbor.fullname" .) -}} +{{- end -}} + +{{- define "harbor.registry" -}} + {{- printf "%s-registry" (include "harbor.fullname" .) -}} +{{- end -}} + +{{- define "harbor.registryCtl" -}} + {{- printf "%s-registryctl" (include "harbor.fullname" .) -}} +{{- end -}} + +{{- define "harbor.database" -}} + {{- printf "%s-database" (include "harbor.fullname" .) -}} +{{- end -}} + +{{- define "harbor.trivy" -}} + {{- printf "%s-trivy" (include "harbor.fullname" .) -}} +{{- end -}} + +{{- define "harbor.nginx" -}} + {{- printf "%s-nginx" (include "harbor.fullname" .) -}} +{{- end -}} + +{{- define "harbor.exporter" -}} + {{- printf "%s-exporter" (include "harbor.fullname" .) -}} +{{- end -}} + +{{- define "harbor.ingress" -}} + {{- printf "%s-ingress" (include "harbor.fullname" .) -}} +{{- end -}} + +{{- define "harbor.route" -}} + {{- printf "%s-route" (include "harbor.fullname" .) -}} +{{- end -}} + +{{- define "harbor.noProxy" -}} + {{- printf "%s,%s,%s,%s,%s,%s,%s,%s" (include "harbor.core" .) (include "harbor.jobservice" .) (include "harbor.database" .) (include "harbor.registry" .) (include "harbor.portal" .) (include "harbor.trivy" .) (include "harbor.exporter" .) .Values.proxy.noProxy -}} +{{- end -}} + +{{- define "harbor.caBundleVolume" -}} +- name: ca-bundle-certs + secret: + secretName: {{ .Values.caBundleSecretName }} +{{- end -}} + +{{- define "harbor.caBundleVolumeMount" -}} +- name: ca-bundle-certs + mountPath: /harbor_cust_cert/custom-ca.crt + subPath: ca.crt +{{- end -}} + +{{/* scheme for all components because it only support http mode */}} +{{- define "harbor.component.scheme" -}} + {{- if .Values.internalTLS.enabled -}} + {{- printf "https" -}} + {{- else -}} + {{- printf "http" -}} + {{- end -}} +{{- end -}} + +{{/* core component container port */}} +{{- define "harbor.core.containerPort" -}} + {{- if .Values.internalTLS.enabled -}} + {{- printf "8443" -}} + {{- else -}} + {{- printf "8080" -}} + {{- end -}} +{{- end -}} + +{{/* core component service port */}} +{{- define "harbor.core.servicePort" -}} + {{- if .Values.internalTLS.enabled -}} + {{- printf "443" -}} + {{- else -}} + {{- printf "80" -}} + {{- end -}} +{{- end -}} + +{{/* jobservice component container port */}} +{{- define "harbor.jobservice.containerPort" -}} + {{- if .Values.internalTLS.enabled -}} + {{- printf "8443" -}} + {{- else -}} + {{- printf "8080" -}} + {{- end -}} +{{- end -}} + +{{/* jobservice component service port */}} +{{- define "harbor.jobservice.servicePort" -}} + {{- if .Values.internalTLS.enabled -}} + {{- printf "443" -}} + {{- else -}} + {{- printf "80" -}} + {{- end -}} +{{- end -}} + +{{/* portal component container port */}} +{{- define "harbor.portal.containerPort" -}} + {{- if .Values.internalTLS.enabled -}} + {{- printf "8443" -}} + {{- else -}} + {{- printf "8080" -}} + {{- end -}} +{{- end -}} + +{{/* portal component service port */}} +{{- define "harbor.portal.servicePort" -}} + {{- if .Values.internalTLS.enabled -}} + {{- printf "443" -}} + {{- else -}} + {{- printf "80" -}} + {{- end -}} +{{- end -}} + +{{/* registry component container port */}} +{{- define "harbor.registry.containerPort" -}} + {{- if .Values.internalTLS.enabled -}} + {{- printf "5443" -}} + {{- else -}} + {{- printf "5000" -}} + {{- end -}} +{{- end -}} + +{{/* registry component service port */}} +{{- define "harbor.registry.servicePort" -}} + {{- if .Values.internalTLS.enabled -}} + {{- printf "5443" -}} + {{- else -}} + {{- printf "5000" -}} + {{- end -}} +{{- end -}} + +{{/* registryctl component container port */}} +{{- define "harbor.registryctl.containerPort" -}} + {{- if .Values.internalTLS.enabled -}} + {{- printf "8443" -}} + {{- else -}} + {{- printf "8080" -}} + {{- end -}} +{{- end -}} + +{{/* registryctl component service port */}} +{{- define "harbor.registryctl.servicePort" -}} + {{- if .Values.internalTLS.enabled -}} + {{- printf "8443" -}} + {{- else -}} + {{- printf "8080" -}} + {{- end -}} +{{- end -}} + +{{/* trivy component container port */}} +{{- define "harbor.trivy.containerPort" -}} + {{- if .Values.internalTLS.enabled -}} + {{- printf "8443" -}} + {{- else -}} + {{- printf "8080" -}} + {{- end -}} +{{- end -}} + +{{/* trivy component service port */}} +{{- define "harbor.trivy.servicePort" -}} + {{- if .Values.internalTLS.enabled -}} + {{- printf "8443" -}} + {{- else -}} + {{- printf "8080" -}} + {{- end -}} +{{- end -}} + +{{/* CORE_URL */}} +{{/* port is included in this url as a workaround for issue https://github.com/aquasecurity/harbor-scanner-trivy/issues/108 */}} +{{- define "harbor.coreURL" -}} + {{- printf "%s://%s:%s" (include "harbor.component.scheme" .) (include "harbor.core" .) (include "harbor.core.servicePort" .) -}} +{{- end -}} + +{{/* JOBSERVICE_URL */}} +{{- define "harbor.jobserviceURL" -}} + {{- printf "%s://%s-jobservice" (include "harbor.component.scheme" .) (include "harbor.fullname" .) -}} +{{- end -}} + +{{/* PORTAL_URL */}} +{{- define "harbor.portalURL" -}} + {{- printf "%s://%s" (include "harbor.component.scheme" .) (include "harbor.portal" .) -}} +{{- end -}} + +{{/* REGISTRY_URL */}} +{{- define "harbor.registryURL" -}} + {{- printf "%s://%s:%s" (include "harbor.component.scheme" .) (include "harbor.registry" .) (include "harbor.registry.servicePort" .) -}} +{{- end -}} + +{{/* REGISTRY_CONTROLLER_URL */}} +{{- define "harbor.registryControllerURL" -}} + {{- printf "%s://%s:%s" (include "harbor.component.scheme" .) (include "harbor.registry" .) (include "harbor.registryctl.servicePort" .) -}} +{{- end -}} + +{{/* TOKEN_SERVICE_URL */}} +{{- define "harbor.tokenServiceURL" -}} + {{- printf "%s/service/token" (include "harbor.coreURL" .) -}} +{{- end -}} + +{{/* TRIVY_ADAPTER_URL */}} +{{- define "harbor.trivyAdapterURL" -}} + {{- printf "%s://%s:%s" (include "harbor.component.scheme" .) (include "harbor.trivy" .) (include "harbor.trivy.servicePort" .) -}} +{{- end -}} + +{{- define "harbor.internalTLS.core.secretName" -}} + {{- if eq .Values.internalTLS.certSource "secret" -}} + {{- .Values.internalTLS.core.secretName -}} + {{- else -}} + {{- printf "%s-core-internal-tls" (include "harbor.fullname" .) -}} + {{- end -}} +{{- end -}} + +{{- define "harbor.internalTLS.jobservice.secretName" -}} + {{- if eq .Values.internalTLS.certSource "secret" -}} + {{- .Values.internalTLS.jobservice.secretName -}} + {{- else -}} + {{- printf "%s-jobservice-internal-tls" (include "harbor.fullname" .) -}} + {{- end -}} +{{- end -}} + +{{- define "harbor.internalTLS.portal.secretName" -}} + {{- if eq .Values.internalTLS.certSource "secret" -}} + {{- .Values.internalTLS.portal.secretName -}} + {{- else -}} + {{- printf "%s-portal-internal-tls" (include "harbor.fullname" .) -}} + {{- end -}} +{{- end -}} + +{{- define "harbor.internalTLS.registry.secretName" -}} + {{- if eq .Values.internalTLS.certSource "secret" -}} + {{- .Values.internalTLS.registry.secretName -}} + {{- else -}} + {{- printf "%s-registry-internal-tls" (include "harbor.fullname" .) -}} + {{- end -}} +{{- end -}} + +{{- define "harbor.internalTLS.trivy.secretName" -}} + {{- if eq .Values.internalTLS.certSource "secret" -}} + {{- .Values.internalTLS.trivy.secretName -}} + {{- else -}} + {{- printf "%s-trivy-internal-tls" (include "harbor.fullname" .) -}} + {{- end -}} +{{- end -}} + +{{- define "harbor.tlsCoreSecretForIngress" -}} + {{- if eq .Values.expose.tls.certSource "none" -}} + {{- printf "" -}} + {{- else if eq .Values.expose.tls.certSource "secret" -}} + {{- .Values.expose.tls.secret.secretName -}} + {{- else -}} + {{- include "harbor.ingress" . -}} + {{- end -}} +{{- end -}} + +{{- define "harbor.tlsSecretForNginx" -}} + {{- if eq .Values.expose.tls.certSource "secret" -}} + {{- .Values.expose.tls.secret.secretName -}} + {{- else -}} + {{- include "harbor.nginx" . -}} + {{- end -}} +{{- end -}} + +{{- define "harbor.metricsPortName" -}} + {{- if .Values.internalTLS.enabled }} + {{- printf "https-metrics" -}} + {{- else -}} + {{- printf "http-metrics" -}} + {{- end -}} +{{- end -}} + +{{- define "harbor.traceEnvs" -}} + TRACE_ENABLED: "{{ .Values.trace.enabled }}" + TRACE_SAMPLE_RATE: "{{ .Values.trace.sample_rate }}" + TRACE_NAMESPACE: "{{ .Values.trace.namespace }}" + {{- if .Values.trace.attributes }} + TRACE_ATTRIBUTES: {{ .Values.trace.attributes | toJson | squote }} + {{- end }} + {{- if eq .Values.trace.provider "jaeger" }} + TRACE_JAEGER_ENDPOINT: "{{ .Values.trace.jaeger.endpoint }}" + TRACE_JAEGER_USERNAME: "{{ .Values.trace.jaeger.username }}" + TRACE_JAEGER_AGENT_HOSTNAME: "{{ .Values.trace.jaeger.agent_host }}" + TRACE_JAEGER_AGENT_PORT: "{{ .Values.trace.jaeger.agent_port }}" + {{- else }} + TRACE_OTEL_ENDPOINT: "{{ .Values.trace.otel.endpoint }}" + TRACE_OTEL_URL_PATH: "{{ .Values.trace.otel.url_path }}" + TRACE_OTEL_COMPRESSION: "{{ .Values.trace.otel.compression }}" + TRACE_OTEL_INSECURE: "{{ .Values.trace.otel.insecure }}" + TRACE_OTEL_TIMEOUT: "{{ .Values.trace.otel.timeout }}" + {{- end }} +{{- end -}} + +{{- define "harbor.traceEnvsForCore" -}} + {{- if .Values.trace.enabled }} + TRACE_SERVICE_NAME: "harbor-core" + {{ include "harbor.traceEnvs" . }} + {{- end }} +{{- end -}} + +{{- define "harbor.traceEnvsForJobservice" -}} + {{- if .Values.trace.enabled }} + TRACE_SERVICE_NAME: "harbor-jobservice" + {{ include "harbor.traceEnvs" . }} + {{- end }} +{{- end -}} + +{{- define "harbor.traceEnvsForRegistryCtl" -}} + {{- if .Values.trace.enabled }} + TRACE_SERVICE_NAME: "harbor-registryctl" + {{ include "harbor.traceEnvs" . }} + {{- end }} +{{- end -}} + +{{- define "harbor.traceJaegerPassword" -}} + {{- if and .Values.trace.enabled (eq .Values.trace.provider "jaeger") }} + TRACE_JAEGER_PASSWORD: "{{ .Values.trace.jaeger.password | default "" | b64enc }}" + {{- end }} +{{- end -}} + +{{/* Allow KubeVersion to be overridden. */}} +{{- define "harbor.ingress.kubeVersion" -}} + {{- default .Capabilities.KubeVersion.Version .Values.expose.ingress.kubeVersionOverride -}} +{{- end -}} diff --git a/packages/system/harbor/charts/harbor/templates/core/core-cm.yaml b/packages/system/harbor/charts/harbor/templates/core/core-cm.yaml new file mode 100644 index 00000000..17a82078 --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/core/core-cm.yaml @@ -0,0 +1,92 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ template "harbor.core" . }} + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} +data: + app.conf: |+ + appname = Harbor + runmode = prod + enablegzip = true + + [prod] + httpport = {{ ternary "8443" "8080" .Values.internalTLS.enabled }} + PORT: "{{ ternary "8443" "8080" .Values.internalTLS.enabled }}" + DATABASE_TYPE: "postgresql" + POSTGRESQL_HOST: "{{ template "harbor.database.host" . }}" + POSTGRESQL_PORT: "{{ template "harbor.database.port" . }}" + POSTGRESQL_USERNAME: "{{ template "harbor.database.username" . }}" + POSTGRESQL_DATABASE: "{{ template "harbor.database.coreDatabase" . }}" + POSTGRESQL_SSLMODE: "{{ template "harbor.database.sslmode" . }}" + POSTGRESQL_MAX_IDLE_CONNS: "{{ .Values.database.maxIdleConns }}" + POSTGRESQL_MAX_OPEN_CONNS: "{{ .Values.database.maxOpenConns }}" + EXT_ENDPOINT: "{{ .Values.externalURL }}" + CORE_URL: "{{ template "harbor.coreURL" . }}" + JOBSERVICE_URL: "{{ template "harbor.jobserviceURL" . }}" + REGISTRY_URL: "{{ template "harbor.registryURL" . }}" + TOKEN_SERVICE_URL: "{{ template "harbor.tokenServiceURL" . }}" + CORE_LOCAL_URL: "{{ ternary "https://127.0.0.1:8443" "http://127.0.0.1:8080" .Values.internalTLS.enabled }}" + WITH_TRIVY: {{ .Values.trivy.enabled | quote }} + TRIVY_ADAPTER_URL: "{{ template "harbor.trivyAdapterURL" . }}" + REGISTRY_STORAGE_PROVIDER_NAME: "{{ .Values.persistence.imageChartStorage.type }}" + LOG_LEVEL: "{{ .Values.logLevel }}" + CONFIG_PATH: "/etc/core/app.conf" + CHART_CACHE_DRIVER: "redis" + _REDIS_URL_CORE: "{{ template "harbor.redis.urlForCore" . }}" + _REDIS_URL_REG: "{{ template "harbor.redis.urlForRegistry" . }}" + {{- if or (and (eq .Values.redis.type "internal") .Values.redis.internal.harborDatabaseIndex) (and (eq .Values.redis.type "external") .Values.redis.external.harborDatabaseIndex) }} + _REDIS_URL_HARBOR: "{{ template "harbor.redis.urlForHarbor" . }}" + {{- end }} + {{- if or (and (eq .Values.redis.type "internal") .Values.redis.internal.cacheLayerDatabaseIndex) (and (eq .Values.redis.type "external") .Values.redis.external.cacheLayerDatabaseIndex) }} + _REDIS_URL_CACHE_LAYER: "{{ template "harbor.redis.urlForCache" . }}" + {{- end }} + PORTAL_URL: "{{ template "harbor.portalURL" . }}" + REGISTRY_CONTROLLER_URL: "{{ template "harbor.registryControllerURL" . }}" + REGISTRY_CREDENTIAL_USERNAME: "{{ .Values.registry.credentials.username }}" + {{- if .Values.uaaSecretName }} + UAA_CA_ROOT: "/etc/core/auth-ca/auth-ca.crt" + {{- end }} + {{- if has "core" .Values.proxy.components }} + HTTP_PROXY: "{{ .Values.proxy.httpProxy }}" + HTTPS_PROXY: "{{ .Values.proxy.httpsProxy }}" + NO_PROXY: "{{ template "harbor.noProxy" . }}" + {{- end }} + PERMITTED_REGISTRY_TYPES_FOR_PROXY_CACHE: "docker-hub,harbor,azure-acr,ali-acr,aws-ecr,google-gcr,docker-registry,github-ghcr,jfrog-artifactory" + REPLICATION_ADAPTER_WHITELIST: "ali-acr,aws-ecr,azure-acr,docker-hub,docker-registry,github-ghcr,google-gcr,harbor,huawei-SWR,jfrog-artifactory,tencent-tcr,volcengine-cr" + {{- if .Values.metrics.enabled}} + METRIC_ENABLE: "true" + METRIC_PATH: "{{ .Values.metrics.core.path }}" + METRIC_PORT: "{{ .Values.metrics.core.port }}" + METRIC_NAMESPACE: harbor + METRIC_SUBSYSTEM: core + {{- end }} + + {{- if hasKey .Values.core "gcTimeWindowHours" }} + #make the GC time window configurable for testing + GC_TIME_WINDOW_HOURS: "{{ .Values.core.gcTimeWindowHours }}" + {{- end }} + {{- template "harbor.traceEnvsForCore" . }} + + {{- if .Values.core.artifactPullAsyncFlushDuration }} + ARTIFACT_PULL_ASYNC_FLUSH_DURATION: {{ .Values.core.artifactPullAsyncFlushDuration | quote }} + {{- end }} + + {{- if .Values.core.gdpr}} + {{- if .Values.core.gdpr.deleteUser}} + GDPR_DELETE_USER: "true" + {{- end }} + {{- if .Values.core.gdpr.auditLogsCompliant}} + GDPR_AUDIT_LOGS: "true" + {{- end }} + {{- end }} + + {{- if .Values.cache.enabled }} + CACHE_ENABLED: "true" + CACHE_EXPIRE_HOURS: "{{ .Values.cache.expireHours }}" + {{- end }} + + {{- if .Values.core.quotaUpdateProvider }} + QUOTA_UPDATE_PROVIDER: "{{ .Values.core.quotaUpdateProvider }}" + {{- end }} diff --git a/packages/system/harbor/charts/harbor/templates/core/core-dpl.yaml b/packages/system/harbor/charts/harbor/templates/core/core-dpl.yaml new file mode 100644 index 00000000..4705c5f6 --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/core/core-dpl.yaml @@ -0,0 +1,258 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ template "harbor.core" . }} + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} + component: core + app.kubernetes.io/component: core +spec: + replicas: {{ .Values.core.replicas }} + revisionHistoryLimit: {{ .Values.core.revisionHistoryLimit }} + selector: + matchLabels: +{{ include "harbor.matchLabels" . | indent 6 }} + component: core + template: + metadata: + labels: +{{ include "harbor.labels" . | indent 8 }} + component: core + app.kubernetes.io/component: core +{{- if .Values.core.podLabels }} +{{ toYaml .Values.core.podLabels | indent 8 }} +{{- end }} + annotations: + checksum/configmap: {{ include (print $.Template.BasePath "/core/core-cm.yaml") . | sha256sum }} + checksum/secret: {{ include (print $.Template.BasePath "/core/core-secret.yaml") . | sha256sum }} + checksum/secret-jobservice: {{ include (print $.Template.BasePath "/jobservice/jobservice-secrets.yaml") . | sha256sum }} +{{- if and .Values.internalTLS.enabled (eq .Values.internalTLS.certSource "auto") }} + checksum/tls: {{ include (print $.Template.BasePath "/internal/auto-tls.yaml") . | sha256sum }} +{{- else if and .Values.internalTLS.enabled (eq .Values.internalTLS.certSource "manual") }} + checksum/tls: {{ include (print $.Template.BasePath "/core/core-tls.yaml") . | sha256sum }} +{{- end }} +{{- if .Values.core.podAnnotations }} +{{ toYaml .Values.core.podAnnotations | indent 8 }} +{{- end }} + spec: + securityContext: + runAsUser: 10000 + fsGroup: 10000 +{{- if .Values.core.serviceAccountName }} + serviceAccountName: {{ .Values.core.serviceAccountName }} +{{- end -}} + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + automountServiceAccountToken: {{ .Values.core.automountServiceAccountToken | default false }} + terminationGracePeriodSeconds: 120 +{{- with .Values.core.topologySpreadConstraints}} + topologySpreadConstraints: +{{- range . }} + - {{ . | toYaml | indent 8 | trim }} + labelSelector: + matchLabels: +{{ include "harbor.matchLabels" $ | indent 12 }} + component: core +{{- end }} +{{- end }} + {{- with .Values.core.initContainers }} + initContainers: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: core + image: {{ .Values.core.image.repository }}:{{ .Values.core.image.tag }} + imagePullPolicy: {{ .Values.imagePullPolicy }} + {{- if .Values.core.startupProbe.enabled }} + startupProbe: + httpGet: + path: /api/v2.0/ping + scheme: {{ include "harbor.component.scheme" . | upper }} + port: {{ template "harbor.core.containerPort" . }} + failureThreshold: 360 + initialDelaySeconds: {{ .Values.core.startupProbe.initialDelaySeconds }} + periodSeconds: 10 + {{- end }} + livenessProbe: + httpGet: + path: /api/v2.0/ping + scheme: {{ include "harbor.component.scheme" . | upper }} + port: {{ template "harbor.core.containerPort" . }} + failureThreshold: 2 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /api/v2.0/ping + scheme: {{ include "harbor.component.scheme" . | upper }} + port: {{ template "harbor.core.containerPort" . }} + failureThreshold: 2 + periodSeconds: 10 + envFrom: + - configMapRef: + name: "{{ template "harbor.core" . }}" + - secretRef: + name: "{{ template "harbor.core" . }}" + env: + - name: CORE_SECRET + valueFrom: + secretKeyRef: + name: {{ default (include "harbor.core" .) .Values.core.existingSecret }} + key: secret + - name: JOBSERVICE_SECRET + valueFrom: + secretKeyRef: + name: {{ default (include "harbor.jobservice" .) .Values.jobservice.existingSecret }} + {{- if .Values.jobservice.existingSecret }} + key: {{ .Values.jobservice.existingSecretKey }} + {{- else }} + key: JOBSERVICE_SECRET + {{- end }} + {{- if .Values.existingSecretAdminPassword }} + - name: HARBOR_ADMIN_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.existingSecretAdminPassword }} + key: {{ .Values.existingSecretAdminPasswordKey }} + {{- end }} + {{- if .Values.internalTLS.enabled }} + - name: INTERNAL_TLS_ENABLED + value: "true" + - name: INTERNAL_TLS_KEY_PATH + value: /etc/harbor/ssl/core/tls.key + - name: INTERNAL_TLS_CERT_PATH + value: /etc/harbor/ssl/core/tls.crt + - name: INTERNAL_TLS_TRUST_CA_PATH + value: /etc/harbor/ssl/core/ca.crt + {{- end }} + {{- if .Values.database.external.existingSecret }} + - name: POSTGRESQL_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.database.external.existingSecret }} + key: password + {{- end }} + {{- if .Values.registry.credentials.existingSecret }} + - name: REGISTRY_CREDENTIAL_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.registry.credentials.existingSecret }} + key: REGISTRY_PASSWD + {{- end }} + {{- if .Values.core.existingXsrfSecret }} + - name: CSRF_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.core.existingXsrfSecret }} + key: {{ .Values.core.existingXsrfSecretKey }} + {{- end }} +{{- with .Values.core.extraEnvVars }} +{{- toYaml . | nindent 10 }} +{{- end }} + {{- if not (empty .Values.containerSecurityContext) }} + securityContext: {{ .Values.containerSecurityContext | toYaml | nindent 10 }} + {{- end }} + ports: + - containerPort: {{ template "harbor.core.containerPort" . }} + volumeMounts: + - name: config + mountPath: /etc/core/app.conf + subPath: app.conf + - name: secret-key + mountPath: /etc/core/key + subPath: key + - name: token-service-private-key + mountPath: /etc/core/private_key.pem + subPath: tls.key + {{- if .Values.expose.tls.enabled }} + - name: ca-download + mountPath: /etc/core/ca + {{- end }} + {{- if .Values.uaaSecretName }} + - name: auth-ca-cert + mountPath: /etc/core/auth-ca/auth-ca.crt + subPath: auth-ca.crt + {{- end }} + {{- if .Values.internalTLS.enabled }} + - name: core-internal-certs + mountPath: /etc/harbor/ssl/core + {{- end }} + - name: psc + mountPath: /etc/core/token + {{- if .Values.caBundleSecretName }} +{{ include "harbor.caBundleVolumeMount" . | indent 8 }} + {{- end }} +{{- if .Values.core.resources }} + resources: +{{ toYaml .Values.core.resources | indent 10 }} +{{- end }} + volumes: + - name: config + configMap: + name: {{ template "harbor.core" . }} + items: + - key: app.conf + path: app.conf + - name: secret-key + secret: + {{- if .Values.existingSecretSecretKey }} + secretName: {{ .Values.existingSecretSecretKey }} + {{- else }} + secretName: {{ template "harbor.core" . }} + {{- end }} + items: + - key: secretKey + path: key + - name: token-service-private-key + secret: + {{- if .Values.core.secretName }} + secretName: {{ .Values.core.secretName }} + {{- else }} + secretName: {{ template "harbor.core" . }} + {{- end }} + {{- if .Values.expose.tls.enabled }} + - name: ca-download + secret: + {{- if .Values.caSecretName }} + secretName: {{ .Values.caSecretName }} + {{- else if eq (include "harbor.autoGenCertForIngress" .) "true" }} + secretName: "{{ template "harbor.ingress" . }}" + {{- else if eq (include "harbor.autoGenCertForNginx" .) "true" }} + secretName: {{ template "harbor.tlsSecretForNginx" . }} + {{- end }} + {{- end }} + {{- if .Values.uaaSecretName }} + - name: auth-ca-cert + secret: + secretName: {{ .Values.uaaSecretName }} + items: + - key: ca.crt + path: auth-ca.crt + {{- end }} + {{- if .Values.internalTLS.enabled }} + - name: core-internal-certs + secret: + secretName: {{ template "harbor.internalTLS.core.secretName" . }} + {{- end }} + - name: psc + emptyDir: {} + {{- if .Values.caBundleSecretName }} +{{ include "harbor.caBundleVolume" . | indent 6 }} + {{- end }} + {{- with .Values.core.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.core.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.core.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + {{- if .Values.core.priorityClassName }} + priorityClassName: {{ .Values.core.priorityClassName }} + {{- end }} diff --git a/packages/system/harbor/charts/harbor/templates/core/core-pre-upgrade-job.yaml b/packages/system/harbor/charts/harbor/templates/core/core-pre-upgrade-job.yaml new file mode 100644 index 00000000..87271569 --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/core/core-pre-upgrade-job.yaml @@ -0,0 +1,78 @@ +{{- if .Values.enableMigrateHelmHook }} +apiVersion: batch/v1 +kind: Job +metadata: + name: migration-job + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} + component: migrator + annotations: + # This is what defines this resource as a hook. Without this line, the + # job is considered part of the release. + "helm.sh/hook": pre-upgrade + "helm.sh/hook-weight": "-5" +spec: + template: + metadata: + labels: +{{ include "harbor.matchLabels" . | indent 8 }} + component: migrator + spec: + restartPolicy: Never + securityContext: + runAsUser: 10000 + fsGroup: 10000 +{{- if .Values.core.serviceAccountName }} + serviceAccountName: {{ .Values.core.serviceAccountName }} +{{- end -}} + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + terminationGracePeriodSeconds: 120 + containers: + - name: core-job + image: {{ .Values.core.image.repository }}:{{ .Values.core.image.tag }} + imagePullPolicy: {{ .Values.imagePullPolicy }} + command: ["/harbor/harbor_core", "-mode=migrate"] + envFrom: + - configMapRef: + name: "{{ template "harbor.core" . }}" + - secretRef: + name: "{{ template "harbor.core" . }}" + {{- if .Values.database.external.existingSecret }} + env: + - name: POSTGRESQL_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.database.external.existingSecret }} + key: password + {{- end }} + {{- if not (empty .Values.containerSecurityContext) }} + securityContext: {{ .Values.containerSecurityContext | toYaml | nindent 10 }} + {{- end }} + volumeMounts: + - name: config + mountPath: /etc/core/app.conf + subPath: app.conf + volumes: + - name: config + configMap: + name: {{ template "harbor.core" . }} + items: + - key: app.conf + path: app.conf + {{- with .Values.core.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.core.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.core.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} +{{- end }} diff --git a/packages/system/harbor/charts/harbor/templates/core/core-secret.yaml b/packages/system/harbor/charts/harbor/templates/core/core-secret.yaml new file mode 100644 index 00000000..ea9d4cfa --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/core/core-secret.yaml @@ -0,0 +1,37 @@ +{{- $existingSecret := lookup "v1" "Secret" .Release.Namespace (include "harbor.core" .) }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ template "harbor.core" . }} + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} +type: Opaque +data: + {{- if not .Values.existingSecretSecretKey }} + secretKey: {{ .Values.secretKey | b64enc | quote }} + {{- end }} + {{- if not .Values.core.existingSecret }} + secret: {{ .Values.core.secret | default (include "harbor.secretKeyHelper" (dict "key" "secret" "data" $existingSecret.data)) | default (randAlphaNum 16) | b64enc | quote }} + {{- end }} + {{- if not .Values.core.secretName }} + {{- $ca := genCA "harbor-token-ca" 365 }} + tls.key: {{ .Values.core.tokenKey | default $ca.Key | b64enc | quote }} + tls.crt: {{ .Values.core.tokenCert | default $ca.Cert | b64enc | quote }} + {{- end }} + {{- if not .Values.existingSecretAdminPassword }} + HARBOR_ADMIN_PASSWORD: {{ .Values.harborAdminPassword | b64enc | quote }} + {{- end }} + {{- if not .Values.database.external.existingSecret }} + POSTGRESQL_PASSWORD: {{ template "harbor.database.encryptedPassword" . }} + {{- end }} + {{- if not .Values.registry.credentials.existingSecret }} + REGISTRY_CREDENTIAL_PASSWORD: {{ .Values.registry.credentials.password | b64enc | quote }} + {{- end }} + {{- if not .Values.core.existingXsrfSecret }} + CSRF_KEY: {{ .Values.core.xsrfKey | default (include "harbor.secretKeyHelper" (dict "key" "CSRF_KEY" "data" $existingSecret.data)) | default (randAlphaNum 32) | b64enc | quote }} + {{- end }} +{{- if .Values.core.configureUserSettings }} + CONFIG_OVERWRITE_JSON: {{ .Values.core.configureUserSettings | b64enc | quote }} +{{- end }} + {{- template "harbor.traceJaegerPassword" . }} diff --git a/packages/system/harbor/charts/harbor/templates/core/core-svc.yaml b/packages/system/harbor/charts/harbor/templates/core/core-svc.yaml new file mode 100644 index 00000000..a1d2368d --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/core/core-svc.yaml @@ -0,0 +1,32 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ template "harbor.core" . }} + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} +{{- with .Values.core.serviceAnnotations }} + annotations: + {{- toYaml . | nindent 4 }} +{{- end }} +spec: +{{- if or (eq .Values.expose.ingress.controller "gce") (eq .Values.expose.ingress.controller "alb") (eq .Values.expose.ingress.controller "f5-bigip") }} + type: NodePort +{{- end }} +{{- if .Values.ipFamily.policy }} + ipFamilyPolicy: {{ .Values.ipFamily.policy }} +{{- end }} +{{- if .Values.ipFamily.families }} + ipFamilies: {{ toYaml .Values.ipFamily.families | nindent 4 }} +{{- end }} + ports: + - name: {{ ternary "https-web" "http-web" .Values.internalTLS.enabled }} + port: {{ template "harbor.core.servicePort" . }} + targetPort: {{ template "harbor.core.containerPort" . }} +{{- if .Values.metrics.enabled}} + - name: {{ template "harbor.metricsPortName" . }} + port: {{ .Values.metrics.core.port }} +{{- end }} + selector: +{{ include "harbor.matchLabels" . | indent 4 }} + component: core diff --git a/packages/system/harbor/charts/harbor/templates/core/core-tls.yaml b/packages/system/harbor/charts/harbor/templates/core/core-tls.yaml new file mode 100644 index 00000000..d90d30c8 --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/core/core-tls.yaml @@ -0,0 +1,16 @@ +{{- if and .Values.internalTLS.enabled }} +{{- if eq .Values.internalTLS.certSource "manual" }} +apiVersion: v1 +kind: Secret +metadata: + name: "{{ template "harbor.internalTLS.core.secretName" . }}" + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} +type: kubernetes.io/tls +data: + ca.crt: {{ (required "The \"internalTLS.trustCa\" is required!" .Values.internalTLS.trustCa) | b64enc | quote }} + tls.crt: {{ (required "The \"internalTLS.core.crt\" is required!" .Values.internalTLS.core.crt) | b64enc | quote }} + tls.key: {{ (required "The \"internalTLS.core.key\" is required!" .Values.internalTLS.core.key) | b64enc | quote }} +{{- end }} +{{- end }} diff --git a/packages/system/harbor/charts/harbor/templates/database/database-secret.yaml b/packages/system/harbor/charts/harbor/templates/database/database-secret.yaml new file mode 100644 index 00000000..0d07ec26 --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/database/database-secret.yaml @@ -0,0 +1,12 @@ +{{- if eq .Values.database.type "internal" -}} +apiVersion: v1 +kind: Secret +metadata: + name: "{{ template "harbor.database" . }}" + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} +type: Opaque +data: + POSTGRES_PASSWORD: {{ template "harbor.database.encryptedPassword" . }} +{{- end -}} diff --git a/packages/system/harbor/charts/harbor/templates/database/database-ss.yaml b/packages/system/harbor/charts/harbor/templates/database/database-ss.yaml new file mode 100644 index 00000000..8bddd291 --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/database/database-ss.yaml @@ -0,0 +1,165 @@ +{{- if eq .Values.database.type "internal" -}} +{{- $database := .Values.persistence.persistentVolumeClaim.database -}} +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: "{{ template "harbor.database" . }}" + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} + component: database + app.kubernetes.io/component: database +spec: + replicas: 1 + serviceName: "{{ template "harbor.database" . }}" + selector: + matchLabels: +{{ include "harbor.matchLabels" . | indent 6 }} + component: database + template: + metadata: + labels: +{{ include "harbor.labels" . | indent 8 }} + component: database + app.kubernetes.io/component: database +{{- if .Values.database.podLabels }} +{{ toYaml .Values.database.podLabels | indent 8 }} +{{- end }} + annotations: + checksum/secret: {{ include (print $.Template.BasePath "/database/database-secret.yaml") . | sha256sum }} +{{- if .Values.database.podAnnotations }} +{{ toYaml .Values.database.podAnnotations | indent 8 }} +{{- end }} + spec: + securityContext: + runAsUser: 999 + fsGroup: 999 +{{- if .Values.database.internal.serviceAccountName }} + serviceAccountName: {{ .Values.database.internal.serviceAccountName }} +{{- end -}} + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + automountServiceAccountToken: {{ .Values.database.internal.automountServiceAccountToken | default false }} + terminationGracePeriodSeconds: 120 + initContainers: + # with "fsGroup" set, each time a volume is mounted, Kubernetes must recursively chown() and chmod() all the files and directories inside the volume + # this causes the postgresql reports the "data directory /var/lib/postgresql/data/pgdata has group or world access" issue when using some CSIs e.g. Ceph + # use this init container to correct the permission + # as "fsGroup" applied before the init container running, the container has enough permission to execute the command + - name: "data-permissions-ensurer" + image: {{ .Values.database.internal.image.repository }}:{{ .Values.database.internal.image.tag }} + imagePullPolicy: {{ .Values.imagePullPolicy }} + {{- if not (empty .Values.containerSecurityContext) }} + securityContext: {{ .Values.containerSecurityContext | toYaml | nindent 10 }} + {{- end }} + command: ["/bin/sh"] + args: ["-c", "chmod -R 700 /var/lib/postgresql/data/pgdata || true"] +{{- if .Values.database.internal.initContainer.permissions.resources }} + resources: +{{ toYaml .Values.database.internal.initContainer.permissions.resources | indent 10 }} +{{- end }} + volumeMounts: + - name: database-data + mountPath: /var/lib/postgresql/data + subPath: {{ $database.subPath }} + {{- with .Values.database.internal.extrInitContainers }} + {{- toYaml . | nindent 6 }} + {{- end }} + containers: + - name: database + image: {{ .Values.database.internal.image.repository }}:{{ .Values.database.internal.image.tag }} + imagePullPolicy: {{ .Values.imagePullPolicy }} + {{- if not (empty .Values.containerSecurityContext) }} + securityContext: {{ .Values.containerSecurityContext | toYaml | nindent 10 }} + {{- end }} + livenessProbe: + exec: + command: + - /docker-healthcheck.sh + initialDelaySeconds: 300 + periodSeconds: 10 + timeoutSeconds: {{ .Values.database.internal.livenessProbe.timeoutSeconds }} + readinessProbe: + exec: + command: + - /docker-healthcheck.sh + initialDelaySeconds: 1 + periodSeconds: 10 + timeoutSeconds: {{ .Values.database.internal.readinessProbe.timeoutSeconds }} +{{- if .Values.database.internal.resources }} + resources: +{{ toYaml .Values.database.internal.resources | indent 10 }} +{{- end }} + envFrom: + - secretRef: + name: "{{ template "harbor.database" . }}" + env: + # put the data into a sub directory to avoid the permission issue in k8s with restricted psp enabled + # more detail refer to https://github.com/goharbor/harbor-helm/issues/756 + - name: PGDATA + value: "/var/lib/postgresql/data/pgdata" +{{- with .Values.database.internal.extraEnvVars }} +{{- toYaml . | nindent 10 }} +{{- end }} + volumeMounts: + - name: database-data + mountPath: /var/lib/postgresql/data + subPath: {{ $database.subPath }} + - name: shm-volume + mountPath: /dev/shm + volumes: + - name: shm-volume + emptyDir: + medium: Memory + sizeLimit: {{ .Values.database.internal.shmSizeLimit }} + {{- if not .Values.persistence.enabled }} + - name: "database-data" + emptyDir: {} + {{- else if $database.existingClaim }} + - name: "database-data" + persistentVolumeClaim: + claimName: {{ $database.existingClaim }} + {{- end -}} + {{- with .Values.database.internal.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.database.internal.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.database.internal.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + {{- if .Values.database.internal.priorityClassName }} + priorityClassName: {{ .Values.database.internal.priorityClassName }} + {{- end }} + {{- if and .Values.persistence.enabled (not $database.existingClaim) }} + volumeClaimTemplates: + - apiVersion: v1 + kind: PersistentVolumeClaim + metadata: + name: "database-data" + labels: +{{ include "harbor.legacy.labels" . | indent 8 }} + annotations: + {{- range $key, $value := $database.annotations }} + {{ $key }}: {{ $value | quote }} + {{- end }} + spec: + accessModes: [{{ $database.accessMode | quote }}] + {{- if $database.storageClass }} + {{- if (eq "-" $database.storageClass) }} + storageClassName: "" + {{- else }} + storageClassName: "{{ $database.storageClass }}" + {{- end }} + {{- end }} + resources: + requests: + storage: {{ $database.size | quote }} + {{- end -}} + {{- end -}} diff --git a/packages/system/harbor/charts/harbor/templates/database/database-svc.yaml b/packages/system/harbor/charts/harbor/templates/database/database-svc.yaml new file mode 100644 index 00000000..aef4c6df --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/database/database-svc.yaml @@ -0,0 +1,21 @@ +{{- if eq .Values.database.type "internal" -}} +apiVersion: v1 +kind: Service +metadata: + name: "{{ template "harbor.database" . }}" + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} +spec: +{{- if .Values.ipFamily.policy }} + ipFamilyPolicy: {{ .Values.ipFamily.policy }} +{{- end }} +{{- if .Values.ipFamily.families }} + ipFamilies: {{ toYaml .Values.ipFamily.families | nindent 4 }} +{{- end }} + ports: + - port: 5432 + selector: +{{ include "harbor.matchLabels" . | indent 4 }} + component: database +{{- end -}} diff --git a/packages/system/harbor/charts/harbor/templates/exporter/exporter-cm-env.yaml b/packages/system/harbor/charts/harbor/templates/exporter/exporter-cm-env.yaml new file mode 100644 index 00000000..3f911032 --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/exporter/exporter-cm-env.yaml @@ -0,0 +1,36 @@ +{{- if .Values.metrics.enabled}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: "{{ template "harbor.exporter" . }}-env" + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} +data: + {{- if has "jobservice" .Values.proxy.components }} + HTTP_PROXY: "{{ .Values.proxy.httpProxy }}" + HTTPS_PROXY: "{{ .Values.proxy.httpsProxy }}" + NO_PROXY: "{{ template "harbor.noProxy" . }}" + {{- end }} + LOG_LEVEL: "{{ .Values.logLevel }}" + HARBOR_EXPORTER_PORT: "{{ .Values.metrics.exporter.port }}" + HARBOR_EXPORTER_METRICS_PATH: "{{ .Values.metrics.exporter.path }}" + HARBOR_EXPORTER_METRICS_ENABLED: "{{ .Values.metrics.enabled }}" + HARBOR_EXPORTER_CACHE_TIME: "{{ .Values.exporter.cacheDuration }}" + HARBOR_EXPORTER_CACHE_CLEAN_INTERVAL: "{{ .Values.exporter.cacheCleanInterval }}" + HARBOR_METRIC_NAMESPACE: harbor + HARBOR_METRIC_SUBSYSTEM: exporter + HARBOR_REDIS_URL: "{{ template "harbor.redis.urlForJobservice" . }}" + HARBOR_REDIS_NAMESPACE: harbor_job_service_namespace + HARBOR_REDIS_TIMEOUT: "3600" + HARBOR_SERVICE_SCHEME: "{{ template "harbor.component.scheme" . }}" + HARBOR_SERVICE_HOST: "{{ template "harbor.core" . }}" + HARBOR_SERVICE_PORT: "{{ template "harbor.core.servicePort" . }}" + HARBOR_DATABASE_HOST: "{{ template "harbor.database.host" . }}" + HARBOR_DATABASE_PORT: "{{ template "harbor.database.port" . }}" + HARBOR_DATABASE_USERNAME: "{{ template "harbor.database.username" . }}" + HARBOR_DATABASE_DBNAME: "{{ template "harbor.database.coreDatabase" . }}" + HARBOR_DATABASE_SSLMODE: "{{ template "harbor.database.sslmode" . }}" + HARBOR_DATABASE_MAX_IDLE_CONNS: "{{ .Values.database.maxIdleConns }}" + HARBOR_DATABASE_MAX_OPEN_CONNS: "{{ .Values.database.maxOpenConns }}" +{{- end}} diff --git a/packages/system/harbor/charts/harbor/templates/exporter/exporter-dpl.yaml b/packages/system/harbor/charts/harbor/templates/exporter/exporter-dpl.yaml new file mode 100644 index 00000000..30894a6d --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/exporter/exporter-dpl.yaml @@ -0,0 +1,146 @@ +{{- if .Values.metrics.enabled}} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ template "harbor.exporter" . }} + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} + component: exporter + app.kubernetes.io/component: exporter +spec: + replicas: {{ .Values.exporter.replicas }} + revisionHistoryLimit: {{ .Values.exporter.revisionHistoryLimit }} + selector: + matchLabels: +{{ include "harbor.matchLabels" . | indent 6 }} + component: exporter + template: + metadata: + labels: +{{ include "harbor.labels" . | indent 8 }} + component: exporter + app.kubernetes.io/component: exporter +{{- if .Values.exporter.podLabels }} +{{ toYaml .Values.exporter.podLabels | indent 8 }} +{{- end }} + annotations: + checksum/configmap: {{ include (print $.Template.BasePath "/exporter/exporter-cm-env.yaml") . | sha256sum }} + checksum/secret: {{ include (print $.Template.BasePath "/exporter/exporter-secret.yaml") . | sha256sum }} +{{- if and .Values.internalTLS.enabled (eq .Values.internalTLS.certSource "auto") }} + checksum/tls: {{ include (print $.Template.BasePath "/internal/auto-tls.yaml") . | sha256sum }} +{{- else if and .Values.internalTLS.enabled (eq .Values.internalTLS.certSource "manual") }} + checksum/tls: {{ include (print $.Template.BasePath "/core/core-tls.yaml") . | sha256sum }} +{{- end }} +{{- if .Values.exporter.podAnnotations }} +{{ toYaml .Values.exporter.podAnnotations | indent 8 }} +{{- end }} + spec: + securityContext: + runAsUser: 10000 + fsGroup: 10000 +{{- if .Values.exporter.serviceAccountName }} + serviceAccountName: {{ .Values.exporter.serviceAccountName }} +{{- end -}} + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + automountServiceAccountToken: {{ .Values.exporter.automountServiceAccountToken | default false }} +{{- with .Values.exporter.topologySpreadConstraints }} + topologySpreadConstraints: +{{- range . }} + - {{ . | toYaml | indent 8 | trim }} + labelSelector: + matchLabels: +{{ include "harbor.matchLabels" $ | indent 12 }} + component: exporter +{{- end }} +{{- end }} + containers: + - name: exporter + image: {{ .Values.exporter.image.repository }}:{{ .Values.exporter.image.tag }} + imagePullPolicy: {{ .Values.imagePullPolicy }} + livenessProbe: + httpGet: + path: / + port: {{ .Values.metrics.exporter.port }} + initialDelaySeconds: 300 + periodSeconds: 10 + readinessProbe: + httpGet: + path: / + port: {{ .Values.metrics.exporter.port }} + initialDelaySeconds: 30 + periodSeconds: 10 + args: ["-log-level", "{{ .Values.logLevel }}"] + envFrom: + - configMapRef: + name: "{{ template "harbor.exporter" . }}-env" + - secretRef: + name: "{{ template "harbor.exporter" . }}" + env: + {{- if .Values.database.external.existingSecret }} + - name: HARBOR_DATABASE_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.database.external.existingSecret }} + key: password + {{- end }} + {{- if .Values.existingSecretAdminPassword }} + - name: HARBOR_ADMIN_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.existingSecretAdminPassword }} + key: {{ .Values.existingSecretAdminPasswordKey }} + {{- end }} + {{- with .Values.exporter.extraEnvVars }} + {{- toYaml . | nindent 8 }} + {{- end }} +{{- if .Values.exporter.resources }} + resources: +{{ toYaml .Values.exporter.resources | indent 10 }} +{{- end }} + {{- if not (empty .Values.containerSecurityContext) }} + securityContext: {{ .Values.containerSecurityContext | toYaml | nindent 10 }} + {{- end }} + ports: + - containerPort: {{ .Values.metrics.exporter.port }} + volumeMounts: + {{- if .Values.caBundleSecretName }} +{{ include "harbor.caBundleVolumeMount" . | indent 8 }} + {{- end }} + {{- if .Values.internalTLS.enabled }} + - name: core-internal-certs + mountPath: /etc/harbor/ssl/core + # There are some metric data are collectd from harbor core. + # When internal TLS is enabled, the Exporter need the CA file to collect these data. + {{- end }} + volumes: + - name: config + secret: + secretName: "{{ template "harbor.exporter" . }}" + {{- if .Values.internalTLS.enabled }} + - name: core-internal-certs + secret: + secretName: {{ template "harbor.internalTLS.core.secretName" . }} + {{- end }} + {{- if .Values.caBundleSecretName }} +{{ include "harbor.caBundleVolume" . | indent 6 }} + {{- end }} + {{- with .Values.exporter.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.exporter.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.exporter.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + {{- if .Values.exporter.priorityClassName }} + priorityClassName: {{ .Values.exporter.priorityClassName }} + {{- end }} +{{ end }} diff --git a/packages/system/harbor/charts/harbor/templates/exporter/exporter-secret.yaml b/packages/system/harbor/charts/harbor/templates/exporter/exporter-secret.yaml new file mode 100644 index 00000000..02c74d03 --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/exporter/exporter-secret.yaml @@ -0,0 +1,17 @@ +{{- if .Values.metrics.enabled}} +apiVersion: v1 +kind: Secret +metadata: + name: {{ template "harbor.exporter" . }} + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} +type: Opaque +data: +{{- if not .Values.existingSecretAdminPassword }} + HARBOR_ADMIN_PASSWORD: {{ .Values.harborAdminPassword | b64enc | quote }} +{{- end }} +{{- if not .Values.database.external.existingSecret }} + HARBOR_DATABASE_PASSWORD: {{ template "harbor.database.encryptedPassword" . }} +{{- end }} +{{- end }} diff --git a/packages/system/harbor/charts/harbor/templates/exporter/exporter-svc.yaml b/packages/system/harbor/charts/harbor/templates/exporter/exporter-svc.yaml new file mode 100644 index 00000000..11306e27 --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/exporter/exporter-svc.yaml @@ -0,0 +1,22 @@ +{{- if .Values.metrics.enabled}} +apiVersion: v1 +kind: Service +metadata: + name: "{{ template "harbor.exporter" . }}" + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} +spec: +{{- if .Values.ipFamily.policy }} + ipFamilyPolicy: {{ .Values.ipFamily.policy }} +{{- end }} +{{- if .Values.ipFamily.families }} + ipFamilies: {{ toYaml .Values.ipFamily.families | nindent 4 }} +{{- end }} + ports: + - name: {{ template "harbor.metricsPortName" . }} + port: {{ .Values.metrics.exporter.port }} + selector: +{{ include "harbor.matchLabels" . | indent 4 }} + component: exporter +{{ end }} diff --git a/packages/system/harbor/charts/harbor/templates/gateway-apis/route.yaml b/packages/system/harbor/charts/harbor/templates/gateway-apis/route.yaml new file mode 100644 index 00000000..0999a5ff --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/gateway-apis/route.yaml @@ -0,0 +1,55 @@ +{{- if eq .Values.expose.type "route" }} +{{- $route := .Values.expose.route -}} +{{- $_ := set . "path_type" "PathPrefix" -}} +{{- $_ := set . "portal_path" "/" -}} +{{- $_ := set . "api_path" "/api/" -}} +{{- $_ := set . "service_path" "/service/" -}} +{{- $_ := set . "v2_path" "/v2/" -}} +{{- $_ := set . "controller_path" "/c/" -}} +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: "{{ template "harbor.route" . }}" + namespace: {{ .Release.Namespace | quote }} +{{- if $route.labels }} + labels: +{{ include "harbor.labels" . | indent 4 }} +{{ toYaml $route.labels | indent 4 }} +{{- end }} +{{- if $route.annotations }} + annotations: +{{ toYaml $route.annotations | indent 4 }} +{{- end }} +spec: + parentRefs: + {{- toYaml $route.parentRefs | nindent 2 }} + hostnames: + {{- toYaml $route.hosts | nindent 2 }} + rules: + - matches: + - path: + type: {{ .path_type }} + value: {{ .api_path }} + - path: + type: {{ .path_type }} + value: {{ .service_path }} + - path: + type: {{ .path_type }} + value: {{ .v2_path }} + - path: + type: {{ .path_type }} + value: {{ .controller_path }} + backendRefs: + - name: {{ template "harbor.core" . }} + namespace: {{ .Release.Namespace | quote }} + port: {{ template "harbor.core.servicePort" . }} + - matches: + - path: + type: {{ .path_type }} + value: {{ .portal_path }} + backendRefs: + - name: {{ template "harbor.portal" . }} + namespace: {{ .Release.Namespace | quote }} + port: {{ template "harbor.portal.servicePort" . }} +{{- end }} diff --git a/packages/system/harbor/charts/harbor/templates/ingress/ingress.yaml b/packages/system/harbor/charts/harbor/templates/ingress/ingress.yaml new file mode 100644 index 00000000..06096b86 --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/ingress/ingress.yaml @@ -0,0 +1,132 @@ +{{- if eq .Values.expose.type "ingress" }} +{{- $ingress := .Values.expose.ingress -}} +{{- $tls := .Values.expose.tls -}} +{{- if eq .Values.expose.ingress.controller "gce" }} + {{- $_ := set . "path_type" "ImplementationSpecific" -}} + {{- $_ := set . "portal_path" "/*" -}} + {{- $_ := set . "api_path" "/api/*" -}} + {{- $_ := set . "service_path" "/service/*" -}} + {{- $_ := set . "v2_path" "/v2/*" -}} + {{- $_ := set . "controller_path" "/c/*" -}} +{{- else if eq .Values.expose.ingress.controller "ncp" }} + {{- $_ := set . "path_type" "Prefix" -}} + {{- $_ := set . "portal_path" "/.*" -}} + {{- $_ := set . "api_path" "/api/.*" -}} + {{- $_ := set . "service_path" "/service/.*" -}} + {{- $_ := set . "v2_path" "/v2/.*" -}} + {{- $_ := set . "controller_path" "/c/.*" -}} +{{- else }} + {{- $_ := set . "path_type" "Prefix" -}} + {{- $_ := set . "portal_path" "/" -}} + {{- $_ := set . "api_path" "/api/" -}} + {{- $_ := set . "service_path" "/service/" -}} + {{- $_ := set . "v2_path" "/v2/" -}} + {{- $_ := set . "controller_path" "/c/" -}} +{{- end }} + +--- +{{- if semverCompare "<1.14-0" (include "harbor.ingress.kubeVersion" .) }} +apiVersion: extensions/v1beta1 +{{- else if semverCompare "<1.19-0" (include "harbor.ingress.kubeVersion" .) }} +apiVersion: networking.k8s.io/v1beta1 +{{- else }} +apiVersion: networking.k8s.io/v1 +{{- end }} +kind: Ingress +metadata: + name: "{{ template "harbor.ingress" . }}" + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} +{{- if $ingress.labels }} +{{ toYaml $ingress.labels | indent 4 }} +{{- end }} + annotations: +{{ toYaml $ingress.annotations | indent 4 }} +{{- if .Values.internalTLS.enabled }} + nginx.ingress.kubernetes.io/backend-protocol: "HTTPS" +{{- end }} +{{- if eq .Values.expose.ingress.controller "ncp" }} + ncp/use-regex: "true" + {{- if $tls.enabled }} + ncp/http-redirect: "true" + {{- end }} +{{- end }} +spec: + {{- if $ingress.className }} + ingressClassName: {{ $ingress.className }} + {{- end }} + {{- if $tls.enabled }} + tls: + - secretName: {{ template "harbor.tlsCoreSecretForIngress" . }} + {{- if $ingress.hosts.core }} + hosts: + - {{ $ingress.hosts.core }} + {{- end }} + {{- end }} + rules: + - http: + paths: +{{- if semverCompare "<1.19-0" (include "harbor.ingress.kubeVersion" .) }} + - path: {{ .api_path }} + backend: + serviceName: {{ template "harbor.core" . }} + servicePort: {{ template "harbor.core.servicePort" . }} + - path: {{ .service_path }} + backend: + serviceName: {{ template "harbor.core" . }} + servicePort: {{ template "harbor.core.servicePort" . }} + - path: {{ .v2_path }} + backend: + serviceName: {{ template "harbor.core" . }} + servicePort: {{ template "harbor.core.servicePort" . }} + - path: {{ .controller_path }} + backend: + serviceName: {{ template "harbor.core" . }} + servicePort: {{ template "harbor.core.servicePort" . }} + - path: {{ .portal_path }} + backend: + serviceName: {{ template "harbor.portal" . }} + servicePort: {{ template "harbor.portal.servicePort" . }} +{{- else }} + - path: {{ .api_path }} + pathType: {{ .path_type }} + backend: + service: + name: {{ template "harbor.core" . }} + port: + number: {{ template "harbor.core.servicePort" . }} + - path: {{ .service_path }} + pathType: {{ .path_type }} + backend: + service: + name: {{ template "harbor.core" . }} + port: + number: {{ template "harbor.core.servicePort" . }} + - path: {{ .v2_path }} + pathType: {{ .path_type }} + backend: + service: + name: {{ template "harbor.core" . }} + port: + number: {{ template "harbor.core.servicePort" . }} + - path: {{ .controller_path }} + pathType: {{ .path_type }} + backend: + service: + name: {{ template "harbor.core" . }} + port: + number: {{ template "harbor.core.servicePort" . }} + - path: {{ .portal_path }} + pathType: {{ .path_type }} + backend: + service: + name: {{ template "harbor.portal" . }} + port: + number: {{ template "harbor.portal.servicePort" . }} +{{- end }} + {{- if $ingress.hosts.core }} + host: {{ $ingress.hosts.core }} + {{- end }} + +{{- end }} diff --git a/packages/system/harbor/charts/harbor/templates/ingress/secret.yaml b/packages/system/harbor/charts/harbor/templates/ingress/secret.yaml new file mode 100644 index 00000000..48343b0f --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/ingress/secret.yaml @@ -0,0 +1,18 @@ +{{- if eq .Values.expose.type "ingress" }} +{{- if eq (include "harbor.autoGenCertForIngress" .) "true" }} +{{- $ca := genCA "harbor-ca" 365 }} +{{- $cert := genSignedCert .Values.expose.ingress.hosts.core nil (list .Values.expose.ingress.hosts.core) 365 $ca }} +apiVersion: v1 +kind: Secret +metadata: + name: "{{ template "harbor.ingress" . }}" + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} +type: kubernetes.io/tls +data: + tls.crt: {{ $cert.Cert | b64enc | quote }} + tls.key: {{ $cert.Key | b64enc | quote }} + ca.crt: {{ $ca.Cert | b64enc | quote }} +{{- end }} +{{- end }} diff --git a/packages/system/harbor/charts/harbor/templates/internal/auto-tls.yaml b/packages/system/harbor/charts/harbor/templates/internal/auto-tls.yaml new file mode 100644 index 00000000..32807cfd --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/internal/auto-tls.yaml @@ -0,0 +1,86 @@ +{{- if and .Values.internalTLS.enabled (eq .Values.internalTLS.certSource "auto") }} +{{- $ca := genCA "harbor-internal-ca" 365 }} +{{- $coreCN := (include "harbor.core" .) }} +{{- $coreCrt := genSignedCert $coreCN (list "127.0.0.1") (list "localhost" $coreCN) 365 $ca }} +{{- $jsCN := (include "harbor.jobservice" .) }} +{{- $jsCrt := genSignedCert $jsCN nil (list $jsCN) 365 $ca }} +{{- $regCN := (include "harbor.registry" .) }} +{{- $regCrt := genSignedCert $regCN nil (list $regCN) 365 $ca }} +{{- $portalCN := (include "harbor.portal" .) }} +{{- $portalCrt := genSignedCert $portalCN nil (list $portalCN) 365 $ca }} + +--- +apiVersion: v1 +kind: Secret +metadata: + name: "{{ template "harbor.internalTLS.core.secretName" . }}" + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} +type: kubernetes.io/tls +data: + ca.crt: {{ $ca.Cert | b64enc | quote }} + tls.crt: {{ $coreCrt.Cert | b64enc | quote }} + tls.key: {{ $coreCrt.Key | b64enc | quote }} + +--- +apiVersion: v1 +kind: Secret +metadata: + name: "{{ template "harbor.internalTLS.jobservice.secretName" . }}" + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} +type: kubernetes.io/tls +data: + ca.crt: {{ $ca.Cert | b64enc | quote }} + tls.crt: {{ $jsCrt.Cert | b64enc | quote }} + tls.key: {{ $jsCrt.Key | b64enc | quote }} + +--- +apiVersion: v1 +kind: Secret +metadata: + name: "{{ template "harbor.internalTLS.registry.secretName" . }}" + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} +type: kubernetes.io/tls +data: + ca.crt: {{ $ca.Cert | b64enc | quote }} + tls.crt: {{ $regCrt.Cert | b64enc | quote }} + tls.key: {{ $regCrt.Key | b64enc | quote }} + +--- +apiVersion: v1 +kind: Secret +metadata: + name: "{{ template "harbor.internalTLS.portal.secretName" . }}" + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} +type: kubernetes.io/tls +data: + ca.crt: {{ $ca.Cert | b64enc | quote }} + tls.crt: {{ $portalCrt.Cert | b64enc | quote }} + tls.key: {{ $portalCrt.Key | b64enc | quote }} + +{{- if and .Values.trivy.enabled}} +--- +{{- $trivyCN := (include "harbor.trivy" .) }} +{{- $trivyCrt := genSignedCert $trivyCN nil (list $trivyCN) 365 $ca }} +apiVersion: v1 +kind: Secret +metadata: + name: "{{ template "harbor.internalTLS.trivy.secretName" . }}" + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} +type: kubernetes.io/tls +data: + ca.crt: {{ $ca.Cert | b64enc | quote }} + tls.crt: {{ $trivyCrt.Cert | b64enc | quote }} + tls.key: {{ $trivyCrt.Key | b64enc | quote }} +{{- end }} + +{{- end }} diff --git a/packages/system/harbor/charts/harbor/templates/jobservice/jobservice-cm-env.yaml b/packages/system/harbor/charts/harbor/templates/jobservice/jobservice-cm-env.yaml new file mode 100644 index 00000000..f1359131 --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/jobservice/jobservice-cm-env.yaml @@ -0,0 +1,37 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: "{{ template "harbor.jobservice" . }}-env" + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} +data: + CORE_URL: "{{ template "harbor.coreURL" . }}" + TOKEN_SERVICE_URL: "{{ template "harbor.tokenServiceURL" . }}" + REGISTRY_URL: "{{ template "harbor.registryURL" . }}" + REGISTRY_CONTROLLER_URL: "{{ template "harbor.registryControllerURL" . }}" + REGISTRY_CREDENTIAL_USERNAME: "{{ .Values.registry.credentials.username }}" + + JOBSERVICE_WEBHOOK_JOB_MAX_RETRY: "{{ .Values.jobservice.notification.webhook_job_max_retry }}" + JOBSERVICE_WEBHOOK_JOB_HTTP_CLIENT_TIMEOUT: "{{ .Values.jobservice.notification.webhook_job_http_client_timeout }}" + + LOG_LEVEL: "{{ .Values.logLevel }}" + + {{- if has "jobservice" .Values.proxy.components }} + HTTP_PROXY: "{{ .Values.proxy.httpProxy }}" + HTTPS_PROXY: "{{ .Values.proxy.httpsProxy }}" + NO_PROXY: "{{ template "harbor.noProxy" . }}" + {{- end }} + {{- if .Values.metrics.enabled}} + METRIC_NAMESPACE: harbor + METRIC_SUBSYSTEM: jobservice + {{- end }} + {{- template "harbor.traceEnvsForJobservice" . }} + {{- if .Values.cache.enabled }} + _REDIS_URL_CORE: "{{ template "harbor.redis.urlForCore" . }}" + CACHE_ENABLED: "true" + CACHE_EXPIRE_HOURS: "{{ .Values.cache.expireHours }}" + {{- end }} + {{- if or (and (eq .Values.redis.type "internal") .Values.redis.internal.cacheLayerDatabaseIndex) (and (eq .Values.redis.type "external") .Values.redis.external.cacheLayerDatabaseIndex) }} + _REDIS_URL_CACHE_LAYER: "{{ template "harbor.redis.urlForCache" . }}" + {{- end }} diff --git a/packages/system/harbor/charts/harbor/templates/jobservice/jobservice-cm.yaml b/packages/system/harbor/charts/harbor/templates/jobservice/jobservice-cm.yaml new file mode 100644 index 00000000..c950e678 --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/jobservice/jobservice-cm.yaml @@ -0,0 +1,58 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: "{{ template "harbor.jobservice" . }}" + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} +data: + config.yml: |+ + #Server listening port + protocol: "{{ template "harbor.component.scheme" . }}" + port: {{ template "harbor.jobservice.containerPort". }} + {{- if .Values.internalTLS.enabled }} + https_config: + cert: "/etc/harbor/ssl/jobservice/tls.crt" + key: "/etc/harbor/ssl/jobservice/tls.key" + {{- end }} + worker_pool: + workers: {{ .Values.jobservice.maxJobWorkers }} + backend: "redis" + redis_pool: + redis_url: "{{ template "harbor.redis.urlForJobservice" . }}" + namespace: "harbor_job_service_namespace" + idle_timeout_second: 3600 + job_loggers: + {{- if has "file" .Values.jobservice.jobLoggers }} + - name: "FILE" + level: {{ .Values.logLevel | upper }} + settings: # Customized settings of logger + base_dir: "/var/log/jobs" + sweeper: + duration: {{ .Values.jobservice.loggerSweeperDuration }} #days + settings: # Customized settings of sweeper + work_dir: "/var/log/jobs" + {{- end }} + {{- if has "database" .Values.jobservice.jobLoggers }} + - name: "DB" + level: {{ .Values.logLevel | upper }} + sweeper: + duration: {{ .Values.jobservice.loggerSweeperDuration }} #days + {{- end }} + {{- if has "stdout" .Values.jobservice.jobLoggers }} + - name: "STD_OUTPUT" + level: {{ .Values.logLevel | upper }} + {{- end }} + metric: + enabled: {{ .Values.metrics.enabled }} + path: {{ .Values.metrics.jobservice.path }} + port: {{ .Values.metrics.jobservice.port }} + #Loggers for the job service + loggers: + - name: "STD_OUTPUT" + level: {{ .Values.logLevel | upper }} + reaper: + # the max time to wait for a task to finish, if unfinished after max_update_hours, the task will be mark as error, but the task will continue to run, default value is 24 + max_update_hours: {{ .Values.jobservice.reaper.max_update_hours }} + # the max time for execution in running state without new task created + max_dangling_hours: {{ .Values.jobservice.reaper.max_dangling_hours }} diff --git a/packages/system/harbor/charts/harbor/templates/jobservice/jobservice-dpl.yaml b/packages/system/harbor/charts/harbor/templates/jobservice/jobservice-dpl.yaml new file mode 100644 index 00000000..3e426694 --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/jobservice/jobservice-dpl.yaml @@ -0,0 +1,183 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: "{{ template "harbor.jobservice" . }}" + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} + component: jobservice + app.kubernetes.io/component: jobservice +spec: + replicas: {{ .Values.jobservice.replicas }} + revisionHistoryLimit: {{ .Values.jobservice.revisionHistoryLimit }} + strategy: + type: {{ .Values.updateStrategy.type }} + {{- if eq .Values.updateStrategy.type "Recreate" }} + rollingUpdate: null + {{- end }} + selector: + matchLabels: +{{ include "harbor.matchLabels" . | indent 6 }} + component: jobservice + template: + metadata: + labels: +{{ include "harbor.labels" . | indent 8 }} + component: jobservice + app.kubernetes.io/component: jobservice +{{- if .Values.jobservice.podLabels }} +{{ toYaml .Values.jobservice.podLabels | indent 8 }} +{{- end }} + annotations: + checksum/configmap: {{ include (print $.Template.BasePath "/jobservice/jobservice-cm.yaml") . | sha256sum }} + checksum/configmap-env: {{ include (print $.Template.BasePath "/jobservice/jobservice-cm-env.yaml") . | sha256sum }} + checksum/secret: {{ include (print $.Template.BasePath "/jobservice/jobservice-secrets.yaml") . | sha256sum }} + checksum/secret-core: {{ include (print $.Template.BasePath "/core/core-secret.yaml") . | sha256sum }} +{{- if and .Values.internalTLS.enabled (eq .Values.internalTLS.certSource "auto") }} + checksum/tls: {{ include (print $.Template.BasePath "/internal/auto-tls.yaml") . | sha256sum }} +{{- else if and .Values.internalTLS.enabled (eq .Values.internalTLS.certSource "manual") }} + checksum/tls: {{ include (print $.Template.BasePath "/jobservice/jobservice-tls.yaml") . | sha256sum }} +{{- end }} +{{- if .Values.jobservice.podAnnotations }} +{{ toYaml .Values.jobservice.podAnnotations | indent 8 }} +{{- end }} + spec: + securityContext: + runAsUser: 10000 + fsGroup: 10000 +{{- if .Values.jobservice.serviceAccountName }} + serviceAccountName: {{ .Values.jobservice.serviceAccountName }} +{{- end -}} + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + automountServiceAccountToken: {{ .Values.jobservice.automountServiceAccountToken | default false }} + terminationGracePeriodSeconds: 120 +{{- with .Values.jobservice.topologySpreadConstraints}} + topologySpreadConstraints: +{{- range . }} + - {{ . | toYaml | indent 8 | trim }} + labelSelector: + matchLabels: +{{ include "harbor.matchLabels" $ | indent 12 }} + component: jobservice +{{- end }} +{{- end }} + {{- with .Values.jobservice.initContainers }} + initContainers: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: jobservice + image: {{ .Values.jobservice.image.repository }}:{{ .Values.jobservice.image.tag }} + imagePullPolicy: {{ .Values.imagePullPolicy }} + livenessProbe: + httpGet: + path: /api/v1/stats + scheme: {{ include "harbor.component.scheme" . | upper }} + port: {{ template "harbor.jobservice.containerPort" . }} + initialDelaySeconds: 300 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /api/v1/stats + scheme: {{ include "harbor.component.scheme" . | upper }} + port: {{ template "harbor.jobservice.containerPort" . }} + initialDelaySeconds: 20 + periodSeconds: 10 +{{- if .Values.jobservice.resources }} + resources: +{{ toYaml .Values.jobservice.resources | indent 10 }} +{{- end }} + env: + - name: CORE_SECRET + valueFrom: + secretKeyRef: + name: {{ default (include "harbor.core" .) .Values.core.existingSecret }} + key: secret + {{- if .Values.jobservice.existingSecret }} + - name: JOBSERVICE_SECRET + valueFrom: + secretKeyRef: + name: {{ .Values.jobservice.existingSecret }} + key: {{ .Values.jobservice.existingSecretKey }} + {{- end }} + {{- if .Values.internalTLS.enabled }} + - name: INTERNAL_TLS_ENABLED + value: "true" + - name: INTERNAL_TLS_KEY_PATH + value: /etc/harbor/ssl/jobservice/tls.key + - name: INTERNAL_TLS_CERT_PATH + value: /etc/harbor/ssl/jobservice/tls.crt + - name: INTERNAL_TLS_TRUST_CA_PATH + value: /etc/harbor/ssl/jobservice/ca.crt + {{- end }} + {{- if .Values.registry.credentials.existingSecret }} + - name: REGISTRY_CREDENTIAL_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.registry.credentials.existingSecret }} + key: REGISTRY_PASSWD + {{- end }} +{{- with .Values.jobservice.extraEnvVars }} +{{- toYaml . | nindent 10 }} +{{- end }} + {{- if not (empty .Values.containerSecurityContext) }} + securityContext: {{ .Values.containerSecurityContext | toYaml | nindent 10 }} + {{- end }} + envFrom: + - configMapRef: + name: "{{ template "harbor.jobservice" . }}-env" + - secretRef: + name: "{{ template "harbor.jobservice" . }}" + ports: + - containerPort: {{ template "harbor.jobservice.containerPort" . }} + volumeMounts: + - name: jobservice-config + mountPath: /etc/jobservice/config.yml + subPath: config.yml + - name: job-logs + mountPath: /var/log/jobs + subPath: {{ .Values.persistence.persistentVolumeClaim.jobservice.jobLog.subPath }} + {{- if .Values.internalTLS.enabled }} + - name: jobservice-internal-certs + mountPath: /etc/harbor/ssl/jobservice + {{- end }} + {{- if .Values.caBundleSecretName }} +{{ include "harbor.caBundleVolumeMount" . | indent 8 }} + {{- end }} + volumes: + - name: jobservice-config + configMap: + name: "{{ template "harbor.jobservice" . }}" + - name: job-logs + {{- if and .Values.persistence.enabled (has "file" .Values.jobservice.jobLoggers) }} + persistentVolumeClaim: + claimName: {{ .Values.persistence.persistentVolumeClaim.jobservice.jobLog.existingClaim | default (include "harbor.jobservice" .) }} + {{- else }} + emptyDir: {} + {{- end }} + {{- if .Values.internalTLS.enabled }} + - name: jobservice-internal-certs + secret: + secretName: {{ template "harbor.internalTLS.jobservice.secretName" . }} + {{- end }} + {{- if .Values.caBundleSecretName }} +{{ include "harbor.caBundleVolume" . | indent 6 }} + {{- end }} + {{- with .Values.jobservice.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.jobservice.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.jobservice.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + {{- if .Values.jobservice.priorityClassName }} + priorityClassName: {{ .Values.jobservice.priorityClassName }} + {{- end }} diff --git a/packages/system/harbor/charts/harbor/templates/jobservice/jobservice-pvc.yaml b/packages/system/harbor/charts/harbor/templates/jobservice/jobservice-pvc.yaml new file mode 100644 index 00000000..eb781eed --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/jobservice/jobservice-pvc.yaml @@ -0,0 +1,32 @@ +{{- $jobLog := .Values.persistence.persistentVolumeClaim.jobservice.jobLog -}} +{{- if and .Values.persistence.enabled (not $jobLog.existingClaim) (has "file" .Values.jobservice.jobLoggers) }} +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: {{ template "harbor.jobservice" . }} + namespace: {{ .Release.Namespace | quote }} + annotations: + {{- range $key, $value := $jobLog.annotations }} + {{ $key }}: {{ $value | quote }} + {{- end }} + {{- if eq .Values.persistence.resourcePolicy "keep" }} + helm.sh/resource-policy: keep + {{- end }} + labels: +{{ include "harbor.labels" . | indent 4 }} + component: jobservice + app.kubernetes.io/component: jobservice +spec: + accessModes: + - {{ $jobLog.accessMode }} + resources: + requests: + storage: {{ $jobLog.size }} + {{- if $jobLog.storageClass }} + {{- if eq "-" $jobLog.storageClass }} + storageClassName: "" + {{- else }} + storageClassName: {{ $jobLog.storageClass }} + {{- end }} + {{- end }} +{{- end }} diff --git a/packages/system/harbor/charts/harbor/templates/jobservice/jobservice-secrets.yaml b/packages/system/harbor/charts/harbor/templates/jobservice/jobservice-secrets.yaml new file mode 100644 index 00000000..7706c351 --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/jobservice/jobservice-secrets.yaml @@ -0,0 +1,17 @@ +{{- $existingSecret := lookup "v1" "Secret" .Release.Namespace (include "harbor.jobservice" .) }} +apiVersion: v1 +kind: Secret +metadata: + name: "{{ template "harbor.jobservice" . }}" + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} +type: Opaque +data: + {{- if not .Values.jobservice.existingSecret }} + JOBSERVICE_SECRET: {{ .Values.jobservice.secret | default (include "harbor.secretKeyHelper" (dict "key" "JOBSERVICE_SECRET" "data" $existingSecret.data)) | default (randAlphaNum 16) | b64enc | quote }} + {{- end }} + {{- if not .Values.registry.credentials.existingSecret }} + REGISTRY_CREDENTIAL_PASSWORD: {{ .Values.registry.credentials.password | b64enc | quote }} + {{- end }} + {{- template "harbor.traceJaegerPassword" . }} diff --git a/packages/system/harbor/charts/harbor/templates/jobservice/jobservice-svc.yaml b/packages/system/harbor/charts/harbor/templates/jobservice/jobservice-svc.yaml new file mode 100644 index 00000000..9bddc43c --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/jobservice/jobservice-svc.yaml @@ -0,0 +1,25 @@ +apiVersion: v1 +kind: Service +metadata: + name: "{{ template "harbor.jobservice" . }}" + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} +spec: +{{- if .Values.ipFamily.policy }} + ipFamilyPolicy: {{ .Values.ipFamily.policy }} +{{- end }} +{{- if .Values.ipFamily.families }} + ipFamilies: {{ toYaml .Values.ipFamily.families | nindent 4 }} +{{- end }} + ports: + - name: {{ ternary "https-jobservice" "http-jobservice" .Values.internalTLS.enabled }} + port: {{ template "harbor.jobservice.servicePort" . }} + targetPort: {{ template "harbor.jobservice.containerPort" . }} +{{- if .Values.metrics.enabled }} + - name: {{ template "harbor.metricsPortName" . }} + port: {{ .Values.metrics.jobservice.port }} +{{- end }} + selector: +{{ include "harbor.matchLabels" . | indent 4 }} + component: jobservice diff --git a/packages/system/harbor/charts/harbor/templates/jobservice/jobservice-tls.yaml b/packages/system/harbor/charts/harbor/templates/jobservice/jobservice-tls.yaml new file mode 100644 index 00000000..58809ec4 --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/jobservice/jobservice-tls.yaml @@ -0,0 +1,16 @@ +{{- if and .Values.internalTLS.enabled }} +{{- if eq .Values.internalTLS.certSource "manual" }} +apiVersion: v1 +kind: Secret +metadata: + name: "{{ template "harbor.internalTLS.jobservice.secretName" . }}" + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} +type: kubernetes.io/tls +data: + ca.crt: {{ (required "The \"internalTLS.trustCa\" is required!" .Values.internalTLS.trustCa) | b64enc | quote }} + tls.crt: {{ (required "The \"internalTLS.jobservice.crt\" is required!" .Values.internalTLS.jobservice.crt) | b64enc | quote }} + tls.key: {{ (required "The \"internalTLS.jobservice.key\" is required!" .Values.internalTLS.jobservice.key) | b64enc | quote }} +{{- end }} +{{- end }} diff --git a/packages/system/harbor/charts/harbor/templates/metrics/metrics-svcmon.yaml b/packages/system/harbor/charts/harbor/templates/metrics/metrics-svcmon.yaml new file mode 100644 index 00000000..d566285e --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/metrics/metrics-svcmon.yaml @@ -0,0 +1,29 @@ +{{- if and .Values.metrics.enabled .Values.metrics.serviceMonitor.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ template "harbor.fullname" . }} + namespace: {{ .Release.Namespace | quote }} + labels: {{ include "harbor.labels" . | nindent 4 }} +{{- if .Values.metrics.serviceMonitor.additionalLabels }} +{{ toYaml .Values.metrics.serviceMonitor.additionalLabels | indent 4 }} +{{- end }} +spec: + jobLabel: app.kubernetes.io/name + endpoints: + - port: {{ template "harbor.metricsPortName" . }} + {{- if .Values.metrics.serviceMonitor.interval }} + interval: {{ .Values.metrics.serviceMonitor.interval }} + {{- end }} + honorLabels: true +{{- if .Values.metrics.serviceMonitor.metricRelabelings }} + metricRelabelings: +{{ tpl (toYaml .Values.metrics.serviceMonitor.metricRelabelings | indent 4) . }} +{{- end }} +{{- if .Values.metrics.serviceMonitor.relabelings }} + relabelings: +{{ toYaml .Values.metrics.serviceMonitor.relabelings | indent 4 }} +{{- end }} + selector: + matchLabels: {{ include "harbor.matchLabels" . | nindent 6 }} +{{- end }} diff --git a/packages/system/harbor/charts/harbor/templates/nginx/configmap-http.yaml b/packages/system/harbor/charts/harbor/templates/nginx/configmap-http.yaml new file mode 100644 index 00000000..78efa187 --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/nginx/configmap-http.yaml @@ -0,0 +1,136 @@ +{{- if and (ne .Values.expose.type "ingress") (ne .Values.expose.type "route") (not .Values.expose.tls.enabled) }} +{{- $scheme := (include "harbor.component.scheme" .) -}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ template "harbor.nginx" . }} + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} +data: + nginx.conf: |+ + worker_processes auto; + pid /tmp/nginx.pid; + + events { + worker_connections 3096; + use epoll; + multi_accept on; + } + + http { + client_body_temp_path /tmp/client_body_temp; + proxy_temp_path /tmp/proxy_temp; + fastcgi_temp_path /tmp/fastcgi_temp; + uwsgi_temp_path /tmp/uwsgi_temp; + scgi_temp_path /tmp/scgi_temp; + tcp_nodelay on; + + # this is necessary for us to be able to disable request buffering in all cases + proxy_http_version 1.1; + + upstream core { + server "{{ template "harbor.core" . }}:{{ template "harbor.core.servicePort" . }}"; + } + + upstream portal { + server {{ template "harbor.portal" . }}:{{ template "harbor.portal.servicePort" . }}; + } + + log_format timed_combined '[$time_local]:$remote_addr - ' + '"$request" $status $body_bytes_sent ' + '"$http_referer" "$http_user_agent" ' + '$request_time $upstream_response_time $pipe'; + + access_log /dev/stdout timed_combined; + + map $http_x_forwarded_proto $x_forwarded_proto { + default $http_x_forwarded_proto; + "" $scheme; + } + + server { + {{- if .Values.ipFamily.ipv4.enabled}} + listen 8080; + {{- end}} + {{- if .Values.ipFamily.ipv6.enabled }} + listen [::]:8080; + {{- end }} + server_tokens off; + # disable any limits to avoid HTTP 413 for large image uploads + client_max_body_size 0; + + # Add extra headers + add_header X-Frame-Options DENY; + add_header Content-Security-Policy "frame-ancestors 'none'"; + + location / { + proxy_pass {{ $scheme }}://portal/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $x_forwarded_proto; + + proxy_buffering off; + proxy_request_buffering off; + } + + location /api/ { + proxy_pass {{ $scheme }}://core/api/; + {{- if and .Values.internalTLS.enabled }} + proxy_ssl_verify off; + proxy_ssl_session_reuse on; + {{- end }} + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $x_forwarded_proto; + + proxy_buffering off; + proxy_request_buffering off; + } + + location /c/ { + proxy_pass {{ $scheme }}://core/c/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $x_forwarded_proto; + + proxy_buffering off; + proxy_request_buffering off; + } + + location /v1/ { + return 404; + } + + location /v2/ { + proxy_pass {{ $scheme }}://core/v2/; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $x_forwarded_proto; + proxy_buffering off; + proxy_request_buffering off; + proxy_send_timeout 900; + proxy_read_timeout 900; + } + + location /service/ { + proxy_pass {{ $scheme }}://core/service/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $x_forwarded_proto; + + proxy_buffering off; + proxy_request_buffering off; + } + + location /service/notifications { + return 404; + } + } + } +{{- end }} diff --git a/packages/system/harbor/charts/harbor/templates/nginx/configmap-https.yaml b/packages/system/harbor/charts/harbor/templates/nginx/configmap-https.yaml new file mode 100644 index 00000000..3a80e292 --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/nginx/configmap-https.yaml @@ -0,0 +1,173 @@ +{{- if and (ne .Values.expose.type "ingress") (ne .Values.expose.type "route") (.Values.expose.tls.enabled) }} +{{- $scheme := (include "harbor.component.scheme" .) -}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ template "harbor.nginx" . }} + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} +data: + nginx.conf: |+ + worker_processes auto; + pid /tmp/nginx.pid; + + events { + worker_connections 3096; + use epoll; + multi_accept on; + } + + http { + client_body_temp_path /tmp/client_body_temp; + proxy_temp_path /tmp/proxy_temp; + fastcgi_temp_path /tmp/fastcgi_temp; + uwsgi_temp_path /tmp/uwsgi_temp; + scgi_temp_path /tmp/scgi_temp; + tcp_nodelay on; + + # this is necessary for us to be able to disable request buffering in all cases + proxy_http_version 1.1; + + upstream core { + server "{{ template "harbor.core" . }}:{{ template "harbor.core.servicePort" . }}"; + } + + upstream portal { + server "{{ template "harbor.portal" . }}:{{ template "harbor.portal.servicePort" . }}"; + } + + log_format timed_combined '[$time_local]:$remote_addr - ' + '"$request" $status $body_bytes_sent ' + '"$http_referer" "$http_user_agent" ' + '$request_time $upstream_response_time $pipe'; + + access_log /dev/stdout timed_combined; + + map $http_x_forwarded_proto $x_forwarded_proto { + default $http_x_forwarded_proto; + "" $scheme; + } + + server { + {{- if .Values.ipFamily.ipv4.enabled }} + listen 8443 ssl; + {{- end}} + {{- if .Values.ipFamily.ipv6.enabled }} + listen [::]:8443 ssl; + {{- end }} + # server_name harbordomain.com; + server_tokens off; + # SSL + ssl_certificate /etc/nginx/cert/tls.crt; + ssl_certificate_key /etc/nginx/cert/tls.key; + + # Recommendations from https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html + ssl_protocols TLSv1.2 TLSv1.3; + {{- if .Values.internalTLS.strong_ssl_ciphers }} + ssl_ciphers ECDHE+AESGCM:DHE+AESGCM:ECDHE+RSA+SHA256:DHE+RSA+SHA256:!AES128; + {{ else }} + ssl_ciphers '!aNULL:kECDH+AESGCM:ECDH+AESGCM:RSA+AESGCM:kECDH+AES:ECDH+AES:RSA+AES:'; + {{- end }} + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + + # disable any limits to avoid HTTP 413 for large image uploads + client_max_body_size 0; + + # required to avoid HTTP 411: see Issue #1486 (https://github.com/docker/docker/issues/1486) + chunked_transfer_encoding on; + + # Add extra headers + add_header Strict-Transport-Security "max-age=31536000; includeSubdomains; preload"; + add_header X-Frame-Options DENY; + add_header Content-Security-Policy "frame-ancestors 'none'"; + + location / { + proxy_pass {{ $scheme }}://portal/; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $x_forwarded_proto; + + proxy_cookie_path / "/; HttpOnly; Secure"; + + proxy_buffering off; + proxy_request_buffering off; + } + + location /api/ { + proxy_pass {{ $scheme }}://core/api/; + {{- if and .Values.internalTLS.enabled }} + proxy_ssl_verify off; + proxy_ssl_session_reuse on; + {{- end }} + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $x_forwarded_proto; + + proxy_cookie_path / "/; Secure"; + + proxy_buffering off; + proxy_request_buffering off; + } + + location /c/ { + proxy_pass {{ $scheme }}://core/c/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $x_forwarded_proto; + + proxy_cookie_path / "/; Secure"; + + proxy_buffering off; + proxy_request_buffering off; + } + + location /v1/ { + return 404; + } + + location /v2/ { + proxy_pass {{ $scheme }}://core/v2/; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $x_forwarded_proto; + proxy_buffering off; + proxy_request_buffering off; + proxy_send_timeout 900; + proxy_read_timeout 900; + } + + location /service/ { + proxy_pass {{ $scheme }}://core/service/; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $x_forwarded_proto; + + proxy_cookie_path / "/; Secure"; + + proxy_buffering off; + proxy_request_buffering off; + } + + location /service/notifications { + return 404; + } + } + server { + {{- if .Values.ipFamily.ipv4.enabled }} + listen 8080; + {{- end}} + {{- if .Values.ipFamily.ipv6.enabled }} + listen [::]:8080; + {{- end}} + #server_name harbordomain.com; + return 301 https://$host$request_uri; + } + } +{{- end }} diff --git a/packages/system/harbor/charts/harbor/templates/nginx/deployment.yaml b/packages/system/harbor/charts/harbor/templates/nginx/deployment.yaml new file mode 100644 index 00000000..0c8bc40f --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/nginx/deployment.yaml @@ -0,0 +1,133 @@ +{{- if and (ne .Values.expose.type "ingress") (ne .Values.expose.type "route") }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ template "harbor.nginx" . }} + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} + component: nginx + app.kubernetes.io/component: nginx +spec: + replicas: {{ .Values.nginx.replicas }} + revisionHistoryLimit: {{ .Values.nginx.revisionHistoryLimit }} + selector: + matchLabels: +{{ include "harbor.matchLabels" . | indent 6 }} + component: nginx + template: + metadata: + labels: +{{ include "harbor.labels" . | indent 8 }} + component: nginx + app.kubernetes.io/component: nginx +{{- if .Values.nginx.podLabels }} +{{ toYaml .Values.nginx.podLabels | indent 8 }} +{{- end }} + annotations: + {{- if not .Values.expose.tls.enabled }} + checksum/configmap: {{ include (print $.Template.BasePath "/nginx/configmap-http.yaml") . | sha256sum }} + {{- else }} + checksum/configmap: {{ include (print $.Template.BasePath "/nginx/configmap-https.yaml") . | sha256sum }} + {{- end }} + {{- if eq (include "harbor.autoGenCertForNginx" .) "true" }} + checksum/secret: {{ include (print $.Template.BasePath "/nginx/secret.yaml") . | sha256sum }} + {{- end }} +{{- if .Values.nginx.podAnnotations }} +{{ toYaml .Values.nginx.podAnnotations | indent 8 }} +{{- end }} + spec: +{{- if .Values.nginx.serviceAccountName }} + serviceAccountName: {{ .Values.nginx.serviceAccountName }} +{{- end }} + securityContext: + runAsUser: 10000 + fsGroup: 10000 + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + automountServiceAccountToken: {{ .Values.nginx.automountServiceAccountToken | default false }} +{{- with .Values.nginx.topologySpreadConstraints}} + topologySpreadConstraints: +{{- range . }} + - {{ . | toYaml | indent 8 | trim }} + labelSelector: + matchLabels: +{{ include "harbor.matchLabels" $ | indent 12 }} + component: nginx +{{- end }} +{{- end }} + containers: + - name: nginx + image: "{{ .Values.nginx.image.repository }}:{{ .Values.nginx.image.tag }}" + imagePullPolicy: "{{ .Values.imagePullPolicy }}" + {{- $_ := set . "scheme" "HTTP" -}} + {{- $_ := set . "port" "8080" -}} + {{- if .Values.expose.tls.enabled }} + {{- $_ := set . "scheme" "HTTPS" -}} + {{- $_ := set . "port" "8443" -}} + {{- end }} + livenessProbe: + httpGet: + scheme: {{ .scheme }} + path: / + port: {{ .port }} + initialDelaySeconds: 300 + periodSeconds: 10 + readinessProbe: + httpGet: + scheme: {{ .scheme }} + path: / + port: {{ .port }} + initialDelaySeconds: 1 + periodSeconds: 10 +{{- if .Values.nginx.resources }} + resources: +{{ toYaml .Values.nginx.resources | indent 10 }} +{{- end }} +{{- with .Values.nginx.extraEnvVars }} + env: +{{- toYaml . | nindent 10 }} +{{- end }} + {{- if not (empty .Values.containerSecurityContext) }} + securityContext: {{ .Values.containerSecurityContext | toYaml | nindent 10 }} + {{- end }} + ports: + - containerPort: 8080 + {{- if .Values.expose.tls.enabled }} + - containerPort: 8443 + {{- end }} + volumeMounts: + - name: config + mountPath: /etc/nginx/nginx.conf + subPath: nginx.conf + {{- if .Values.expose.tls.enabled }} + - name: certificate + mountPath: /etc/nginx/cert + {{- end }} + volumes: + - name: config + configMap: + name: {{ template "harbor.nginx" . }} + {{- if .Values.expose.tls.enabled }} + - name: certificate + secret: + secretName: {{ template "harbor.tlsSecretForNginx" . }} + {{- end }} + {{- with .Values.nginx.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.nginx.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.nginx.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + {{- if .Values.nginx.priorityClassName }} + priorityClassName: {{ .Values.nginx.priorityClassName }} + {{- end }} +{{- end }} diff --git a/packages/system/harbor/charts/harbor/templates/nginx/secret.yaml b/packages/system/harbor/charts/harbor/templates/nginx/secret.yaml new file mode 100644 index 00000000..b855e7e9 --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/nginx/secret.yaml @@ -0,0 +1,26 @@ +{{- if and (ne .Values.expose.type "ingress") (ne .Values.expose.type "route") (.Values.expose.tls.enabled) }} +{{- if eq (include "harbor.autoGenCertForNginx" .) "true" }} +{{- $ca := genCA "harbor-ca" 365 }} +{{- $cn := (required "The \"expose.tls.auto.commonName\" is required!" .Values.expose.tls.auto.commonName) }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ template "harbor.nginx" . }} + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} +type: Opaque +data: + {{- if regexMatch `^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$` $cn }} + {{- $cert := genSignedCert $cn (list $cn) nil 365 $ca }} + tls.crt: {{ $cert.Cert | b64enc | quote }} + tls.key: {{ $cert.Key | b64enc | quote }} + ca.crt: {{ $ca.Cert | b64enc | quote }} + {{- else }} + {{- $cert := genSignedCert $cn nil (list $cn) 365 $ca }} + tls.crt: {{ $cert.Cert | b64enc | quote }} + tls.key: {{ $cert.Key | b64enc | quote }} + ca.crt: {{ $ca.Cert | b64enc | quote }} + {{- end }} +{{- end }} +{{- end }} diff --git a/packages/system/harbor/charts/harbor/templates/nginx/service.yaml b/packages/system/harbor/charts/harbor/templates/nginx/service.yaml new file mode 100644 index 00000000..9554cd43 --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/nginx/service.yaml @@ -0,0 +1,101 @@ +{{- if or (eq .Values.expose.type "clusterIP") (eq .Values.expose.type "nodePort") (eq .Values.expose.type "loadBalancer") }} +apiVersion: v1 +kind: Service +metadata: +{{- if eq .Values.expose.type "clusterIP" }} +{{- $clusterIP := .Values.expose.clusterIP }} + name: {{ $clusterIP.name }} + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} +{{- if .Values.expose.clusterIP.labels }} +{{ toYaml $clusterIP.labels | indent 4 }} +{{- end }} +{{- with $clusterIP.annotations }} + annotations: + {{- toYaml . | nindent 4 }} +{{- end }} +spec: + type: ClusterIP + {{- if .Values.expose.clusterIP.staticClusterIP }} + clusterIP: {{ .Values.expose.clusterIP.staticClusterIP }} + {{- end }} + {{- if .Values.ipFamily.policy }} + ipFamilyPolicy: {{ .Values.ipFamily.policy }} + {{- end }} + {{- if .Values.ipFamily.families }} + ipFamilies: {{ toYaml .Values.ipFamily.families }} + {{- end }} + ports: + - name: http + port: {{ $clusterIP.ports.httpPort }} + targetPort: 8080 + {{- if .Values.expose.tls.enabled }} + - name: https + port: {{ $clusterIP.ports.httpsPort }} + targetPort: 8443 + {{- end }} +{{- else if eq .Values.expose.type "nodePort" }} +{{- $nodePort := .Values.expose.nodePort }} + name: {{ $nodePort.name }} + labels: +{{ include "harbor.labels" . | indent 4 }} +{{- if .Values.expose.nodePort.labels }} +{{ toYaml $nodePort.labels | indent 4 }} +{{- end }} +{{- with $nodePort.annotations }} + annotations: + {{- toYaml . | nindent 4 }} +{{- end }} +spec: + type: NodePort + ports: + - name: http + port: {{ $nodePort.ports.http.port }} + targetPort: 8080 + {{- if $nodePort.ports.http.nodePort }} + nodePort: {{ $nodePort.ports.http.nodePort }} + {{- end }} + {{- if .Values.expose.tls.enabled }} + - name: https + port: {{ $nodePort.ports.https.port }} + targetPort: 8443 + {{- if $nodePort.ports.https.nodePort }} + nodePort: {{ $nodePort.ports.https.nodePort }} + {{- end }} + {{- end }} +{{- else if eq .Values.expose.type "loadBalancer" }} +{{- $loadBalancer := .Values.expose.loadBalancer }} + name: {{ $loadBalancer.name }} + labels: +{{ include "harbor.labels" . | indent 4 }} +{{- if .Values.expose.loadBalancer.labels }} +{{ toYaml $loadBalancer.labels | indent 4 }} +{{- end }} +{{- with $loadBalancer.annotations }} + annotations: + {{- toYaml . | nindent 4 }} +{{- end }} +spec: + type: LoadBalancer + {{- with $loadBalancer.sourceRanges }} + loadBalancerSourceRanges: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- if $loadBalancer.IP }} + loadBalancerIP: {{ $loadBalancer.IP }} + {{- end }} + ports: + - name: http + port: {{ $loadBalancer.ports.httpPort }} + targetPort: 8080 + {{- if .Values.expose.tls.enabled }} + - name: https + port: {{ $loadBalancer.ports.httpsPort }} + targetPort: 8443 + {{- end }} +{{- end }} + selector: +{{ include "harbor.matchLabels" . | indent 4 }} + component: nginx +{{- end }} diff --git a/packages/system/harbor/charts/harbor/templates/portal/configmap.yaml b/packages/system/harbor/charts/harbor/templates/portal/configmap.yaml new file mode 100644 index 00000000..af56783a --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/portal/configmap.yaml @@ -0,0 +1,68 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: "{{ template "harbor.portal" . }}" + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} +data: + nginx.conf: |+ + worker_processes auto; + pid /tmp/nginx.pid; + events { + worker_connections 1024; + } + http { + client_body_temp_path /tmp/client_body_temp; + proxy_temp_path /tmp/proxy_temp; + fastcgi_temp_path /tmp/fastcgi_temp; + uwsgi_temp_path /tmp/uwsgi_temp; + scgi_temp_path /tmp/scgi_temp; + server { + {{- if .Values.internalTLS.enabled }} + {{- if .Values.ipFamily.ipv4.enabled}} + listen {{ template "harbor.portal.containerPort" . }} ssl; + {{- end }} + {{- if .Values.ipFamily.ipv6.enabled}} + listen [::]:{{ template "harbor.portal.containerPort" . }} ssl; + {{- end }} + # SSL + ssl_certificate /etc/harbor/ssl/portal/tls.crt; + ssl_certificate_key /etc/harbor/ssl/portal/tls.key; + + # Recommendations from https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html + ssl_protocols TLSv1.2 TLSv1.3; + {{- if .Values.internalTLS.strong_ssl_ciphers }} + ssl_ciphers ECDHE+AESGCM:DHE+AESGCM:ECDHE+RSA+SHA256:DHE+RSA+SHA256:!AES128; + {{ else }} + ssl_ciphers '!aNULL:kECDH+AESGCM:ECDH+AESGCM:RSA+AESGCM:kECDH+AES:ECDH+AES:RSA+AES:'; + {{- end }} + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + {{- else }} + {{- if .Values.ipFamily.ipv4.enabled }} + listen {{ template "harbor.portal.containerPort" . }}; + {{- end }} + {{- if .Values.ipFamily.ipv6.enabled}} + listen [::]:{{ template "harbor.portal.containerPort" . }}; + {{- end }} + {{- end }} + server_name localhost; + root /usr/share/nginx/html; + index index.html index.htm; + include /etc/nginx/mime.types; + gzip on; + gzip_min_length 1000; + gzip_proxied expired no-cache no-store private auth; + gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript; + location /devcenter-api-2.0 { + try_files $uri $uri/ /swagger-ui-index.html; + } + location / { + try_files $uri $uri/ /index.html; + } + location = /index.html { + add_header Cache-Control "no-store, no-cache, must-revalidate"; + } + } + } diff --git a/packages/system/harbor/charts/harbor/templates/portal/deployment.yaml b/packages/system/harbor/charts/harbor/templates/portal/deployment.yaml new file mode 100644 index 00000000..88bcd497 --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/portal/deployment.yaml @@ -0,0 +1,124 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: "{{ template "harbor.portal" . }}" + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} + component: portal + app.kubernetes.io/component: portal +spec: + replicas: {{ .Values.portal.replicas }} + revisionHistoryLimit: {{ .Values.portal.revisionHistoryLimit }} + selector: + matchLabels: +{{ include "harbor.matchLabels" . | indent 6 }} + component: portal + template: + metadata: + labels: +{{ include "harbor.labels" . | indent 8 }} + component: portal + app.kubernetes.io/component: portal +{{- if .Values.portal.podLabels }} +{{ toYaml .Values.portal.podLabels | indent 8 }} +{{- end }} + annotations: +{{- if and .Values.internalTLS.enabled (eq .Values.internalTLS.certSource "auto") }} + checksum/tls: {{ include (print $.Template.BasePath "/internal/auto-tls.yaml") . | sha256sum }} +{{- else if and .Values.internalTLS.enabled (eq .Values.internalTLS.certSource "manual") }} + checksum/tls: {{ include (print $.Template.BasePath "/portal/tls.yaml") . | sha256sum }} +{{- end }} + checksum/configmap: {{ include (print $.Template.BasePath "/portal/configmap.yaml") . | sha256sum }} +{{- if .Values.portal.podAnnotations }} +{{ toYaml .Values.portal.podAnnotations | indent 8 }} +{{- end }} + spec: + securityContext: + runAsUser: 10000 + fsGroup: 10000 + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- if .Values.portal.serviceAccountName }} + serviceAccountName: {{ .Values.portal.serviceAccountName }} +{{- end }} + automountServiceAccountToken: {{ .Values.portal.automountServiceAccountToken | default false }} +{{- with .Values.portal.topologySpreadConstraints}} + topologySpreadConstraints: +{{- range . }} + - {{ . | toYaml | indent 8 | trim }} + labelSelector: + matchLabels: +{{ include "harbor.matchLabels" $ | indent 12 }} + component: portal +{{- end }} +{{- end }} + {{- with .Values.portal.initContainers }} + initContainers: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: portal + image: {{ .Values.portal.image.repository }}:{{ .Values.portal.image.tag }} + imagePullPolicy: {{ .Values.imagePullPolicy }} +{{- if .Values.portal.resources }} + resources: +{{ toYaml .Values.portal.resources | indent 10 }} +{{- end }} +{{- with .Values.portal.extraEnvVars }} + env: +{{- toYaml . | nindent 10 }} +{{- end }} + {{- if not (empty .Values.containerSecurityContext) }} + securityContext: {{ .Values.containerSecurityContext | toYaml | nindent 10 }} + {{- end }} + livenessProbe: + httpGet: + path: / + scheme: {{ include "harbor.component.scheme" . | upper }} + port: {{ template "harbor.portal.containerPort" . }} + initialDelaySeconds: 300 + periodSeconds: 10 + readinessProbe: + httpGet: + path: / + scheme: {{ include "harbor.component.scheme" . | upper }} + port: {{ template "harbor.portal.containerPort" . }} + initialDelaySeconds: 1 + periodSeconds: 10 + ports: + - containerPort: {{ template "harbor.portal.containerPort" . }} + volumeMounts: + - name: portal-config + mountPath: /etc/nginx/nginx.conf + subPath: nginx.conf + {{- if .Values.internalTLS.enabled }} + - name: portal-internal-certs + mountPath: /etc/harbor/ssl/portal + {{- end }} + volumes: + - name: portal-config + configMap: + name: "{{ template "harbor.portal" . }}" + {{- if .Values.internalTLS.enabled }} + - name: portal-internal-certs + secret: + secretName: {{ template "harbor.internalTLS.portal.secretName" . }} + {{- end }} + {{- with .Values.portal.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.portal.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.portal.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + {{- if .Values.portal.priorityClassName }} + priorityClassName: {{ .Values.portal.priorityClassName }} + {{- end }} diff --git a/packages/system/harbor/charts/harbor/templates/portal/service.yaml b/packages/system/harbor/charts/harbor/templates/portal/service.yaml new file mode 100644 index 00000000..2ce2482b --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/portal/service.yaml @@ -0,0 +1,27 @@ +apiVersion: v1 +kind: Service +metadata: + name: "{{ template "harbor.portal" . }}" + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} +{{- with .Values.portal.serviceAnnotations }} + annotations: + {{- toYaml . | nindent 4 }} +{{- end }} +spec: +{{- if or (eq .Values.expose.ingress.controller "gce") (eq .Values.expose.ingress.controller "alb") (eq .Values.expose.ingress.controller "f5-bigip") }} + type: NodePort +{{- end }} +{{- if .Values.ipFamily.policy }} + ipFamilyPolicy: {{ .Values.ipFamily.policy }} +{{- end }} +{{- if .Values.ipFamily.families }} + ipFamilies: {{ toYaml .Values.ipFamily.families | nindent 4 }} +{{- end }} + ports: + - port: {{ template "harbor.portal.servicePort" . }} + targetPort: {{ template "harbor.portal.containerPort" . }} + selector: +{{ include "harbor.matchLabels" . | indent 4 }} + component: portal diff --git a/packages/system/harbor/charts/harbor/templates/portal/tls.yaml b/packages/system/harbor/charts/harbor/templates/portal/tls.yaml new file mode 100644 index 00000000..e61a7d3a --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/portal/tls.yaml @@ -0,0 +1,16 @@ +{{- if and .Values.internalTLS.enabled }} +{{- if eq .Values.internalTLS.certSource "manual" }} +apiVersion: v1 +kind: Secret +metadata: + name: "{{ template "harbor.internalTLS.portal.secretName" . }}" + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} +type: kubernetes.io/tls +data: + ca.crt: {{ (required "The \"internalTLS.trustCa\" is required!" .Values.internalTLS.trustCa) | b64enc | quote }} + tls.crt: {{ (required "The \"internalTLS.portal.crt\" is required!" .Values.internalTLS.portal.crt) | b64enc | quote }} + tls.key: {{ (required "The \"internalTLS.portal.key\" is required!" .Values.internalTLS.portal.key) | b64enc | quote }} +{{- end }} +{{- end }} diff --git a/packages/system/harbor/charts/harbor/templates/redis/service.yaml b/packages/system/harbor/charts/harbor/templates/redis/service.yaml new file mode 100644 index 00000000..8bb8e85b --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/redis/service.yaml @@ -0,0 +1,21 @@ +{{- if eq .Values.redis.type "internal" -}} +apiVersion: v1 +kind: Service +metadata: + name: {{ template "harbor.redis" . }} + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} +spec: + {{- if .Values.ipFamily.policy }} + ipFamilyPolicy: {{ .Values.ipFamily.policy }} + {{- end }} + {{- if .Values.ipFamily.families }} + ipFamilies: {{ toYaml .Values.ipFamily.families }} + {{- end }} + ports: + - port: 6379 + selector: +{{ include "harbor.matchLabels" . | indent 4 }} + component: redis +{{- end -}} diff --git a/packages/system/harbor/charts/harbor/templates/redis/statefulset.yaml b/packages/system/harbor/charts/harbor/templates/redis/statefulset.yaml new file mode 100644 index 00000000..2316f69b --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/redis/statefulset.yaml @@ -0,0 +1,128 @@ +{{- if eq .Values.redis.type "internal" -}} +{{- $redis := .Values.persistence.persistentVolumeClaim.redis -}} +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ template "harbor.redis" . }} + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} + component: redis + app.kubernetes.io/component: redis +spec: + replicas: 1 + serviceName: {{ template "harbor.redis" . }} + selector: + matchLabels: +{{ include "harbor.matchLabels" . | indent 6 }} + component: redis + template: + metadata: + labels: +{{ include "harbor.labels" . | indent 8 }} + component: redis + app.kubernetes.io/component: redis +{{- if .Values.redis.podLabels }} +{{ toYaml .Values.redis.podLabels | indent 8 }} +{{- end }} +{{- if .Values.redis.podAnnotations }} + annotations: +{{ toYaml .Values.redis.podAnnotations | indent 8 }} +{{- end }} + spec: + securityContext: + runAsUser: 999 + fsGroup: 999 +{{- if .Values.redis.internal.serviceAccountName }} + serviceAccountName: {{ .Values.redis.internal.serviceAccountName }} +{{- end -}} + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + automountServiceAccountToken: {{ .Values.redis.internal.automountServiceAccountToken | default false }} + terminationGracePeriodSeconds: 120 + {{- with .Values.redis.internal.initContainers }} + initContainers: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: redis + image: {{ .Values.redis.internal.image.repository }}:{{ .Values.redis.internal.image.tag }} + imagePullPolicy: {{ .Values.imagePullPolicy }} + {{- if not (empty .Values.containerSecurityContext) }} + securityContext: {{ .Values.containerSecurityContext | toYaml | nindent 10 }} + {{- end }} + livenessProbe: + tcpSocket: + port: 6379 + initialDelaySeconds: 300 + periodSeconds: 10 + readinessProbe: + tcpSocket: + port: 6379 + initialDelaySeconds: 1 + periodSeconds: 10 +{{- if .Values.redis.internal.resources }} + resources: +{{ toYaml .Values.redis.internal.resources | indent 10 }} +{{- end }} +{{- with .Values.redis.internal.extraEnvVars }} + env: +{{- toYaml . | nindent 10 }} +{{- end }} + volumeMounts: + - name: data + mountPath: /var/lib/redis + subPath: {{ $redis.subPath }} + {{- if not .Values.persistence.enabled }} + volumes: + - name: data + emptyDir: {} + {{- else if $redis.existingClaim }} + volumes: + - name: data + persistentVolumeClaim: + claimName: {{ $redis.existingClaim }} + {{- end -}} + {{- with .Values.redis.internal.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.redis.internal.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.redis.internal.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + {{- if .Values.redis.internal.priorityClassName }} + priorityClassName: {{ .Values.redis.internal.priorityClassName }} + {{- end }} + {{- if and .Values.persistence.enabled (not $redis.existingClaim) }} + volumeClaimTemplates: + - apiVersion: v1 + kind: PersistentVolumeClaim + metadata: + name: data + labels: +{{ include "harbor.legacy.labels" . | indent 8 }} + annotations: + {{- range $key, $value := $redis.annotations }} + {{ $key }}: {{ $value | quote }} + {{- end }} + spec: + accessModes: [{{ $redis.accessMode | quote }}] + {{- if $redis.storageClass }} + {{- if (eq "-" $redis.storageClass) }} + storageClassName: "" + {{- else }} + storageClassName: "{{ $redis.storageClass }}" + {{- end }} + {{- end }} + resources: + requests: + storage: {{ $redis.size | quote }} + {{- end -}} + {{- end -}} diff --git a/packages/system/harbor/charts/harbor/templates/registry/registry-cm.yaml b/packages/system/harbor/charts/harbor/templates/registry/registry-cm.yaml new file mode 100644 index 00000000..2ef398ed --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/registry/registry-cm.yaml @@ -0,0 +1,248 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: "{{ template "harbor.registry" . }}" + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} +data: + config.yml: |+ + version: 0.1 + log: + {{- if eq .Values.logLevel "warning" }} + level: warn + {{- else if eq .Values.logLevel "fatal" }} + level: error + {{- else }} + level: {{ .Values.logLevel }} + {{- end }} + fields: + service: registry + storage: + {{- $storage := .Values.persistence.imageChartStorage }} + {{- $type := $storage.type }} + {{- if eq $type "filesystem" }} + filesystem: + rootdirectory: {{ $storage.filesystem.rootdirectory }} + {{- if $storage.filesystem.maxthreads }} + maxthreads: {{ $storage.filesystem.maxthreads }} + {{- end }} + {{- else if eq $type "azure" }} + azure: + accountname: {{ $storage.azure.accountname }} + container: {{ $storage.azure.container }} + {{- if $storage.azure.realm }} + realm: {{ $storage.azure.realm }} + {{- end }} + {{- else if eq $type "gcs" }} + gcs: + bucket: {{ $storage.gcs.bucket }} + {{- if not .Values.persistence.imageChartStorage.gcs.useWorkloadIdentity }} + keyfile: /etc/registry/gcs-key.json + {{- end }} + {{- if $storage.gcs.rootdirectory }} + rootdirectory: {{ $storage.gcs.rootdirectory }} + {{- end }} + {{- if $storage.gcs.chunksize }} + chunksize: {{ $storage.gcs.chunksize }} + {{- end }} + {{- else if eq $type "s3" }} + s3: + region: {{ $storage.s3.region }} + bucket: {{ $storage.s3.bucket }} + {{- if $storage.s3.regionendpoint }} + regionendpoint: {{ $storage.s3.regionendpoint }} + {{- end }} + {{- if $storage.s3.encrypt }} + encrypt: {{ $storage.s3.encrypt }} + {{- end }} + {{- if $storage.s3.keyid }} + keyid: {{ $storage.s3.keyid }} + {{- end }} + {{- if $storage.s3.secure }} + secure: {{ $storage.s3.secure }} + {{- end }} + {{- if and $storage.s3.secure $storage.s3.skipverify }} + skipverify: {{ $storage.s3.skipverify }} + {{- end }} + {{- if $storage.s3.v4auth }} + v4auth: {{ $storage.s3.v4auth }} + {{- end }} + {{- if $storage.s3.chunksize }} + chunksize: {{ $storage.s3.chunksize }} + {{- end }} + {{- if $storage.s3.rootdirectory }} + rootdirectory: {{ $storage.s3.rootdirectory }} + {{- end }} + {{- if $storage.s3.storageclass }} + storageclass: {{ $storage.s3.storageclass }} + {{- end }} + {{- if $storage.s3.multipartcopychunksize }} + multipartcopychunksize: {{ $storage.s3.multipartcopychunksize }} + {{- end }} + {{- if $storage.s3.multipartcopymaxconcurrency }} + multipartcopymaxconcurrency: {{ $storage.s3.multipartcopymaxconcurrency }} + {{- end }} + {{- if $storage.s3.multipartcopythresholdsize }} + multipartcopythresholdsize: {{ $storage.s3.multipartcopythresholdsize }} + {{- end }} + {{- else if eq $type "swift" }} + swift: + authurl: {{ $storage.swift.authurl }} + username: {{ $storage.swift.username }} + container: {{ $storage.swift.container }} + {{- if $storage.swift.region }} + region: {{ $storage.swift.region }} + {{- end }} + {{- if $storage.swift.tenant }} + tenant: {{ $storage.swift.tenant }} + {{- end }} + {{- if $storage.swift.tenantid }} + tenantid: {{ $storage.swift.tenantid }} + {{- end }} + {{- if $storage.swift.domain }} + domain: {{ $storage.swift.domain }} + {{- end }} + {{- if $storage.swift.domainid }} + domainid: {{ $storage.swift.domainid }} + {{- end }} + {{- if $storage.swift.trustid }} + trustid: {{ $storage.swift.trustid }} + {{- end }} + {{- if $storage.swift.insecureskipverify }} + insecureskipverify: {{ $storage.swift.insecureskipverify }} + {{- end }} + {{- if $storage.swift.chunksize }} + chunksize: {{ $storage.swift.chunksize }} + {{- end }} + {{- if $storage.swift.prefix }} + prefix: {{ $storage.swift.prefix }} + {{- end }} + {{- if $storage.swift.authversion }} + authversion: {{ $storage.swift.authversion }} + {{- end }} + {{- if $storage.swift.endpointtype }} + endpointtype: {{ $storage.swift.endpointtype }} + {{- end }} + {{- if $storage.swift.tempurlcontainerkey }} + tempurlcontainerkey: {{ $storage.swift.tempurlcontainerkey }} + {{- end }} + {{- if $storage.swift.tempurlmethods }} + tempurlmethods: {{ $storage.swift.tempurlmethods }} + {{- end }} + {{- else if eq $type "oss" }} + oss: + accesskeyid: {{ $storage.oss.accesskeyid }} + region: {{ $storage.oss.region }} + bucket: {{ $storage.oss.bucket }} + {{- if $storage.oss.endpoint }} + endpoint: {{ $storage.oss.bucket }}.{{ $storage.oss.endpoint }} + {{- end }} + {{- if $storage.oss.internal }} + internal: {{ $storage.oss.internal }} + {{- end }} + {{- if $storage.oss.encrypt }} + encrypt: {{ $storage.oss.encrypt }} + {{- end }} + {{- if $storage.oss.secure }} + secure: {{ $storage.oss.secure }} + {{- end }} + {{- if $storage.oss.chunksize }} + chunksize: {{ $storage.oss.chunksize }} + {{- end }} + {{- if $storage.oss.rootdirectory }} + rootdirectory: {{ $storage.oss.rootdirectory }} + {{- end }} + {{- end }} + cache: + layerinfo: redis + maintenance: + uploadpurging: + {{- if .Values.registry.upload_purging.enabled }} + enabled: true + age: {{ .Values.registry.upload_purging.age }} + interval: {{ .Values.registry.upload_purging.interval }} + dryrun: {{ .Values.registry.upload_purging.dryrun }} + {{- else }} + enabled: false + {{- end }} + delete: + enabled: true + redirect: + disable: {{ $storage.disableredirect }} + redis: + addr: {{ template "harbor.redis.addr" . }} + {{- if eq "redis+sentinel" (include "harbor.redis.scheme" .) }} + sentinelMasterSet: {{ template "harbor.redis.masterSet" . }} + {{- end }} + db: {{ template "harbor.redis.dbForRegistry" . }} + {{- if not (eq (include "harbor.redis.password" .) "") }} + password: {{ template "harbor.redis.password" . }} + {{- end }} + readtimeout: 10s + writetimeout: 10s + dialtimeout: 10s + enableTLS: {{ template "harbor.redis.enableTLS" . }} + pool: + maxidle: 100 + maxactive: 500 + idletimeout: 60s + http: + addr: :{{ template "harbor.registry.containerPort" . }} + relativeurls: {{ .Values.registry.relativeurls }} + {{- if .Values.internalTLS.enabled }} + tls: + certificate: /etc/harbor/ssl/registry/tls.crt + key: /etc/harbor/ssl/registry/tls.key + minimumtls: tls1.2 + {{- end }} + # set via environment variable + # secret: placeholder + debug: + {{- if .Values.metrics.enabled}} + addr: :{{ .Values.metrics.registry.port }} + prometheus: + enabled: true + path: {{ .Values.metrics.registry.path }} + {{- else }} + addr: localhost:5001 + {{- end }} + auth: + htpasswd: + realm: harbor-registry-basic-realm + path: /etc/registry/passwd + validation: + disabled: true + compatibility: + schema1: + enabled: true + + {{- if .Values.registry.middleware.enabled }} + {{- $middleware := .Values.registry.middleware }} + {{- $middlewareType := $middleware.type }} + {{- if eq $middlewareType "cloudFront" }} + middleware: + storage: + - name: cloudfront + options: + baseurl: {{ $middleware.cloudFront.baseurl }} + privatekey: /etc/registry/pk.pem + keypairid: {{ $middleware.cloudFront.keypairid }} + duration: {{ $middleware.cloudFront.duration }} + ipfilteredby: {{ $middleware.cloudFront.ipfilteredby }} + {{- end }} + {{- end }} + ctl-config.yml: |+ + --- + {{- if .Values.internalTLS.enabled }} + protocol: "https" + port: 8443 + https_config: + cert: "/etc/harbor/ssl/registry/tls.crt" + key: "/etc/harbor/ssl/registry/tls.key" + {{- else }} + protocol: "http" + port: 8080 + {{- end }} + log_level: {{ .Values.logLevel }} + registry_config: "/etc/registry/config.yml" diff --git a/packages/system/harbor/charts/harbor/templates/registry/registry-dpl.yaml b/packages/system/harbor/charts/harbor/templates/registry/registry-dpl.yaml new file mode 100644 index 00000000..a86e2eee --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/registry/registry-dpl.yaml @@ -0,0 +1,431 @@ +{{- $storage := .Values.persistence.imageChartStorage }} +{{- $type := $storage.type }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: "{{ template "harbor.registry" . }}" + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} + component: registry + app.kubernetes.io/component: registry +spec: + replicas: {{ .Values.registry.replicas }} + revisionHistoryLimit: {{ .Values.registry.revisionHistoryLimit }} + strategy: + type: {{ .Values.updateStrategy.type }} + {{- if eq .Values.updateStrategy.type "Recreate" }} + rollingUpdate: null + {{- end }} + selector: + matchLabels: +{{ include "harbor.matchLabels" . | indent 6 }} + component: registry + template: + metadata: + labels: +{{ include "harbor.labels" . | indent 8 }} + component: registry + app.kubernetes.io/component: registry +{{- if .Values.registry.podLabels }} +{{ toYaml .Values.registry.podLabels | indent 8 }} +{{- end }} + annotations: + checksum/configmap: {{ include (print $.Template.BasePath "/registry/registry-cm.yaml") . | sha256sum }} + checksum/secret: {{ include (print $.Template.BasePath "/registry/registry-secret.yaml") . | sha256sum }} + checksum/secret-jobservice: {{ include (print $.Template.BasePath "/jobservice/jobservice-secrets.yaml") . | sha256sum }} + checksum/secret-core: {{ include (print $.Template.BasePath "/core/core-secret.yaml") . | sha256sum }} +{{- if and .Values.internalTLS.enabled (eq .Values.internalTLS.certSource "auto") }} + checksum/tls: {{ include (print $.Template.BasePath "/internal/auto-tls.yaml") . | sha256sum }} +{{- else if and .Values.internalTLS.enabled (eq .Values.internalTLS.certSource "manual") }} + checksum/tls: {{ include (print $.Template.BasePath "/registry/registry-tls.yaml") . | sha256sum }} +{{- end }} +{{- if .Values.registry.podAnnotations }} +{{ toYaml .Values.registry.podAnnotations | indent 8 }} +{{- end }} + spec: + securityContext: + runAsUser: 10000 + fsGroup: 10000 + fsGroupChangePolicy: OnRootMismatch +{{- if .Values.registry.serviceAccountName }} + serviceAccountName: {{ .Values.registry.serviceAccountName }} +{{- end -}} + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + automountServiceAccountToken: {{ .Values.registry.automountServiceAccountToken | default false }} + terminationGracePeriodSeconds: 120 +{{- with .Values.registry.topologySpreadConstraints}} + topologySpreadConstraints: +{{- range . }} + - {{ . | toYaml | indent 8 | trim }} + labelSelector: + matchLabels: +{{ include "harbor.matchLabels" $ | indent 12 }} + component: registry +{{- end }} +{{- end }} + {{- with .Values.registry.initContainers }} + initContainers: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: registry + image: {{ .Values.registry.registry.image.repository }}:{{ .Values.registry.registry.image.tag }} + imagePullPolicy: {{ .Values.imagePullPolicy }} + livenessProbe: + httpGet: + path: / + scheme: {{ include "harbor.component.scheme" . | upper }} + port: {{ template "harbor.registry.containerPort" . }} + initialDelaySeconds: 300 + periodSeconds: 10 + readinessProbe: + httpGet: + path: / + scheme: {{ include "harbor.component.scheme" . | upper }} + port: {{ template "harbor.registry.containerPort" . }} + initialDelaySeconds: 1 + periodSeconds: 10 +{{- if .Values.registry.registry.resources }} + resources: +{{ toYaml .Values.registry.registry.resources | indent 10 }} +{{- end }} + {{- if not (empty .Values.containerSecurityContext) }} + securityContext: {{ .Values.containerSecurityContext | toYaml | nindent 10 }} + {{- end }} + envFrom: + - secretRef: + name: "{{ template "harbor.registry" . }}" + {{- if .Values.persistence.imageChartStorage.s3.existingSecret }} + - secretRef: + name: {{ .Values.persistence.imageChartStorage.s3.existingSecret }} + {{- end }} + env: + {{- if .Values.registry.existingSecret }} + - name: REGISTRY_HTTP_SECRET + valueFrom: + secretKeyRef: + name: {{ .Values.registry.existingSecret }} + key: {{ .Values.registry.existingSecretKey }} + {{- end }} + {{- if has "registry" .Values.proxy.components }} + - name: HTTP_PROXY + value: "{{ .Values.proxy.httpProxy }}" + - name: HTTPS_PROXY + value: "{{ .Values.proxy.httpsProxy }}" + - name: NO_PROXY + value: "{{ template "harbor.noProxy" . }}" + {{- end }} + {{- if .Values.internalTLS.enabled }} + - name: INTERNAL_TLS_ENABLED + value: "true" + - name: INTERNAL_TLS_KEY_PATH + value: /etc/harbor/ssl/registry/tls.key + - name: INTERNAL_TLS_CERT_PATH + value: /etc/harbor/ssl/registry/tls.crt + - name: INTERNAL_TLS_TRUST_CA_PATH + value: /etc/harbor/ssl/registry/ca.crt + {{- end }} + {{- if .Values.redis.external.existingSecret }} + - name: REGISTRY_REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.redis.external.existingSecret }} + key: REDIS_PASSWORD + {{- end }} + {{- if .Values.persistence.imageChartStorage.azure.existingSecret }} + - name: REGISTRY_STORAGE_AZURE_ACCOUNTKEY + valueFrom: + secretKeyRef: + name: {{ .Values.persistence.imageChartStorage.azure.existingSecret }} + key: AZURE_STORAGE_ACCESS_KEY + {{- end }} + {{- if .Values.persistence.imageChartStorage.swift.existingSecret }} + - name: REGISTRY_STORAGE_SWIFT_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.persistence.imageChartStorage.swift.existingSecret }} + key: REGISTRY_STORAGE_SWIFT_PASSWORD + - name: REGISTRY_STORAGE_SWIFT_SECRETKEY + valueFrom: + secretKeyRef: + name: {{ .Values.persistence.imageChartStorage.swift.existingSecret }} + key: REGISTRY_STORAGE_SWIFT_SECRETKEY + optional: true + - name: REGISTRY_STORAGE_SWIFT_ACCESSKEY + valueFrom: + secretKeyRef: + name: {{ .Values.persistence.imageChartStorage.swift.existingSecret }} + key: REGISTRY_STORAGE_SWIFT_ACCESSKEY + optional: true + {{- end }} + {{- if .Values.persistence.imageChartStorage.oss.existingSecret }} + - name: REGISTRY_STORAGE_OSS_ACCESSKEYSECRET + valueFrom: + secretKeyRef: + name: {{ .Values.persistence.imageChartStorage.oss.existingSecret }} + key: REGISTRY_STORAGE_OSS_ACCESSKEYSECRET + optional: true + {{- end}} +{{- with .Values.registry.registry.extraEnvVars }} +{{- toYaml . | nindent 8 }} +{{- end }} + ports: + - containerPort: {{ template "harbor.registry.containerPort" . }} + - containerPort: {{ ternary .Values.metrics.registry.port 5001 .Values.metrics.enabled }} + volumeMounts: + - name: registry-data + mountPath: {{ .Values.persistence.imageChartStorage.filesystem.rootdirectory }} + subPath: {{ .Values.persistence.persistentVolumeClaim.registry.subPath }} + - name: registry-htpasswd + mountPath: /etc/registry/passwd + subPath: passwd + - name: registry-config + mountPath: /etc/registry/config.yml + subPath: config.yml + {{- if .Values.internalTLS.enabled }} + - name: registry-internal-certs + mountPath: /etc/harbor/ssl/registry + {{- end }} + {{- if and (and .Values.persistence.enabled (eq .Values.persistence.imageChartStorage.type "gcs")) (not .Values.persistence.imageChartStorage.gcs.useWorkloadIdentity) }} + - name: gcs-key + mountPath: /etc/registry/gcs-key.json + subPath: gcs-key.json + {{- end }} + {{- if .Values.persistence.imageChartStorage.caBundleSecretName }} + - name: storage-service-ca + mountPath: /harbor_cust_cert/custom-ca-bundle.crt + subPath: ca.crt + {{- end }} + {{- if .Values.registry.middleware.enabled }} + {{- if eq .Values.registry.middleware.type "cloudFront" }} + - name: cloudfront-key + mountPath: /etc/registry/pk.pem + subPath: pk.pem + {{- end }} + {{- end }} + {{- if .Values.caBundleSecretName }} +{{ include "harbor.caBundleVolumeMount" . | indent 8 }} + {{- end }} + - name: registryctl + image: {{ .Values.registry.controller.image.repository }}:{{ .Values.registry.controller.image.tag }} + imagePullPolicy: {{ .Values.imagePullPolicy }} + livenessProbe: + httpGet: + path: /api/health + scheme: {{ include "harbor.component.scheme" . | upper }} + port: {{ template "harbor.registryctl.containerPort" . }} + initialDelaySeconds: 300 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /api/health + scheme: {{ include "harbor.component.scheme" . | upper }} + port: {{ template "harbor.registryctl.containerPort" . }} + initialDelaySeconds: 1 + periodSeconds: 10 +{{- if .Values.registry.controller.resources }} + resources: +{{ toYaml .Values.registry.controller.resources | indent 10 }} +{{- end }} + {{- if not (empty .Values.containerSecurityContext) }} + securityContext: {{ .Values.containerSecurityContext | toYaml | nindent 10 }} + {{- end }} + envFrom: + - configMapRef: + name: "{{ template "harbor.registryCtl" . }}" + - secretRef: + name: "{{ template "harbor.registry" . }}" + - secretRef: + name: "{{ template "harbor.registryCtl" . }}" + {{- if .Values.persistence.imageChartStorage.s3.existingSecret }} + - secretRef: + name: {{ .Values.persistence.imageChartStorage.s3.existingSecret }} + {{- end }} + env: + {{- if .Values.registry.existingSecret }} + - name: REGISTRY_HTTP_SECRET + valueFrom: + secretKeyRef: + name: {{ .Values.registry.existingSecret }} + key: {{ .Values.registry.existingSecretKey }} + {{- end }} + - name: CORE_SECRET + valueFrom: + secretKeyRef: + name: {{ default (include "harbor.core" .) .Values.core.existingSecret }} + key: secret + - name: JOBSERVICE_SECRET + valueFrom: + secretKeyRef: + name: {{ default (include "harbor.jobservice" .) .Values.jobservice.existingSecret }} + {{- if .Values.jobservice.existingSecret }} + key: {{ .Values.jobservice.existingSecretKey }} + {{- else }} + key: JOBSERVICE_SECRET + {{- end }} + {{- if has "registry" .Values.proxy.components }} + - name: HTTP_PROXY + value: "{{ .Values.proxy.httpProxy }}" + - name: HTTPS_PROXY + value: "{{ .Values.proxy.httpsProxy }}" + - name: NO_PROXY + value: "{{ template "harbor.noProxy" . }}" + {{- end }} + {{- if .Values.internalTLS.enabled }} + - name: INTERNAL_TLS_ENABLED + value: "true" + - name: INTERNAL_TLS_KEY_PATH + value: /etc/harbor/ssl/registry/tls.key + - name: INTERNAL_TLS_CERT_PATH + value: /etc/harbor/ssl/registry/tls.crt + - name: INTERNAL_TLS_TRUST_CA_PATH + value: /etc/harbor/ssl/registry/ca.crt + {{- end }} + {{- if .Values.redis.external.existingSecret }} + - name: REGISTRY_REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.redis.external.existingSecret }} + key: REDIS_PASSWORD + {{- end }} + {{- if .Values.persistence.imageChartStorage.azure.existingSecret }} + - name: REGISTRY_STORAGE_AZURE_ACCOUNTKEY + valueFrom: + secretKeyRef: + name: {{ .Values.persistence.imageChartStorage.azure.existingSecret }} + key: AZURE_STORAGE_ACCESS_KEY + {{- end }} + {{- if .Values.persistence.imageChartStorage.swift.existingSecret }} + - name: REGISTRY_STORAGE_SWIFT_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.persistence.imageChartStorage.swift.existingSecret }} + key: REGISTRY_STORAGE_SWIFT_PASSWORD + - name: REGISTRY_STORAGE_SWIFT_SECRETKEY + valueFrom: + secretKeyRef: + name: {{ .Values.persistence.imageChartStorage.swift.existingSecret }} + key: REGISTRY_STORAGE_SWIFT_SECRETKEY + optional: true + - name: REGISTRY_STORAGE_SWIFT_ACCESSKEY + valueFrom: + secretKeyRef: + name: {{ .Values.persistence.imageChartStorage.swift.existingSecret }} + key: REGISTRY_STORAGE_SWIFT_ACCESSKEY + optional: true + {{- end }} + {{- if .Values.persistence.imageChartStorage.oss.existingSecret }} + - name: REGISTRY_STORAGE_OSS_ACCESSKEYSECRET + valueFrom: + secretKeyRef: + name: {{ .Values.persistence.imageChartStorage.oss.existingSecret }} + key: REGISTRY_STORAGE_OSS_ACCESSKEYSECRET + optional: true + {{- end}} +{{- with .Values.registry.controller.extraEnvVars }} +{{- toYaml . | nindent 8 }} +{{- end }} + ports: + - containerPort: {{ template "harbor.registryctl.containerPort" . }} + volumeMounts: + - name: registry-data + mountPath: {{ .Values.persistence.imageChartStorage.filesystem.rootdirectory }} + subPath: {{ .Values.persistence.persistentVolumeClaim.registry.subPath }} + - name: registry-config + mountPath: /etc/registry/config.yml + subPath: config.yml + - name: registry-config + mountPath: /etc/registryctl/config.yml + subPath: ctl-config.yml + {{- if .Values.internalTLS.enabled }} + - name: registry-internal-certs + mountPath: /etc/harbor/ssl/registry + {{- end }} + {{- if .Values.persistence.imageChartStorage.caBundleSecretName }} + - name: storage-service-ca + mountPath: /harbor_cust_cert/custom-ca-bundle.crt + subPath: ca.crt + {{- end }} + {{- if and (and .Values.persistence.enabled (eq .Values.persistence.imageChartStorage.type "gcs")) (not .Values.persistence.imageChartStorage.gcs.useWorkloadIdentity ) }} + - name: gcs-key + mountPath: /etc/registry/gcs-key.json + subPath: gcs-key.json + {{- end }} + {{- if .Values.caBundleSecretName }} +{{ include "harbor.caBundleVolumeMount" . | indent 8 }} + {{- end }} + volumes: + - name: registry-htpasswd + secret: + {{- if not .Values.registry.credentials.existingSecret }} + secretName: {{ template "harbor.registry" . }}-htpasswd + {{ else }} + secretName: {{ .Values.registry.credentials.existingSecret }} + {{- end }} + items: + - key: REGISTRY_HTPASSWD + path: passwd + - name: registry-config + configMap: + name: "{{ template "harbor.registry" . }}" + - name: registry-data + {{- if and .Values.persistence.enabled (eq .Values.persistence.imageChartStorage.type "filesystem") }} + persistentVolumeClaim: + claimName: {{ .Values.persistence.persistentVolumeClaim.registry.existingClaim | default (include "harbor.registry" .) }} + {{- else }} + emptyDir: {} + {{- end }} + {{- if .Values.internalTLS.enabled }} + - name: registry-internal-certs + secret: + secretName: {{ template "harbor.internalTLS.registry.secretName" . }} + {{- end }} + {{- if and (and .Values.persistence.enabled (eq .Values.persistence.imageChartStorage.type "gcs")) (not .Values.persistence.imageChartStorage.gcs.useWorkloadIdentity ) }} + - name: gcs-key + secret: + {{- if and (eq $type "gcs") $storage.gcs.existingSecret }} + secretName: {{ $storage.gcs.existingSecret }} + {{- else }} + secretName: {{ template "harbor.registry" . }} + {{- end }} + items: + - key: GCS_KEY_DATA + path: gcs-key.json + {{- end }} + {{- if .Values.persistence.imageChartStorage.caBundleSecretName }} + - name: storage-service-ca + secret: + secretName: {{ .Values.persistence.imageChartStorage.caBundleSecretName }} + {{- end }} + {{- if .Values.registry.middleware.enabled }} + {{- if eq .Values.registry.middleware.type "cloudFront" }} + - name: cloudfront-key + secret: + secretName: {{ .Values.registry.middleware.cloudFront.privateKeySecret }} + items: + - key: CLOUDFRONT_KEY_DATA + path: pk.pem + {{- end }} + {{- end }} + {{- if .Values.caBundleSecretName }} +{{ include "harbor.caBundleVolume" . | indent 6 }} + {{- end }} + {{- with .Values.registry.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.registry.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.registry.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + {{- if .Values.registry.priorityClassName }} + priorityClassName: {{ .Values.registry.priorityClassName }} + {{- end }} diff --git a/packages/system/harbor/charts/harbor/templates/registry/registry-pvc.yaml b/packages/system/harbor/charts/harbor/templates/registry/registry-pvc.yaml new file mode 100644 index 00000000..712c2117 --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/registry/registry-pvc.yaml @@ -0,0 +1,34 @@ +{{- if .Values.persistence.enabled }} +{{- $registry := .Values.persistence.persistentVolumeClaim.registry -}} +{{- if and (not $registry.existingClaim) (eq .Values.persistence.imageChartStorage.type "filesystem") }} +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: {{ template "harbor.registry" . }} + namespace: {{ .Release.Namespace | quote }} + annotations: + {{- range $key, $value := $registry.annotations }} + {{ $key }}: {{ $value | quote }} + {{- end }} + {{- if eq .Values.persistence.resourcePolicy "keep" }} + helm.sh/resource-policy: keep + {{- end }} + labels: +{{ include "harbor.labels" . | indent 4 }} + component: registry + app.kubernetes.io/component: registry +spec: + accessModes: + - {{ $registry.accessMode }} + resources: + requests: + storage: {{ $registry.size }} + {{- if $registry.storageClass }} + {{- if eq "-" $registry.storageClass }} + storageClassName: "" + {{- else }} + storageClassName: {{ $registry.storageClass }} + {{- end }} + {{- end }} +{{- end }} +{{- end }} diff --git a/packages/system/harbor/charts/harbor/templates/registry/registry-secret.yaml b/packages/system/harbor/charts/harbor/templates/registry/registry-secret.yaml new file mode 100644 index 00000000..11ada3b7 --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/registry/registry-secret.yaml @@ -0,0 +1,57 @@ +{{- $existingSecret := lookup "v1" "Secret" .Release.Namespace (include "harbor.registry" .) }} +apiVersion: v1 +kind: Secret +metadata: + name: "{{ template "harbor.registry" . }}" + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} +type: Opaque +data: + {{- if not .Values.registry.existingSecret }} + REGISTRY_HTTP_SECRET: {{ .Values.registry.secret | default (include "harbor.secretKeyHelper" (dict "key" "REGISTRY_HTTP_SECRET" "data" $existingSecret.data)) | default (randAlphaNum 16) | b64enc | quote }} + {{- end }} + {{- if not .Values.redis.external.existingSecret }} + REGISTRY_REDIS_PASSWORD: {{ include "harbor.redis.password" . | b64enc | quote }} + {{- end }} + {{- $storage := .Values.persistence.imageChartStorage }} + {{- $type := $storage.type }} + {{- if and (eq $type "azure") (not $storage.azure.existingSecret) }} + REGISTRY_STORAGE_AZURE_ACCOUNTKEY: {{ $storage.azure.accountkey | b64enc | quote }} + {{- else if and (and (eq $type "gcs") (not $storage.gcs.existingSecret)) (not $storage.gcs.useWorkloadIdentity) }} + GCS_KEY_DATA: {{ $storage.gcs.encodedkey | quote }} + {{- else if eq $type "s3" }} + {{- if and (not $storage.s3.existingSecret) ($storage.s3.accesskey) }} + REGISTRY_STORAGE_S3_ACCESSKEY: {{ $storage.s3.accesskey | b64enc | quote }} + {{- end }} + {{- if and (not $storage.s3.existingSecret) ($storage.s3.secretkey) }} + REGISTRY_STORAGE_S3_SECRETKEY: {{ $storage.s3.secretkey | b64enc | quote }} + {{- end }} + {{- else if and (eq $type "swift") (not ($storage.swift.existingSecret)) }} + REGISTRY_STORAGE_SWIFT_PASSWORD: {{ $storage.swift.password | b64enc | quote }} + {{- if $storage.swift.secretkey }} + REGISTRY_STORAGE_SWIFT_SECRETKEY: {{ $storage.swift.secretkey | b64enc | quote }} + {{- end }} + {{- if $storage.swift.accesskey }} + REGISTRY_STORAGE_SWIFT_ACCESSKEY: {{ $storage.swift.accesskey | b64enc | quote }} + {{- end }} + {{- else if and (eq $type "oss") ((not ($storage.oss.existingSecret))) }} + REGISTRY_STORAGE_OSS_ACCESSKEYSECRET: {{ $storage.oss.accesskeysecret | b64enc | quote }} + {{- end }} +{{- if not .Values.registry.credentials.existingSecret }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: "{{ template "harbor.registry" . }}-htpasswd" + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} +type: Opaque +data: + {{- if .Values.registry.credentials.htpasswdString }} + REGISTRY_HTPASSWD: {{ .Values.registry.credentials.htpasswdString | b64enc | quote }} + {{- else }} + REGISTRY_HTPASSWD: {{ htpasswd .Values.registry.credentials.username .Values.registry.credentials.password | b64enc | quote }} + {{- end }} +{{- end }} diff --git a/packages/system/harbor/charts/harbor/templates/registry/registry-svc.yaml b/packages/system/harbor/charts/harbor/templates/registry/registry-svc.yaml new file mode 100644 index 00000000..7e30c478 --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/registry/registry-svc.yaml @@ -0,0 +1,27 @@ +apiVersion: v1 +kind: Service +metadata: + name: "{{ template "harbor.registry" . }}" + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} +spec: +{{- if .Values.ipFamily.policy }} + ipFamilyPolicy: {{ .Values.ipFamily.policy }} +{{- end }} +{{- if .Values.ipFamily.families }} + ipFamilies: {{ toYaml .Values.ipFamily.families | nindent 4 }} +{{- end }} + ports: + - name: {{ ternary "https-registry" "http-registry" .Values.internalTLS.enabled }} + port: {{ template "harbor.registry.servicePort" . }} + + - name: {{ ternary "https-controller" "http-controller" .Values.internalTLS.enabled }} + port: {{ template "harbor.registryctl.servicePort" . }} +{{- if .Values.metrics.enabled}} + - name: {{ template "harbor.metricsPortName" . }} + port: {{ .Values.metrics.registry.port }} +{{- end }} + selector: +{{ include "harbor.matchLabels" . | indent 4 }} + component: registry diff --git a/packages/system/harbor/charts/harbor/templates/registry/registry-tls.yaml b/packages/system/harbor/charts/harbor/templates/registry/registry-tls.yaml new file mode 100644 index 00000000..ec4540c2 --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/registry/registry-tls.yaml @@ -0,0 +1,16 @@ +{{- if and .Values.internalTLS.enabled }} +{{- if eq .Values.internalTLS.certSource "manual" }} +apiVersion: v1 +kind: Secret +metadata: + name: "{{ template "harbor.internalTLS.registry.secretName" . }}" + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} +type: kubernetes.io/tls +data: + ca.crt: {{ (required "The \"internalTLS.trustCa\" is required!" .Values.internalTLS.trustCa) | b64enc | quote }} + tls.crt: {{ (required "The \"internalTLS.registry.crt\" is required!" .Values.internalTLS.registry.crt) | b64enc | quote }} + tls.key: {{ (required "The \"internalTLS.registry.key\" is required!" .Values.internalTLS.registry.key) | b64enc | quote }} +{{- end }} +{{- end }} diff --git a/packages/system/harbor/charts/harbor/templates/registry/registryctl-cm.yaml b/packages/system/harbor/charts/harbor/templates/registry/registryctl-cm.yaml new file mode 100644 index 00000000..61b2c5e1 --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/registry/registryctl-cm.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: "{{ template "harbor.registryCtl" . }}" + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} +data: + {{- template "harbor.traceEnvsForRegistryCtl" . }} diff --git a/packages/system/harbor/charts/harbor/templates/registry/registryctl-secret.yaml b/packages/system/harbor/charts/harbor/templates/registry/registryctl-secret.yaml new file mode 100644 index 00000000..324a2e03 --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/registry/registryctl-secret.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Secret +metadata: + name: "{{ template "harbor.registryCtl" . }}" + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} +type: Opaque +data: + {{- template "harbor.traceJaegerPassword" . }} diff --git a/packages/system/harbor/charts/harbor/templates/trivy/trivy-secret.yaml b/packages/system/harbor/charts/harbor/templates/trivy/trivy-secret.yaml new file mode 100644 index 00000000..b13f8800 --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/trivy/trivy-secret.yaml @@ -0,0 +1,13 @@ +{{- if .Values.trivy.enabled }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ template "harbor.trivy" . }} + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} +type: Opaque +data: + redisURL: {{ include "harbor.redis.urlForTrivy" . | b64enc }} + gitHubToken: {{ .Values.trivy.gitHubToken | default "" | b64enc | quote }} +{{- end }} diff --git a/packages/system/harbor/charts/harbor/templates/trivy/trivy-sts.yaml b/packages/system/harbor/charts/harbor/templates/trivy/trivy-sts.yaml new file mode 100644 index 00000000..19b01a2e --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/trivy/trivy-sts.yaml @@ -0,0 +1,237 @@ +{{- if .Values.trivy.enabled }} +{{- $trivy := .Values.persistence.persistentVolumeClaim.trivy }} +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ template "harbor.trivy" . }} + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} + component: trivy + app.kubernetes.io/component: trivy +spec: + replicas: {{ .Values.trivy.replicas }} + serviceName: {{ template "harbor.trivy" . }} + selector: + matchLabels: +{{ include "harbor.matchLabels" . | indent 6 }} + component: trivy + template: + metadata: + labels: +{{ include "harbor.labels" . | indent 8 }} + component: trivy + app.kubernetes.io/component: trivy +{{- if .Values.trivy.podLabels }} +{{ toYaml .Values.trivy.podLabels | indent 8 }} +{{- end }} + annotations: + checksum/secret: {{ include (print $.Template.BasePath "/trivy/trivy-secret.yaml") . | sha256sum }} +{{- if and .Values.internalTLS.enabled (eq .Values.internalTLS.certSource "auto") }} + checksum/tls: {{ include (print $.Template.BasePath "/internal/auto-tls.yaml") . | sha256sum }} +{{- else if and .Values.internalTLS.enabled (eq .Values.internalTLS.certSource "manual") }} + checksum/tls: {{ include (print $.Template.BasePath "/trivy/trivy-tls.yaml") . | sha256sum }} +{{- end }} +{{- if .Values.trivy.podAnnotations }} +{{ toYaml .Values.trivy.podAnnotations | indent 8 }} +{{- end }} + spec: +{{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} +{{- end }} +{{- if .Values.trivy.serviceAccountName }} + serviceAccountName: {{ .Values.trivy.serviceAccountName }} +{{- end }} + securityContext: + runAsUser: 10000 + fsGroup: 10000 + automountServiceAccountToken: {{ .Values.trivy.automountServiceAccountToken | default false }} +{{- with .Values.trivy.topologySpreadConstraints}} + topologySpreadConstraints: +{{- range . }} + - {{ . | toYaml | indent 8 | trim }} + labelSelector: + matchLabels: +{{ include "harbor.matchLabels" $ | indent 12 }} + component: trivy +{{- end }} +{{- end }} + {{- with .Values.trivy.initContainers }} + initContainers: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: trivy + image: {{ .Values.trivy.image.repository }}:{{ .Values.trivy.image.tag }} + imagePullPolicy: {{ .Values.imagePullPolicy }} + {{- if not (empty .Values.containerSecurityContext) }} + securityContext: {{ .Values.containerSecurityContext | toYaml | nindent 12 }} + {{- end }} + env: + {{- if has "trivy" .Values.proxy.components }} + - name: HTTP_PROXY + value: "{{ .Values.proxy.httpProxy }}" + - name: HTTPS_PROXY + value: "{{ .Values.proxy.httpsProxy }}" + - name: NO_PROXY + value: "{{ template "harbor.noProxy" . }}" + {{- end }} + - name: "SCANNER_LOG_LEVEL" + value: {{ .Values.logLevel | quote }} + - name: "SCANNER_TRIVY_CACHE_DIR" + value: "/home/scanner/.cache/trivy" + - name: "SCANNER_TRIVY_REPORTS_DIR" + value: "/home/scanner/.cache/reports" + - name: "SCANNER_TRIVY_DEBUG_MODE" + value: {{ .Values.trivy.debugMode | quote }} + - name: "SCANNER_TRIVY_VULN_TYPE" + value: {{ .Values.trivy.vulnType | quote }} + - name: "SCANNER_TRIVY_TIMEOUT" + value: {{ .Values.trivy.timeout | quote }} + - name: "SCANNER_TRIVY_GITHUB_TOKEN" + valueFrom: + secretKeyRef: + name: {{ template "harbor.trivy" . }} + key: gitHubToken + - name: "SCANNER_TRIVY_SEVERITY" + value: {{ .Values.trivy.severity | quote }} + - name: "SCANNER_TRIVY_IGNORE_UNFIXED" + value: {{ .Values.trivy.ignoreUnfixed | default false | quote }} + - name: "SCANNER_TRIVY_SKIP_UPDATE" + value: {{ .Values.trivy.skipUpdate | default false | quote }} + - name: "SCANNER_TRIVY_SKIP_JAVA_DB_UPDATE" + value: {{ .Values.trivy.skipJavaDBUpdate | default false | quote }} + - name: "SCANNER_TRIVY_DB_REPOSITORY" + value: {{ .Values.trivy.dbRepository | join "," | quote }} + - name: "SCANNER_TRIVY_JAVA_DB_REPOSITORY" + value: {{ .Values.trivy.javaDBRepository | join "," | quote }} + - name: "SCANNER_TRIVY_OFFLINE_SCAN" + value: {{ .Values.trivy.offlineScan | default false | quote }} + - name: "SCANNER_TRIVY_SECURITY_CHECKS" + value: {{ .Values.trivy.securityCheck | quote }} + - name: "SCANNER_TRIVY_INSECURE" + value: {{ .Values.trivy.insecure | default false | quote }} + - name: SCANNER_API_SERVER_ADDR + value: ":{{ template "harbor.trivy.containerPort" . }}" + {{- if .Values.internalTLS.enabled }} + - name: INTERNAL_TLS_ENABLED + value: "true" + - name: SCANNER_API_SERVER_TLS_KEY + value: /etc/harbor/ssl/trivy/tls.key + - name: SCANNER_API_SERVER_TLS_CERTIFICATE + value: /etc/harbor/ssl/trivy/tls.crt + {{- end }} + - name: "SCANNER_REDIS_URL" + valueFrom: + secretKeyRef: + name: {{ template "harbor.trivy" . }} + key: redisURL + - name: "SCANNER_STORE_REDIS_URL" + valueFrom: + secretKeyRef: + name: {{ template "harbor.trivy" . }} + key: redisURL + - name: "SCANNER_JOB_QUEUE_REDIS_URL" + valueFrom: + secretKeyRef: + name: {{ template "harbor.trivy" . }} + key: redisURL +{{- with .Values.trivy.extraEnvVars }} +{{- toYaml . | nindent 12 }} +{{- end }} + ports: + - name: api-server + containerPort: {{ template "harbor.trivy.containerPort" . }} + volumeMounts: + - name: data + mountPath: /home/scanner/.cache + subPath: {{ .Values.persistence.persistentVolumeClaim.trivy.subPath }} + readOnly: false + {{- if .Values.internalTLS.enabled }} + - name: trivy-internal-certs + mountPath: /etc/harbor/ssl/trivy + {{- end }} + {{- if .Values.caBundleSecretName }} +{{ include "harbor.caBundleVolumeMount" . | indent 10 }} + {{- end }} + livenessProbe: + httpGet: + scheme: {{ include "harbor.component.scheme" . | upper }} + path: /probe/healthy + port: api-server + initialDelaySeconds: 5 + periodSeconds: 10 + successThreshold: 1 + failureThreshold: 10 + readinessProbe: + httpGet: + scheme: {{ include "harbor.component.scheme" . | upper }} + path: /probe/ready + port: api-server + initialDelaySeconds: 5 + periodSeconds: 10 + successThreshold: 1 + failureThreshold: 3 + resources: +{{ toYaml .Values.trivy.resources | indent 12 }} + {{- if or (or .Values.internalTLS.enabled .Values.caBundleSecretName) (or (not .Values.persistence.enabled) $trivy.existingClaim) }} + volumes: + {{- if .Values.internalTLS.enabled }} + - name: trivy-internal-certs + secret: + secretName: {{ template "harbor.internalTLS.trivy.secretName" . }} + {{- end }} + {{- if .Values.caBundleSecretName }} +{{ include "harbor.caBundleVolume" . | indent 6 }} + {{- end }} + {{- if not .Values.persistence.enabled }} + - name: "data" + emptyDir: {} + {{- else if $trivy.existingClaim }} + - name: "data" + persistentVolumeClaim: + claimName: {{ $trivy.existingClaim }} + {{- end }} + {{- end }} + {{- with .Values.trivy.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.trivy.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.trivy.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + {{- if .Values.trivy.priorityClassName }} + priorityClassName: {{ .Values.trivy.priorityClassName }} + {{- end }} +{{- if and .Values.persistence.enabled (not $trivy.existingClaim) }} + volumeClaimTemplates: + - apiVersion: v1 + kind: PersistentVolumeClaim + metadata: + name: data + labels: +{{ include "harbor.legacy.labels" . | indent 8 }} + annotations: + {{- range $key, $value := $trivy.annotations }} + {{ $key }}: {{ $value | quote }} + {{- end }} + spec: + accessModes: [{{ $trivy.accessMode | quote }}] + {{- if $trivy.storageClass }} + {{- if (eq "-" $trivy.storageClass) }} + storageClassName: "" + {{- else }} + storageClassName: "{{ $trivy.storageClass }}" + {{- end }} + {{- end }} + resources: + requests: + storage: {{ $trivy.size | quote }} +{{- end }} +{{- end }} diff --git a/packages/system/harbor/charts/harbor/templates/trivy/trivy-svc.yaml b/packages/system/harbor/charts/harbor/templates/trivy/trivy-svc.yaml new file mode 100644 index 00000000..a9fb9bf4 --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/trivy/trivy-svc.yaml @@ -0,0 +1,23 @@ +{{ if .Values.trivy.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: "{{ template "harbor.trivy" . }}" + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} +spec: +{{- if .Values.ipFamily.policy }} + ipFamilyPolicy: {{ .Values.ipFamily.policy }} +{{- end }} +{{- if .Values.ipFamily.families }} + ipFamilies: {{ toYaml .Values.ipFamily.families | nindent 4 }} +{{- end }} + ports: + - name: {{ ternary "https-trivy" "http-trivy" .Values.internalTLS.enabled }} + protocol: TCP + port: {{ template "harbor.trivy.servicePort" . }} + selector: +{{ include "harbor.matchLabels" . | indent 4 }} + component: trivy +{{ end }} diff --git a/packages/system/harbor/charts/harbor/templates/trivy/trivy-tls.yaml b/packages/system/harbor/charts/harbor/templates/trivy/trivy-tls.yaml new file mode 100644 index 00000000..58bce4ec --- /dev/null +++ b/packages/system/harbor/charts/harbor/templates/trivy/trivy-tls.yaml @@ -0,0 +1,16 @@ +{{- if and .Values.trivy.enabled .Values.internalTLS.enabled }} +{{- if eq .Values.internalTLS.certSource "manual" }} +apiVersion: v1 +kind: Secret +metadata: + name: "{{ template "harbor.internalTLS.trivy.secretName" . }}" + namespace: {{ .Release.Namespace | quote }} + labels: +{{ include "harbor.labels" . | indent 4 }} +type: kubernetes.io/tls +data: + ca.crt: {{ (required "The \"internalTLS.trustCa\" is required!" .Values.internalTLS.trustCa) | b64enc | quote }} + tls.crt: {{ (required "The \"internalTLS.trivy.crt\" is required!" .Values.internalTLS.trivy.crt) | b64enc | quote }} + tls.key: {{ (required "The \"internalTLS.trivy.key\" is required!" .Values.internalTLS.trivy.key) | b64enc | quote }} +{{- end }} +{{- end }} diff --git a/packages/system/harbor/charts/harbor/values.yaml b/packages/system/harbor/charts/harbor/values.yaml new file mode 100644 index 00000000..e00fafc3 --- /dev/null +++ b/packages/system/harbor/charts/harbor/values.yaml @@ -0,0 +1,1095 @@ +expose: + # Set how to expose the service. Set the type as "ingress", "clusterIP", "nodePort" or "loadBalancer" + # and fill the information in the corresponding section + type: ingress + tls: + # Enable TLS or not. + # Delete the "ssl-redirect" annotations in "expose.ingress.annotations" when TLS is disabled and "expose.type" is "ingress" + # Note: if the "expose.type" is "ingress" and TLS is disabled, + # the port must be included in the command when pulling/pushing images. + # Refer to https://github.com/goharbor/harbor/issues/5291 for details. + enabled: true + # The source of the tls certificate. Set as "auto", "secret" + # or "none" and fill the information in the corresponding section + # 1) auto: generate the tls certificate automatically + # 2) secret: read the tls certificate from the specified secret. + # The tls certificate can be generated manually or by cert manager + # 3) none: configure no tls certificate for the ingress. If the default + # tls certificate is configured in the ingress controller, choose this option + certSource: auto + auto: + # The common name used to generate the certificate, it's necessary + # when the type isn't "ingress" + commonName: "" + secret: + # The name of secret which contains keys named: + # "tls.crt" - the certificate + # "tls.key" - the private key + secretName: "" + ingress: + hosts: + core: core.harbor.domain + # set to the type of ingress controller if it has specific requirements. + # leave as `default` for most ingress controllers. + # set to `gce` if using the GCE ingress controller + # set to `ncp` if using the NCP (NSX-T Container Plugin) ingress controller + # set to `alb` if using the ALB ingress controller + # set to `f5-bigip` if using the F5 BIG-IP ingress controller + controller: default + ## Allow .Capabilities.KubeVersion.Version to be overridden while creating ingress + kubeVersionOverride: "" + className: "" + annotations: + # note different ingress controllers may require a different ssl-redirect annotation + # for Envoy, use ingress.kubernetes.io/force-ssl-redirect: "true" and remove the nginx lines below + ingress.kubernetes.io/ssl-redirect: "true" + ingress.kubernetes.io/proxy-body-size: "0" + nginx.ingress.kubernetes.io/ssl-redirect: "true" + nginx.ingress.kubernetes.io/proxy-body-size: "0" + # ingress-specific labels + labels: {} + route: + labels: {} + annotations: {} + # - name: envoy-internal + # namespace: networking + # sectionName: https + # group: gateway.networking.k8s.io + # kind: Gateway + parentRefs: {} + # - "harbor.example.com" + hosts: [] + clusterIP: + # The name of ClusterIP service + name: harbor + # The ip address of the ClusterIP service (leave empty for acquiring dynamic ip) + staticClusterIP: "" + ports: + # The service port Harbor listens on when serving HTTP + httpPort: 80 + # The service port Harbor listens on when serving HTTPS + httpsPort: 443 + # Annotations on the ClusterIP service + annotations: {} + # ClusterIP-specific labels + labels: {} + nodePort: + # The name of NodePort service + name: harbor + ports: + http: + # The service port Harbor listens on when serving HTTP + port: 80 + # The node port Harbor listens on when serving HTTP + nodePort: 30002 + https: + # The service port Harbor listens on when serving HTTPS + port: 443 + # The node port Harbor listens on when serving HTTPS + nodePort: 30003 + # Annotations on the nodePort service + annotations: {} + # nodePort-specific labels + labels: {} + loadBalancer: + # The name of LoadBalancer service + name: harbor + # Set the IP if the LoadBalancer supports assigning IP + IP: "" + ports: + # The service port Harbor listens on when serving HTTP + httpPort: 80 + # The service port Harbor listens on when serving HTTPS + httpsPort: 443 + # Annotations on the loadBalancer service + annotations: {} + # loadBalancer-specific labels + labels: {} + sourceRanges: [] + +# The external URL for Harbor core service. It is used to +# 1) populate the docker/helm commands showed on portal +# 2) populate the token service URL returned to docker client +# +# Format: protocol://domain[:port]. Usually: +# 1) if "expose.type" is "ingress", the "domain" should be +# the value of "expose.ingress.hosts.core" +# 2) if "expose.type" is "clusterIP", the "domain" should be +# the value of "expose.clusterIP.name" +# 3) if "expose.type" is "nodePort", the "domain" should be +# the IP address of k8s node +# +# If Harbor is deployed behind the proxy, set it as the URL of proxy +externalURL: https://core.harbor.domain + +# The persistence is enabled by default and a default StorageClass +# is needed in the k8s cluster to provision volumes dynamically. +# Specify another StorageClass in the "storageClass" or set "existingClaim" +# if you already have existing persistent volumes to use +# +# For storing images and charts, you can also use "azure", "gcs", "s3", +# "swift" or "oss". Set it in the "imageChartStorage" section +persistence: + enabled: true + # Setting it to "keep" to avoid removing PVCs during a helm delete + # operation. Leaving it empty will delete PVCs after the chart deleted + # (this does not apply for PVCs that are created for internal database + # and redis components, i.e. they are never deleted automatically) + resourcePolicy: "keep" + persistentVolumeClaim: + registry: + # Use the existing PVC which must be created manually before bound, + # and specify the "subPath" if the PVC is shared with other components + existingClaim: "" + # Specify the "storageClass" used to provision the volume. Or the default + # StorageClass will be used (the default). + # Set it to "-" to disable dynamic provisioning + storageClass: "" + subPath: "" + accessMode: ReadWriteOnce + size: 5Gi + annotations: {} + jobservice: + jobLog: + existingClaim: "" + storageClass: "" + subPath: "" + accessMode: ReadWriteOnce + size: 1Gi + annotations: {} + # If external database is used, the following settings for database will + # be ignored + database: + existingClaim: "" + storageClass: "" + subPath: "" + accessMode: ReadWriteOnce + size: 1Gi + annotations: {} + # If external Redis is used, the following settings for Redis will + # be ignored + redis: + existingClaim: "" + storageClass: "" + subPath: "" + accessMode: ReadWriteOnce + size: 1Gi + annotations: {} + trivy: + existingClaim: "" + storageClass: "" + subPath: "" + accessMode: ReadWriteOnce + size: 5Gi + annotations: {} + # Define which storage backend is used for registry to store + # images and charts. Refer to + # https://github.com/distribution/distribution/blob/release/2.8/docs/configuration.md#storage + # for the detail. + imageChartStorage: + # Specify whether to disable `redirect` for images and chart storage, for + # backends which not supported it (such as using minio for `s3` storage type), please disable + # it. To disable redirects, simply set `disableredirect` to `true` instead. + # Refer to + # https://github.com/distribution/distribution/blob/release/2.8/docs/configuration.md#redirect + # for the detail. + disableredirect: false + # Specify the "caBundleSecretName" if the storage service uses a self-signed certificate. + # The secret must contain keys named "ca.crt" which will be injected into the trust store + # of registry's containers. + # caBundleSecretName: + + # Specify the type of storage: "filesystem", "azure", "gcs", "s3", "swift", + # "oss" and fill the information needed in the corresponding section. The type + # must be "filesystem" if you want to use persistent volumes for registry + type: filesystem + filesystem: + rootdirectory: /storage + #maxthreads: 100 + azure: + accountname: accountname + accountkey: base64encodedaccountkey + container: containername + #realm: core.windows.net + # To use existing secret, the key must be AZURE_STORAGE_ACCESS_KEY + existingSecret: "" + gcs: + bucket: bucketname + # The base64 encoded json file which contains the key + encodedkey: base64-encoded-json-key-file + #rootdirectory: /gcs/object/name/prefix + #chunksize: "5242880" + # To use existing secret, the key must be GCS_KEY_DATA + existingSecret: "" + useWorkloadIdentity: false + s3: + # Set an existing secret for S3 accesskey and secretkey + # keys in the secret should be REGISTRY_STORAGE_S3_ACCESSKEY and REGISTRY_STORAGE_S3_SECRETKEY for registry + #existingSecret: "" + region: us-west-1 + bucket: bucketname + #accesskey: awsaccesskey + #secretkey: awssecretkey + #regionendpoint: http://myobjects.local + #encrypt: false + #keyid: mykeyid + #secure: true + #skipverify: false + #v4auth: true + #chunksize: "5242880" + #rootdirectory: /s3/object/name/prefix + #storageclass: STANDARD + #multipartcopychunksize: "33554432" + #multipartcopymaxconcurrency: 100 + #multipartcopythresholdsize: "33554432" + swift: + authurl: https://storage.myprovider.com/v3/auth + username: username + password: password + container: containername + # keys in existing secret must be REGISTRY_STORAGE_SWIFT_PASSWORD, REGISTRY_STORAGE_SWIFT_SECRETKEY, REGISTRY_STORAGE_SWIFT_ACCESSKEY + existingSecret: "" + #region: fr + #tenant: tenantname + #tenantid: tenantid + #domain: domainname + #domainid: domainid + #trustid: trustid + #insecureskipverify: false + #chunksize: 5M + #prefix: + #secretkey: secretkey + #accesskey: accesskey + #authversion: 3 + #endpointtype: public + #tempurlcontainerkey: false + #tempurlmethods: + oss: + accesskeyid: accesskeyid + accesskeysecret: accesskeysecret + region: regionname + bucket: bucketname + # key in existingSecret must be REGISTRY_STORAGE_OSS_ACCESSKEYSECRET + existingSecret: "" + #endpoint: endpoint + #internal: false + #encrypt: false + #secure: true + #chunksize: 10M + #rootdirectory: rootdirectory + +# The initial password of Harbor admin. Change it from portal after launching Harbor +# or give an existing secret for it +# key in secret is given via (default to HARBOR_ADMIN_PASSWORD) +existingSecretAdminPassword: "" +existingSecretAdminPasswordKey: HARBOR_ADMIN_PASSWORD +harborAdminPassword: "Harbor12345" + +# The internal TLS used for harbor components secure communicating. In order to enable https +# in each component tls cert files need to provided in advance. +internalTLS: + # If internal TLS enabled + enabled: false + # enable strong ssl ciphers (default: false) + strong_ssl_ciphers: false + # There are three ways to provide tls + # 1) "auto" will generate cert automatically + # 2) "manual" need provide cert file manually in following value + # 3) "secret" internal certificates from secret + certSource: "auto" + # The content of trust ca, only available when `certSource` is "manual" + trustCa: "" + # core related cert configuration + core: + # secret name for core's tls certs + secretName: "" + # Content of core's TLS cert file, only available when `certSource` is "manual" + crt: "" + # Content of core's TLS key file, only available when `certSource` is "manual" + key: "" + # jobservice related cert configuration + jobservice: + # secret name for jobservice's tls certs + secretName: "" + # Content of jobservice's TLS key file, only available when `certSource` is "manual" + crt: "" + # Content of jobservice's TLS key file, only available when `certSource` is "manual" + key: "" + # registry related cert configuration + registry: + # secret name for registry's tls certs + secretName: "" + # Content of registry's TLS key file, only available when `certSource` is "manual" + crt: "" + # Content of registry's TLS key file, only available when `certSource` is "manual" + key: "" + # portal related cert configuration + portal: + # secret name for portal's tls certs + secretName: "" + # Content of portal's TLS key file, only available when `certSource` is "manual" + crt: "" + # Content of portal's TLS key file, only available when `certSource` is "manual" + key: "" + # trivy related cert configuration + trivy: + # secret name for trivy's tls certs + secretName: "" + # Content of trivy's TLS key file, only available when `certSource` is "manual" + crt: "" + # Content of trivy's TLS key file, only available when `certSource` is "manual" + key: "" + +ipFamily: + # ipv6Enabled set to true if ipv6 is enabled in cluster, currently it affected the nginx related component + ipv6: + enabled: true + # ipv4Enabled set to true if ipv4 is enabled in cluster, currently it affected the nginx related component + ipv4: + enabled: true + + # Sets the IP family policy for services to be able to configure dual-stack; see [Configure dual-stack](https://kubernetes.io/docs/concepts/services-networking/dual-stack/#services). + policy: "" + # A list of IP families for services that should be supported, in the order in which they should be applied to ClusterIP. Can be "IPv4" and/or "IPv6". + families: [] + +imagePullPolicy: IfNotPresent + +# Use this set to assign a list of default pullSecrets +imagePullSecrets: +# - name: docker-registry-secret +# - name: internal-registry-secret + +# The update strategy for deployments with persistent volumes(jobservice, registry): "RollingUpdate" or "Recreate" +# Set it as "Recreate" when "RWM" for volumes isn't supported +updateStrategy: + type: RollingUpdate + +# debug, info, warning, error or fatal +logLevel: info + +# The name of the secret which contains key named "ca.crt". Setting this enables the +# download link on portal to download the CA certificate when the certificate isn't +# generated automatically +caSecretName: "" + +# The secret key used for encryption. Must be a string of 16 chars. +secretKey: "not-a-secure-key" +# If using existingSecretSecretKey, the key must be secretKey +existingSecretSecretKey: "" + +# The proxy settings for updating trivy vulnerabilities from the Internet and replicating +# artifacts from/to the registries that cannot be reached directly +proxy: + httpProxy: + httpsProxy: + noProxy: 127.0.0.1,localhost,.local,.internal + components: + - core + - jobservice + - trivy + +# Run the migration job via helm hook +enableMigrateHelmHook: false + +# The custom ca bundle secret, the secret must contain key named "ca.crt" +# which will be injected into the trust store for core, jobservice, registry, trivy components +# caBundleSecretName: "" + +## UAA Authentication Options +# If you're using UAA for authentication behind a self-signed +# certificate you will need to provide the CA Cert. +# Set uaaSecretName below to provide a pre-created secret that +# contains a base64 encoded CA Certificate named `ca.crt`. +# uaaSecretName: + +metrics: + enabled: false + core: + path: /metrics + port: 8001 + registry: + path: /metrics + port: 8001 + jobservice: + path: /metrics + port: 8001 + exporter: + path: /metrics + port: 8001 + ## Create prometheus serviceMonitor to scrape harbor metrics. + ## This requires the monitoring.coreos.com/v1 CRD. Please see + ## https://github.com/prometheus-operator/prometheus-operator/blob/main/Documentation/user-guides/getting-started.md + ## + serviceMonitor: + enabled: false + additionalLabels: {} + # Scrape interval. If not set, the Prometheus default scrape interval is used. + interval: "" + # Metric relabel configs to apply to samples before ingestion. + metricRelabelings: + [] + # - action: keep + # regex: 'kube_(daemonset|deployment|pod|namespace|node|statefulset).+' + # sourceLabels: [__name__] + # Relabel configs to apply to samples before ingestion. + relabelings: + [] + # - sourceLabels: [__meta_kubernetes_pod_node_name] + # separator: ; + # regex: ^(.*)$ + # targetLabel: nodename + # replacement: $1 + # action: replace + +trace: + enabled: false + # trace provider: jaeger or otel + # jaeger should be 1.26+ + provider: jaeger + # set sample_rate to 1 if you wanna sampling 100% of trace data; set 0.5 if you wanna sampling 50% of trace data, and so forth + sample_rate: 1 + # namespace used to differentiate different harbor services + # namespace: + # attributes is a key value dict contains user defined attributes used to initialize trace provider + # attributes: + # application: harbor + jaeger: + # jaeger supports two modes: + # collector mode(uncomment endpoint and uncomment username, password if needed) + # agent mode(uncomment agent_host and agent_port) + endpoint: http://hostname:14268/api/traces + # username: + # password: + # agent_host: hostname + # export trace data by jaeger.thrift in compact mode + # agent_port: 6831 + otel: + endpoint: hostname:4318 + url_path: /v1/traces + compression: false + insecure: true + # timeout is in seconds + timeout: 10 + +# cache layer configurations +# if this feature enabled, harbor will cache the resource +# `project/project_metadata/repository/artifact/manifest` in the redis +# which help to improve the performance of high concurrent pulling manifest. +cache: + # default is not enabled. + enabled: false + # default keep cache for one day. + expireHours: 24 + +## set Container Security Context to comply with PSP restricted policy if necessary +## each of the conatiner will apply the same security context +## containerSecurityContext:{} is initially an empty yaml that you could edit it on demand, we just filled with a common template for convenience +containerSecurityContext: + privileged: false + allowPrivilegeEscalation: false + seccompProfile: + type: RuntimeDefault + runAsNonRoot: true + capabilities: + drop: + - ALL + +# If service exposed via "ingress", the Nginx will not be used +nginx: + image: + repository: goharbor/nginx-photon + tag: v2.14.2 + # set the service account to be used, default if left empty + serviceAccountName: "" + # mount the service account token + automountServiceAccountToken: false + replicas: 1 + revisionHistoryLimit: 10 + # resources: + # requests: + # memory: 256Mi + # cpu: 100m + extraEnvVars: [] + nodeSelector: {} + tolerations: [] + affinity: {} + # Spread Pods across failure-domains like regions, availability zones or nodes + topologySpreadConstraints: [] + # - maxSkew: 1 + # topologyKey: topology.kubernetes.io/zone + # nodeTaintsPolicy: Honor + # whenUnsatisfiable: DoNotSchedule + ## Additional deployment annotations + podAnnotations: {} + ## Additional deployment labels + podLabels: {} + ## The priority class to run the pod as + priorityClassName: + +portal: + image: + repository: goharbor/harbor-portal + tag: v2.14.2 + # set the service account to be used, default if left empty + serviceAccountName: "" + # mount the service account token + automountServiceAccountToken: false + replicas: 1 + revisionHistoryLimit: 10 + # resources: + # requests: + # memory: 256Mi + # cpu: 100m + extraEnvVars: [] + nodeSelector: {} + tolerations: [] + affinity: {} + # Spread Pods across failure-domains like regions, availability zones or nodes + topologySpreadConstraints: [] + # - maxSkew: 1 + # topologyKey: topology.kubernetes.io/zone + # nodeTaintsPolicy: Honor + # whenUnsatisfiable: DoNotSchedule + ## Additional deployment annotations + podAnnotations: {} + ## Additional deployment labels + podLabels: {} + ## Additional service annotations + serviceAnnotations: {} + ## The priority class to run the pod as + priorityClassName: + # containers to be run before the controller's container starts. + initContainers: [] + # Example: + # + # - name: wait + # image: busybox + # command: [ 'sh', '-c', "sleep 20" ] + +core: + image: + repository: goharbor/harbor-core + tag: v2.14.2 + # set the service account to be used, default if left empty + serviceAccountName: "" + # mount the service account token + automountServiceAccountToken: false + replicas: 1 + revisionHistoryLimit: 10 + ## Startup probe values + startupProbe: + enabled: true + initialDelaySeconds: 10 + # resources: + # requests: + # memory: 256Mi + # cpu: 100m + extraEnvVars: [] + nodeSelector: {} + tolerations: [] + affinity: {} + # Spread Pods across failure-domains like regions, availability zones or nodes + topologySpreadConstraints: [] + # - maxSkew: 1 + # topologyKey: topology.kubernetes.io/zone + # nodeTaintsPolicy: Honor + # whenUnsatisfiable: DoNotSchedule + ## Additional deployment annotations + podAnnotations: {} + ## Additional deployment labels + podLabels: {} + ## Additional service annotations + serviceAnnotations: {} + ## The priority class to run the pod as + priorityClassName: + # containers to be run before the controller's container starts. + initContainers: [] + # Example: + # + # - name: wait + # image: busybox + # command: [ 'sh', '-c', "sleep 20" ] + ## User settings configuration json string + configureUserSettings: + # The provider for updating project quota(usage), there are 2 options, redis or db. + # By default it is implemented by db but you can configure it to redis which + # can improve the performance of high concurrent pushing to the same project, + # and reduce the database connections spike and occupies. + # Using redis will bring up some delay for quota usage updation for display, so only + # suggest switch provider to redis if you were ran into the db connections spike around + # the scenario of high concurrent pushing to same project, no improvment for other scenes. + quotaUpdateProvider: db # Or redis + # Secret is used when core server communicates with other components. + # If a secret key is not specified, Helm will generate one. Alternatively set existingSecret to use an existing secret + # Must be a string of 16 chars. + secret: "" + # Fill in the name of a kubernetes secret if you want to use your own + # If using existingSecret, the key must be secret + existingSecret: "" + # Fill the name of a kubernetes secret if you want to use your own + # TLS certificate and private key for token encryption/decryption. + # The secret must contain keys named: + # "tls.key" - the private key + # "tls.crt" - the certificate + secretName: "" + # If not specifying a preexisting secret, a secret can be created from tokenKey and tokenCert and used instead. + # If none of secretName, tokenKey, and tokenCert are specified, an ephemeral key and certificate will be autogenerated. + # tokenKey and tokenCert must BOTH be set or BOTH unset. + # The tokenKey value is formatted as a multiline string containing a PEM-encoded RSA key, indented one more than tokenKey on the following line. + tokenKey: | + # If tokenKey is set, the value of tokenCert must be set as a PEM-encoded certificate signed by tokenKey, and supplied as a multiline string, indented one more than tokenCert on the following line. + tokenCert: | + # The XSRF key. Will be generated automatically if it isn't specified + # While you specified, Please make sure it is 32 characters, otherwise would have validation issue at the harbor-core runtime + # https://github.com/goharbor/harbor/pull/21154 + xsrfKey: "" + # If using existingSecret, the key is defined by core.existingXsrfSecretKey + existingXsrfSecret: "" + # If using existingSecret, the key + existingXsrfSecretKey: CSRF_KEY + # The time duration for async update artifact pull_time and repository + # pull_count, the unit is second. Will be 10 seconds if it isn't set. + # eg. artifactPullAsyncFlushDuration: 10 + artifactPullAsyncFlushDuration: + gdpr: + deleteUser: false + auditLogsCompliant: false + +jobservice: + image: + repository: goharbor/harbor-jobservice + tag: v2.14.2 + # set the service account to be used, default if left empty + serviceAccountName: "" + # mount the service account token + automountServiceAccountToken: false + replicas: 1 + revisionHistoryLimit: 10 + # resources: + # requests: + # memory: 256Mi + # cpu: 100m + extraEnvVars: [] + nodeSelector: {} + tolerations: [] + affinity: {} + # Spread Pods across failure-domains like regions, availability zones or nodes + topologySpreadConstraints: + # - maxSkew: 1 + # topologyKey: topology.kubernetes.io/zone + # nodeTaintsPolicy: Honor + # whenUnsatisfiable: DoNotSchedule + ## Additional deployment annotations + podAnnotations: {} + ## Additional deployment labels + podLabels: {} + ## The priority class to run the pod as + priorityClassName: + # containers to be run before the controller's container starts. + initContainers: [] + # Example: + # + # - name: wait + # image: busybox + # command: [ 'sh', '-c', "sleep 20" ] + maxJobWorkers: 10 + # The logger for jobs: "file", "database" or "stdout" + jobLoggers: + - file + # - database + # - stdout + # The jobLogger sweeper duration (ignored if `jobLogger` is `stdout`) + loggerSweeperDuration: 14 #days + notification: + webhook_job_max_retry: 3 + webhook_job_http_client_timeout: 3 # in seconds + reaper: + # the max time to wait for a task to finish, if unfinished after max_update_hours, the task will be mark as error, but the task will continue to run, default value is 24 + max_update_hours: 24 + # the max time for execution in running state without new task created + max_dangling_hours: 168 + # Secret is used when job service communicates with other components. + # If a secret key is not specified, Helm will generate one. + # Must be a string of 16 chars. + secret: "" + # Use an existing secret resource + existingSecret: "" + # Key within the existing secret for the job service secret + existingSecretKey: JOBSERVICE_SECRET + +registry: + registry: + image: + repository: goharbor/registry-photon + tag: v2.14.2 + # resources: + # requests: + # memory: 256Mi + # cpu: 100m + extraEnvVars: [] + controller: + image: + repository: goharbor/harbor-registryctl + tag: v2.14.2 + # resources: + # requests: + # memory: 256Mi + # cpu: 100m + extraEnvVars: [] + # set the service account to be used, default if left empty + serviceAccountName: "" + # mount the service account token + automountServiceAccountToken: false + replicas: 1 + revisionHistoryLimit: 10 + nodeSelector: {} + tolerations: [] + affinity: {} + # Spread Pods across failure-domains like regions, availability zones or nodes + topologySpreadConstraints: [] + # - maxSkew: 1 + # topologyKey: topology.kubernetes.io/zone + # nodeTaintsPolicy: Honor + # whenUnsatisfiable: DoNotSchedule + ## Additional deployment annotations + podAnnotations: {} + ## Additional deployment labels + podLabels: {} + ## The priority class to run the pod as + priorityClassName: + # containers to be run before the controller's container starts. + initContainers: [] + # Example: + # + # - name: wait + # image: busybox + # command: [ 'sh', '-c', "sleep 20" ] + # Secret is used to secure the upload state from client + # and registry storage backend. + # See: https://github.com/distribution/distribution/blob/release/2.8/docs/configuration.md#http + # If a secret key is not specified, Helm will generate one. + # Must be a string of 16 chars. + secret: "" + # Use an existing secret resource + existingSecret: "" + # Key within the existing secret for the registry service secret + existingSecretKey: REGISTRY_HTTP_SECRET + # If true, the registry returns relative URLs in Location headers. The client is responsible for resolving the correct URL. + relativeurls: false + credentials: + username: "harbor_registry_user" + password: "harbor_registry_password" + # If using existingSecret, the key must be REGISTRY_PASSWD and REGISTRY_HTPASSWD + existingSecret: "" + # Login and password in htpasswd string format. Excludes `registry.credentials.username` and `registry.credentials.password`. May come in handy when integrating with tools like argocd or flux. This allows the same line to be generated each time the template is rendered, instead of the `htpasswd` function from helm, which generates different lines each time because of the salt. + # htpasswdString: $apr1$XLefHzeG$Xl4.s00sMSCCcMyJljSZb0 # example string + htpasswdString: "" + middleware: + enabled: false + type: cloudFront + cloudFront: + baseurl: example.cloudfront.net + keypairid: KEYPAIRID + duration: 3000s + ipfilteredby: none + # The secret key that should be present is CLOUDFRONT_KEY_DATA, which should be the encoded private key + # that allows access to CloudFront + privateKeySecret: "my-secret" + # enable purge _upload directories + upload_purging: + enabled: true + # remove files in _upload directories which exist for a period of time, default is one week. + age: 168h + # the interval of the purge operations + interval: 24h + dryrun: false + +trivy: + # enabled the flag to enable Trivy scanner + enabled: true + image: + # repository the repository for Trivy adapter image + repository: goharbor/trivy-adapter-photon + # tag the tag for Trivy adapter image + tag: v2.14.2 + # set the service account to be used, default if left empty + serviceAccountName: "" + # mount the service account token + automountServiceAccountToken: false + # replicas the number of Pod replicas + replicas: 1 + resources: + requests: + cpu: 200m + memory: 512Mi + limits: + cpu: 1 + memory: 1Gi + extraEnvVars: [] + nodeSelector: {} + tolerations: [] + affinity: {} + # Spread Pods across failure-domains like regions, availability zones or nodes + topologySpreadConstraints: [] + # - maxSkew: 1 + # topologyKey: topology.kubernetes.io/zone + # nodeTaintsPolicy: Honor + # whenUnsatisfiable: DoNotSchedule + ## Additional deployment annotations + podAnnotations: {} + ## Additional deployment labels + podLabels: {} + ## The priority class to run the pod as + priorityClassName: + # containers to be run before the controller's container starts. + initContainers: [] + # Example: + # + # - name: wait + # image: busybox + # command: [ 'sh', '-c', "sleep 20" ] + # debugMode the flag to enable Trivy debug mode with more verbose scanning log + debugMode: false + # vulnType a comma-separated list of vulnerability types. Possible values are `os` and `library`. + vulnType: "os,library" + # severity a comma-separated list of severities to be checked + severity: "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL" + # ignoreUnfixed the flag to display only fixed vulnerabilities + ignoreUnfixed: false + # insecure the flag to skip verifying registry certificate + insecure: false + # gitHubToken the GitHub access token to download Trivy DB + # + # Trivy DB contains vulnerability information from NVD, Red Hat, and many other upstream vulnerability databases. + # It is downloaded by Trivy from the GitHub release page https://github.com/aquasecurity/trivy-db/releases and cached + # in the local file system (`/home/scanner/.cache/trivy/db/trivy.db`). In addition, the database contains the update + # timestamp so Trivy can detect whether it should download a newer version from the Internet or use the cached one. + # Currently, the database is updated every 12 hours and published as a new release to GitHub. + # + # Anonymous downloads from GitHub are subject to the limit of 60 requests per hour. Normally such rate limit is enough + # for production operations. If, for any reason, it's not enough, you could increase the rate limit to 5000 + # requests per hour by specifying the GitHub access token. For more details on GitHub rate limiting please consult + # https://developer.github.com/v3/#rate-limiting + # + # You can create a GitHub token by following the instructions in + # https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line + gitHubToken: "" + # skipUpdate the flag to disable Trivy DB downloads from GitHub + # + # You might want to set the value of this flag to `true` in test or CI/CD environments to avoid GitHub rate limiting issues. + # If the value is set to `true` you have to manually download the `trivy.db` file and mount it in the + # `/home/scanner/.cache/trivy/db/trivy.db` path. + skipUpdate: false + # skipJavaDBUpdate If the flag is enabled you have to manually download the `trivy-java.db` file and mount it in the + # `/home/scanner/.cache/trivy/java-db/trivy-java.db` path + skipJavaDBUpdate: false + # The dbRepository and javaDBRepository flags can take multiple values, improving reliability when downloading databases. + # Databases are downloaded in priority order until one is successful. + # An attempt to download from the next repository is only made if a temporary error is received (e.g. status 429 or 5xx). + # + # OCI repository(ies) to retrieve the trivy vulnerability database in order of priority + dbRepository: + - "mirror.gcr.io/aquasec/trivy-db" + - "ghcr.io/aquasecurity/trivy-db" + # OCI repository(ies) to retrieve the Java trivy vulnerability database in order of priority + javaDBRepository: + - "mirror.gcr.io/aquasec/trivy-java-db" + - "ghcr.io/aquasecurity/trivy-java-db" + # The offlineScan option prevents Trivy from sending API requests to identify dependencies. + # + # Scanning JAR files and pom.xml may require Internet access for better detection, but this option tries to avoid it. + # For example, the offline mode will not try to resolve transitive dependencies in pom.xml when the dependency doesn't + # exist in the local repositories. It means a number of detected vulnerabilities might be fewer in offline mode. + # It would work if all the dependencies are in local. + # This option doesn’t affect DB download. You need to specify skipUpdate as well as offlineScan in an air-gapped environment. + offlineScan: false + # Comma-separated list of what security issues to detect. Defaults to `vuln`. + securityCheck: "vuln" + # The duration to wait for scan completion + timeout: 5m0s + +database: + # if external database is used, set "type" to "external" + # and fill the connection information in "external" section + type: internal + internal: + image: + repository: goharbor/harbor-db + tag: v2.14.2 + # set the service account to be used, default if left empty + serviceAccountName: "" + # mount the service account token + automountServiceAccountToken: false + # resources: + # requests: + # memory: 256Mi + # cpu: 100m + # The timeout used in livenessProbe; 1 to 5 seconds + livenessProbe: + timeoutSeconds: 1 + # The timeout used in readinessProbe; 1 to 5 seconds + readinessProbe: + timeoutSeconds: 1 + extraEnvVars: [] + nodeSelector: {} + tolerations: [] + affinity: {} + ## The priority class to run the pod as + priorityClassName: + # containers to be run before the controller's container starts. + extrInitContainers: [] + # Example: + # + # - name: wait + # image: busybox + # command: [ 'sh', '-c', "sleep 20" ] + # The initial superuser password for internal database + password: "changeit" + # The size limit for Shared memory, pgSQL use it for shared_buffer + # More details see: + # https://github.com/goharbor/harbor/issues/15034 + shmSizeLimit: 512Mi + initContainer: + migrator: {} + # resources: + # requests: + # memory: 128Mi + # cpu: 100m + permissions: {} + # resources: + # requests: + # memory: 128Mi + # cpu: 100m + external: + host: "192.168.0.1" + port: "5432" + username: "user" + password: "password" + coreDatabase: "registry" + # if using existing secret, the key must be "password" + existingSecret: "" + # "disable" - No SSL + # "require" - Always SSL (skip verification) + # "verify-ca" - Always SSL (verify that the certificate presented by the + # server was signed by a trusted CA) + # "verify-full" - Always SSL (verify that the certification presented by the + # server was signed by a trusted CA and the server host name matches the one + # in the certificate) + sslmode: "disable" + # The maximum number of connections in the idle connection pool per pod (core+exporter). + # If it <=0, no idle connections are retained. + maxIdleConns: 100 + # The maximum number of open connections to the database per pod (core+exporter). + # If it <= 0, then there is no limit on the number of open connections. + # Note: the default number of connections is 1024 for harbor's postgres. + maxOpenConns: 900 + ## Additional deployment annotations + podAnnotations: {} + ## Additional deployment labels + podLabels: {} + +redis: + # if external Redis is used, set "type" to "external" + # and fill the connection information in "external" section + type: internal + internal: + image: + repository: goharbor/redis-photon + tag: v2.14.2 + # set the service account to be used, default if left empty + serviceAccountName: "" + # mount the service account token + automountServiceAccountToken: false + # resources: + # requests: + # memory: 256Mi + # cpu: 100m + extraEnvVars: [] + nodeSelector: {} + tolerations: [] + affinity: {} + ## The priority class to run the pod as + priorityClassName: + # containers to be run before the controller's container starts. + initContainers: [] + # Example: + # + # - name: wait + # image: busybox + # command: [ 'sh', '-c', "sleep 20" ] + # # jobserviceDatabaseIndex defaults to "1" + # # registryDatabaseIndex defaults to "2" + # # trivyAdapterIndex defaults to "5" + # # harborDatabaseIndex defaults to "0", but it can be configured to "6", this config is optional + # # cacheLayerDatabaseIndex defaults to "0", but it can be configured to "7", this config is optional + jobserviceDatabaseIndex: "1" + registryDatabaseIndex: "2" + trivyAdapterIndex: "5" + # harborDatabaseIndex: "6" + # cacheLayerDatabaseIndex: "7" + external: + # support redis, redis+sentinel + # addr for redis: : + # addr for redis+sentinel: :,:,: + addr: "192.168.0.2:6379" + # The name of the set of Redis instances to monitor, it must be set to support redis+sentinel + sentinelMasterSet: "" + # TLS configuration for redis connection + # only server-authentication is supported, mTLS for redis connection is not supported + # tls connection will be disable by default + # Once `tlsOptions.enable` set as true, tls/ssl connection will be used for redis + # Please set the `caBundleSecretName` in this configuration file which conatins redis server rootCA if it is self-signed. + # The secret must contain keys named "ca.crt" which will be injected into the trust store + tlsOptions: + enable: false + # The "coreDatabaseIndex" must be "0" as the library Harbor + # used doesn't support configuring it + # harborDatabaseIndex defaults to "0", but it can be configured to "6", this config is optional + # cacheLayerDatabaseIndex defaults to "0", but it can be configured to "7", this config is optional + coreDatabaseIndex: "0" + jobserviceDatabaseIndex: "1" + registryDatabaseIndex: "2" + trivyAdapterIndex: "5" + # harborDatabaseIndex: "6" + # cacheLayerDatabaseIndex: "7" + # username field can be an empty string, and it will be authenticated against the default user + username: "" + password: "" + # If using existingSecret, the key must be REDIS_PASSWORD, if ACL mode enabled, also inlcudes data of username, the keys must be REDIS_USERNAME + existingSecret: "" + ## Additional deployment annotations + podAnnotations: {} + ## Additional deployment labels + podLabels: {} + +exporter: + image: + repository: goharbor/harbor-exporter + tag: v2.14.2 + serviceAccountName: "" + # mount the service account token + automountServiceAccountToken: false + replicas: 1 + revisionHistoryLimit: 10 + # resources: + # requests: + # memory: 256Mi + # cpu: 100m + extraEnvVars: [] + podAnnotations: {} + ## Additional deployment labels + podLabels: {} + nodeSelector: {} + tolerations: [] + affinity: {} + # Spread Pods across failure-domains like regions, availability zones or nodes + topologySpreadConstraints: [] + ## The priority class to run the pod as + priorityClassName: + # - maxSkew: 1 + # topologyKey: topology.kubernetes.io/zone + # nodeTaintsPolicy: Honor + # whenUnsatisfiable: DoNotSchedule + cacheDuration: 23 + cacheCleanInterval: 14400 diff --git a/packages/system/harbor/values.yaml b/packages/system/harbor/values.yaml new file mode 100644 index 00000000..4c8388fa --- /dev/null +++ b/packages/system/harbor/values.yaml @@ -0,0 +1 @@ +harbor: {} From 305495d023c652455ce63cf415ab49edaea2678e Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Mon, 16 Feb 2026 19:51:12 +0300 Subject: [PATCH 089/666] [harbor] Fix YAML quoting for Go template values in ApplicationDefinition Quote resourceNames values starting with {{ to prevent YAML parser from interpreting them as flow mappings. Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- packages/system/harbor-rd/cozyrds/harbor.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/system/harbor-rd/cozyrds/harbor.yaml b/packages/system/harbor-rd/cozyrds/harbor.yaml index da8cfb62..33c0eaab 100644 --- a/packages/system/harbor-rd/cozyrds/harbor.yaml +++ b/packages/system/harbor-rd/cozyrds/harbor.yaml @@ -34,14 +34,14 @@ spec: exclude: [] include: - resourceNames: - - {{ .name }}-credentials + - "{{ .name }}-credentials" services: exclude: [] include: - resourceNames: - - {{ .name }} + - "{{ .name }}" ingresses: exclude: [] include: - resourceNames: - - {{ .name }}-ingress + - "{{ .name }}-ingress" From 2dd3c03279c47470a6c3014071b46cc36dd6831d Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Mon, 16 Feb 2026 20:36:52 +0300 Subject: [PATCH 090/666] [harbor] Use CPNG and redis-operator instead of internal databases Replace Harbor's internal PostgreSQL with CloudNativePG operator and internal Redis with redis-operator (RedisFailover), following established Cozystack patterns from seaweedfs and redis apps. Additional fixes from code review: - Fix registry resources nesting level (registry.registry/controller) - Persist token CA across upgrades to prevent JWT invalidation - Update values schema and ApplicationDefinition Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- hack/e2e-apps/harbor.bats | 6 +- packages/extra/harbor/README.md | 14 +-- packages/extra/harbor/templates/harbor.yaml | 66 ++++++++--- packages/extra/harbor/values.schema.json | 106 ++---------------- packages/extra/harbor/values.yaml | 20 ++-- packages/system/harbor-rd/cozyrds/harbor.yaml | 4 +- .../system/harbor/templates/database.yaml | 24 ++++ packages/system/harbor/templates/redis.yaml | 66 +++++++++++ packages/system/harbor/values.yaml | 8 ++ 9 files changed, 175 insertions(+), 139 deletions(-) create mode 100644 packages/system/harbor/templates/database.yaml create mode 100644 packages/system/harbor/templates/redis.yaml diff --git a/hack/e2e-apps/harbor.bats b/hack/e2e-apps/harbor.bats index 8d1daecb..c28eee3a 100644 --- a/hack/e2e-apps/harbor.bats +++ b/hack/e2e-apps/harbor.bats @@ -28,12 +28,10 @@ spec: resourcesPreset: "nano" database: size: 2Gi - resources: {} - resourcesPreset: "nano" + replicas: 1 redis: size: 1Gi - resources: {} - resourcesPreset: "nano" + replicas: 1 EOF sleep 5 kubectl -n tenant-test wait hr $name --timeout=60s --for=condition=ready diff --git a/packages/extra/harbor/README.md b/packages/extra/harbor/README.md index 62bf072e..6425e6fe 100644 --- a/packages/extra/harbor/README.md +++ b/packages/extra/harbor/README.md @@ -39,16 +39,10 @@ Harbor is an open source trusted cloud native registry project that stores, sign | `trivy.resources.cpu` | Number of CPU cores allocated. | `quantity` | `""` | | `trivy.resources.memory` | Amount of memory allocated. | `quantity` | `""` | | `trivy.resourcesPreset` | Default sizing preset used when `resources` is omitted. | `string` | `nano` | -| `database` | Internal PostgreSQL database configuration. | `object` | `{}` | +| `database` | PostgreSQL database configuration. | `object` | `{}` | | `database.size` | Persistent Volume size for database storage. | `quantity` | `5Gi` | -| `database.resources` | Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied. | `object` | `{}` | -| `database.resources.cpu` | Number of CPU cores allocated. | `quantity` | `""` | -| `database.resources.memory` | Amount of memory allocated. | `quantity` | `""` | -| `database.resourcesPreset` | Default sizing preset used when `resources` is omitted. | `string` | `nano` | -| `redis` | Internal Redis cache configuration. | `object` | `{}` | +| `database.replicas` | Number of database instances. | `int` | `2` | +| `redis` | Redis cache configuration. | `object` | `{}` | | `redis.size` | Persistent Volume size for cache storage. | `quantity` | `1Gi` | -| `redis.resources` | Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied. | `object` | `{}` | -| `redis.resources.cpu` | Number of CPU cores allocated. | `quantity` | `""` | -| `redis.resources.memory` | Amount of memory allocated. | `quantity` | `""` | -| `redis.resourcesPreset` | Default sizing preset used when `resources` is omitted. | `string` | `nano` | +| `redis.replicas` | Number of Redis replicas. | `int` | `2` | diff --git a/packages/extra/harbor/templates/harbor.yaml b/packages/extra/harbor/templates/harbor.yaml index 23c06641..ceef4f1f 100644 --- a/packages/extra/harbor/templates/harbor.yaml +++ b/packages/extra/harbor/templates/harbor.yaml @@ -8,6 +8,18 @@ {{- $adminPassword = index $existingSecret.data "admin-password" | b64dec }} {{- end }} +{{- $existingCoreSecret := lookup "v1" "Secret" .Release.Namespace (printf "%s-core" .Release.Name) }} +{{- $tokenKey := "" }} +{{- $tokenCert := "" }} +{{- if $existingCoreSecret }} + {{- if hasKey $existingCoreSecret.data "tls.key" }} + {{- $tokenKey = index $existingCoreSecret.data "tls.key" | b64dec }} + {{- end }} + {{- if hasKey $existingCoreSecret.data "tls.crt" }} + {{- $tokenCert = index $existingCoreSecret.data "tls.crt" | b64dec }} + {{- end }} +{{- end }} + apiVersion: v1 kind: Secret metadata: @@ -42,6 +54,18 @@ spec: - kind: Secret name: cozystack-values values: + db: + replicas: {{ .Values.database.replicas }} + size: {{ .Values.database.size }} + {{- with .Values.storageClass }} + storageClass: {{ . }} + {{- end }} + redis: + replicas: {{ .Values.redis.replicas }} + size: {{ .Values.redis.size }} + {{- with .Values.storageClass }} + storageClass: {{ . }} + {{- end }} harbor: fullnameOverride: {{ .Release.Name }} harborAdminPassword: {{ $adminPassword | quote }} @@ -61,16 +85,6 @@ spec: {{- with .Values.storageClass }} storageClass: {{ . }} {{- end }} - database: - size: {{ .Values.database.size }} - {{- with .Values.storageClass }} - storageClass: {{ . }} - {{- end }} - redis: - size: {{ .Values.redis.size }} - {{- with .Values.storageClass }} - storageClass: {{ . }} - {{- end }} {{- if .Values.trivy.enabled }} trivy: size: {{ .Values.trivy.size }} @@ -81,9 +95,16 @@ spec: portal: resources: {{- include "cozy-lib.resources.defaultingSanitize" (list "nano" (dict) $) | nindent 10 }} core: + {{- if $tokenKey }} + tokenKey: {{ $tokenKey | quote }} + tokenCert: {{ $tokenCert | quote }} + {{- end }} resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.core.resourcesPreset .Values.core.resources $) | nindent 10 }} registry: - resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.registry.resourcesPreset .Values.registry.resources $) | nindent 10 }} + registry: + resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.registry.resourcesPreset .Values.registry.resources $) | nindent 12 }} + controller: + resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.registry.resourcesPreset .Values.registry.resources $) | nindent 12 }} jobservice: resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.jobservice.resourcesPreset .Values.jobservice.resources $) | nindent 10 }} trivy: @@ -92,13 +113,24 @@ spec: resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.trivy.resourcesPreset .Values.trivy.resources $) | nindent 10 }} {{- end }} database: - type: internal - internal: - resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.database.resourcesPreset .Values.database.resources $) | nindent 12 }} + type: external + external: + host: "{{ .Release.Name }}-db-rw" + port: "5432" + username: app + coreDatabase: app + sslmode: disable + existingSecret: "{{ .Release.Name }}-db-app" redis: - type: internal - internal: - resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.redis.resourcesPreset .Values.redis.resources $) | nindent 12 }} + type: external + external: + addr: "rfs-{{ .Release.Name }}-redis:26379" + sentinelMasterSet: "mymaster" + existingSecret: "{{ .Release.Name }}-redis-auth" + coreDatabaseIndex: "0" + jobserviceDatabaseIndex: "1" + registryDatabaseIndex: "2" + trivyAdapterIndex: "5" metrics: enabled: true serviceMonitor: diff --git a/packages/extra/harbor/values.schema.json b/packages/extra/harbor/values.schema.json index a4632de7..2fa0beba 100644 --- a/packages/extra/harbor/values.schema.json +++ b/packages/extra/harbor/values.schema.json @@ -57,59 +57,18 @@ } }, "database": { - "description": "Internal PostgreSQL database configuration.", + "description": "PostgreSQL database configuration.", "type": "object", "default": {}, "required": [ + "replicas", "size" ], "properties": { - "resources": { - "description": "Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.", - "type": "object", - "default": {}, - "properties": { - "cpu": { - "description": "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": "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.", - "type": "string", - "default": "nano", - "enum": [ - "nano", - "micro", - "small", - "medium", - "large", - "xlarge", - "2xlarge" - ] + "replicas": { + "description": "Number of database instances.", + "type": "integer", + "default": 2 }, "size": { "description": "Persistent Volume size for database storage.", @@ -187,59 +146,18 @@ } }, "redis": { - "description": "Internal Redis cache configuration.", + "description": "Redis cache configuration.", "type": "object", "default": {}, "required": [ + "replicas", "size" ], "properties": { - "resources": { - "description": "Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.", - "type": "object", - "default": {}, - "properties": { - "cpu": { - "description": "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": "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.", - "type": "string", - "default": "nano", - "enum": [ - "nano", - "micro", - "small", - "medium", - "large", - "xlarge", - "2xlarge" - ] + "replicas": { + "description": "Number of Redis replicas.", + "type": "integer", + "default": 2 }, "size": { "description": "Persistent Volume size for cache storage.", diff --git a/packages/extra/harbor/values.yaml b/packages/extra/harbor/values.yaml index 50d2ff5f..c5d89b1d 100644 --- a/packages/extra/harbor/values.yaml +++ b/packages/extra/harbor/values.yaml @@ -67,24 +67,20 @@ trivy: resources: {} resourcesPreset: "nano" -## @typedef {struct} Database - Internal PostgreSQL database configuration. +## @typedef {struct} Database - PostgreSQL database configuration (provisioned via CloudNativePG). ## @field {quantity} size - Persistent Volume size for database storage. -## @field {Resources} [resources] - Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied. -## @field {ResourcesPreset} [resourcesPreset] - Default sizing preset used when `resources` is omitted. +## @field {int} replicas - Number of database instances. -## @param {Database} database - Internal PostgreSQL database configuration. +## @param {Database} database - PostgreSQL database configuration. database: size: 5Gi - resources: {} - resourcesPreset: "nano" + replicas: 2 -## @typedef {struct} Redis - Internal Redis cache configuration. +## @typedef {struct} Redis - Redis cache configuration (provisioned via redis-operator). ## @field {quantity} size - Persistent Volume size for cache storage. -## @field {Resources} [resources] - Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied. -## @field {ResourcesPreset} [resourcesPreset] - Default sizing preset used when `resources` is omitted. +## @field {int} replicas - Number of Redis replicas. -## @param {Redis} redis - Internal Redis cache configuration. +## @param {Redis} redis - Redis cache configuration. redis: size: 1Gi - resources: {} - resourcesPreset: "nano" + replicas: 2 diff --git a/packages/system/harbor-rd/cozyrds/harbor.yaml b/packages/system/harbor-rd/cozyrds/harbor.yaml index 33c0eaab..8f159a45 100644 --- a/packages/system/harbor-rd/cozyrds/harbor.yaml +++ b/packages/system/harbor-rd/cozyrds/harbor.yaml @@ -8,7 +8,7 @@ spec: singular: harbor plural: harbors openAPISchema: |- - {"title":"Chart Values","type":"object","properties":{"core":{"description":"Core API server configuration.","type":"object","default":{},"properties":{"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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.","type":"string","default":"small","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]}}},"database":{"description":"Internal PostgreSQL database configuration.","type":"object","default":{},"required":["size"],"properties":{"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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.","type":"string","default":"nano","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]},"size":{"description":"Persistent Volume size for database storage.","default":"5Gi","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}}},"host":{"description":"Hostname for external access to Harbor (defaults to 'harbor' subdomain for the tenant host).","type":"string","default":""},"jobservice":{"description":"Background job service configuration.","type":"object","default":{},"properties":{"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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.","type":"string","default":"nano","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]}}},"redis":{"description":"Internal Redis cache configuration.","type":"object","default":{},"required":["size"],"properties":{"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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.","type":"string","default":"nano","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]},"size":{"description":"Persistent Volume size for cache storage.","default":"1Gi","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}}},"registry":{"description":"Container image registry configuration.","type":"object","default":{},"required":["size"],"properties":{"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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.","type":"string","default":"small","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]},"size":{"description":"Persistent Volume size for container image storage.","default":"50Gi","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","default":""},"trivy":{"description":"Trivy vulnerability scanner configuration.","type":"object","default":{},"required":["enabled","size"],"properties":{"enabled":{"description":"Enable or disable the vulnerability scanner.","type":"boolean","default":true},"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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.","type":"string","default":"nano","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]},"size":{"description":"Persistent Volume size for vulnerability database cache.","default":"5Gi","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}}}}} + {"title":"Chart Values","type":"object","properties":{"core":{"description":"Core API server configuration.","type":"object","default":{},"properties":{"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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.","type":"string","default":"small","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]}}},"database":{"description":"PostgreSQL database configuration.","type":"object","default":{},"required":["replicas","size"],"properties":{"replicas":{"description":"Number of database instances.","type":"integer","default":2},"size":{"description":"Persistent Volume size for database storage.","default":"5Gi","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}}},"host":{"description":"Hostname for external access to Harbor (defaults to 'harbor' subdomain for the tenant host).","type":"string","default":""},"jobservice":{"description":"Background job service configuration.","type":"object","default":{},"properties":{"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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.","type":"string","default":"nano","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]}}},"redis":{"description":"Redis cache configuration.","type":"object","default":{},"required":["replicas","size"],"properties":{"replicas":{"description":"Number of Redis replicas.","type":"integer","default":2},"size":{"description":"Persistent Volume size for cache storage.","default":"1Gi","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}}},"registry":{"description":"Container image registry configuration.","type":"object","default":{},"required":["size"],"properties":{"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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.","type":"string","default":"small","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]},"size":{"description":"Persistent Volume size for container image storage.","default":"50Gi","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","default":""},"trivy":{"description":"Trivy vulnerability scanner configuration.","type":"object","default":{},"required":["enabled","size"],"properties":{"enabled":{"description":"Enable or disable the vulnerability scanner.","type":"boolean","default":true},"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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.","type":"string","default":"nano","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]},"size":{"description":"Persistent Volume size for vulnerability database cache.","default":"5Gi","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}}}}} release: prefix: "" labels: @@ -29,7 +29,7 @@ spec: - registry - container icon: "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHJvbGU9ImltZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHZpZXdCb3g9IjEuMDUgLTEuOTUgMzU5LjQxIDM2MS42NiI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXItZ3JhZGllbnQiIHgxPSIyNjQuNzkiIHgyPSIyNjcuMjciIHkxPSI5NTIuMzkiIHkyPSI5NTIuMzkiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMzAuNDMgMCAwIC0zMC40MyAtNzk1NS4yMiAyOTI4NS43NSkiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiM2MGI5MzIiLz48c3RvcCBvZmZzZXQ9Ii4yOCIgc3RvcC1jb2xvcj0iIzYwYjkzMiIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzM2N2MzNCIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXItZ3JhZGllbnQtMiIgeDE9IjI2My43NyIgeDI9IjI2Ni4yNiIgeTE9Ijk1NS42NSIgeTI9Ijk1NS42NSIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgyNy4yMSAwIDAgLTI3LjIxIC03MDczLjg1IDI2MTY5LjQxKSIgeGxpbms6aHJlZj0iI2xpbmVhci1ncmFkaWVudCIvPjxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyLWdyYWRpZW50LTMiIHgxPSIyNjMuMjgiIHgyPSIyNjUuNzYiIHkxPSI5NTMuNzQiIHkyPSI5NTMuNzQiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMjUuNzUgMCAwIC0yNS43NSAtNjY3MS4xMyAyNDgxMi4yMykiIHhsaW5rOmhyZWY9IiNsaW5lYXItZ3JhZGllbnQiLz48bGluZWFyR3JhZGllbnQgaWQ9ImxpbmVhci1ncmFkaWVudC00IiB4MT0iMjYzLjc3IiB4Mj0iMjY2LjI1IiB5MT0iOTUzLjIiIHkyPSI5NTMuMiIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgyNy4xIDAgMCAtMjcuMSAtNzA0MC45IDI2MTAyLjQ5KSIgeGxpbms6aHJlZj0iI2xpbmVhci1ncmFkaWVudCIvPjxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyLWdyYWRpZW50LTUiIHgxPSIyNjIuNzMiIHgyPSIyNjUuMjEiIHkxPSI5NTQuMzQiIHkyPSI5NTQuMzQiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMjQuNCAwIDAgLTI0LjQgLTYzMDEuMzYgMjM1MjEuOTcpIiB4bGluazpocmVmPSIjbGluZWFyLWdyYWRpZW50Ii8+PGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXItZ3JhZGllbnQtNiIgeDE9IjI3Mi4xNCIgeDI9IjI3NC42MiIgeTE9Ijk1NS4xNSIgeTI9Ijk1NS4xNSIgZ3JhZGllbnRUcmFuc2Zvcm09InNjYWxlKDY2LjA5IC02Ni4wOSkgcm90YXRlKDM2LjUyIDE1ODguMTUzIDY4LjE0OCkiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiM0NTk2ZDgiLz48c3RvcCBvZmZzZXQ9Ii4yIiBzdG9wLWNvbG9yPSIjNDU5NmQ4Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjZmZmIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9ImxpbmVhci1ncmFkaWVudC03IiB4MT0iMjcwLjY1IiB4Mj0iMjczLjEzIiB5MT0iOTUyLjM4IiB5Mj0iOTUyLjM4IiBncmFkaWVudFRyYW5zZm9ybT0ic2NhbGUoNzcuOCAtNzcuOCkgcm90YXRlKC0xMS41NCAtNDU4Ny4yMDkgMTgwMy4zMjMpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjNDE5NGQ3Ii8+PHN0b3Agb2Zmc2V0PSIuMiIgc3RvcC1jb2xvcj0iIzQxOTRkNyIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI2ZmZiIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXItZ3JhZGllbnQtOCIgeDE9IjI3MC45NyIgeDI9IjI3My40NSIgeTE9Ijk1My43NSIgeTI9Ijk1My43NSIgZ3JhZGllbnRUcmFuc2Zvcm09InNjYWxlKDcxLjM1IC03MS4zNSkgcm90YXRlKDEwLjIzIDU0NzcuMzcgLTEwMjQuNjAyKSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzQxOTRkNyIvPjxzdG9wIG9mZnNldD0iLjMzIiBzdG9wLWNvbG9yPSIjNDQ5NmQ4Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjZmZmIi8+PC9saW5lYXJHcmFkaWVudD48Y2xpcFBhdGggaWQ9ImNsaXAtcGF0aCI+PHBhdGggZD0iTTE3NC40IDMwMi41MmwtNjguNjUtMzAuMjYtMy44MyA0My4yMSA3NS41IDIzLjk4LTMuMDItMzYuOTN6IiBjbGFzcz0iY2xzLTEiLz48L2NsaXBQYXRoPjxjbGlwUGF0aCBpZD0iY2xpcC1wYXRoLTIiPjxwYXRoIGQ9Ik0xMTkuMTkgMTM1LjM4aDM4LjU1djMyLjg5aC05LjE1di0xNGExMC4xMyAxMC4xMyAwIDEgMC0yMC4yNiAwdjE0aC05LjE1em0tNy43IDcybDU3LjIgMjUuMjEtMy45NC00OC4yNGg3LjQ5di0xNi4wOGgtNS41NXYtMzIuODloNS41NXYtOS40NWwtMzAuODYtMzAuMTl2LTIuMTJhMi45MSAyLjkxIDAgMCAwLTUuODIgMHYyLjEybC0zMC44NiAzMC4xOXY5LjQ1aDUuNTZ2MzIuODloLTUuNTZ2MTYuMTJoOC44MnoiIGNsYXNzPSJjbHMtMSIvPjwvY2xpcFBhdGg+PGNsaXBQYXRoIGlkPSJjbGlwLXBhdGgtMyI+PHBhdGggZD0iTTEwOC4xNCAyNDUuMjhsNjMuODggMjguMTYtLjk2LTExLjczLTYxLjk2LTI3LjMtLjk2IDEwLjg3eiIgY2xhc3M9ImNscy0xIi8+PC9jbGlwUGF0aD48Y2xpcFBhdGggaWQ9ImNsaXAtcGF0aC00Ij48cGF0aCBkPSJNMTA2LjQ2IDI2NC4yMWw2Ny4yMyAyOS42My0uOTYtMTEuNzItNjUuMzEtMjguNzgtLjk2IDEwLjg3eiIgY2xhc3M9ImNscy0xIi8+PC9jbGlwUGF0aD48Y2xpcFBhdGggaWQ9ImNsaXAtcGF0aC01Ij48cGF0aCBkPSJNMTEwLjc3IDIxNS40OGwtLjk2IDEwLjg3IDYwLjU0IDI2LjY4LS45NS0xMS43Mi01OC42My0yNS44M3oiIGNsYXNzPSJjbHMtMSIvPjwvY2xpcFBhdGg+PGNsaXBQYXRoIGlkPSJjbGlwLXBhdGgtNiI+PHBhdGggZD0iTTMxMy4xMyA2Ny41OWExNzUuMzEgMTc1LjMxIDAgMCAwLTI5Ljc1LTI4LjEzYy0xLjU3LTEuMTctMy4xOC0yLjMtNC43OS0zLjQyTDI1NiA1OS41bC04Mi4zNCA4NS42MyAxMTMuNDEtNTguNjggMjkuMDctMTVjLTEuMDEtMS4zMS0xLjk4LTIuNjItMy4wMS0zLjg2eiIgY2xhc3M9ImNscy0xIi8+PC9jbGlwUGF0aD48Y2xpcFBhdGggaWQ9ImNsaXAtcGF0aC03Ij48cGF0aCBkPSJNMzUzLjU5IDE3Ny42MWMwLTItLjE0LTQtLjIyLTUuOTNsLTMyLjIxLTIuMzEtMTQ3LjQ3LTEwLjU4TDMxOC4zNiAyMDlsMzAuNDEgMTAuNTVjLjA5LS4zNi4xOS0uNzEuMjgtMS4wOGExNzMuNjUgMTczLjY1IDAgMCAwIDQuNTctMzkuNDd2LTEuMzd6IiBjbGFzcz0iY2xzLTEiLz48L2NsaXBQYXRoPjxjbGlwUGF0aCBpZD0iY2xpcC1wYXRoLTgiPjxwYXRoIGQ9Ik0zNDguODQgMTM4LjYxYTE3Mi41NSAxNzIuNTUgMCAwIDAtMTMuODQtMzcuN2wtMzAuOTQgOS45NEwxNzUuOTIgMTUybDE0Mi00LjgzIDMyLjUtMS4xMWMtLjUtMi40OC0uOTktNC45OC0xLjU4LTcuNDV6IiBjbGFzcz0iY2xzLTEiLz48L2NsaXBQYXRoPjxzdHlsZT4uY2xzLTF7ZmlsbDpub25lfS5jbHMtMTN7ZmlsbDojNjk2NTY2fTwvc3R5bGU+PC9kZWZzPjxnIGlkPSJnMTIiPjxwYXRoIGlkPSJwYXRoMTQiIGZpbGw9IiNmZmYiIGQ9Ik0zMC44OSAxNzlhMTQ4Ljg3IDE0OC44NyAwIDEgMSAxNDguODcgMTQ4Ljg1QTE0OC44NyAxNDguODcgMCAwIDEgMzAuODkgMTc5Ii8+PGcgaWQ9ImczMCIgY2xpcC1wYXRoPSJ1cmwoI2NsaXAtcGF0aCkiPjxnIGlkPSJnMzIiPjxwYXRoIGlkPSJwYXRoNDYiIGZpbGw9InVybCgjbGluZWFyLWdyYWRpZW50KSIgZD0iTTE3NC40IDMwMi41MmwtNjguNjUtMzAuMjUtMy44MiA0My4yIDc1LjUgMjQtMy0zNi45MyIvPjwvZz48L2c+PGcgaWQ9Imc0OCIgY2xpcC1wYXRoPSJ1cmwoI2NsaXAtcGF0aC0yKSI+PGcgaWQ9Imc1MCI+PHBhdGggaWQ9InBhdGg2NCIgZmlsbD0idXJsKCNsaW5lYXItZ3JhZGllbnQtMikiIGQ9Ik0xMTkuMTkgMTM1LjM4aDM4LjU1djMyLjg5aC05LjE1di0xNGExMC4xMyAxMC4xMyAwIDEgMC0yMC4yNiAwdjE0aC05LjE1em0tNy43IDcybDU3LjIgMjUuMjEtMy45NC00OC4yNGg3LjQ5di0xNi4wOGgtNS41NXYtMzIuODloNS41NXYtOS40NWwtMzAuODYtMzAuMTl2LTIuMTJhMi45MSAyLjkxIDAgMCAwLTUuODIgMHYyLjEybC0zMC44NiAzMC4xOXY5LjQ1aDUuNTZ2MzIuODloLTUuNTZ2MTYuMTJoOC44MmwtMiAyMyIvPjwvZz48L2c+PGcgaWQ9Imc2NiIgY2xpcC1wYXRoPSJ1cmwoI2NsaXAtcGF0aC0zKSI+PGcgaWQ9Imc2OCI+PHBhdGggaWQ9InBhdGg4MiIgZmlsbD0idXJsKCNsaW5lYXItZ3JhZGllbnQtMykiIGQ9Ik0xMDguMTMgMjQ1LjI4TDE3MiAyNzMuNDRsLTEtMTEuNzItNjItMjcuMzEtMSAxMC44NyIvPjwvZz48L2c+PGcgaWQ9Imc4NCIgY2xpcC1wYXRoPSJ1cmwoI2NsaXAtcGF0aC00KSI+PGcgaWQ9Imc4NiI+PHBhdGggaWQ9InBhdGgxMDAiIGZpbGw9InVybCgjbGluZWFyLWdyYWRpZW50LTQpIiBkPSJNMTA2LjQ2IDI2NC4yMWw2Ny4yMyAyOS42My0xLTExLjcyLTY1LjMxLTI4Ljc4LTEgMTAuODciLz48L2c+PC9nPjxnIGlkPSJnMTAyIiBjbGlwLXBhdGg9InVybCgjY2xpcC1wYXRoLTUpIj48ZyBpZD0iZzEwNCI+PHBhdGggaWQ9InBhdGgxMTgiIGZpbGw9InVybCgjbGluZWFyLWdyYWRpZW50LTUpIiBkPSJNMTEwLjc3IDIxNS40OGwtMSAxMC44OEwxNzAuMzUgMjUzbC0xLTExLjcyLTU4LjYyLTI1LjgzIi8+PC9nPjwvZz48cGF0aCBpZD0icGF0aDEyMCIgZD0iTTMwNC4wNyAxMTAuODVsMzAuOTMtOS45NGMtLjExLS4yMi0uMjEtLjQ1LS4zMi0uNjZhMTc0LjQxIDE3NC40MSAwIDAgMC0xOC41NS0yOC44M2wtMjkuMDcgMTVhMTQyLjcxIDE0Mi43MSAwIDAgMSAxNi43MyAyMy44N2MuMS4xNy4xOC4zNS4yNy41MyIgY2xhc3M9ImNscy0xMyIvPjxwYXRoIGlkPSJwYXRoMTIyIiBkPSJNMzIxLjE1IDE2OS4zN2wzMi4yMSAyLjMxYTE3Mi44NiAxNzIuODYgMCAwIDAtMy0yNS41OWwtMzIuNSAxLjExYTE0MSAxNDEgMCAwIDEgMy4yNSAyMi4xNyIgY2xhc3M9ImNscy0xMyIvPjxwYXRoIGlkPSJwYXRoMTI0IiBkPSJNMTgyIDMyMC44MWMtNzguMiAwLTE0MS44My02My42Mi0xNDEuODMtMTQxLjgyUzEwMy44MiAzNy4xNiAxODIgMzcuMTZhMTQwLjkzIDE0MC45MyAwIDAgMSA3Ni4zIDIyLjM0TDI4MC44NSAzNkExNzIuODYgMTcyLjg2IDAgMCAwIDE4MiA1LjExQzg2LjE1IDUuMTEgOC4xNSA4My4xMSA4LjE1IDE3OXM3OCAxNzMuODUgMTczLjg1IDE3My44NWM4MS45IDAgMTUwLjY5LTU3IDE2OS0xMzMuMzJMMzIwLjYyIDIwOWMtMTMuOCA2My44NC03MC42OSAxMTEuODMtMTM4LjYgMTExLjgzIiBjbGFzcz0iY2xzLTEzIi8+PGcgaWQ9ImcxMjYiIGNsaXAtcGF0aD0idXJsKCNjbGlwLXBhdGgtNikiPjxnIGlkPSJnMTI4Ij48cGF0aCBpZD0icGF0aDE0MiIgZmlsbD0idXJsKCNsaW5lYXItZ3JhZGllbnQtNikiIGQ9Ik0zMTMuMTMgNjcuNTlhMTc1LjMxIDE3NS4zMSAwIDAgMC0yOS43NS0yOC4xM2MtMS41Ny0xLjE3LTMuMTgtMi4zLTQuNzktMy40MkwyNTYgNTkuNWwtODIuMzQgODUuNjMgMTEzLjQxLTU4LjY4IDI5LjA3LTE1Yy0xLTEuMjctMi0yLjU4LTMtMy44MiIvPjwvZz48L2c+PGcgaWQ9ImcxNDQiIGNsaXAtcGF0aD0idXJsKCNjbGlwLXBhdGgtNykiPjxnIGlkPSJnMTQ2Ij48cGF0aCBpZD0icGF0aDE2MCIgZmlsbD0idXJsKCNsaW5lYXItZ3JhZGllbnQtNykiIGQ9Ik0zNTMuNTkgMTc3LjYxYzAtMi0uMTQtNC0uMjItNS45M2wtMzIuMjEtMi4zMS0xNDcuNDctMTAuNThMMzE4LjM2IDIwOWwzMC40MSAxMC41NWMuMDktLjM2LjE5LS43MS4yOC0xLjA4YTE3My42NSAxNzMuNjUgMCAwIDAgNC41Ny0zOS40N3YtMS4zNyIvPjwvZz48L2c+PGcgaWQ9ImcxNjIiIGNsaXAtcGF0aD0idXJsKCNjbGlwLXBhdGgtOCkiPjxnIGlkPSJnMTY0Ij48cGF0aCBpZD0icGF0aDE3OCIgZmlsbD0idXJsKCNsaW5lYXItZ3JhZGllbnQtOCkiIGQ9Ik0zNDguODQgMTM4LjYxYTE3Mi41NSAxNzIuNTUgMCAwIDAtMTMuODQtMzcuN2wtMzAuOTQgOS45NEwxNzUuOTIgMTUybDE0Mi00LjgzIDMyLjUtMS4xMWMtLjQ4LTIuNTEtMS01LTEuNTYtNy40OCIvPjwvZz48L2c+PC9nPjwvc3ZnPg==" - keysOrder: [["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata", "name"], ["spec", "host"], ["spec", "storageClass"], ["spec", "core"], ["spec", "core", "resources"], ["spec", "core", "resourcesPreset"], ["spec", "registry"], ["spec", "registry", "size"], ["spec", "registry", "resources"], ["spec", "registry", "resourcesPreset"], ["spec", "jobservice"], ["spec", "jobservice", "resources"], ["spec", "jobservice", "resourcesPreset"], ["spec", "trivy"], ["spec", "trivy", "enabled"], ["spec", "trivy", "size"], ["spec", "trivy", "resources"], ["spec", "trivy", "resourcesPreset"], ["spec", "database"], ["spec", "database", "size"], ["spec", "database", "resources"], ["spec", "database", "resourcesPreset"], ["spec", "redis"], ["spec", "redis", "size"], ["spec", "redis", "resources"], ["spec", "redis", "resourcesPreset"]] + keysOrder: [["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata", "name"], ["spec", "host"], ["spec", "storageClass"], ["spec", "core"], ["spec", "core", "resources"], ["spec", "core", "resourcesPreset"], ["spec", "registry"], ["spec", "registry", "size"], ["spec", "registry", "resources"], ["spec", "registry", "resourcesPreset"], ["spec", "jobservice"], ["spec", "jobservice", "resources"], ["spec", "jobservice", "resourcesPreset"], ["spec", "trivy"], ["spec", "trivy", "enabled"], ["spec", "trivy", "size"], ["spec", "trivy", "resources"], ["spec", "trivy", "resourcesPreset"], ["spec", "database"], ["spec", "database", "size"], ["spec", "database", "replicas"], ["spec", "redis"], ["spec", "redis", "size"], ["spec", "redis", "replicas"]] secrets: exclude: [] include: diff --git a/packages/system/harbor/templates/database.yaml b/packages/system/harbor/templates/database.yaml new file mode 100644 index 00000000..5d1827aa --- /dev/null +++ b/packages/system/harbor/templates/database.yaml @@ -0,0 +1,24 @@ +--- +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: {{ .Values.harbor.fullnameOverride }}-db +spec: + instances: {{ .Values.db.replicas }} + storage: + size: {{ .Values.db.size }} + {{- with .Values.db.storageClass }} + storageClass: {{ . }} + {{- end }} + monitoring: + enablePodMonitor: true + resources: + limits: + cpu: "1" + memory: 2048Mi + requests: + cpu: 100m + memory: 512Mi + inheritedMetadata: + labels: + policy.cozystack.io/allow-to-apiserver: "true" diff --git a/packages/system/harbor/templates/redis.yaml b/packages/system/harbor/templates/redis.yaml new file mode 100644 index 00000000..7a71f1ce --- /dev/null +++ b/packages/system/harbor/templates/redis.yaml @@ -0,0 +1,66 @@ +{{- $existingPassword := lookup "v1" "Secret" .Release.Namespace (printf "%s-redis-auth" .Values.harbor.fullnameOverride) }} +{{- $password := randAlphaNum 32 | b64enc }} +{{- if $existingPassword }} + {{- $password = index $existingPassword.data "password" }} +{{- end }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Values.harbor.fullnameOverride }}-redis-auth +data: + password: {{ $password }} +--- +apiVersion: databases.spotahome.com/v1 +kind: RedisFailover +metadata: + name: {{ .Values.harbor.fullnameOverride }}-redis + labels: + app.kubernetes.io/instance: {{ .Values.harbor.fullnameOverride }}-redis + app.kubernetes.io/managed-by: {{ .Release.Service }} +spec: + sentinel: + replicas: 3 + resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 50m + memory: 64Mi + redis: + replicas: {{ .Values.redis.replicas }} + resources: + limits: + cpu: "1" + memory: 1024Mi + requests: + cpu: 100m + memory: 256Mi + storage: + persistentVolumeClaim: + metadata: + name: redisfailover-persistent-data + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.redis.size }} + {{- with .Values.redis.storageClass }} + storageClassName: {{ . }} + {{- end }} + exporter: + enabled: true + image: oliver006/redis_exporter:v1.55.0-alpine + args: + - --web.telemetry-path + - /metrics + env: + - name: REDIS_EXPORTER_LOG_FORMAT + value: txt + customConfig: + - tcp-keepalive 0 + - loglevel notice + auth: + secretPath: {{ .Values.harbor.fullnameOverride }}-redis-auth diff --git a/packages/system/harbor/values.yaml b/packages/system/harbor/values.yaml index 4c8388fa..40e924e0 100644 --- a/packages/system/harbor/values.yaml +++ b/packages/system/harbor/values.yaml @@ -1 +1,9 @@ harbor: {} +db: + replicas: 2 + size: 5Gi + storageClass: "" +redis: + replicas: 2 + size: 1Gi + storageClass: "" From 0c85639fed98637a598e019387e4be8f680c43ef Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Mon, 16 Feb 2026 21:17:10 +0300 Subject: [PATCH 091/666] [harbor] Move to apps/, use S3 via BucketClaim for registry storage Move Harbor from packages/extra/ to packages/apps/ as it is a self-sufficient end-user application, not a singleton tenant module. Update bundle entry from system to paas accordingly. Replace registry PVC storage with S3 via COSI BucketClaim/BucketAccess, provisioned from the namespace's SeaweedFS instance. S3 credentials are injected into the HelmRelease via valuesFrom with targetPath. Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- hack/e2e-apps/harbor.bats | 1 - packages/{extra => apps}/harbor/Chart.yaml | 0 packages/{extra => apps}/harbor/Makefile | 0 packages/{extra => apps}/harbor/README.md | 1 - .../{extra => apps}/harbor/charts/cozy-lib | 0 .../{extra => apps}/harbor/logos/harbor.svg | 0 packages/apps/harbor/templates/bucket.yaml | 19 +++++++++++++ .../templates/dashboard-resourcemap.yaml | 0 .../harbor/templates/harbor.yaml | 28 ++++++++++++++----- .../harbor/templates/ingress.yaml | 0 .../{extra => apps}/harbor/values.schema.json | 17 ----------- packages/{extra => apps}/harbor/values.yaml | 2 -- .../platform/sources/harbor-application.yaml | 2 +- .../core/platform/templates/bundles/paas.yaml | 1 + .../platform/templates/bundles/system.yaml | 1 - packages/system/harbor-rd/cozyrds/harbor.yaml | 6 ++-- 16 files changed, 45 insertions(+), 33 deletions(-) rename packages/{extra => apps}/harbor/Chart.yaml (100%) rename packages/{extra => apps}/harbor/Makefile (100%) rename packages/{extra => apps}/harbor/README.md (97%) rename packages/{extra => apps}/harbor/charts/cozy-lib (100%) rename packages/{extra => apps}/harbor/logos/harbor.svg (100%) create mode 100644 packages/apps/harbor/templates/bucket.yaml rename packages/{extra => apps}/harbor/templates/dashboard-resourcemap.yaml (100%) rename packages/{extra => apps}/harbor/templates/harbor.yaml (87%) rename packages/{extra => apps}/harbor/templates/ingress.yaml (100%) rename packages/{extra => apps}/harbor/values.schema.json (94%) rename packages/{extra => apps}/harbor/values.yaml (97%) diff --git a/hack/e2e-apps/harbor.bats b/hack/e2e-apps/harbor.bats index c28eee3a..d4a0055f 100644 --- a/hack/e2e-apps/harbor.bats +++ b/hack/e2e-apps/harbor.bats @@ -15,7 +15,6 @@ spec: resources: {} resourcesPreset: "nano" registry: - size: 5Gi resources: {} resourcesPreset: "nano" jobservice: diff --git a/packages/extra/harbor/Chart.yaml b/packages/apps/harbor/Chart.yaml similarity index 100% rename from packages/extra/harbor/Chart.yaml rename to packages/apps/harbor/Chart.yaml diff --git a/packages/extra/harbor/Makefile b/packages/apps/harbor/Makefile similarity index 100% rename from packages/extra/harbor/Makefile rename to packages/apps/harbor/Makefile diff --git a/packages/extra/harbor/README.md b/packages/apps/harbor/README.md similarity index 97% rename from packages/extra/harbor/README.md rename to packages/apps/harbor/README.md index 6425e6fe..df44ca8d 100644 --- a/packages/extra/harbor/README.md +++ b/packages/apps/harbor/README.md @@ -22,7 +22,6 @@ Harbor is an open source trusted cloud native registry project that stores, sign | `core.resources.memory` | Amount of memory allocated. | `quantity` | `""` | | `core.resourcesPreset` | Default sizing preset used when `resources` is omitted. | `string` | `small` | | `registry` | Container image registry configuration. | `object` | `{}` | -| `registry.size` | Persistent Volume size for container image storage. | `quantity` | `50Gi` | | `registry.resources` | Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied. | `object` | `{}` | | `registry.resources.cpu` | Number of CPU cores allocated. | `quantity` | `""` | | `registry.resources.memory` | Amount of memory allocated. | `quantity` | `""` | diff --git a/packages/extra/harbor/charts/cozy-lib b/packages/apps/harbor/charts/cozy-lib similarity index 100% rename from packages/extra/harbor/charts/cozy-lib rename to packages/apps/harbor/charts/cozy-lib diff --git a/packages/extra/harbor/logos/harbor.svg b/packages/apps/harbor/logos/harbor.svg similarity index 100% rename from packages/extra/harbor/logos/harbor.svg rename to packages/apps/harbor/logos/harbor.svg diff --git a/packages/apps/harbor/templates/bucket.yaml b/packages/apps/harbor/templates/bucket.yaml new file mode 100644 index 00000000..a9e60988 --- /dev/null +++ b/packages/apps/harbor/templates/bucket.yaml @@ -0,0 +1,19 @@ +{{- $seaweedfs := .Values._namespace.seaweedfs }} +apiVersion: objectstorage.k8s.io/v1alpha1 +kind: BucketClaim +metadata: + name: {{ .Release.Name }}-registry +spec: + bucketClassName: {{ $seaweedfs }} + protocols: + - s3 +--- +apiVersion: objectstorage.k8s.io/v1alpha1 +kind: BucketAccess +metadata: + name: {{ .Release.Name }}-registry +spec: + bucketAccessClassName: {{ $seaweedfs }} + bucketClaimName: {{ .Release.Name }}-registry + credentialsSecretName: {{ .Release.Name }}-registry-bucket + protocol: s3 diff --git a/packages/extra/harbor/templates/dashboard-resourcemap.yaml b/packages/apps/harbor/templates/dashboard-resourcemap.yaml similarity index 100% rename from packages/extra/harbor/templates/dashboard-resourcemap.yaml rename to packages/apps/harbor/templates/dashboard-resourcemap.yaml diff --git a/packages/extra/harbor/templates/harbor.yaml b/packages/apps/harbor/templates/harbor.yaml similarity index 87% rename from packages/extra/harbor/templates/harbor.yaml rename to packages/apps/harbor/templates/harbor.yaml index ceef4f1f..59758950 100644 --- a/packages/extra/harbor/templates/harbor.yaml +++ b/packages/apps/harbor/templates/harbor.yaml @@ -53,6 +53,18 @@ spec: valuesFrom: - kind: Secret name: cozystack-values + - kind: Secret + name: {{ .Release.Name }}-registry-bucket + valuesKey: accessKey + targetPath: harbor.persistence.imageChartStorage.s3.accesskey + - kind: Secret + name: {{ .Release.Name }}-registry-bucket + valuesKey: secretKey + targetPath: harbor.persistence.imageChartStorage.s3.secretkey + - kind: Secret + name: {{ .Release.Name }}-registry-bucket + valuesKey: endpoint + targetPath: harbor.persistence.imageChartStorage.s3.regionendpoint values: db: replicas: {{ .Values.database.replicas }} @@ -79,19 +91,21 @@ spec: persistence: enabled: true resourcePolicy: "keep" + imageChartStorage: + type: s3 + s3: + region: us-east-1 + bucket: {{ .Release.Name }}-registry + secure: false + v4auth: true + {{- if .Values.trivy.enabled }} persistentVolumeClaim: - registry: - size: {{ .Values.registry.size }} - {{- with .Values.storageClass }} - storageClass: {{ . }} - {{- end }} - {{- if .Values.trivy.enabled }} trivy: size: {{ .Values.trivy.size }} {{- with .Values.storageClass }} storageClass: {{ . }} {{- end }} - {{- end }} + {{- end }} portal: resources: {{- include "cozy-lib.resources.defaultingSanitize" (list "nano" (dict) $) | nindent 10 }} core: diff --git a/packages/extra/harbor/templates/ingress.yaml b/packages/apps/harbor/templates/ingress.yaml similarity index 100% rename from packages/extra/harbor/templates/ingress.yaml rename to packages/apps/harbor/templates/ingress.yaml diff --git a/packages/extra/harbor/values.schema.json b/packages/apps/harbor/values.schema.json similarity index 94% rename from packages/extra/harbor/values.schema.json rename to packages/apps/harbor/values.schema.json index 2fa0beba..1352f5dc 100644 --- a/packages/extra/harbor/values.schema.json +++ b/packages/apps/harbor/values.schema.json @@ -179,9 +179,6 @@ "description": "Container image registry configuration.", "type": "object", "default": {}, - "required": [ - "size" - ], "properties": { "resources": { "description": "Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.", @@ -229,20 +226,6 @@ "xlarge", "2xlarge" ] - }, - "size": { - "description": "Persistent Volume size for container image storage.", - "default": "50Gi", - "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 } } }, diff --git a/packages/extra/harbor/values.yaml b/packages/apps/harbor/values.yaml similarity index 97% rename from packages/extra/harbor/values.yaml rename to packages/apps/harbor/values.yaml index c5d89b1d..de44eaff 100644 --- a/packages/extra/harbor/values.yaml +++ b/packages/apps/harbor/values.yaml @@ -35,13 +35,11 @@ core: resourcesPreset: "small" ## @typedef {struct} Registry - Container image registry configuration. -## @field {quantity} size - Persistent Volume size for container image storage. ## @field {Resources} [resources] - Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied. ## @field {ResourcesPreset} [resourcesPreset] - Default sizing preset used when `resources` is omitted. ## @param {Registry} registry - Container image registry configuration. registry: - size: 50Gi resources: {} resourcesPreset: "small" diff --git a/packages/core/platform/sources/harbor-application.yaml b/packages/core/platform/sources/harbor-application.yaml index 31aa70ee..b05d7a01 100644 --- a/packages/core/platform/sources/harbor-application.yaml +++ b/packages/core/platform/sources/harbor-application.yaml @@ -20,7 +20,7 @@ spec: - name: harbor-system path: system/harbor - name: harbor - path: extra/harbor + path: apps/harbor libraries: ["cozy-lib"] - name: harbor-rd path: system/harbor-rd diff --git a/packages/core/platform/templates/bundles/paas.yaml b/packages/core/platform/templates/bundles/paas.yaml index 5a478f9d..6a126304 100644 --- a/packages/core/platform/templates/bundles/paas.yaml +++ b/packages/core/platform/templates/bundles/paas.yaml @@ -11,6 +11,7 @@ {{include "cozystack.platform.package.default" (list "cozystack.mongodb-operator" $) }} {{include "cozystack.platform.package.default" (list "cozystack.clickhouse-application" $) }} {{include "cozystack.platform.package.default" (list "cozystack.foundationdb-application" $) }} +{{include "cozystack.platform.package.default" (list "cozystack.harbor-application" $) }} {{include "cozystack.platform.package.default" (list "cozystack.kafka-application" $) }} {{include "cozystack.platform.package.default" (list "cozystack.mariadb-application" $) }} {{include "cozystack.platform.package.default" (list "cozystack.mongodb-application" $) }} diff --git a/packages/core/platform/templates/bundles/system.yaml b/packages/core/platform/templates/bundles/system.yaml index 33b882c6..c24726ef 100644 --- a/packages/core/platform/templates/bundles/system.yaml +++ b/packages/core/platform/templates/bundles/system.yaml @@ -126,7 +126,6 @@ {{- $_ := set $tenantComponents "tenant" (dict "values" $tenantClusterValues) }} {{include "cozystack.platform.package" (list "cozystack.tenant-application" "default" $ $tenantComponents) }} {{include "cozystack.platform.package.default" (list "cozystack.ingress-application" $) }} -{{include "cozystack.platform.package.default" (list "cozystack.harbor-application" $) }} {{include "cozystack.platform.package.default" (list "cozystack.seaweedfs-application" $) }} {{include "cozystack.platform.package.default" (list "cozystack.info-application" $) }} {{include "cozystack.platform.package.default" (list "cozystack.monitoring-application" $) }} diff --git a/packages/system/harbor-rd/cozyrds/harbor.yaml b/packages/system/harbor-rd/cozyrds/harbor.yaml index 8f159a45..0112dc11 100644 --- a/packages/system/harbor-rd/cozyrds/harbor.yaml +++ b/packages/system/harbor-rd/cozyrds/harbor.yaml @@ -8,9 +8,9 @@ spec: singular: harbor plural: harbors openAPISchema: |- - {"title":"Chart Values","type":"object","properties":{"core":{"description":"Core API server configuration.","type":"object","default":{},"properties":{"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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.","type":"string","default":"small","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]}}},"database":{"description":"PostgreSQL database configuration.","type":"object","default":{},"required":["replicas","size"],"properties":{"replicas":{"description":"Number of database instances.","type":"integer","default":2},"size":{"description":"Persistent Volume size for database storage.","default":"5Gi","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}}},"host":{"description":"Hostname for external access to Harbor (defaults to 'harbor' subdomain for the tenant host).","type":"string","default":""},"jobservice":{"description":"Background job service configuration.","type":"object","default":{},"properties":{"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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.","type":"string","default":"nano","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]}}},"redis":{"description":"Redis cache configuration.","type":"object","default":{},"required":["replicas","size"],"properties":{"replicas":{"description":"Number of Redis replicas.","type":"integer","default":2},"size":{"description":"Persistent Volume size for cache storage.","default":"1Gi","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}}},"registry":{"description":"Container image registry configuration.","type":"object","default":{},"required":["size"],"properties":{"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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.","type":"string","default":"small","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]},"size":{"description":"Persistent Volume size for container image storage.","default":"50Gi","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","default":""},"trivy":{"description":"Trivy vulnerability scanner configuration.","type":"object","default":{},"required":["enabled","size"],"properties":{"enabled":{"description":"Enable or disable the vulnerability scanner.","type":"boolean","default":true},"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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.","type":"string","default":"nano","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]},"size":{"description":"Persistent Volume size for vulnerability database cache.","default":"5Gi","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}}}}} + {"title":"Chart Values","type":"object","properties":{"core":{"description":"Core API server configuration.","type":"object","default":{},"properties":{"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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.","type":"string","default":"small","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]}}},"database":{"description":"PostgreSQL database configuration.","type":"object","default":{},"required":["replicas","size"],"properties":{"replicas":{"description":"Number of database instances.","type":"integer","default":2},"size":{"description":"Persistent Volume size for database storage.","default":"5Gi","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}}},"host":{"description":"Hostname for external access to Harbor (defaults to 'harbor' subdomain for the tenant host).","type":"string","default":""},"jobservice":{"description":"Background job service configuration.","type":"object","default":{},"properties":{"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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.","type":"string","default":"nano","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]}}},"redis":{"description":"Redis cache configuration.","type":"object","default":{},"required":["replicas","size"],"properties":{"replicas":{"description":"Number of Redis replicas.","type":"integer","default":2},"size":{"description":"Persistent Volume size for cache storage.","default":"1Gi","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}}},"registry":{"description":"Container image registry configuration.","type":"object","default":{},"properties":{"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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.","type":"string","default":"small","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]}}},"storageClass":{"description":"StorageClass used to store the data.","type":"string","default":""},"trivy":{"description":"Trivy vulnerability scanner configuration.","type":"object","default":{},"required":["enabled","size"],"properties":{"enabled":{"description":"Enable or disable the vulnerability scanner.","type":"boolean","default":true},"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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.","type":"string","default":"nano","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]},"size":{"description":"Persistent Volume size for vulnerability database cache.","default":"5Gi","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}}}}} release: - prefix: "" + prefix: "harbor-" labels: sharding.fluxcd.io/key: tenants internal.cozystack.io/tenantmodule: "true" @@ -29,7 +29,7 @@ spec: - registry - container icon: "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHJvbGU9ImltZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHZpZXdCb3g9IjEuMDUgLTEuOTUgMzU5LjQxIDM2MS42NiI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXItZ3JhZGllbnQiIHgxPSIyNjQuNzkiIHgyPSIyNjcuMjciIHkxPSI5NTIuMzkiIHkyPSI5NTIuMzkiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMzAuNDMgMCAwIC0zMC40MyAtNzk1NS4yMiAyOTI4NS43NSkiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiM2MGI5MzIiLz48c3RvcCBvZmZzZXQ9Ii4yOCIgc3RvcC1jb2xvcj0iIzYwYjkzMiIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzM2N2MzNCIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXItZ3JhZGllbnQtMiIgeDE9IjI2My43NyIgeDI9IjI2Ni4yNiIgeTE9Ijk1NS42NSIgeTI9Ijk1NS42NSIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgyNy4yMSAwIDAgLTI3LjIxIC03MDczLjg1IDI2MTY5LjQxKSIgeGxpbms6aHJlZj0iI2xpbmVhci1ncmFkaWVudCIvPjxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyLWdyYWRpZW50LTMiIHgxPSIyNjMuMjgiIHgyPSIyNjUuNzYiIHkxPSI5NTMuNzQiIHkyPSI5NTMuNzQiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMjUuNzUgMCAwIC0yNS43NSAtNjY3MS4xMyAyNDgxMi4yMykiIHhsaW5rOmhyZWY9IiNsaW5lYXItZ3JhZGllbnQiLz48bGluZWFyR3JhZGllbnQgaWQ9ImxpbmVhci1ncmFkaWVudC00IiB4MT0iMjYzLjc3IiB4Mj0iMjY2LjI1IiB5MT0iOTUzLjIiIHkyPSI5NTMuMiIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgyNy4xIDAgMCAtMjcuMSAtNzA0MC45IDI2MTAyLjQ5KSIgeGxpbms6aHJlZj0iI2xpbmVhci1ncmFkaWVudCIvPjxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyLWdyYWRpZW50LTUiIHgxPSIyNjIuNzMiIHgyPSIyNjUuMjEiIHkxPSI5NTQuMzQiIHkyPSI5NTQuMzQiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMjQuNCAwIDAgLTI0LjQgLTYzMDEuMzYgMjM1MjEuOTcpIiB4bGluazpocmVmPSIjbGluZWFyLWdyYWRpZW50Ii8+PGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXItZ3JhZGllbnQtNiIgeDE9IjI3Mi4xNCIgeDI9IjI3NC42MiIgeTE9Ijk1NS4xNSIgeTI9Ijk1NS4xNSIgZ3JhZGllbnRUcmFuc2Zvcm09InNjYWxlKDY2LjA5IC02Ni4wOSkgcm90YXRlKDM2LjUyIDE1ODguMTUzIDY4LjE0OCkiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiM0NTk2ZDgiLz48c3RvcCBvZmZzZXQ9Ii4yIiBzdG9wLWNvbG9yPSIjNDU5NmQ4Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjZmZmIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9ImxpbmVhci1ncmFkaWVudC03IiB4MT0iMjcwLjY1IiB4Mj0iMjczLjEzIiB5MT0iOTUyLjM4IiB5Mj0iOTUyLjM4IiBncmFkaWVudFRyYW5zZm9ybT0ic2NhbGUoNzcuOCAtNzcuOCkgcm90YXRlKC0xMS41NCAtNDU4Ny4yMDkgMTgwMy4zMjMpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjNDE5NGQ3Ii8+PHN0b3Agb2Zmc2V0PSIuMiIgc3RvcC1jb2xvcj0iIzQxOTRkNyIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI2ZmZiIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXItZ3JhZGllbnQtOCIgeDE9IjI3MC45NyIgeDI9IjI3My40NSIgeTE9Ijk1My43NSIgeTI9Ijk1My43NSIgZ3JhZGllbnRUcmFuc2Zvcm09InNjYWxlKDcxLjM1IC03MS4zNSkgcm90YXRlKDEwLjIzIDU0NzcuMzcgLTEwMjQuNjAyKSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzQxOTRkNyIvPjxzdG9wIG9mZnNldD0iLjMzIiBzdG9wLWNvbG9yPSIjNDQ5NmQ4Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjZmZmIi8+PC9saW5lYXJHcmFkaWVudD48Y2xpcFBhdGggaWQ9ImNsaXAtcGF0aCI+PHBhdGggZD0iTTE3NC40IDMwMi41MmwtNjguNjUtMzAuMjYtMy44MyA0My4yMSA3NS41IDIzLjk4LTMuMDItMzYuOTN6IiBjbGFzcz0iY2xzLTEiLz48L2NsaXBQYXRoPjxjbGlwUGF0aCBpZD0iY2xpcC1wYXRoLTIiPjxwYXRoIGQ9Ik0xMTkuMTkgMTM1LjM4aDM4LjU1djMyLjg5aC05LjE1di0xNGExMC4xMyAxMC4xMyAwIDEgMC0yMC4yNiAwdjE0aC05LjE1em0tNy43IDcybDU3LjIgMjUuMjEtMy45NC00OC4yNGg3LjQ5di0xNi4wOGgtNS41NXYtMzIuODloNS41NXYtOS40NWwtMzAuODYtMzAuMTl2LTIuMTJhMi45MSAyLjkxIDAgMCAwLTUuODIgMHYyLjEybC0zMC44NiAzMC4xOXY5LjQ1aDUuNTZ2MzIuODloLTUuNTZ2MTYuMTJoOC44MnoiIGNsYXNzPSJjbHMtMSIvPjwvY2xpcFBhdGg+PGNsaXBQYXRoIGlkPSJjbGlwLXBhdGgtMyI+PHBhdGggZD0iTTEwOC4xNCAyNDUuMjhsNjMuODggMjguMTYtLjk2LTExLjczLTYxLjk2LTI3LjMtLjk2IDEwLjg3eiIgY2xhc3M9ImNscy0xIi8+PC9jbGlwUGF0aD48Y2xpcFBhdGggaWQ9ImNsaXAtcGF0aC00Ij48cGF0aCBkPSJNMTA2LjQ2IDI2NC4yMWw2Ny4yMyAyOS42My0uOTYtMTEuNzItNjUuMzEtMjguNzgtLjk2IDEwLjg3eiIgY2xhc3M9ImNscy0xIi8+PC9jbGlwUGF0aD48Y2xpcFBhdGggaWQ9ImNsaXAtcGF0aC01Ij48cGF0aCBkPSJNMTEwLjc3IDIxNS40OGwtLjk2IDEwLjg3IDYwLjU0IDI2LjY4LS45NS0xMS43Mi01OC42My0yNS44M3oiIGNsYXNzPSJjbHMtMSIvPjwvY2xpcFBhdGg+PGNsaXBQYXRoIGlkPSJjbGlwLXBhdGgtNiI+PHBhdGggZD0iTTMxMy4xMyA2Ny41OWExNzUuMzEgMTc1LjMxIDAgMCAwLTI5Ljc1LTI4LjEzYy0xLjU3LTEuMTctMy4xOC0yLjMtNC43OS0zLjQyTDI1NiA1OS41bC04Mi4zNCA4NS42MyAxMTMuNDEtNTguNjggMjkuMDctMTVjLTEuMDEtMS4zMS0xLjk4LTIuNjItMy4wMS0zLjg2eiIgY2xhc3M9ImNscy0xIi8+PC9jbGlwUGF0aD48Y2xpcFBhdGggaWQ9ImNsaXAtcGF0aC03Ij48cGF0aCBkPSJNMzUzLjU5IDE3Ny42MWMwLTItLjE0LTQtLjIyLTUuOTNsLTMyLjIxLTIuMzEtMTQ3LjQ3LTEwLjU4TDMxOC4zNiAyMDlsMzAuNDEgMTAuNTVjLjA5LS4zNi4xOS0uNzEuMjgtMS4wOGExNzMuNjUgMTczLjY1IDAgMCAwIDQuNTctMzkuNDd2LTEuMzd6IiBjbGFzcz0iY2xzLTEiLz48L2NsaXBQYXRoPjxjbGlwUGF0aCBpZD0iY2xpcC1wYXRoLTgiPjxwYXRoIGQ9Ik0zNDguODQgMTM4LjYxYTE3Mi41NSAxNzIuNTUgMCAwIDAtMTMuODQtMzcuN2wtMzAuOTQgOS45NEwxNzUuOTIgMTUybDE0Mi00LjgzIDMyLjUtMS4xMWMtLjUtMi40OC0uOTktNC45OC0xLjU4LTcuNDV6IiBjbGFzcz0iY2xzLTEiLz48L2NsaXBQYXRoPjxzdHlsZT4uY2xzLTF7ZmlsbDpub25lfS5jbHMtMTN7ZmlsbDojNjk2NTY2fTwvc3R5bGU+PC9kZWZzPjxnIGlkPSJnMTIiPjxwYXRoIGlkPSJwYXRoMTQiIGZpbGw9IiNmZmYiIGQ9Ik0zMC44OSAxNzlhMTQ4Ljg3IDE0OC44NyAwIDEgMSAxNDguODcgMTQ4Ljg1QTE0OC44NyAxNDguODcgMCAwIDEgMzAuODkgMTc5Ii8+PGcgaWQ9ImczMCIgY2xpcC1wYXRoPSJ1cmwoI2NsaXAtcGF0aCkiPjxnIGlkPSJnMzIiPjxwYXRoIGlkPSJwYXRoNDYiIGZpbGw9InVybCgjbGluZWFyLWdyYWRpZW50KSIgZD0iTTE3NC40IDMwMi41MmwtNjguNjUtMzAuMjUtMy44MiA0My4yIDc1LjUgMjQtMy0zNi45MyIvPjwvZz48L2c+PGcgaWQ9Imc0OCIgY2xpcC1wYXRoPSJ1cmwoI2NsaXAtcGF0aC0yKSI+PGcgaWQ9Imc1MCI+PHBhdGggaWQ9InBhdGg2NCIgZmlsbD0idXJsKCNsaW5lYXItZ3JhZGllbnQtMikiIGQ9Ik0xMTkuMTkgMTM1LjM4aDM4LjU1djMyLjg5aC05LjE1di0xNGExMC4xMyAxMC4xMyAwIDEgMC0yMC4yNiAwdjE0aC05LjE1em0tNy43IDcybDU3LjIgMjUuMjEtMy45NC00OC4yNGg3LjQ5di0xNi4wOGgtNS41NXYtMzIuODloNS41NXYtOS40NWwtMzAuODYtMzAuMTl2LTIuMTJhMi45MSAyLjkxIDAgMCAwLTUuODIgMHYyLjEybC0zMC44NiAzMC4xOXY5LjQ1aDUuNTZ2MzIuODloLTUuNTZ2MTYuMTJoOC44MmwtMiAyMyIvPjwvZz48L2c+PGcgaWQ9Imc2NiIgY2xpcC1wYXRoPSJ1cmwoI2NsaXAtcGF0aC0zKSI+PGcgaWQ9Imc2OCI+PHBhdGggaWQ9InBhdGg4MiIgZmlsbD0idXJsKCNsaW5lYXItZ3JhZGllbnQtMykiIGQ9Ik0xMDguMTMgMjQ1LjI4TDE3MiAyNzMuNDRsLTEtMTEuNzItNjItMjcuMzEtMSAxMC44NyIvPjwvZz48L2c+PGcgaWQ9Imc4NCIgY2xpcC1wYXRoPSJ1cmwoI2NsaXAtcGF0aC00KSI+PGcgaWQ9Imc4NiI+PHBhdGggaWQ9InBhdGgxMDAiIGZpbGw9InVybCgjbGluZWFyLWdyYWRpZW50LTQpIiBkPSJNMTA2LjQ2IDI2NC4yMWw2Ny4yMyAyOS42My0xLTExLjcyLTY1LjMxLTI4Ljc4LTEgMTAuODciLz48L2c+PC9nPjxnIGlkPSJnMTAyIiBjbGlwLXBhdGg9InVybCgjY2xpcC1wYXRoLTUpIj48ZyBpZD0iZzEwNCI+PHBhdGggaWQ9InBhdGgxMTgiIGZpbGw9InVybCgjbGluZWFyLWdyYWRpZW50LTUpIiBkPSJNMTEwLjc3IDIxNS40OGwtMSAxMC44OEwxNzAuMzUgMjUzbC0xLTExLjcyLTU4LjYyLTI1LjgzIi8+PC9nPjwvZz48cGF0aCBpZD0icGF0aDEyMCIgZD0iTTMwNC4wNyAxMTAuODVsMzAuOTMtOS45NGMtLjExLS4yMi0uMjEtLjQ1LS4zMi0uNjZhMTc0LjQxIDE3NC40MSAwIDAgMC0xOC41NS0yOC44M2wtMjkuMDcgMTVhMTQyLjcxIDE0Mi43MSAwIDAgMSAxNi43MyAyMy44N2MuMS4xNy4xOC4zNS4yNy41MyIgY2xhc3M9ImNscy0xMyIvPjxwYXRoIGlkPSJwYXRoMTIyIiBkPSJNMzIxLjE1IDE2OS4zN2wzMi4yMSAyLjMxYTE3Mi44NiAxNzIuODYgMCAwIDAtMy0yNS41OWwtMzIuNSAxLjExYTE0MSAxNDEgMCAwIDEgMy4yNSAyMi4xNyIgY2xhc3M9ImNscy0xMyIvPjxwYXRoIGlkPSJwYXRoMTI0IiBkPSJNMTgyIDMyMC44MWMtNzguMiAwLTE0MS44My02My42Mi0xNDEuODMtMTQxLjgyUzEwMy44MiAzNy4xNiAxODIgMzcuMTZhMTQwLjkzIDE0MC45MyAwIDAgMSA3Ni4zIDIyLjM0TDI4MC44NSAzNkExNzIuODYgMTcyLjg2IDAgMCAwIDE4MiA1LjExQzg2LjE1IDUuMTEgOC4xNSA4My4xMSA4LjE1IDE3OXM3OCAxNzMuODUgMTczLjg1IDE3My44NWM4MS45IDAgMTUwLjY5LTU3IDE2OS0xMzMuMzJMMzIwLjYyIDIwOWMtMTMuOCA2My44NC03MC42OSAxMTEuODMtMTM4LjYgMTExLjgzIiBjbGFzcz0iY2xzLTEzIi8+PGcgaWQ9ImcxMjYiIGNsaXAtcGF0aD0idXJsKCNjbGlwLXBhdGgtNikiPjxnIGlkPSJnMTI4Ij48cGF0aCBpZD0icGF0aDE0MiIgZmlsbD0idXJsKCNsaW5lYXItZ3JhZGllbnQtNikiIGQ9Ik0zMTMuMTMgNjcuNTlhMTc1LjMxIDE3NS4zMSAwIDAgMC0yOS43NS0yOC4xM2MtMS41Ny0xLjE3LTMuMTgtMi4zLTQuNzktMy40MkwyNTYgNTkuNWwtODIuMzQgODUuNjMgMTEzLjQxLTU4LjY4IDI5LjA3LTE1Yy0xLTEuMjctMi0yLjU4LTMtMy44MiIvPjwvZz48L2c+PGcgaWQ9ImcxNDQiIGNsaXAtcGF0aD0idXJsKCNjbGlwLXBhdGgtNykiPjxnIGlkPSJnMTQ2Ij48cGF0aCBpZD0icGF0aDE2MCIgZmlsbD0idXJsKCNsaW5lYXItZ3JhZGllbnQtNykiIGQ9Ik0zNTMuNTkgMTc3LjYxYzAtMi0uMTQtNC0uMjItNS45M2wtMzIuMjEtMi4zMS0xNDcuNDctMTAuNThMMzE4LjM2IDIwOWwzMC40MSAxMC41NWMuMDktLjM2LjE5LS43MS4yOC0xLjA4YTE3My42NSAxNzMuNjUgMCAwIDAgNC41Ny0zOS40N3YtMS4zNyIvPjwvZz48L2c+PGcgaWQ9ImcxNjIiIGNsaXAtcGF0aD0idXJsKCNjbGlwLXBhdGgtOCkiPjxnIGlkPSJnMTY0Ij48cGF0aCBpZD0icGF0aDE3OCIgZmlsbD0idXJsKCNsaW5lYXItZ3JhZGllbnQtOCkiIGQ9Ik0zNDguODQgMTM4LjYxYTE3Mi41NSAxNzIuNTUgMCAwIDAtMTMuODQtMzcuN2wtMzAuOTQgOS45NEwxNzUuOTIgMTUybDE0Mi00LjgzIDMyLjUtMS4xMWMtLjQ4LTIuNTEtMS01LTEuNTYtNy40OCIvPjwvZz48L2c+PC9nPjwvc3ZnPg==" - keysOrder: [["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata", "name"], ["spec", "host"], ["spec", "storageClass"], ["spec", "core"], ["spec", "core", "resources"], ["spec", "core", "resourcesPreset"], ["spec", "registry"], ["spec", "registry", "size"], ["spec", "registry", "resources"], ["spec", "registry", "resourcesPreset"], ["spec", "jobservice"], ["spec", "jobservice", "resources"], ["spec", "jobservice", "resourcesPreset"], ["spec", "trivy"], ["spec", "trivy", "enabled"], ["spec", "trivy", "size"], ["spec", "trivy", "resources"], ["spec", "trivy", "resourcesPreset"], ["spec", "database"], ["spec", "database", "size"], ["spec", "database", "replicas"], ["spec", "redis"], ["spec", "redis", "size"], ["spec", "redis", "replicas"]] + keysOrder: [["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata", "name"], ["spec", "host"], ["spec", "storageClass"], ["spec", "core"], ["spec", "core", "resources"], ["spec", "core", "resourcesPreset"], ["spec", "registry"], ["spec", "registry", "resources"], ["spec", "registry", "resourcesPreset"], ["spec", "jobservice"], ["spec", "jobservice", "resources"], ["spec", "jobservice", "resourcesPreset"], ["spec", "trivy"], ["spec", "trivy", "enabled"], ["spec", "trivy", "size"], ["spec", "trivy", "resources"], ["spec", "trivy", "resourcesPreset"], ["spec", "database"], ["spec", "database", "size"], ["spec", "database", "replicas"], ["spec", "redis"], ["spec", "redis", "size"], ["spec", "redis", "replicas"]] secrets: exclude: [] include: From 6c447b2fcb87c10ecfc388ce487d6125b3245182 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Mon, 16 Feb 2026 21:47:49 +0300 Subject: [PATCH 092/666] [harbor] Improve ingress template: quote hosts, handle cloudflare issuer Add | quote to host values in ingress for proper YAML escaping. Add cloudflare issuer type handling following bucket/dashboard pattern. Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- packages/apps/harbor/templates/ingress.yaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/apps/harbor/templates/ingress.yaml b/packages/apps/harbor/templates/ingress.yaml index 785b4a79..d2e3c204 100644 --- a/packages/apps/harbor/templates/ingress.yaml +++ b/packages/apps/harbor/templates/ingress.yaml @@ -1,6 +1,7 @@ {{- $ingress := .Values._namespace.ingress }} {{- $host := .Values._namespace.host }} {{- $harborHost := .Values.host | default (printf "harbor.%s" $host) }} +{{- $issuerType := (index .Values._cluster "clusterissuer") | default "http01" }} --- apiVersion: networking.k8s.io/v1 kind: Ingress @@ -12,16 +13,18 @@ metadata: nginx.ingress.kubernetes.io/proxy-send-timeout: "900" nginx.ingress.kubernetes.io/ssl-redirect: "true" nginx.ingress.kubernetes.io/backend-protocol: "HTTP" + {{- if ne $issuerType "cloudflare" }} acme.cert-manager.io/http01-ingress-class: {{ $ingress }} + {{- end }} cert-manager.io/cluster-issuer: letsencrypt-prod spec: ingressClassName: {{ $ingress }} tls: - hosts: - - {{ $harborHost }} + - {{ $harborHost | quote }} secretName: {{ .Release.Name }}-ingress-tls rules: - - host: {{ $harborHost }} + - host: {{ $harborHost | quote }} http: paths: - path: / From c815725bcfd2976cfa275030fb9688a108cebd6e Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Mon, 16 Feb 2026 23:09:35 +0300 Subject: [PATCH 093/666] [harbor] Fix E2E test: use correct HelmRelease name with prefix ApplicationDefinition has prefix "harbor-", so CR name "harbor" produces HelmRelease "harbor-harbor". Use name="test" and release="harbor-test" to correctly reference all resources. Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- hack/e2e-apps/harbor.bats | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/hack/e2e-apps/harbor.bats b/hack/e2e-apps/harbor.bats index d4a0055f..7109ad4f 100644 --- a/hack/e2e-apps/harbor.bats +++ b/hack/e2e-apps/harbor.bats @@ -1,7 +1,8 @@ #!/usr/bin/env bats @test "Create Harbor" { - name='harbor' + name='test' + release="harbor-$name" kubectl apply -f- < Date: Tue, 17 Feb 2026 00:36:53 +0300 Subject: [PATCH 094/666] [harbor] Make registry storage configurable: S3 or PVC Add registry.storageType parameter (pvc/s3) to let users choose between PVC storage and S3 via COSI BucketClaim. Default is pvc, which works without SeaweedFS in the tenant namespace. Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- hack/e2e-apps/harbor.bats | 2 ++ packages/apps/harbor/README.md | 2 ++ packages/apps/harbor/templates/bucket.yaml | 2 ++ packages/apps/harbor/templates/harbor.yaml | 15 +++++++++-- packages/apps/harbor/values.schema.json | 26 +++++++++++++++++++ packages/apps/harbor/values.yaml | 8 ++++++ packages/system/harbor-rd/cozyrds/harbor.yaml | 4 +-- 7 files changed, 55 insertions(+), 4 deletions(-) diff --git a/hack/e2e-apps/harbor.bats b/hack/e2e-apps/harbor.bats index 7109ad4f..8c734565 100644 --- a/hack/e2e-apps/harbor.bats +++ b/hack/e2e-apps/harbor.bats @@ -16,6 +16,8 @@ spec: resources: {} resourcesPreset: "nano" registry: + storageType: "pvc" + size: 5Gi resources: {} resourcesPreset: "nano" jobservice: diff --git a/packages/apps/harbor/README.md b/packages/apps/harbor/README.md index df44ca8d..3c870be3 100644 --- a/packages/apps/harbor/README.md +++ b/packages/apps/harbor/README.md @@ -22,6 +22,8 @@ Harbor is an open source trusted cloud native registry project that stores, sign | `core.resources.memory` | Amount of memory allocated. | `quantity` | `""` | | `core.resourcesPreset` | Default sizing preset used when `resources` is omitted. | `string` | `small` | | `registry` | Container image registry configuration. | `object` | `{}` | +| `registry.storageType` | Storage backend type for images. | `string` | `pvc` | +| `registry.size` | Persistent Volume size (only used when storageType is pvc). | `quantity` | `10Gi` | | `registry.resources` | Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied. | `object` | `{}` | | `registry.resources.cpu` | Number of CPU cores allocated. | `quantity` | `""` | | `registry.resources.memory` | Amount of memory allocated. | `quantity` | `""` | diff --git a/packages/apps/harbor/templates/bucket.yaml b/packages/apps/harbor/templates/bucket.yaml index a9e60988..885cbfc8 100644 --- a/packages/apps/harbor/templates/bucket.yaml +++ b/packages/apps/harbor/templates/bucket.yaml @@ -1,3 +1,4 @@ +{{- if eq .Values.registry.storageType "s3" }} {{- $seaweedfs := .Values._namespace.seaweedfs }} apiVersion: objectstorage.k8s.io/v1alpha1 kind: BucketClaim @@ -17,3 +18,4 @@ spec: bucketClaimName: {{ .Release.Name }}-registry credentialsSecretName: {{ .Release.Name }}-registry-bucket protocol: s3 +{{- end }} diff --git a/packages/apps/harbor/templates/harbor.yaml b/packages/apps/harbor/templates/harbor.yaml index 59758950..97f28942 100644 --- a/packages/apps/harbor/templates/harbor.yaml +++ b/packages/apps/harbor/templates/harbor.yaml @@ -53,6 +53,7 @@ spec: valuesFrom: - kind: Secret name: cozystack-values + {{- if eq .Values.registry.storageType "s3" }} - kind: Secret name: {{ .Release.Name }}-registry-bucket valuesKey: accessKey @@ -65,6 +66,7 @@ spec: name: {{ .Release.Name }}-registry-bucket valuesKey: endpoint targetPath: harbor.persistence.imageChartStorage.s3.regionendpoint + {{- end }} values: db: replicas: {{ .Values.database.replicas }} @@ -91,6 +93,7 @@ spec: persistence: enabled: true resourcePolicy: "keep" + {{- if eq .Values.registry.storageType "s3" }} imageChartStorage: type: s3 s3: @@ -98,14 +101,22 @@ spec: bucket: {{ .Release.Name }}-registry secure: false v4auth: true - {{- if .Values.trivy.enabled }} + {{- end }} persistentVolumeClaim: + {{- if eq .Values.registry.storageType "pvc" }} + registry: + size: {{ .Values.registry.size }} + {{- with .Values.storageClass }} + storageClass: {{ . }} + {{- end }} + {{- end }} + {{- if .Values.trivy.enabled }} trivy: size: {{ .Values.trivy.size }} {{- with .Values.storageClass }} storageClass: {{ . }} {{- end }} - {{- end }} + {{- end }} portal: resources: {{- include "cozy-lib.resources.defaultingSanitize" (list "nano" (dict) $) | nindent 10 }} core: diff --git a/packages/apps/harbor/values.schema.json b/packages/apps/harbor/values.schema.json index 1352f5dc..71cc1d98 100644 --- a/packages/apps/harbor/values.schema.json +++ b/packages/apps/harbor/values.schema.json @@ -179,6 +179,9 @@ "description": "Container image registry configuration.", "type": "object", "default": {}, + "required": [ + "storageType" + ], "properties": { "resources": { "description": "Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.", @@ -226,6 +229,29 @@ "xlarge", "2xlarge" ] + }, + "size": { + "description": "Persistent Volume size (only used when storageType is pvc).", + "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 + }, + "storageType": { + "description": "Storage backend type for images.", + "type": "string", + "default": "pvc", + "enum": [ + "s3", + "pvc" + ] } } }, diff --git a/packages/apps/harbor/values.yaml b/packages/apps/harbor/values.yaml index de44eaff..f1131bd1 100644 --- a/packages/apps/harbor/values.yaml +++ b/packages/apps/harbor/values.yaml @@ -34,12 +34,20 @@ core: resources: {} resourcesPreset: "small" +## @enum {string} StorageType - Storage backend for container image registry. +## @value s3 - S3-compatible object storage via COSI BucketClaim (requires SeaweedFS in tenant). +## @value pvc - Persistent Volume Claim. + ## @typedef {struct} Registry - Container image registry configuration. +## @field {StorageType} storageType - Storage backend type for images. +## @field {quantity} [size] - Persistent Volume size (only used when storageType is pvc). ## @field {Resources} [resources] - Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied. ## @field {ResourcesPreset} [resourcesPreset] - Default sizing preset used when `resources` is omitted. ## @param {Registry} registry - Container image registry configuration. registry: + storageType: "pvc" + size: 10Gi resources: {} resourcesPreset: "small" diff --git a/packages/system/harbor-rd/cozyrds/harbor.yaml b/packages/system/harbor-rd/cozyrds/harbor.yaml index 0112dc11..80924530 100644 --- a/packages/system/harbor-rd/cozyrds/harbor.yaml +++ b/packages/system/harbor-rd/cozyrds/harbor.yaml @@ -8,7 +8,7 @@ spec: singular: harbor plural: harbors openAPISchema: |- - {"title":"Chart Values","type":"object","properties":{"core":{"description":"Core API server configuration.","type":"object","default":{},"properties":{"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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.","type":"string","default":"small","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]}}},"database":{"description":"PostgreSQL database configuration.","type":"object","default":{},"required":["replicas","size"],"properties":{"replicas":{"description":"Number of database instances.","type":"integer","default":2},"size":{"description":"Persistent Volume size for database storage.","default":"5Gi","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}}},"host":{"description":"Hostname for external access to Harbor (defaults to 'harbor' subdomain for the tenant host).","type":"string","default":""},"jobservice":{"description":"Background job service configuration.","type":"object","default":{},"properties":{"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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.","type":"string","default":"nano","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]}}},"redis":{"description":"Redis cache configuration.","type":"object","default":{},"required":["replicas","size"],"properties":{"replicas":{"description":"Number of Redis replicas.","type":"integer","default":2},"size":{"description":"Persistent Volume size for cache storage.","default":"1Gi","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}}},"registry":{"description":"Container image registry configuration.","type":"object","default":{},"properties":{"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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.","type":"string","default":"small","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]}}},"storageClass":{"description":"StorageClass used to store the data.","type":"string","default":""},"trivy":{"description":"Trivy vulnerability scanner configuration.","type":"object","default":{},"required":["enabled","size"],"properties":{"enabled":{"description":"Enable or disable the vulnerability scanner.","type":"boolean","default":true},"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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.","type":"string","default":"nano","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]},"size":{"description":"Persistent Volume size for vulnerability database cache.","default":"5Gi","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}}}}} + {"title":"Chart Values","type":"object","properties":{"core":{"description":"Core API server configuration.","type":"object","default":{},"properties":{"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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.","type":"string","default":"small","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]}}},"database":{"description":"PostgreSQL database configuration.","type":"object","default":{},"required":["replicas","size"],"properties":{"replicas":{"description":"Number of database instances.","type":"integer","default":2},"size":{"description":"Persistent Volume size for database storage.","default":"5Gi","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}}},"host":{"description":"Hostname for external access to Harbor (defaults to 'harbor' subdomain for the tenant host).","type":"string","default":""},"jobservice":{"description":"Background job service configuration.","type":"object","default":{},"properties":{"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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.","type":"string","default":"nano","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]}}},"redis":{"description":"Redis cache configuration.","type":"object","default":{},"required":["replicas","size"],"properties":{"replicas":{"description":"Number of Redis replicas.","type":"integer","default":2},"size":{"description":"Persistent Volume size for cache storage.","default":"1Gi","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}}},"registry":{"description":"Container image registry configuration.","type":"object","default":{},"required":["storageType"],"properties":{"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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.","type":"string","default":"small","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]},"size":{"description":"Persistent Volume size (only used when storageType is pvc).","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},"storageType":{"description":"Storage backend type for images.","type":"string","default":"pvc","enum":["s3","pvc"]}}},"storageClass":{"description":"StorageClass used to store the data.","type":"string","default":""},"trivy":{"description":"Trivy vulnerability scanner configuration.","type":"object","default":{},"required":["enabled","size"],"properties":{"enabled":{"description":"Enable or disable the vulnerability scanner.","type":"boolean","default":true},"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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.","type":"string","default":"nano","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]},"size":{"description":"Persistent Volume size for vulnerability database cache.","default":"5Gi","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}}}}} release: prefix: "harbor-" labels: @@ -29,7 +29,7 @@ spec: - registry - container icon: "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHJvbGU9ImltZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHZpZXdCb3g9IjEuMDUgLTEuOTUgMzU5LjQxIDM2MS42NiI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXItZ3JhZGllbnQiIHgxPSIyNjQuNzkiIHgyPSIyNjcuMjciIHkxPSI5NTIuMzkiIHkyPSI5NTIuMzkiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMzAuNDMgMCAwIC0zMC40MyAtNzk1NS4yMiAyOTI4NS43NSkiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiM2MGI5MzIiLz48c3RvcCBvZmZzZXQ9Ii4yOCIgc3RvcC1jb2xvcj0iIzYwYjkzMiIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzM2N2MzNCIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXItZ3JhZGllbnQtMiIgeDE9IjI2My43NyIgeDI9IjI2Ni4yNiIgeTE9Ijk1NS42NSIgeTI9Ijk1NS42NSIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgyNy4yMSAwIDAgLTI3LjIxIC03MDczLjg1IDI2MTY5LjQxKSIgeGxpbms6aHJlZj0iI2xpbmVhci1ncmFkaWVudCIvPjxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyLWdyYWRpZW50LTMiIHgxPSIyNjMuMjgiIHgyPSIyNjUuNzYiIHkxPSI5NTMuNzQiIHkyPSI5NTMuNzQiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMjUuNzUgMCAwIC0yNS43NSAtNjY3MS4xMyAyNDgxMi4yMykiIHhsaW5rOmhyZWY9IiNsaW5lYXItZ3JhZGllbnQiLz48bGluZWFyR3JhZGllbnQgaWQ9ImxpbmVhci1ncmFkaWVudC00IiB4MT0iMjYzLjc3IiB4Mj0iMjY2LjI1IiB5MT0iOTUzLjIiIHkyPSI5NTMuMiIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgyNy4xIDAgMCAtMjcuMSAtNzA0MC45IDI2MTAyLjQ5KSIgeGxpbms6aHJlZj0iI2xpbmVhci1ncmFkaWVudCIvPjxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyLWdyYWRpZW50LTUiIHgxPSIyNjIuNzMiIHgyPSIyNjUuMjEiIHkxPSI5NTQuMzQiIHkyPSI5NTQuMzQiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMjQuNCAwIDAgLTI0LjQgLTYzMDEuMzYgMjM1MjEuOTcpIiB4bGluazpocmVmPSIjbGluZWFyLWdyYWRpZW50Ii8+PGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXItZ3JhZGllbnQtNiIgeDE9IjI3Mi4xNCIgeDI9IjI3NC42MiIgeTE9Ijk1NS4xNSIgeTI9Ijk1NS4xNSIgZ3JhZGllbnRUcmFuc2Zvcm09InNjYWxlKDY2LjA5IC02Ni4wOSkgcm90YXRlKDM2LjUyIDE1ODguMTUzIDY4LjE0OCkiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiM0NTk2ZDgiLz48c3RvcCBvZmZzZXQ9Ii4yIiBzdG9wLWNvbG9yPSIjNDU5NmQ4Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjZmZmIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9ImxpbmVhci1ncmFkaWVudC03IiB4MT0iMjcwLjY1IiB4Mj0iMjczLjEzIiB5MT0iOTUyLjM4IiB5Mj0iOTUyLjM4IiBncmFkaWVudFRyYW5zZm9ybT0ic2NhbGUoNzcuOCAtNzcuOCkgcm90YXRlKC0xMS41NCAtNDU4Ny4yMDkgMTgwMy4zMjMpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjNDE5NGQ3Ii8+PHN0b3Agb2Zmc2V0PSIuMiIgc3RvcC1jb2xvcj0iIzQxOTRkNyIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI2ZmZiIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXItZ3JhZGllbnQtOCIgeDE9IjI3MC45NyIgeDI9IjI3My40NSIgeTE9Ijk1My43NSIgeTI9Ijk1My43NSIgZ3JhZGllbnRUcmFuc2Zvcm09InNjYWxlKDcxLjM1IC03MS4zNSkgcm90YXRlKDEwLjIzIDU0NzcuMzcgLTEwMjQuNjAyKSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzQxOTRkNyIvPjxzdG9wIG9mZnNldD0iLjMzIiBzdG9wLWNvbG9yPSIjNDQ5NmQ4Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjZmZmIi8+PC9saW5lYXJHcmFkaWVudD48Y2xpcFBhdGggaWQ9ImNsaXAtcGF0aCI+PHBhdGggZD0iTTE3NC40IDMwMi41MmwtNjguNjUtMzAuMjYtMy44MyA0My4yMSA3NS41IDIzLjk4LTMuMDItMzYuOTN6IiBjbGFzcz0iY2xzLTEiLz48L2NsaXBQYXRoPjxjbGlwUGF0aCBpZD0iY2xpcC1wYXRoLTIiPjxwYXRoIGQ9Ik0xMTkuMTkgMTM1LjM4aDM4LjU1djMyLjg5aC05LjE1di0xNGExMC4xMyAxMC4xMyAwIDEgMC0yMC4yNiAwdjE0aC05LjE1em0tNy43IDcybDU3LjIgMjUuMjEtMy45NC00OC4yNGg3LjQ5di0xNi4wOGgtNS41NXYtMzIuODloNS41NXYtOS40NWwtMzAuODYtMzAuMTl2LTIuMTJhMi45MSAyLjkxIDAgMCAwLTUuODIgMHYyLjEybC0zMC44NiAzMC4xOXY5LjQ1aDUuNTZ2MzIuODloLTUuNTZ2MTYuMTJoOC44MnoiIGNsYXNzPSJjbHMtMSIvPjwvY2xpcFBhdGg+PGNsaXBQYXRoIGlkPSJjbGlwLXBhdGgtMyI+PHBhdGggZD0iTTEwOC4xNCAyNDUuMjhsNjMuODggMjguMTYtLjk2LTExLjczLTYxLjk2LTI3LjMtLjk2IDEwLjg3eiIgY2xhc3M9ImNscy0xIi8+PC9jbGlwUGF0aD48Y2xpcFBhdGggaWQ9ImNsaXAtcGF0aC00Ij48cGF0aCBkPSJNMTA2LjQ2IDI2NC4yMWw2Ny4yMyAyOS42My0uOTYtMTEuNzItNjUuMzEtMjguNzgtLjk2IDEwLjg3eiIgY2xhc3M9ImNscy0xIi8+PC9jbGlwUGF0aD48Y2xpcFBhdGggaWQ9ImNsaXAtcGF0aC01Ij48cGF0aCBkPSJNMTEwLjc3IDIxNS40OGwtLjk2IDEwLjg3IDYwLjU0IDI2LjY4LS45NS0xMS43Mi01OC42My0yNS44M3oiIGNsYXNzPSJjbHMtMSIvPjwvY2xpcFBhdGg+PGNsaXBQYXRoIGlkPSJjbGlwLXBhdGgtNiI+PHBhdGggZD0iTTMxMy4xMyA2Ny41OWExNzUuMzEgMTc1LjMxIDAgMCAwLTI5Ljc1LTI4LjEzYy0xLjU3LTEuMTctMy4xOC0yLjMtNC43OS0zLjQyTDI1NiA1OS41bC04Mi4zNCA4NS42MyAxMTMuNDEtNTguNjggMjkuMDctMTVjLTEuMDEtMS4zMS0xLjk4LTIuNjItMy4wMS0zLjg2eiIgY2xhc3M9ImNscy0xIi8+PC9jbGlwUGF0aD48Y2xpcFBhdGggaWQ9ImNsaXAtcGF0aC03Ij48cGF0aCBkPSJNMzUzLjU5IDE3Ny42MWMwLTItLjE0LTQtLjIyLTUuOTNsLTMyLjIxLTIuMzEtMTQ3LjQ3LTEwLjU4TDMxOC4zNiAyMDlsMzAuNDEgMTAuNTVjLjA5LS4zNi4xOS0uNzEuMjgtMS4wOGExNzMuNjUgMTczLjY1IDAgMCAwIDQuNTctMzkuNDd2LTEuMzd6IiBjbGFzcz0iY2xzLTEiLz48L2NsaXBQYXRoPjxjbGlwUGF0aCBpZD0iY2xpcC1wYXRoLTgiPjxwYXRoIGQ9Ik0zNDguODQgMTM4LjYxYTE3Mi41NSAxNzIuNTUgMCAwIDAtMTMuODQtMzcuN2wtMzAuOTQgOS45NEwxNzUuOTIgMTUybDE0Mi00LjgzIDMyLjUtMS4xMWMtLjUtMi40OC0uOTktNC45OC0xLjU4LTcuNDV6IiBjbGFzcz0iY2xzLTEiLz48L2NsaXBQYXRoPjxzdHlsZT4uY2xzLTF7ZmlsbDpub25lfS5jbHMtMTN7ZmlsbDojNjk2NTY2fTwvc3R5bGU+PC9kZWZzPjxnIGlkPSJnMTIiPjxwYXRoIGlkPSJwYXRoMTQiIGZpbGw9IiNmZmYiIGQ9Ik0zMC44OSAxNzlhMTQ4Ljg3IDE0OC44NyAwIDEgMSAxNDguODcgMTQ4Ljg1QTE0OC44NyAxNDguODcgMCAwIDEgMzAuODkgMTc5Ii8+PGcgaWQ9ImczMCIgY2xpcC1wYXRoPSJ1cmwoI2NsaXAtcGF0aCkiPjxnIGlkPSJnMzIiPjxwYXRoIGlkPSJwYXRoNDYiIGZpbGw9InVybCgjbGluZWFyLWdyYWRpZW50KSIgZD0iTTE3NC40IDMwMi41MmwtNjguNjUtMzAuMjUtMy44MiA0My4yIDc1LjUgMjQtMy0zNi45MyIvPjwvZz48L2c+PGcgaWQ9Imc0OCIgY2xpcC1wYXRoPSJ1cmwoI2NsaXAtcGF0aC0yKSI+PGcgaWQ9Imc1MCI+PHBhdGggaWQ9InBhdGg2NCIgZmlsbD0idXJsKCNsaW5lYXItZ3JhZGllbnQtMikiIGQ9Ik0xMTkuMTkgMTM1LjM4aDM4LjU1djMyLjg5aC05LjE1di0xNGExMC4xMyAxMC4xMyAwIDEgMC0yMC4yNiAwdjE0aC05LjE1em0tNy43IDcybDU3LjIgMjUuMjEtMy45NC00OC4yNGg3LjQ5di0xNi4wOGgtNS41NXYtMzIuODloNS41NXYtOS40NWwtMzAuODYtMzAuMTl2LTIuMTJhMi45MSAyLjkxIDAgMCAwLTUuODIgMHYyLjEybC0zMC44NiAzMC4xOXY5LjQ1aDUuNTZ2MzIuODloLTUuNTZ2MTYuMTJoOC44MmwtMiAyMyIvPjwvZz48L2c+PGcgaWQ9Imc2NiIgY2xpcC1wYXRoPSJ1cmwoI2NsaXAtcGF0aC0zKSI+PGcgaWQ9Imc2OCI+PHBhdGggaWQ9InBhdGg4MiIgZmlsbD0idXJsKCNsaW5lYXItZ3JhZGllbnQtMykiIGQ9Ik0xMDguMTMgMjQ1LjI4TDE3MiAyNzMuNDRsLTEtMTEuNzItNjItMjcuMzEtMSAxMC44NyIvPjwvZz48L2c+PGcgaWQ9Imc4NCIgY2xpcC1wYXRoPSJ1cmwoI2NsaXAtcGF0aC00KSI+PGcgaWQ9Imc4NiI+PHBhdGggaWQ9InBhdGgxMDAiIGZpbGw9InVybCgjbGluZWFyLWdyYWRpZW50LTQpIiBkPSJNMTA2LjQ2IDI2NC4yMWw2Ny4yMyAyOS42My0xLTExLjcyLTY1LjMxLTI4Ljc4LTEgMTAuODciLz48L2c+PC9nPjxnIGlkPSJnMTAyIiBjbGlwLXBhdGg9InVybCgjY2xpcC1wYXRoLTUpIj48ZyBpZD0iZzEwNCI+PHBhdGggaWQ9InBhdGgxMTgiIGZpbGw9InVybCgjbGluZWFyLWdyYWRpZW50LTUpIiBkPSJNMTEwLjc3IDIxNS40OGwtMSAxMC44OEwxNzAuMzUgMjUzbC0xLTExLjcyLTU4LjYyLTI1LjgzIi8+PC9nPjwvZz48cGF0aCBpZD0icGF0aDEyMCIgZD0iTTMwNC4wNyAxMTAuODVsMzAuOTMtOS45NGMtLjExLS4yMi0uMjEtLjQ1LS4zMi0uNjZhMTc0LjQxIDE3NC40MSAwIDAgMC0xOC41NS0yOC44M2wtMjkuMDcgMTVhMTQyLjcxIDE0Mi43MSAwIDAgMSAxNi43MyAyMy44N2MuMS4xNy4xOC4zNS4yNy41MyIgY2xhc3M9ImNscy0xMyIvPjxwYXRoIGlkPSJwYXRoMTIyIiBkPSJNMzIxLjE1IDE2OS4zN2wzMi4yMSAyLjMxYTE3Mi44NiAxNzIuODYgMCAwIDAtMy0yNS41OWwtMzIuNSAxLjExYTE0MSAxNDEgMCAwIDEgMy4yNSAyMi4xNyIgY2xhc3M9ImNscy0xMyIvPjxwYXRoIGlkPSJwYXRoMTI0IiBkPSJNMTgyIDMyMC44MWMtNzguMiAwLTE0MS44My02My42Mi0xNDEuODMtMTQxLjgyUzEwMy44MiAzNy4xNiAxODIgMzcuMTZhMTQwLjkzIDE0MC45MyAwIDAgMSA3Ni4zIDIyLjM0TDI4MC44NSAzNkExNzIuODYgMTcyLjg2IDAgMCAwIDE4MiA1LjExQzg2LjE1IDUuMTEgOC4xNSA4My4xMSA4LjE1IDE3OXM3OCAxNzMuODUgMTczLjg1IDE3My44NWM4MS45IDAgMTUwLjY5LTU3IDE2OS0xMzMuMzJMMzIwLjYyIDIwOWMtMTMuOCA2My44NC03MC42OSAxMTEuODMtMTM4LjYgMTExLjgzIiBjbGFzcz0iY2xzLTEzIi8+PGcgaWQ9ImcxMjYiIGNsaXAtcGF0aD0idXJsKCNjbGlwLXBhdGgtNikiPjxnIGlkPSJnMTI4Ij48cGF0aCBpZD0icGF0aDE0MiIgZmlsbD0idXJsKCNsaW5lYXItZ3JhZGllbnQtNikiIGQ9Ik0zMTMuMTMgNjcuNTlhMTc1LjMxIDE3NS4zMSAwIDAgMC0yOS43NS0yOC4xM2MtMS41Ny0xLjE3LTMuMTgtMi4zLTQuNzktMy40MkwyNTYgNTkuNWwtODIuMzQgODUuNjMgMTEzLjQxLTU4LjY4IDI5LjA3LTE1Yy0xLTEuMjctMi0yLjU4LTMtMy44MiIvPjwvZz48L2c+PGcgaWQ9ImcxNDQiIGNsaXAtcGF0aD0idXJsKCNjbGlwLXBhdGgtNykiPjxnIGlkPSJnMTQ2Ij48cGF0aCBpZD0icGF0aDE2MCIgZmlsbD0idXJsKCNsaW5lYXItZ3JhZGllbnQtNykiIGQ9Ik0zNTMuNTkgMTc3LjYxYzAtMi0uMTQtNC0uMjItNS45M2wtMzIuMjEtMi4zMS0xNDcuNDctMTAuNThMMzE4LjM2IDIwOWwzMC40MSAxMC41NWMuMDktLjM2LjE5LS43MS4yOC0xLjA4YTE3My42NSAxNzMuNjUgMCAwIDAgNC41Ny0zOS40N3YtMS4zNyIvPjwvZz48L2c+PGcgaWQ9ImcxNjIiIGNsaXAtcGF0aD0idXJsKCNjbGlwLXBhdGgtOCkiPjxnIGlkPSJnMTY0Ij48cGF0aCBpZD0icGF0aDE3OCIgZmlsbD0idXJsKCNsaW5lYXItZ3JhZGllbnQtOCkiIGQ9Ik0zNDguODQgMTM4LjYxYTE3Mi41NSAxNzIuNTUgMCAwIDAtMTMuODQtMzcuN2wtMzAuOTQgOS45NEwxNzUuOTIgMTUybDE0Mi00LjgzIDMyLjUtMS4xMWMtLjQ4LTIuNTEtMS01LTEuNTYtNy40OCIvPjwvZz48L2c+PC9nPjwvc3ZnPg==" - keysOrder: [["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata", "name"], ["spec", "host"], ["spec", "storageClass"], ["spec", "core"], ["spec", "core", "resources"], ["spec", "core", "resourcesPreset"], ["spec", "registry"], ["spec", "registry", "resources"], ["spec", "registry", "resourcesPreset"], ["spec", "jobservice"], ["spec", "jobservice", "resources"], ["spec", "jobservice", "resourcesPreset"], ["spec", "trivy"], ["spec", "trivy", "enabled"], ["spec", "trivy", "size"], ["spec", "trivy", "resources"], ["spec", "trivy", "resourcesPreset"], ["spec", "database"], ["spec", "database", "size"], ["spec", "database", "replicas"], ["spec", "redis"], ["spec", "redis", "size"], ["spec", "redis", "replicas"]] + keysOrder: [["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata", "name"], ["spec", "host"], ["spec", "storageClass"], ["spec", "core"], ["spec", "core", "resources"], ["spec", "core", "resourcesPreset"], ["spec", "registry"], ["spec", "registry", "storageType"], ["spec", "registry", "size"], ["spec", "registry", "resources"], ["spec", "registry", "resourcesPreset"], ["spec", "jobservice"], ["spec", "jobservice", "resources"], ["spec", "jobservice", "resourcesPreset"], ["spec", "trivy"], ["spec", "trivy", "enabled"], ["spec", "trivy", "size"], ["spec", "trivy", "resources"], ["spec", "trivy", "resourcesPreset"], ["spec", "database"], ["spec", "database", "size"], ["spec", "database", "replicas"], ["spec", "redis"], ["spec", "redis", "size"], ["spec", "redis", "replicas"]] secrets: exclude: [] include: From 490faaf2920c2a01622c4eeaf43b739c4072c90e Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Tue, 17 Feb 2026 02:01:12 +0300 Subject: [PATCH 095/666] fix(harbor): add operator dependencies, fix persistence rendering, increase E2E timeout Add postgres-operator and redis-operator to PackageSource dependsOn to ensure CRDs are available before Harbor system chart deploys. Make persistentVolumeClaim conditional to avoid empty YAML mapping when using S3 storage without Trivy. Increase E2E system HelmRelease timeout from 300s to 600s to account for CPNG + Redis + Harbor bootstrap time on QEMU. Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- hack/e2e-apps/harbor.bats | 2 +- packages/apps/harbor/templates/harbor.yaml | 2 ++ packages/core/platform/sources/harbor-application.yaml | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/hack/e2e-apps/harbor.bats b/hack/e2e-apps/harbor.bats index 8c734565..e1b6ab2b 100644 --- a/hack/e2e-apps/harbor.bats +++ b/hack/e2e-apps/harbor.bats @@ -37,7 +37,7 @@ spec: EOF sleep 5 kubectl -n tenant-test wait hr $release --timeout=60s --for=condition=ready - kubectl -n tenant-test wait hr $release-system --timeout=300s --for=condition=ready + kubectl -n tenant-test wait hr $release-system --timeout=600s --for=condition=ready kubectl -n tenant-test wait deploy $release-core --timeout=120s --for=condition=available kubectl -n tenant-test wait deploy $release-registry --timeout=120s --for=condition=available kubectl -n tenant-test wait deploy $release-portal --timeout=120s --for=condition=available diff --git a/packages/apps/harbor/templates/harbor.yaml b/packages/apps/harbor/templates/harbor.yaml index 97f28942..4196ebcd 100644 --- a/packages/apps/harbor/templates/harbor.yaml +++ b/packages/apps/harbor/templates/harbor.yaml @@ -102,6 +102,7 @@ spec: secure: false v4auth: true {{- end }} + {{- if or (eq .Values.registry.storageType "pvc") .Values.trivy.enabled }} persistentVolumeClaim: {{- if eq .Values.registry.storageType "pvc" }} registry: @@ -117,6 +118,7 @@ spec: storageClass: {{ . }} {{- end }} {{- end }} + {{- end }} portal: resources: {{- include "cozy-lib.resources.defaultingSanitize" (list "nano" (dict) $) | nindent 10 }} core: diff --git a/packages/core/platform/sources/harbor-application.yaml b/packages/core/platform/sources/harbor-application.yaml index b05d7a01..b9bc8dc3 100644 --- a/packages/core/platform/sources/harbor-application.yaml +++ b/packages/core/platform/sources/harbor-application.yaml @@ -13,6 +13,8 @@ spec: - name: default dependsOn: - cozystack.networking + - cozystack.postgres-operator + - cozystack.redis-operator libraries: - name: cozy-lib path: library/cozy-lib From 0f2ba5aba25af436c7a80a2f1b676cfc340f38f5 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Tue, 17 Feb 2026 12:59:23 +0300 Subject: [PATCH 096/666] fix(harbor): add diagnostic output on E2E system HelmRelease timeout Dump HelmRelease status, pods, events, and ExternalArtifact info when harbor-test-system fails to become ready, to diagnose the root cause of the persistent timeout. Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- hack/e2e-apps/harbor.bats | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/hack/e2e-apps/harbor.bats b/hack/e2e-apps/harbor.bats index e1b6ab2b..076a1841 100644 --- a/hack/e2e-apps/harbor.bats +++ b/hack/e2e-apps/harbor.bats @@ -37,7 +37,17 @@ spec: EOF sleep 5 kubectl -n tenant-test wait hr $release --timeout=60s --for=condition=ready - kubectl -n tenant-test wait hr $release-system --timeout=600s --for=condition=ready + kubectl -n tenant-test wait hr $release-system --timeout=600s --for=condition=ready || { + echo "=== HelmRelease status ===" + kubectl -n tenant-test get hr $release-system -o yaml 2>&1 || true + echo "=== Pods ===" + kubectl -n tenant-test get pods 2>&1 || true + echo "=== Events ===" + kubectl -n tenant-test get events --sort-by='.lastTimestamp' 2>&1 | tail -30 || true + echo "=== ExternalArtifact ===" + kubectl -n cozy-system get externalartifact cozystack-harbor-application-default-harbor-system -o yaml 2>&1 || true + false + } kubectl -n tenant-test wait deploy $release-core --timeout=120s --for=condition=available kubectl -n tenant-test wait deploy $release-registry --timeout=120s --for=condition=available kubectl -n tenant-test wait deploy $release-portal --timeout=120s --for=condition=available From 0e6ae28bb8597f7ff3af4d78c1ab6ce288784782 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Tue, 17 Feb 2026 15:15:15 +0300 Subject: [PATCH 097/666] fix(harbor): resolve Redis nil pointer on first install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The vendored Harbor chart does an unsafe `lookup` of the Redis auth Secret at template rendering time to extract the password. On first install, the Secret doesn't exist yet (created by the same chart), causing a nil pointer error. Failed installs are rolled back, deleting the Secret, so retries also fail — creating an infinite failure loop. Fix by generating the Redis password in the wrapper chart (same pattern as admin password), storing it in the credentials Secret, and injecting it via HelmRelease valuesFrom with targetPath. This bypasses the vendored chart's lookup entirely — it uses the password value directly instead of looking up the Secret. Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- packages/apps/harbor/templates/harbor.yaml | 14 +++++++++++++- packages/system/harbor/templates/redis.yaml | 9 ++------- packages/system/harbor/values.yaml | 1 + 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/apps/harbor/templates/harbor.yaml b/packages/apps/harbor/templates/harbor.yaml index 4196ebcd..ff59b45f 100644 --- a/packages/apps/harbor/templates/harbor.yaml +++ b/packages/apps/harbor/templates/harbor.yaml @@ -4,8 +4,12 @@ {{- $existingSecret := lookup "v1" "Secret" .Release.Namespace (printf "%s-credentials" .Release.Name) }} {{- $adminPassword := randAlphaNum 16 }} +{{- $redisPassword := randAlphaNum 32 }} {{- if $existingSecret }} {{- $adminPassword = index $existingSecret.data "admin-password" | b64dec }} + {{- if hasKey $existingSecret.data "redis-password" }} + {{- $redisPassword = index $existingSecret.data "redis-password" | b64dec }} + {{- end }} {{- end }} {{- $existingCoreSecret := lookup "v1" "Secret" .Release.Namespace (printf "%s-core" .Release.Name) }} @@ -26,6 +30,7 @@ metadata: name: {{ .Release.Name }}-credentials stringData: admin-password: {{ $adminPassword | quote }} + redis-password: {{ $redisPassword | quote }} url: https://{{ $harborHost }} --- @@ -53,6 +58,14 @@ spec: valuesFrom: - kind: Secret name: cozystack-values + - kind: Secret + name: {{ .Release.Name }}-credentials + valuesKey: redis-password + targetPath: redis.password + - kind: Secret + name: {{ .Release.Name }}-credentials + valuesKey: redis-password + targetPath: harbor.redis.external.password {{- if eq .Values.registry.storageType "s3" }} - kind: Secret name: {{ .Release.Name }}-registry-bucket @@ -153,7 +166,6 @@ spec: external: addr: "rfs-{{ .Release.Name }}-redis:26379" sentinelMasterSet: "mymaster" - existingSecret: "{{ .Release.Name }}-redis-auth" coreDatabaseIndex: "0" jobserviceDatabaseIndex: "1" registryDatabaseIndex: "2" diff --git a/packages/system/harbor/templates/redis.yaml b/packages/system/harbor/templates/redis.yaml index 7a71f1ce..b39c2b04 100644 --- a/packages/system/harbor/templates/redis.yaml +++ b/packages/system/harbor/templates/redis.yaml @@ -1,15 +1,10 @@ -{{- $existingPassword := lookup "v1" "Secret" .Release.Namespace (printf "%s-redis-auth" .Values.harbor.fullnameOverride) }} -{{- $password := randAlphaNum 32 | b64enc }} -{{- if $existingPassword }} - {{- $password = index $existingPassword.data "password" }} -{{- end }} --- apiVersion: v1 kind: Secret metadata: name: {{ .Values.harbor.fullnameOverride }}-redis-auth -data: - password: {{ $password }} +stringData: + password: {{ .Values.redis.password | quote }} --- apiVersion: databases.spotahome.com/v1 kind: RedisFailover diff --git a/packages/system/harbor/values.yaml b/packages/system/harbor/values.yaml index 40e924e0..9038bd38 100644 --- a/packages/system/harbor/values.yaml +++ b/packages/system/harbor/values.yaml @@ -4,6 +4,7 @@ db: size: 5Gi storageClass: "" redis: + password: "" replicas: 2 size: 1Gi storageClass: "" From efb9bc70b3d09f22006f027f6b556c194b302469 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Wed, 18 Feb 2026 00:49:34 +0300 Subject: [PATCH 098/666] fix(harbor): use Release.Name for default host to avoid conflicts Multiple Harbor instances in the same namespace would get the same default hostname when derived from namespace host. Use Release.Name instead for unique hostnames per instance. Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- packages/apps/harbor/templates/harbor.yaml | 2 +- packages/apps/harbor/templates/ingress.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/apps/harbor/templates/harbor.yaml b/packages/apps/harbor/templates/harbor.yaml index ff59b45f..0e9f4fd5 100644 --- a/packages/apps/harbor/templates/harbor.yaml +++ b/packages/apps/harbor/templates/harbor.yaml @@ -1,6 +1,6 @@ {{- $ingress := .Values._namespace.ingress }} {{- $host := .Values._namespace.host }} -{{- $harborHost := .Values.host | default (printf "harbor.%s" $host) }} +{{- $harborHost := .Values.host | default (printf "harbor.%s" .Release.Name) }} {{- $existingSecret := lookup "v1" "Secret" .Release.Namespace (printf "%s-credentials" .Release.Name) }} {{- $adminPassword := randAlphaNum 16 }} diff --git a/packages/apps/harbor/templates/ingress.yaml b/packages/apps/harbor/templates/ingress.yaml index d2e3c204..3f2b8f78 100644 --- a/packages/apps/harbor/templates/ingress.yaml +++ b/packages/apps/harbor/templates/ingress.yaml @@ -1,6 +1,6 @@ {{- $ingress := .Values._namespace.ingress }} {{- $host := .Values._namespace.host }} -{{- $harborHost := .Values.host | default (printf "harbor.%s" $host) }} +{{- $harborHost := .Values.host | default (printf "harbor.%s" .Release.Name) }} {{- $issuerType := (index .Values._cluster "clusterissuer") | default "http01" }} --- apiVersion: networking.k8s.io/v1 From 26178d97be54c60cb43f432e2b623e12417e4ff8 Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Tue, 17 Feb 2026 22:51:00 +0100 Subject: [PATCH 099/666] fix(platform): adopt tenant-root into cozystack-basics during migration In v0.41.x the tenant-root Namespace and HelmRelease were applied via kubectl apply with no Helm release tracking. In v1.0 these resources are managed by the cozystack-basics Helm release. Without proper Helm ownership annotations the install of cozystack-basics fails because the resources already exist. Add migration 31 that annotates and labels both the Namespace and HelmRelease so Helm can adopt them, matching the pattern established in migrations 22 and 27. Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- .../platform/images/migrations/migrations/31 | 45 +++++++++++++++++++ packages/core/platform/values.yaml | 2 +- 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100755 packages/core/platform/images/migrations/migrations/31 diff --git a/packages/core/platform/images/migrations/migrations/31 b/packages/core/platform/images/migrations/migrations/31 new file mode 100755 index 00000000..2a261d9b --- /dev/null +++ b/packages/core/platform/images/migrations/migrations/31 @@ -0,0 +1,45 @@ +#!/bin/sh +# Migration 31 --> 32 +# Adopt tenant-root resources into cozystack-basics Helm release. +# +# In v0.41.x tenant-root Namespace and HelmRelease were applied via +# kubectl apply (no Helm tracking). In v1.0 they are managed by the +# cozystack-basics Helm release. Without Helm ownership annotations +# the install of cozystack-basics fails because the resources already +# exist. This migration adds the required annotations and labels so +# Helm can adopt them. + +set -euo pipefail + +RELEASE_NAME="cozystack-basics" +RELEASE_NS="cozy-system" + +# Adopt Namespace tenant-root +if kubectl get namespace tenant-root >/dev/null 2>&1; then + echo "Adopting Namespace tenant-root into $RELEASE_NAME" + kubectl annotate namespace tenant-root \ + meta.helm.sh/release-name="$RELEASE_NAME" \ + meta.helm.sh/release-namespace="$RELEASE_NS" \ + --overwrite + kubectl label namespace tenant-root \ + app.kubernetes.io/managed-by=Helm \ + --overwrite +fi + +# Adopt HelmRelease tenant-root +if kubectl get helmrelease -n tenant-root tenant-root >/dev/null 2>&1; then + echo "Adopting HelmRelease tenant-root into $RELEASE_NAME" + kubectl annotate helmrelease -n tenant-root tenant-root \ + meta.helm.sh/release-name="$RELEASE_NAME" \ + meta.helm.sh/release-namespace="$RELEASE_NS" \ + helm.sh/resource-policy=keep \ + --overwrite + kubectl label helmrelease -n tenant-root tenant-root \ + app.kubernetes.io/managed-by=Helm \ + sharding.fluxcd.io/key=tenants \ + --overwrite +fi + +# Stamp version +kubectl create configmap -n cozy-system cozystack-version \ + --from-literal=version=32 --dry-run=client -o yaml | kubectl apply -f- diff --git a/packages/core/platform/values.yaml b/packages/core/platform/values.yaml index ca2d9d1b..f9d4c8d3 100644 --- a/packages/core/platform/values.yaml +++ b/packages/core/platform/values.yaml @@ -6,7 +6,7 @@ sourceRef: migrations: enabled: false image: ghcr.io/cozystack/cozystack/platform-migrations:v1.0.0-beta.5@sha256:e7a9e0f0adc33e0be007af42f50ed3af064aa965ad628e48cc6c7943f31f239d - targetVersion: 31 + targetVersion: 32 # Bundle deployment configuration bundles: system: From 7ac989923ddadcda85b4e758014f9a15716eb6c2 Mon Sep 17 00:00:00 2001 From: kklinch0 Date: Wed, 3 Sep 2025 15:27:38 +0300 Subject: [PATCH 100/666] Add monitoring for NATs Co-authored-by: Andrei Kvapil Signed-off-by: kklinch0 Signed-off-by: Andrei Kvapil --- dashboards/nats/nats-jetstream.json | 1541 +++++++++++++++++ dashboards/nats/nats-server.json | 1463 ++++++++++++++++ hack/download-dashboards.sh | 2 + packages/apps/nats/templates/nats.yaml | 4 - packages/system/monitoring/dashboards.list | 2 + packages/system/nats/charts/nats/Chart.yaml | 4 +- packages/system/nats/charts/nats/README.md | 46 +- .../nats/files/stateful-set/pod-template.yaml | 4 + .../stateful-set/prom-exporter-container.yaml | 3 +- .../nats/charts/nats/templates/_helpers.tpl | 14 +- packages/system/nats/charts/nats/values.yaml | 29 +- packages/system/nats/values.yaml | 4 + 12 files changed, 3086 insertions(+), 30 deletions(-) create mode 100644 dashboards/nats/nats-jetstream.json create mode 100644 dashboards/nats/nats-server.json diff --git a/dashboards/nats/nats-jetstream.json b/dashboards/nats/nats-jetstream.json new file mode 100644 index 00000000..d2312e7e --- /dev/null +++ b/dashboards/nats/nats-jetstream.json @@ -0,0 +1,1541 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "description": "NATS JetStream Dashboard", + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "id": 90, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 13, + "x": 0, + "y": 0 + }, + "id": 34, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(nats_stream_total_messages{server_id=~\"$server\"}[5m])", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "messages per second", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "min": 0, + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 5, + "x": 13, + "y": 0 + }, + "id": 32, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum(nats_varz_jetstream_stats_memory{server_id=\"$server\"})", + "interval": "", + "legendFormat": "", + "range": true, + "refId": "A" + } + ], + "title": "Memory Used", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "min": 0, + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 6, + "x": 18, + "y": 0 + }, + "id": 14, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum(nats_varz_connections{server_id=~\"$server\"})", + "interval": "", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Connections", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "min": 0, + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 5, + "x": 13, + "y": 3 + }, + "id": 33, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum(nats_varz_jetstream_config_max_memory{server_id=~\"$server\"})", + "interval": "", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Total Memory", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 6, + "x": 18, + "y": 3 + }, + "id": 29, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum(nats_server_total_consumers{server_id=~\"$server\"})", + "interval": "", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Total Consumers", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "decimals": 3, + "mappings": [], + "max": 1, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "#EAB839", + "value": 0.75 + }, + { + "color": "red", + "value": 0.9 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 0, + "y": 8 + }, + "id": 28, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum(nats_varz_jetstream_stats_storage{server_id=~\"$server\"})/sum(nats_varz_jetstream_config_max_storage{server_id=~\"$server\"})", + "interval": "", + "legendFormat": "", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "", + "interval": "", + "legendFormat": "", + "refId": "B" + } + ], + "title": "Storage Used", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "min": 0, + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 5, + "x": 4, + "y": 8 + }, + "id": 15, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum(nats_varz_jetstream_stats_storage{server_id=~\"$server\"})", + "interval": "", + "legendFormat": "", + "range": true, + "refId": "A" + } + ], + "title": "Total Storage Used", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "decimals": 3, + "mappings": [], + "max": 1, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "#EAB839", + "value": 0.75 + }, + { + "color": "red", + "value": 0.9 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 9, + "y": 8 + }, + "id": 31, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum(nats_varz_jetstream_stats_memory{server_id=~\"$server\"})/sum(nats_varz_jetstream_config_max_memory{server_id=~\"$server\"})", + "interval": "", + "legendFormat": "", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "", + "interval": "", + "legendFormat": "", + "refId": "B" + } + ], + "title": "Memory Used", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "min": 0, + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 5, + "x": 4, + "y": 11 + }, + "id": 30, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum(nats_varz_jetstream_config_max_storage{server_id=~\"$server\"})", + "interval": "", + "legendFormat": "", + "range": true, + "refId": "A" + } + ], + "title": "Max Storage", + "type": "stat" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 14 + }, + "id": 19, + "panels": [], + "title": "Stream metrics", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 15 + }, + "id": 17, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(nats_stream_total_bytes) by (stream_name)", + "interval": "", + "legendFormat": "{{stream_name}}", + "range": true, + "refId": "A" + } + ], + "title": "Stream data size", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 15 + }, + "id": 24, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(nats_stream_total_messages) by (stream_name)", + "interval": "", + "legendFormat": "{{stream_name}}", + "range": true, + "refId": "A" + } + ], + "title": "Stream message count", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "mps" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 15 + }, + "id": 20, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(rate(nats_stream_total_messages{server_id=~\"$server\",stream_name=~\"$stream\"}[$__rate_interval])) by (stream_name)", + "hide": false, + "interval": "", + "legendFormat": "{{stream_name}}", + "range": true, + "refId": "A" + } + ], + "title": "Message Rate (per second)", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 21 + }, + "id": 23, + "panels": [], + "title": "Consumer Metrics", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Messages added & processed per minute per consumer", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "mps" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 24, + "x": 0, + "y": 22 + }, + "id": 25, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(rate(nats_consumer_num_pending{server_id=~\"$server\",stream_name=~\"$stream\",consumer_name=~\"$consumer\"}[$__rate_interval])+rate(nats_consumer_delivered_consumer_seq{server_id=~\"$server\",stream_name=~\"$stream\",consumer_name=~\"$consumer\"}[$__rate_interval])) by (consumer_name)", + "hide": false, + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{consumer_name}} +", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "- sum(rate(nats_consumer_delivered_consumer_seq{server_id=~\"$server\",stream_name=~\"$stream\",consumer_name=~\"$consumer\",consumer_name=~\"$consumer\"}[$__rate_interval])) by (consumer_name)", + "hide": false, + "interval": "", + "legendFormat": "{{consumer_name}} -", + "range": true, + "refId": "A" + } + ], + "title": "Messages per second (++/--)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 28 + }, + "id": 21, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(nats_consumer_delivered_consumer_seq{server_id=~\"$server\",stream_name=~\"$stream\",consumer_name=~\"$consumer\"}) by (consumer_name)", + "hide": false, + "interval": "", + "legendFormat": "{{consumer_name}}", + "range": true, + "refId": "A" + } + ], + "title": "Total delivered messages", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 28 + }, + "id": 26, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(nats_consumer_num_pending{server_id=~\"$server\",stream_name=~\"$stream\",consumer_name=~\"$consumer\"}) by (consumer_name)", + "hide": false, + "interval": "", + "legendFormat": "{{consumer_name}}", + "range": true, + "refId": "A" + } + ], + "title": "Pending messages", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 28 + }, + "id": 27, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(nats_consumer_num_ack_pending{server_id=~\"$server\",stream_name=~\"$stream\",consumer_name=~\"$consumer\"}) by (consumer_name)", + "hide": false, + "interval": "", + "legendFormat": "{{consumer_name}}", + "range": true, + "refId": "A" + } + ], + "title": "Message Acks Pending", + "type": "timeseries" + } + ], + "preload": false, + "refresh": "10s", + "schemaVersion": 40, + "tags": [], + "templating": { + "list": [ + { + "current": { + "text": "vm-shortterm", + "value": "59e01639-a99e-4945-bb94-10821e415ad5" + }, + "description": "", + "label": "datasource", + "name": "DS_PROMETHEUS", + "options": [], + "query": "prometheus", + "refresh": 1, + "regex": "", + "type": "datasource" + }, + { + "current": { + "text": "", + "value": "" + }, + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "definition": "label_values(nats_varz_jetstream_stats_memory,namespace)", + "includeAll": false, + "name": "namespace", + "options": [], + "query": { + "query": "label_values(nats_varz_jetstream_stats_memory,namespace)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "type": "query" + }, + { + "current": { + "text": "All", + "value": "$__all" + }, + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "definition": "label_values(nats_server_total_streams{namespace=\"$namespace\"},server_id)", + "hide": 2, + "includeAll": true, + "label": "Server", + "name": "server", + "options": [], + "query": { + "query": "label_values(nats_server_total_streams{namespace=\"$namespace\"},server_id)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 2, + "regex": "", + "type": "query" + }, + { + "current": { + "text": "All", + "value": "$__all" + }, + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "definition": "label_values(nats_stream_last_seq{server_id=~\"$server\"},stream_name)", + "includeAll": true, + "label": "Stream", + "multi": true, + "name": "stream", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(nats_stream_last_seq{server_id=~\"$server\"},stream_name)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 2, + "regex": "", + "type": "query" + }, + { + "current": { + "text": "All", + "value": "$__all" + }, + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "definition": "label_values(nats_consumer_num_pending,consumer_name)", + "includeAll": true, + "label": "Consumer", + "multi": true, + "name": "consumer", + "options": [], + "query": { + "query": "label_values(nats_consumer_num_pending,consumer_name)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 2, + "regex": "", + "type": "query" + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "NATS JetStream", + "uid": "yQUo5l17k", + "version": 6, + "weekStart": "" +} diff --git a/dashboards/nats/nats-server.json b/dashboards/nats/nats-server.json new file mode 100644 index 00000000..02aa39b2 --- /dev/null +++ b/dashboards/nats/nats-server.json @@ -0,0 +1,1463 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "description": "NATS Server Dashboard for use with built-in Prometheus NATS Exporter into nats official helm charts", + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 91, + "links": [], + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 10, + "panels": [], + "title": "OS Metrics", + "type": "row" + }, + { + "datasource": { + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 1 + }, + "id": 1, + "options": { + "alertThreshold": true, + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "datasource": "PBFA97CFB590B2093", + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "exemplar": true, + "expr": "nats_varz_cpu", + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{server_id}}", + "metric": "nats_varz_cpu", + "refId": "A", + "step": 4 + } + ], + "title": "Server CPU", + "type": "timeseries" + }, + { + "datasource": { + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 1 + }, + "id": 3, + "options": { + "alertThreshold": true, + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "datasource": "PBFA97CFB590B2093", + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "exemplar": true, + "expr": "nats_varz_mem", + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{server_id}}", + "metric": "nats_varz_mem", + "refId": "A", + "step": 4 + } + ], + "title": "Server Memory", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 8 + }, + "id": 11, + "panels": [], + "title": "Throughput", + "type": "row" + }, + { + "datasource": { + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 0, + "y": 9 + }, + "id": 7, + "options": { + "alertThreshold": true, + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "datasource": "PBFA97CFB590B2093", + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "exemplar": true, + "expr": "nats_varz_in_bytes", + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{server_id}}", + "metric": "nats_varz_in_bytes", + "refId": "A", + "step": 10 + } + ], + "title": "Bytes In", + "type": "timeseries" + }, + { + "datasource": { + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 6, + "y": 9 + }, + "id": 8, + "options": { + "alertThreshold": true, + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "datasource": "PBFA97CFB590B2093", + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "exemplar": true, + "expr": "nats_varz_in_msgs", + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{server_id}}", + "metric": "nats_varz_in_msgs", + "refId": "A", + "step": 10 + } + ], + "title": "NATS Msgs In", + "type": "timeseries" + }, + { + "datasource": { + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 12, + "y": 9 + }, + "id": 5, + "options": { + "alertThreshold": true, + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "datasource": "PBFA97CFB590B2093", + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "exemplar": true, + "expr": "nats_varz_out_bytes", + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{server_id}}", + "metric": "nats_varz_out_bytes", + "refId": "A", + "step": 10 + } + ], + "title": "Bytes Out", + "type": "timeseries" + }, + { + "datasource": { + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 18, + "y": 9 + }, + "id": 6, + "options": { + "alertThreshold": true, + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "datasource": "PBFA97CFB590B2093", + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "exemplar": true, + "expr": "nats_varz_out_msgs", + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{server_id}}", + "metric": "nats_varz_out_msgs", + "refId": "A", + "step": 10 + } + ], + "title": "NATS Msgs Out", + "type": "timeseries" + }, + { + "datasource": { + "datasource": "PBFA97CFB590B2093", + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 0, + "y": 16 + }, + "id": 15, + "options": { + "alertThreshold": true, + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "datasource": "PBFA97CFB590B2093", + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "exemplar": true, + "expr": "sum(rate(nats_varz_in_bytes[1m]))", + "interval": "", + "intervalFactor": 2, + "legendFormat": "", + "metric": "nats_varz_in_bytes", + "refId": "A", + "step": 10 + } + ], + "title": "rate(Bytes In)", + "type": "timeseries" + }, + { + "datasource": { + "datasource": "PBFA97CFB590B2093", + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 6, + "y": 16 + }, + "id": 13, + "options": { + "alertThreshold": true, + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "datasource": "PBFA97CFB590B2093", + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "exemplar": true, + "expr": "sum(rate(nats_varz_in_msgs[1m]))", + "interval": "", + "intervalFactor": 2, + "legendFormat": "", + "metric": "nats_varz_in_msgs", + "refId": "A", + "step": 10 + } + ], + "title": "rate(NATS Msgs In)", + "type": "timeseries" + }, + { + "datasource": { + "datasource": "PBFA97CFB590B2093", + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 12, + "y": 16 + }, + "id": 16, + "options": { + "alertThreshold": true, + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "datasource": "PBFA97CFB590B2093", + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "exemplar": true, + "expr": "sum(rate(nats_varz_out_bytes[1m]))", + "interval": "", + "intervalFactor": 2, + "legendFormat": "", + "metric": "nats_varz_out_bytes", + "refId": "A", + "step": 10 + } + ], + "title": "rate(Bytes Out)", + "type": "timeseries" + }, + { + "datasource": { + "datasource": "PBFA97CFB590B2093", + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 18, + "y": 16 + }, + "id": 14, + "options": { + "alertThreshold": true, + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "datasource": "PBFA97CFB590B2093", + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "exemplar": true, + "expr": "sum(rate(nats_varz_out_msgs[1m]))", + "interval": "", + "intervalFactor": 2, + "legendFormat": "", + "metric": "nats_varz_out_msgs", + "refId": "A", + "step": 10 + } + ], + "title": "rate(NATS Msgs Out)", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 23 + }, + "id": 12, + "panels": [], + "title": "Client Metrics", + "type": "row" + }, + { + "datasource": { + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "points", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 6, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "always", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 24 + }, + "id": 2, + "options": { + "alertThreshold": true, + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "datasource": "PBFA97CFB590B2093", + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "exemplar": true, + "expr": "nats_varz_connections", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{server_id}}", + "metric": "nats_varz_connections", + "refId": "A", + "step": 2 + } + ], + "title": "Connections", + "type": "timeseries" + }, + { + "datasource": { + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "points", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 6, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "always", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 24 + }, + "id": 4, + "options": { + "alertThreshold": true, + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "datasource": "PBFA97CFB590B2093", + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "exemplar": true, + "expr": "nats_varz_subscriptions", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{server_id}}", + "metric": "nats_varz_subscriptions", + "refId": "A", + "step": 4 + } + ], + "title": "Subscriptions", + "type": "timeseries" + }, + { + "datasource": { + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "points", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 6, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "always", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 24 + }, + "id": 9, + "options": { + "alertThreshold": true, + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "datasource": "PBFA97CFB590B2093", + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "exemplar": true, + "expr": "nats_varz_slow_consumers", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{server_id}}", + "metric": "nats_varz_slow_consumers", + "refId": "A", + "step": 2 + } + ], + "title": "Slow Consumers", + "type": "timeseries" + } + ], + "preload": false, + "refresh": "", + "schemaVersion": 40, + "tags": [], + "templating": { + "list": [ + { + "current": { + "text": "vm-shortterm", + "value": "59e01639-a99e-4945-bb94-10821e415ad5" + }, + "label": "datasource", + "name": "DS_PROMETHEUS", + "options": [], + "query": "prometheus", + "refresh": 1, + "regex": "", + "type": "datasource" + } + ] + }, + "time": { + "from": "now-5m", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "NATS Server Dashboard", + "uid": "4_zbf287k", + "version": 2, + "weekStart": "" +} diff --git a/hack/download-dashboards.sh b/hack/download-dashboards.sh index a1a6082a..d761f71c 100755 --- a/hack/download-dashboards.sh +++ b/hack/download-dashboards.sh @@ -83,6 +83,8 @@ modules/340-monitoring-kubernetes/monitoring/grafana-dashboards//flux/flux-stats modules/340-monitoring-kubernetes/monitoring/grafana-dashboards//kafka/strimzi-kafka.json modules/340-monitoring-kubernetes/monitoring/grafana-dashboards//seaweedfs/seaweedfs.json modules/340-monitoring-kubernetes/monitoring/grafana-dashboards//goldpinger/goldpinger.json +modules/340-monitoring-kubernetes/monitoring/grafana-dashboards//nats/nats-jetstream.json +modules/340-monitoring-kubernetes/monitoring/grafana-dashboards//nats/nats-server.json EOT diff --git a/packages/apps/nats/templates/nats.yaml b/packages/apps/nats/templates/nats.yaml index 4f52ff11..e5f5cf5d 100644 --- a/packages/apps/nats/templates/nats.yaml +++ b/packages/apps/nats/templates/nats.yaml @@ -95,10 +95,6 @@ spec: {{- with .Values.storageClass }} storageClassName: {{ . }} {{- end }} - promExporter: - enabled: true - podMonitor: - enabled: true {{- if .Values.external }} service: merge: diff --git a/packages/system/monitoring/dashboards.list b/packages/system/monitoring/dashboards.list index 1f4b2eea..6ad8630f 100644 --- a/packages/system/monitoring/dashboards.list +++ b/packages/system/monitoring/dashboards.list @@ -43,3 +43,5 @@ hubble/overview hubble/dns-namespace hubble/l7-http-metrics hubble/network-overview +nats/nats-jetstream +nats/nats-server diff --git a/packages/system/nats/charts/nats/Chart.yaml b/packages/system/nats/charts/nats/Chart.yaml index e59601a9..2f0edf48 100644 --- a/packages/system/nats/charts/nats/Chart.yaml +++ b/packages/system/nats/charts/nats/Chart.yaml @@ -1,5 +1,5 @@ apiVersion: v2 -appVersion: 2.10.17 +appVersion: 2.11.8 description: A Helm chart for the NATS.io High Speed Cloud Native Distributed Communications Technology. home: http://github.com/nats-io/k8s @@ -13,4 +13,4 @@ maintainers: name: The NATS Authors url: https://github.com/nats-io name: nats -version: 1.2.1 +version: 1.3.13 diff --git a/packages/system/nats/charts/nats/README.md b/packages/system/nats/charts/nats/README.md index 0916999d..0096a1bb 100644 --- a/packages/system/nats/charts/nats/README.md +++ b/packages/system/nats/charts/nats/README.md @@ -44,7 +44,7 @@ Everything in the NATS Config or Kubernetes Resources can be overridden by `merg | `container` | nats [k8s Container](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#container-v1-core) | yes | | `reloader` | config reloader [k8s Container](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#container-v1-core) | yes | | `promExporter` | prometheus exporter [k8s Container](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#container-v1-core) | no | -| `promExporter.podMonitor` | [prometheus PodMonitor](https://prometheus-operator.dev/docs/operator/api/#monitoring.coreos.com/v1.PodMonitor) | no | +| `promExporter.podMonitor` | [prometheus PodMonitor](https://prometheus-operator.dev/docs/api-reference/api/#monitoring.coreos.com/v1.PodMonitor) | no | | `service` | [k8s Service](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#service-v1-core) | yes | | `statefulSet` | [k8s StatefulSet](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#statefulset-v1-apps) | yes | | `podTemplate` | [k8s PodTemplate](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#pod-v1-core) | yes | @@ -60,7 +60,7 @@ Everything in the NATS Config or Kubernetes Resources can be overridden by `merg ### Merge -Merging is performed using the Helm `merge` function. Example - add NATS accounts and container resources: +Merging is performed using the Helm [`merge` function](https://helm.sh/docs/chart_template_guide/function_list/#merge-mustmerge). Example - add NATS accounts and container resources: ```yaml config: @@ -119,14 +119,22 @@ podTemplate: ### NATS Container Resources +We recommend setting both **requests and limits** - for both **CPU and memory** - **to the same value** for the following reasons: + +* It ensures your NATS pod has [predictable performance](https://www.datadoghq.com/blog/kubernetes-cpu-requests-limits/#predictability:~:text=If%20containers%20are,available%20capacity%20decreases.). +* The NATS server [automatically sets](https://github.com/nats-io/nats-server/blob/v2.11.0/main.go#L131-L132) [GOMAXPROCS](https://github.com/golang/go/blob/go1.24.1/src/runtime/extern.go#L230-L234) to the number of CPU cores defined in the `limits` section. If `limits` are not set, GOMAXPROCS defaults to the node's physical core count, which can lead to [poor performance](https://github.com/golang/go/issues/33803). +* The pod will be assigned to the ["Guaranteed" QoS class](https://kubernetes.io/docs/concepts/workloads/pods/pod-qos/#guaranteed), making it less likely to be evicted when node resources are constrained. + +Deviate from this recommendation only if you fully understand the implications of your settings. + ```yaml container: env: - # different from k8s units, suffix must be B, KiB, MiB, GiB, or TiB - # should be ~90% of memory limit - GOMEMLIMIT: 7GiB + # Different from k8s units, suffix must be B, KiB, MiB, GiB, or TiB + # Should be ~80% of memory limit + GOMEMLIMIT: 6GiB merge: - # recommended limit is at least 2 CPU cores and 8Gi Memory for production JetStream clusters + # Recommended minimum: at least 2 CPU cores and 8Gi memory for production JetStream clusters resources: requests: cpu: "2" @@ -138,11 +146,27 @@ container: ### Specify Image Version -```yaml -container: - image: - tag: x.y.z-alpine -``` +The container image can now be overridden by specifying either the image tag, an image digest, or a full image name. Examples below illustrate the options: + +- To set the tag: + ```yaml + container: + image: + tag: x.y.z-alpine + ``` +- To use an image digest, which overrides the tag: + ```yaml + container: + image: + repository: nats + digest: sha256:abcdef1234567890... + ``` +- To override the registry, repository, tag, and digest all at once, specify a full image name: + ```yaml + container: + image: + fullImageName: custom-reg.io/myimage@sha256:abcdef1234567890... + ``` ### Operator Mode with NATS Resolver diff --git a/packages/system/nats/charts/nats/files/stateful-set/pod-template.yaml b/packages/system/nats/charts/nats/files/stateful-set/pod-template.yaml index bb1d8d7b..9832ba34 100644 --- a/packages/system/nats/charts/nats/files/stateful-set/pod-template.yaml +++ b/packages/system/nats/charts/nats/files/stateful-set/pod-template.yaml @@ -69,3 +69,7 @@ spec: - {{ merge (dict "topologyKey" $k "labelSelector" (dict "matchLabels" (include "nats.selectorLabels" $ | fromYaml))) $v | toYaml | nindent 4 }} {{- end }} {{- end}} + + # terminationGracePeriodSeconds determines how long to wait for graceful shutdown + # this should be at least `lameDuckGracePeriod` + 20s shutdown overhead + terminationGracePeriodSeconds: 60 diff --git a/packages/system/nats/charts/nats/files/stateful-set/prom-exporter-container.yaml b/packages/system/nats/charts/nats/files/stateful-set/prom-exporter-container.yaml index c3e1b6fb..75f8a77c 100644 --- a/packages/system/nats/charts/nats/files/stateful-set/prom-exporter-container.yaml +++ b/packages/system/nats/charts/nats/files/stateful-set/prom-exporter-container.yaml @@ -27,4 +27,5 @@ args: {{- if .Values.config.gateway.enabled }} - -gatewayz {{- end }} -- http://localhost:{{ .Values.config.monitor.port }}/ +{{- $monitorProto := ternary "https" "http" .Values.config.monitor.tls.enabled }} +- {{ $monitorProto }}://{{ .Values.promExporter.monitorDomain }}:{{ .Values.config.monitor.port }}/ diff --git a/packages/system/nats/charts/nats/templates/_helpers.tpl b/packages/system/nats/charts/nats/templates/_helpers.tpl index ba831397..d8485943 100644 --- a/packages/system/nats/charts/nats/templates/_helpers.tpl +++ b/packages/system/nats/charts/nats/templates/_helpers.tpl @@ -147,10 +147,18 @@ app.kubernetes.io/component: nats-box Print the image */}} {{- define "nats.image" }} -{{- $image := printf "%s:%s" .repository .tag }} +{{- $image := "" }} +{{- if .digest }} +{{- $image = printf "%s@%s" .repository .digest }} +{{- else }} +{{- $image = printf "%s:%s" .repository .tag }} +{{- end }} {{- if or .registry .global.image.registry }} {{- $image = printf "%s/%s" (.registry | default .global.image.registry) $image }} -{{- end -}} +{{- end }} +{{- if .fullImageName }} +{{- $image = .fullImageName }} +{{- end }} image: {{ $image }} {{- if or .pullPolicy .global.image.pullPolicy }} imagePullPolicy: {{ .pullPolicy | default .global.image.pullPolicy }} @@ -274,7 +282,7 @@ output: string with following format rules */}} {{- define "nats.formatConfig" -}} {{- - (regexReplaceAll "\"<<\\s+(.*)\\s+>>\"" + (regexReplaceAll "\"<<\\s+(.*?)\\s+>>\"" (regexReplaceAll "\".*\\$include\": \"(.*)\",?" (include "toPrettyRawJson" .) "include ${1};") "${1}") -}} diff --git a/packages/system/nats/charts/nats/values.yaml b/packages/system/nats/charts/nats/values.yaml index 0b14ebd4..fa402f18 100644 --- a/packages/system/nats/charts/nats/values.yaml +++ b/packages/system/nats/charts/nats/values.yaml @@ -238,6 +238,7 @@ config: tls: # config.nats.tls must be enabled also # when enabled, monitoring port will use HTTPS with the options from config.nats.tls + # if promExporter is also enabled, consider setting promExporter.monitorDomain enabled: false profiling: @@ -312,9 +313,13 @@ config: container: image: repository: nats - tag: 2.10.17-alpine + tag: 2.11.8-alpine pullPolicy: registry: + # if digest is provided, it overrides tag (example: "sha256:abcdef1234567890") + digest: + # if fullImageName is provided, it overrides registry, repository, tag, and digest + fullImageName: # container port options # must be enabled in the config section also @@ -353,9 +358,11 @@ reloader: enabled: true image: repository: natsio/nats-server-config-reloader - tag: 0.15.0 + tag: 0.19.1 pullPolicy: registry: + digest: + fullImageName: # env var map, see nats.env for an example env: {} @@ -363,7 +370,7 @@ reloader: # all nats container volume mounts with the following prefixes # will be mounted into the reloader container natsVolumeMountPrefixes: - - /etc/ + - /etc/ # merge or patch the container # https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#container-v1-core @@ -378,11 +385,16 @@ promExporter: enabled: false image: repository: natsio/prometheus-nats-exporter - tag: 0.15.0 + tag: 0.17.3 pullPolicy: registry: + digest: + fullImageName: port: 7777 + # if config.monitor.tls.enabled is set to true, monitorDomain must be set to the common name + # or a SAN used in the tls certificate + monitorDomain: localhost # env var map, see nats.env for an example env: {} @@ -398,13 +410,12 @@ promExporter: enabled: false # merge or patch the pod monitor - # https://prometheus-operator.dev/docs/operator/api/#monitoring.coreos.com/v1.PodMonitor + # https://prometheus-operator.dev/docs/api-reference/api/#monitoring.coreos.com/v1.PodMonitor merge: {} patch: [] # defaults to "{{ include "nats.fullname" $ }}" name: - ############################################################ # service ############################################################ @@ -511,7 +522,6 @@ serviceAccount: # defaults to "{{ include "nats.fullname" $ }}" name: - ############################################################ # natsBox # @@ -564,9 +574,11 @@ natsBox: container: image: repository: natsio/nats-box - tag: 0.14.3 + tag: 0.18.0 pullPolicy: registry: + digest: + fullImageName: # env var map, see nats.env for an example env: {} @@ -624,7 +636,6 @@ natsBox: # defaults to "{{ include "nats.fullname" $ }}-box" name: - ################################################################################ # Extra user-defined resources ################################################################################ diff --git a/packages/system/nats/values.yaml b/packages/system/nats/values.yaml index a28cadbe..87f730dd 100644 --- a/packages/system/nats/values.yaml +++ b/packages/system/nats/values.yaml @@ -9,3 +9,7 @@ nats: cluster: routeURLs: k8sClusterDomain: cozy.local + promExporter: + enabled: true + podMonitor: + enabled: true From e7ffc21743148afb55b991b0194128163bfa114b Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Wed, 18 Feb 2026 01:23:02 +0300 Subject: [PATCH 102/666] feat(harbor): switch registry storage to S3 via COSI BucketClaim Replace PVC-based registry storage with S3 via COSI BucketClaim/BucketAccess. The system chart parses BucketInfo secret and creates a registry-s3 Secret with REGISTRY_STORAGE_S3_* env vars that override Harbor's ConfigMap values. - Add bucket-secret.yaml to system chart (BucketInfo parser) - Remove storageType/size from registry config (S3 is now the only option) - Use Harbor's existingSecret support for S3 credentials injection - Add objectstorage-controller to PackageSource dependencies - Update E2E test with COSI bucket provisioning waits and diagnostics Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- hack/e2e-apps/harbor.bats | 15 ++++++++-- packages/apps/harbor/README.md | 2 -- packages/apps/harbor/templates/bucket.yaml | 2 -- packages/apps/harbor/templates/harbor.yaml | 30 +++---------------- packages/apps/harbor/values.schema.json | 26 ---------------- packages/apps/harbor/values.yaml | 8 ----- .../platform/sources/harbor-application.yaml | 1 + packages/system/harbor-rd/cozyrds/harbor.yaml | 4 +-- .../harbor/templates/bucket-secret.yaml | 20 +++++++++++++ packages/system/harbor/values.yaml | 2 ++ 10 files changed, 42 insertions(+), 68 deletions(-) create mode 100644 packages/system/harbor/templates/bucket-secret.yaml diff --git a/hack/e2e-apps/harbor.bats b/hack/e2e-apps/harbor.bats index 076a1841..09215546 100644 --- a/hack/e2e-apps/harbor.bats +++ b/hack/e2e-apps/harbor.bats @@ -16,8 +16,6 @@ spec: resources: {} resourcesPreset: "nano" registry: - storageType: "pvc" - size: 5Gi resources: {} resourcesPreset: "nano" jobservice: @@ -37,6 +35,13 @@ spec: EOF sleep 5 kubectl -n tenant-test wait hr $release --timeout=60s --for=condition=ready + + # Wait for COSI to provision bucket + kubectl -n tenant-test wait bucketclaims.objectstorage.k8s.io $release-registry \ + --timeout=300s --for=jsonpath='{.status.bucketReady}'=true + kubectl -n tenant-test wait bucketaccesses.objectstorage.k8s.io $release-registry \ + --timeout=60s --for=jsonpath='{.status.accessGranted}'=true + kubectl -n tenant-test wait hr $release-system --timeout=600s --for=condition=ready || { echo "=== HelmRelease status ===" kubectl -n tenant-test get hr $release-system -o yaml 2>&1 || true @@ -46,6 +51,12 @@ EOF kubectl -n tenant-test get events --sort-by='.lastTimestamp' 2>&1 | tail -30 || true echo "=== ExternalArtifact ===" kubectl -n cozy-system get externalartifact cozystack-harbor-application-default-harbor-system -o yaml 2>&1 || true + echo "=== BucketClaim status ===" + kubectl -n tenant-test get bucketclaims.objectstorage.k8s.io $release-registry -o yaml 2>&1 || true + echo "=== BucketAccess status ===" + kubectl -n tenant-test get bucketaccesses.objectstorage.k8s.io $release-registry -o yaml 2>&1 || true + echo "=== BucketAccess Secret ===" + kubectl -n tenant-test get secret $release-registry-bucket -o jsonpath='{.data.BucketInfo}' 2>&1 | base64 -d 2>&1 || true false } kubectl -n tenant-test wait deploy $release-core --timeout=120s --for=condition=available diff --git a/packages/apps/harbor/README.md b/packages/apps/harbor/README.md index 3c870be3..df44ca8d 100644 --- a/packages/apps/harbor/README.md +++ b/packages/apps/harbor/README.md @@ -22,8 +22,6 @@ Harbor is an open source trusted cloud native registry project that stores, sign | `core.resources.memory` | Amount of memory allocated. | `quantity` | `""` | | `core.resourcesPreset` | Default sizing preset used when `resources` is omitted. | `string` | `small` | | `registry` | Container image registry configuration. | `object` | `{}` | -| `registry.storageType` | Storage backend type for images. | `string` | `pvc` | -| `registry.size` | Persistent Volume size (only used when storageType is pvc). | `quantity` | `10Gi` | | `registry.resources` | Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied. | `object` | `{}` | | `registry.resources.cpu` | Number of CPU cores allocated. | `quantity` | `""` | | `registry.resources.memory` | Amount of memory allocated. | `quantity` | `""` | diff --git a/packages/apps/harbor/templates/bucket.yaml b/packages/apps/harbor/templates/bucket.yaml index 885cbfc8..a9e60988 100644 --- a/packages/apps/harbor/templates/bucket.yaml +++ b/packages/apps/harbor/templates/bucket.yaml @@ -1,4 +1,3 @@ -{{- if eq .Values.registry.storageType "s3" }} {{- $seaweedfs := .Values._namespace.seaweedfs }} apiVersion: objectstorage.k8s.io/v1alpha1 kind: BucketClaim @@ -18,4 +17,3 @@ spec: bucketClaimName: {{ .Release.Name }}-registry credentialsSecretName: {{ .Release.Name }}-registry-bucket protocol: s3 -{{- end }} diff --git a/packages/apps/harbor/templates/harbor.yaml b/packages/apps/harbor/templates/harbor.yaml index 0e9f4fd5..3ba09bfb 100644 --- a/packages/apps/harbor/templates/harbor.yaml +++ b/packages/apps/harbor/templates/harbor.yaml @@ -66,21 +66,9 @@ spec: name: {{ .Release.Name }}-credentials valuesKey: redis-password targetPath: harbor.redis.external.password - {{- if eq .Values.registry.storageType "s3" }} - - kind: Secret - name: {{ .Release.Name }}-registry-bucket - valuesKey: accessKey - targetPath: harbor.persistence.imageChartStorage.s3.accesskey - - kind: Secret - name: {{ .Release.Name }}-registry-bucket - valuesKey: secretKey - targetPath: harbor.persistence.imageChartStorage.s3.secretkey - - kind: Secret - name: {{ .Release.Name }}-registry-bucket - valuesKey: endpoint - targetPath: harbor.persistence.imageChartStorage.s3.regionendpoint - {{- end }} values: + bucket: + secretName: {{ .Release.Name }}-registry-bucket db: replicas: {{ .Values.database.replicas }} size: {{ .Values.database.size }} @@ -106,31 +94,21 @@ spec: persistence: enabled: true resourcePolicy: "keep" - {{- if eq .Values.registry.storageType "s3" }} imageChartStorage: type: s3 s3: + existingSecret: {{ .Release.Name }}-registry-s3 region: us-east-1 bucket: {{ .Release.Name }}-registry secure: false v4auth: true - {{- end }} - {{- if or (eq .Values.registry.storageType "pvc") .Values.trivy.enabled }} + {{- if .Values.trivy.enabled }} persistentVolumeClaim: - {{- if eq .Values.registry.storageType "pvc" }} - registry: - size: {{ .Values.registry.size }} - {{- with .Values.storageClass }} - storageClass: {{ . }} - {{- end }} - {{- end }} - {{- if .Values.trivy.enabled }} trivy: size: {{ .Values.trivy.size }} {{- with .Values.storageClass }} storageClass: {{ . }} {{- end }} - {{- end }} {{- end }} portal: resources: {{- include "cozy-lib.resources.defaultingSanitize" (list "nano" (dict) $) | nindent 10 }} diff --git a/packages/apps/harbor/values.schema.json b/packages/apps/harbor/values.schema.json index 71cc1d98..1352f5dc 100644 --- a/packages/apps/harbor/values.schema.json +++ b/packages/apps/harbor/values.schema.json @@ -179,9 +179,6 @@ "description": "Container image registry configuration.", "type": "object", "default": {}, - "required": [ - "storageType" - ], "properties": { "resources": { "description": "Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.", @@ -229,29 +226,6 @@ "xlarge", "2xlarge" ] - }, - "size": { - "description": "Persistent Volume size (only used when storageType is pvc).", - "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 - }, - "storageType": { - "description": "Storage backend type for images.", - "type": "string", - "default": "pvc", - "enum": [ - "s3", - "pvc" - ] } } }, diff --git a/packages/apps/harbor/values.yaml b/packages/apps/harbor/values.yaml index f1131bd1..de44eaff 100644 --- a/packages/apps/harbor/values.yaml +++ b/packages/apps/harbor/values.yaml @@ -34,20 +34,12 @@ core: resources: {} resourcesPreset: "small" -## @enum {string} StorageType - Storage backend for container image registry. -## @value s3 - S3-compatible object storage via COSI BucketClaim (requires SeaweedFS in tenant). -## @value pvc - Persistent Volume Claim. - ## @typedef {struct} Registry - Container image registry configuration. -## @field {StorageType} storageType - Storage backend type for images. -## @field {quantity} [size] - Persistent Volume size (only used when storageType is pvc). ## @field {Resources} [resources] - Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied. ## @field {ResourcesPreset} [resourcesPreset] - Default sizing preset used when `resources` is omitted. ## @param {Registry} registry - Container image registry configuration. registry: - storageType: "pvc" - size: 10Gi resources: {} resourcesPreset: "small" diff --git a/packages/core/platform/sources/harbor-application.yaml b/packages/core/platform/sources/harbor-application.yaml index b9bc8dc3..c2773bdd 100644 --- a/packages/core/platform/sources/harbor-application.yaml +++ b/packages/core/platform/sources/harbor-application.yaml @@ -15,6 +15,7 @@ spec: - cozystack.networking - cozystack.postgres-operator - cozystack.redis-operator + - cozystack.objectstorage-controller libraries: - name: cozy-lib path: library/cozy-lib diff --git a/packages/system/harbor-rd/cozyrds/harbor.yaml b/packages/system/harbor-rd/cozyrds/harbor.yaml index 80924530..0112dc11 100644 --- a/packages/system/harbor-rd/cozyrds/harbor.yaml +++ b/packages/system/harbor-rd/cozyrds/harbor.yaml @@ -8,7 +8,7 @@ spec: singular: harbor plural: harbors openAPISchema: |- - {"title":"Chart Values","type":"object","properties":{"core":{"description":"Core API server configuration.","type":"object","default":{},"properties":{"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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.","type":"string","default":"small","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]}}},"database":{"description":"PostgreSQL database configuration.","type":"object","default":{},"required":["replicas","size"],"properties":{"replicas":{"description":"Number of database instances.","type":"integer","default":2},"size":{"description":"Persistent Volume size for database storage.","default":"5Gi","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}}},"host":{"description":"Hostname for external access to Harbor (defaults to 'harbor' subdomain for the tenant host).","type":"string","default":""},"jobservice":{"description":"Background job service configuration.","type":"object","default":{},"properties":{"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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.","type":"string","default":"nano","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]}}},"redis":{"description":"Redis cache configuration.","type":"object","default":{},"required":["replicas","size"],"properties":{"replicas":{"description":"Number of Redis replicas.","type":"integer","default":2},"size":{"description":"Persistent Volume size for cache storage.","default":"1Gi","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}}},"registry":{"description":"Container image registry configuration.","type":"object","default":{},"required":["storageType"],"properties":{"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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.","type":"string","default":"small","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]},"size":{"description":"Persistent Volume size (only used when storageType is pvc).","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},"storageType":{"description":"Storage backend type for images.","type":"string","default":"pvc","enum":["s3","pvc"]}}},"storageClass":{"description":"StorageClass used to store the data.","type":"string","default":""},"trivy":{"description":"Trivy vulnerability scanner configuration.","type":"object","default":{},"required":["enabled","size"],"properties":{"enabled":{"description":"Enable or disable the vulnerability scanner.","type":"boolean","default":true},"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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.","type":"string","default":"nano","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]},"size":{"description":"Persistent Volume size for vulnerability database cache.","default":"5Gi","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}}}}} + {"title":"Chart Values","type":"object","properties":{"core":{"description":"Core API server configuration.","type":"object","default":{},"properties":{"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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.","type":"string","default":"small","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]}}},"database":{"description":"PostgreSQL database configuration.","type":"object","default":{},"required":["replicas","size"],"properties":{"replicas":{"description":"Number of database instances.","type":"integer","default":2},"size":{"description":"Persistent Volume size for database storage.","default":"5Gi","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}}},"host":{"description":"Hostname for external access to Harbor (defaults to 'harbor' subdomain for the tenant host).","type":"string","default":""},"jobservice":{"description":"Background job service configuration.","type":"object","default":{},"properties":{"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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.","type":"string","default":"nano","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]}}},"redis":{"description":"Redis cache configuration.","type":"object","default":{},"required":["replicas","size"],"properties":{"replicas":{"description":"Number of Redis replicas.","type":"integer","default":2},"size":{"description":"Persistent Volume size for cache storage.","default":"1Gi","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}}},"registry":{"description":"Container image registry configuration.","type":"object","default":{},"properties":{"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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.","type":"string","default":"small","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]}}},"storageClass":{"description":"StorageClass used to store the data.","type":"string","default":""},"trivy":{"description":"Trivy vulnerability scanner configuration.","type":"object","default":{},"required":["enabled","size"],"properties":{"enabled":{"description":"Enable or disable the vulnerability scanner.","type":"boolean","default":true},"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"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":"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.","type":"string","default":"nano","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]},"size":{"description":"Persistent Volume size for vulnerability database cache.","default":"5Gi","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}}}}} release: prefix: "harbor-" labels: @@ -29,7 +29,7 @@ spec: - registry - container icon: "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHJvbGU9ImltZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHZpZXdCb3g9IjEuMDUgLTEuOTUgMzU5LjQxIDM2MS42NiI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXItZ3JhZGllbnQiIHgxPSIyNjQuNzkiIHgyPSIyNjcuMjciIHkxPSI5NTIuMzkiIHkyPSI5NTIuMzkiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMzAuNDMgMCAwIC0zMC40MyAtNzk1NS4yMiAyOTI4NS43NSkiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiM2MGI5MzIiLz48c3RvcCBvZmZzZXQ9Ii4yOCIgc3RvcC1jb2xvcj0iIzYwYjkzMiIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzM2N2MzNCIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXItZ3JhZGllbnQtMiIgeDE9IjI2My43NyIgeDI9IjI2Ni4yNiIgeTE9Ijk1NS42NSIgeTI9Ijk1NS42NSIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgyNy4yMSAwIDAgLTI3LjIxIC03MDczLjg1IDI2MTY5LjQxKSIgeGxpbms6aHJlZj0iI2xpbmVhci1ncmFkaWVudCIvPjxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyLWdyYWRpZW50LTMiIHgxPSIyNjMuMjgiIHgyPSIyNjUuNzYiIHkxPSI5NTMuNzQiIHkyPSI5NTMuNzQiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMjUuNzUgMCAwIC0yNS43NSAtNjY3MS4xMyAyNDgxMi4yMykiIHhsaW5rOmhyZWY9IiNsaW5lYXItZ3JhZGllbnQiLz48bGluZWFyR3JhZGllbnQgaWQ9ImxpbmVhci1ncmFkaWVudC00IiB4MT0iMjYzLjc3IiB4Mj0iMjY2LjI1IiB5MT0iOTUzLjIiIHkyPSI5NTMuMiIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgyNy4xIDAgMCAtMjcuMSAtNzA0MC45IDI2MTAyLjQ5KSIgeGxpbms6aHJlZj0iI2xpbmVhci1ncmFkaWVudCIvPjxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyLWdyYWRpZW50LTUiIHgxPSIyNjIuNzMiIHgyPSIyNjUuMjEiIHkxPSI5NTQuMzQiIHkyPSI5NTQuMzQiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMjQuNCAwIDAgLTI0LjQgLTYzMDEuMzYgMjM1MjEuOTcpIiB4bGluazpocmVmPSIjbGluZWFyLWdyYWRpZW50Ii8+PGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXItZ3JhZGllbnQtNiIgeDE9IjI3Mi4xNCIgeDI9IjI3NC42MiIgeTE9Ijk1NS4xNSIgeTI9Ijk1NS4xNSIgZ3JhZGllbnRUcmFuc2Zvcm09InNjYWxlKDY2LjA5IC02Ni4wOSkgcm90YXRlKDM2LjUyIDE1ODguMTUzIDY4LjE0OCkiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiM0NTk2ZDgiLz48c3RvcCBvZmZzZXQ9Ii4yIiBzdG9wLWNvbG9yPSIjNDU5NmQ4Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjZmZmIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9ImxpbmVhci1ncmFkaWVudC03IiB4MT0iMjcwLjY1IiB4Mj0iMjczLjEzIiB5MT0iOTUyLjM4IiB5Mj0iOTUyLjM4IiBncmFkaWVudFRyYW5zZm9ybT0ic2NhbGUoNzcuOCAtNzcuOCkgcm90YXRlKC0xMS41NCAtNDU4Ny4yMDkgMTgwMy4zMjMpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjNDE5NGQ3Ii8+PHN0b3Agb2Zmc2V0PSIuMiIgc3RvcC1jb2xvcj0iIzQxOTRkNyIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI2ZmZiIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXItZ3JhZGllbnQtOCIgeDE9IjI3MC45NyIgeDI9IjI3My40NSIgeTE9Ijk1My43NSIgeTI9Ijk1My43NSIgZ3JhZGllbnRUcmFuc2Zvcm09InNjYWxlKDcxLjM1IC03MS4zNSkgcm90YXRlKDEwLjIzIDU0NzcuMzcgLTEwMjQuNjAyKSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzQxOTRkNyIvPjxzdG9wIG9mZnNldD0iLjMzIiBzdG9wLWNvbG9yPSIjNDQ5NmQ4Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjZmZmIi8+PC9saW5lYXJHcmFkaWVudD48Y2xpcFBhdGggaWQ9ImNsaXAtcGF0aCI+PHBhdGggZD0iTTE3NC40IDMwMi41MmwtNjguNjUtMzAuMjYtMy44MyA0My4yMSA3NS41IDIzLjk4LTMuMDItMzYuOTN6IiBjbGFzcz0iY2xzLTEiLz48L2NsaXBQYXRoPjxjbGlwUGF0aCBpZD0iY2xpcC1wYXRoLTIiPjxwYXRoIGQ9Ik0xMTkuMTkgMTM1LjM4aDM4LjU1djMyLjg5aC05LjE1di0xNGExMC4xMyAxMC4xMyAwIDEgMC0yMC4yNiAwdjE0aC05LjE1em0tNy43IDcybDU3LjIgMjUuMjEtMy45NC00OC4yNGg3LjQ5di0xNi4wOGgtNS41NXYtMzIuODloNS41NXYtOS40NWwtMzAuODYtMzAuMTl2LTIuMTJhMi45MSAyLjkxIDAgMCAwLTUuODIgMHYyLjEybC0zMC44NiAzMC4xOXY5LjQ1aDUuNTZ2MzIuODloLTUuNTZ2MTYuMTJoOC44MnoiIGNsYXNzPSJjbHMtMSIvPjwvY2xpcFBhdGg+PGNsaXBQYXRoIGlkPSJjbGlwLXBhdGgtMyI+PHBhdGggZD0iTTEwOC4xNCAyNDUuMjhsNjMuODggMjguMTYtLjk2LTExLjczLTYxLjk2LTI3LjMtLjk2IDEwLjg3eiIgY2xhc3M9ImNscy0xIi8+PC9jbGlwUGF0aD48Y2xpcFBhdGggaWQ9ImNsaXAtcGF0aC00Ij48cGF0aCBkPSJNMTA2LjQ2IDI2NC4yMWw2Ny4yMyAyOS42My0uOTYtMTEuNzItNjUuMzEtMjguNzgtLjk2IDEwLjg3eiIgY2xhc3M9ImNscy0xIi8+PC9jbGlwUGF0aD48Y2xpcFBhdGggaWQ9ImNsaXAtcGF0aC01Ij48cGF0aCBkPSJNMTEwLjc3IDIxNS40OGwtLjk2IDEwLjg3IDYwLjU0IDI2LjY4LS45NS0xMS43Mi01OC42My0yNS44M3oiIGNsYXNzPSJjbHMtMSIvPjwvY2xpcFBhdGg+PGNsaXBQYXRoIGlkPSJjbGlwLXBhdGgtNiI+PHBhdGggZD0iTTMxMy4xMyA2Ny41OWExNzUuMzEgMTc1LjMxIDAgMCAwLTI5Ljc1LTI4LjEzYy0xLjU3LTEuMTctMy4xOC0yLjMtNC43OS0zLjQyTDI1NiA1OS41bC04Mi4zNCA4NS42MyAxMTMuNDEtNTguNjggMjkuMDctMTVjLTEuMDEtMS4zMS0xLjk4LTIuNjItMy4wMS0zLjg2eiIgY2xhc3M9ImNscy0xIi8+PC9jbGlwUGF0aD48Y2xpcFBhdGggaWQ9ImNsaXAtcGF0aC03Ij48cGF0aCBkPSJNMzUzLjU5IDE3Ny42MWMwLTItLjE0LTQtLjIyLTUuOTNsLTMyLjIxLTIuMzEtMTQ3LjQ3LTEwLjU4TDMxOC4zNiAyMDlsMzAuNDEgMTAuNTVjLjA5LS4zNi4xOS0uNzEuMjgtMS4wOGExNzMuNjUgMTczLjY1IDAgMCAwIDQuNTctMzkuNDd2LTEuMzd6IiBjbGFzcz0iY2xzLTEiLz48L2NsaXBQYXRoPjxjbGlwUGF0aCBpZD0iY2xpcC1wYXRoLTgiPjxwYXRoIGQ9Ik0zNDguODQgMTM4LjYxYTE3Mi41NSAxNzIuNTUgMCAwIDAtMTMuODQtMzcuN2wtMzAuOTQgOS45NEwxNzUuOTIgMTUybDE0Mi00LjgzIDMyLjUtMS4xMWMtLjUtMi40OC0uOTktNC45OC0xLjU4LTcuNDV6IiBjbGFzcz0iY2xzLTEiLz48L2NsaXBQYXRoPjxzdHlsZT4uY2xzLTF7ZmlsbDpub25lfS5jbHMtMTN7ZmlsbDojNjk2NTY2fTwvc3R5bGU+PC9kZWZzPjxnIGlkPSJnMTIiPjxwYXRoIGlkPSJwYXRoMTQiIGZpbGw9IiNmZmYiIGQ9Ik0zMC44OSAxNzlhMTQ4Ljg3IDE0OC44NyAwIDEgMSAxNDguODcgMTQ4Ljg1QTE0OC44NyAxNDguODcgMCAwIDEgMzAuODkgMTc5Ii8+PGcgaWQ9ImczMCIgY2xpcC1wYXRoPSJ1cmwoI2NsaXAtcGF0aCkiPjxnIGlkPSJnMzIiPjxwYXRoIGlkPSJwYXRoNDYiIGZpbGw9InVybCgjbGluZWFyLWdyYWRpZW50KSIgZD0iTTE3NC40IDMwMi41MmwtNjguNjUtMzAuMjUtMy44MiA0My4yIDc1LjUgMjQtMy0zNi45MyIvPjwvZz48L2c+PGcgaWQ9Imc0OCIgY2xpcC1wYXRoPSJ1cmwoI2NsaXAtcGF0aC0yKSI+PGcgaWQ9Imc1MCI+PHBhdGggaWQ9InBhdGg2NCIgZmlsbD0idXJsKCNsaW5lYXItZ3JhZGllbnQtMikiIGQ9Ik0xMTkuMTkgMTM1LjM4aDM4LjU1djMyLjg5aC05LjE1di0xNGExMC4xMyAxMC4xMyAwIDEgMC0yMC4yNiAwdjE0aC05LjE1em0tNy43IDcybDU3LjIgMjUuMjEtMy45NC00OC4yNGg3LjQ5di0xNi4wOGgtNS41NXYtMzIuODloNS41NXYtOS40NWwtMzAuODYtMzAuMTl2LTIuMTJhMi45MSAyLjkxIDAgMCAwLTUuODIgMHYyLjEybC0zMC44NiAzMC4xOXY5LjQ1aDUuNTZ2MzIuODloLTUuNTZ2MTYuMTJoOC44MmwtMiAyMyIvPjwvZz48L2c+PGcgaWQ9Imc2NiIgY2xpcC1wYXRoPSJ1cmwoI2NsaXAtcGF0aC0zKSI+PGcgaWQ9Imc2OCI+PHBhdGggaWQ9InBhdGg4MiIgZmlsbD0idXJsKCNsaW5lYXItZ3JhZGllbnQtMykiIGQ9Ik0xMDguMTMgMjQ1LjI4TDE3MiAyNzMuNDRsLTEtMTEuNzItNjItMjcuMzEtMSAxMC44NyIvPjwvZz48L2c+PGcgaWQ9Imc4NCIgY2xpcC1wYXRoPSJ1cmwoI2NsaXAtcGF0aC00KSI+PGcgaWQ9Imc4NiI+PHBhdGggaWQ9InBhdGgxMDAiIGZpbGw9InVybCgjbGluZWFyLWdyYWRpZW50LTQpIiBkPSJNMTA2LjQ2IDI2NC4yMWw2Ny4yMyAyOS42My0xLTExLjcyLTY1LjMxLTI4Ljc4LTEgMTAuODciLz48L2c+PC9nPjxnIGlkPSJnMTAyIiBjbGlwLXBhdGg9InVybCgjY2xpcC1wYXRoLTUpIj48ZyBpZD0iZzEwNCI+PHBhdGggaWQ9InBhdGgxMTgiIGZpbGw9InVybCgjbGluZWFyLWdyYWRpZW50LTUpIiBkPSJNMTEwLjc3IDIxNS40OGwtMSAxMC44OEwxNzAuMzUgMjUzbC0xLTExLjcyLTU4LjYyLTI1LjgzIi8+PC9nPjwvZz48cGF0aCBpZD0icGF0aDEyMCIgZD0iTTMwNC4wNyAxMTAuODVsMzAuOTMtOS45NGMtLjExLS4yMi0uMjEtLjQ1LS4zMi0uNjZhMTc0LjQxIDE3NC40MSAwIDAgMC0xOC41NS0yOC44M2wtMjkuMDcgMTVhMTQyLjcxIDE0Mi43MSAwIDAgMSAxNi43MyAyMy44N2MuMS4xNy4xOC4zNS4yNy41MyIgY2xhc3M9ImNscy0xMyIvPjxwYXRoIGlkPSJwYXRoMTIyIiBkPSJNMzIxLjE1IDE2OS4zN2wzMi4yMSAyLjMxYTE3Mi44NiAxNzIuODYgMCAwIDAtMy0yNS41OWwtMzIuNSAxLjExYTE0MSAxNDEgMCAwIDEgMy4yNSAyMi4xNyIgY2xhc3M9ImNscy0xMyIvPjxwYXRoIGlkPSJwYXRoMTI0IiBkPSJNMTgyIDMyMC44MWMtNzguMiAwLTE0MS44My02My42Mi0xNDEuODMtMTQxLjgyUzEwMy44MiAzNy4xNiAxODIgMzcuMTZhMTQwLjkzIDE0MC45MyAwIDAgMSA3Ni4zIDIyLjM0TDI4MC44NSAzNkExNzIuODYgMTcyLjg2IDAgMCAwIDE4MiA1LjExQzg2LjE1IDUuMTEgOC4xNSA4My4xMSA4LjE1IDE3OXM3OCAxNzMuODUgMTczLjg1IDE3My44NWM4MS45IDAgMTUwLjY5LTU3IDE2OS0xMzMuMzJMMzIwLjYyIDIwOWMtMTMuOCA2My44NC03MC42OSAxMTEuODMtMTM4LjYgMTExLjgzIiBjbGFzcz0iY2xzLTEzIi8+PGcgaWQ9ImcxMjYiIGNsaXAtcGF0aD0idXJsKCNjbGlwLXBhdGgtNikiPjxnIGlkPSJnMTI4Ij48cGF0aCBpZD0icGF0aDE0MiIgZmlsbD0idXJsKCNsaW5lYXItZ3JhZGllbnQtNikiIGQ9Ik0zMTMuMTMgNjcuNTlhMTc1LjMxIDE3NS4zMSAwIDAgMC0yOS43NS0yOC4xM2MtMS41Ny0xLjE3LTMuMTgtMi4zLTQuNzktMy40MkwyNTYgNTkuNWwtODIuMzQgODUuNjMgMTEzLjQxLTU4LjY4IDI5LjA3LTE1Yy0xLTEuMjctMi0yLjU4LTMtMy44MiIvPjwvZz48L2c+PGcgaWQ9ImcxNDQiIGNsaXAtcGF0aD0idXJsKCNjbGlwLXBhdGgtNykiPjxnIGlkPSJnMTQ2Ij48cGF0aCBpZD0icGF0aDE2MCIgZmlsbD0idXJsKCNsaW5lYXItZ3JhZGllbnQtNykiIGQ9Ik0zNTMuNTkgMTc3LjYxYzAtMi0uMTQtNC0uMjItNS45M2wtMzIuMjEtMi4zMS0xNDcuNDctMTAuNThMMzE4LjM2IDIwOWwzMC40MSAxMC41NWMuMDktLjM2LjE5LS43MS4yOC0xLjA4YTE3My42NSAxNzMuNjUgMCAwIDAgNC41Ny0zOS40N3YtMS4zNyIvPjwvZz48L2c+PGcgaWQ9ImcxNjIiIGNsaXAtcGF0aD0idXJsKCNjbGlwLXBhdGgtOCkiPjxnIGlkPSJnMTY0Ij48cGF0aCBpZD0icGF0aDE3OCIgZmlsbD0idXJsKCNsaW5lYXItZ3JhZGllbnQtOCkiIGQ9Ik0zNDguODQgMTM4LjYxYTE3Mi41NSAxNzIuNTUgMCAwIDAtMTMuODQtMzcuN2wtMzAuOTQgOS45NEwxNzUuOTIgMTUybDE0Mi00LjgzIDMyLjUtMS4xMWMtLjQ4LTIuNTEtMS01LTEuNTYtNy40OCIvPjwvZz48L2c+PC9nPjwvc3ZnPg==" - keysOrder: [["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata", "name"], ["spec", "host"], ["spec", "storageClass"], ["spec", "core"], ["spec", "core", "resources"], ["spec", "core", "resourcesPreset"], ["spec", "registry"], ["spec", "registry", "storageType"], ["spec", "registry", "size"], ["spec", "registry", "resources"], ["spec", "registry", "resourcesPreset"], ["spec", "jobservice"], ["spec", "jobservice", "resources"], ["spec", "jobservice", "resourcesPreset"], ["spec", "trivy"], ["spec", "trivy", "enabled"], ["spec", "trivy", "size"], ["spec", "trivy", "resources"], ["spec", "trivy", "resourcesPreset"], ["spec", "database"], ["spec", "database", "size"], ["spec", "database", "replicas"], ["spec", "redis"], ["spec", "redis", "size"], ["spec", "redis", "replicas"]] + keysOrder: [["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata", "name"], ["spec", "host"], ["spec", "storageClass"], ["spec", "core"], ["spec", "core", "resources"], ["spec", "core", "resourcesPreset"], ["spec", "registry"], ["spec", "registry", "resources"], ["spec", "registry", "resourcesPreset"], ["spec", "jobservice"], ["spec", "jobservice", "resources"], ["spec", "jobservice", "resourcesPreset"], ["spec", "trivy"], ["spec", "trivy", "enabled"], ["spec", "trivy", "size"], ["spec", "trivy", "resources"], ["spec", "trivy", "resourcesPreset"], ["spec", "database"], ["spec", "database", "size"], ["spec", "database", "replicas"], ["spec", "redis"], ["spec", "redis", "size"], ["spec", "redis", "replicas"]] secrets: exclude: [] include: diff --git a/packages/system/harbor/templates/bucket-secret.yaml b/packages/system/harbor/templates/bucket-secret.yaml new file mode 100644 index 00000000..241fe638 --- /dev/null +++ b/packages/system/harbor/templates/bucket-secret.yaml @@ -0,0 +1,20 @@ +{{- if .Values.bucket.secretName }} +{{- $existingSecret := lookup "v1" "Secret" .Release.Namespace .Values.bucket.secretName }} +{{- $bucketInfo := fromJson (b64dec (index $existingSecret.data "BucketInfo")) }} +{{- $accessKeyID := index $bucketInfo.spec.secretS3 "accessKeyID" }} +{{- $accessSecretKey := index $bucketInfo.spec.secretS3 "accessSecretKey" }} +{{- $endpoint := index $bucketInfo.spec.secretS3 "endpoint" }} +{{- $bucketName := $bucketInfo.spec.bucketName }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Values.harbor.fullnameOverride }}-registry-s3 +type: Opaque +stringData: + REGISTRY_STORAGE_S3_ACCESSKEY: {{ $accessKeyID | quote }} + REGISTRY_STORAGE_S3_SECRETKEY: {{ $accessSecretKey | quote }} + REGISTRY_STORAGE_S3_REGIONENDPOINT: {{ $endpoint | quote }} + REGISTRY_STORAGE_S3_BUCKET: {{ $bucketName | quote }} + REGISTRY_STORAGE_S3_REGION: "us-east-1" +{{- end }} diff --git a/packages/system/harbor/values.yaml b/packages/system/harbor/values.yaml index 9038bd38..faac7068 100644 --- a/packages/system/harbor/values.yaml +++ b/packages/system/harbor/values.yaml @@ -1,4 +1,6 @@ harbor: {} +bucket: + secretName: "" db: replicas: 2 size: 5Gi From bff5468b52319c05d3d355a1e70402ab1b552413 Mon Sep 17 00:00:00 2001 From: cozystack-bot <217169706+cozystack-bot@users.noreply.github.com> Date: Tue, 17 Feb 2026 22:36:40 +0000 Subject: [PATCH 103/666] Prepare release v1.0.0-beta.6 Signed-off-by: cozystack-bot <217169706+cozystack-bot@users.noreply.github.com> --- packages/apps/http-cache/images/nginx-cache.tag | 2 +- packages/apps/kubernetes/images/kubevirt-csi-driver.tag | 2 +- packages/apps/kubernetes/images/ubuntu-container-disk.tag | 2 +- packages/core/installer/values.yaml | 4 ++-- packages/core/platform/values.yaml | 2 +- packages/core/testing/values.yaml | 2 +- packages/extra/bootbox/images/matchbox.tag | 2 +- packages/extra/seaweedfs/images/objectstorage-sidecar.tag | 2 +- packages/system/backup-controller/values.yaml | 2 +- packages/system/backupstrategy-controller/values.yaml | 2 +- packages/system/cozystack-api/values.yaml | 2 +- packages/system/cozystack-controller/values.yaml | 2 +- packages/system/dashboard/templates/configmap.yaml | 2 +- packages/system/dashboard/values.yaml | 6 +++--- .../system/grafana-operator/images/grafana-dashboards.tag | 2 +- packages/system/kamaji/values.yaml | 4 ++-- packages/system/kubeovn-plunger/values.yaml | 2 +- packages/system/kubeovn-webhook/values.yaml | 2 +- packages/system/kubevirt-csi-node/values.yaml | 2 +- packages/system/lineage-controller-webhook/values.yaml | 2 +- packages/system/objectstorage-controller/values.yaml | 2 +- packages/system/seaweedfs/values.yaml | 2 +- 22 files changed, 26 insertions(+), 26 deletions(-) diff --git a/packages/apps/http-cache/images/nginx-cache.tag b/packages/apps/http-cache/images/nginx-cache.tag index ee4d8890..2d0e803a 100644 --- a/packages/apps/http-cache/images/nginx-cache.tag +++ b/packages/apps/http-cache/images/nginx-cache.tag @@ -1 +1 @@ -ghcr.io/cozystack/cozystack/nginx-cache:0.0.0@sha256:9e34fd50393b418d9516aadb488067a3a63675b045811beb1c0afc9c61e149e8 +ghcr.io/cozystack/cozystack/nginx-cache:0.0.0@sha256:cb25e40cb665b8bbeee8cb1ec39da4c9a7452ef3f2f371912bbc0d1b1e2d40a8 diff --git a/packages/apps/kubernetes/images/kubevirt-csi-driver.tag b/packages/apps/kubernetes/images/kubevirt-csi-driver.tag index a8e48958..d61f214a 100644 --- a/packages/apps/kubernetes/images/kubevirt-csi-driver.tag +++ b/packages/apps/kubernetes/images/kubevirt-csi-driver.tag @@ -1 +1 @@ -ghcr.io/cozystack/cozystack/kubevirt-csi-driver:0.0.0@sha256:1997d623bb60a5b540027a4e0716e7ca84f668ee09f31290e9c43324f0003137 +ghcr.io/cozystack/cozystack/kubevirt-csi-driver:0.0.0@sha256:604561e23df1b8eb25c24cf73fd93c7aaa6d1e7c56affbbda5c6f0f83424e4b1 diff --git a/packages/apps/kubernetes/images/ubuntu-container-disk.tag b/packages/apps/kubernetes/images/ubuntu-container-disk.tag index 930a3fd2..5b0830a6 100644 --- a/packages/apps/kubernetes/images/ubuntu-container-disk.tag +++ b/packages/apps/kubernetes/images/ubuntu-container-disk.tag @@ -1 +1 @@ -ghcr.io/cozystack/cozystack/ubuntu-container-disk:v1.33@sha256:71a74ca30f75967bae309be2758f19aa3d37c60b19426b9b622ff1c33a80362f +ghcr.io/cozystack/cozystack/ubuntu-container-disk:v1.33@sha256:19ee4c76f0b3b7b40b97995ca78988ad8c82f6e9c75288d8b7b4b88a64f75d50 diff --git a/packages/core/installer/values.yaml b/packages/core/installer/values.yaml index 3dd0f1d6..68894b3e 100644 --- a/packages/core/installer/values.yaml +++ b/packages/core/installer/values.yaml @@ -1,9 +1,9 @@ cozystackOperator: # Deployment variant: talos, generic, hosted variant: talos - image: ghcr.io/cozystack/cozystack/cozystack-operator:v1.0.0-beta.5@sha256:9f3089cb13b3e19dab14b8edc1220efde693f6066d3474c0137953e1873d16f3 + image: ghcr.io/cozystack/cozystack/cozystack-operator:v1.0.0-beta.6@sha256:c7490da9c1ccb51bff4dd5657ca6a33a29ac71ad9861dfa8c72fdfc8b5765b93 platformSourceUrl: 'oci://ghcr.io/cozystack/cozystack/cozystack-packages' - platformSourceRef: 'digest=sha256:86daf23f7b2ff2f448b273153733c55ac2f57c2bbe72c779ff14865d71e623cb' + platformSourceRef: 'digest=sha256:b29b87d1a2b80452ffd4db7516a102c30c55121552dcdb237055d4124d12c55d' # Generic variant configuration (only used when cozystackOperator.variant=generic) cozystack: # Kubernetes API server host (IP only, no protocol/port) diff --git a/packages/core/platform/values.yaml b/packages/core/platform/values.yaml index f9d4c8d3..5caab16d 100644 --- a/packages/core/platform/values.yaml +++ b/packages/core/platform/values.yaml @@ -5,7 +5,7 @@ sourceRef: path: / migrations: enabled: false - image: ghcr.io/cozystack/cozystack/platform-migrations:v1.0.0-beta.5@sha256:e7a9e0f0adc33e0be007af42f50ed3af064aa965ad628e48cc6c7943f31f239d + image: ghcr.io/cozystack/cozystack/platform-migrations:v1.0.0-beta.6@sha256:37c78dafcedbdad94acd9912550db0b4875897150666b8a06edfa894de99064e targetVersion: 32 # Bundle deployment configuration bundles: diff --git a/packages/core/testing/values.yaml b/packages/core/testing/values.yaml index 627d3096..0692cab6 100644 --- a/packages/core/testing/values.yaml +++ b/packages/core/testing/values.yaml @@ -1,2 +1,2 @@ e2e: - image: ghcr.io/cozystack/cozystack/e2e-sandbox:v1.0.0-beta.5@sha256:eac71ef0de3450fce96255629e77903630c63ade62b81e7055f1a689f92ee153 + image: ghcr.io/cozystack/cozystack/e2e-sandbox:v1.0.0-beta.6@sha256:09af5901abcbed2b612d2d93c163e8ad3948bc55a1d8beae714b4fb2b8f7d91d diff --git a/packages/extra/bootbox/images/matchbox.tag b/packages/extra/bootbox/images/matchbox.tag index 19014ae6..9729ce6e 100644 --- a/packages/extra/bootbox/images/matchbox.tag +++ b/packages/extra/bootbox/images/matchbox.tag @@ -1 +1 @@ -ghcr.io/cozystack/cozystack/matchbox:v1.0.0-beta.5@sha256:8facb6bbbaf336ff3dd606d6bcbf65f5d1fb7f079bec09966544398632005b47 +ghcr.io/cozystack/cozystack/matchbox:v1.0.0-beta.6@sha256:212f624957447f5a932fd5d4564eb8c97694d336b7dc877a2833c1513c0d074d diff --git a/packages/extra/seaweedfs/images/objectstorage-sidecar.tag b/packages/extra/seaweedfs/images/objectstorage-sidecar.tag index 9f5222a7..9657b956 100644 --- a/packages/extra/seaweedfs/images/objectstorage-sidecar.tag +++ b/packages/extra/seaweedfs/images/objectstorage-sidecar.tag @@ -1 +1 @@ -ghcr.io/cozystack/cozystack/objectstorage-sidecar:v1.0.0-beta.5@sha256:235b194a531b70e266a10ef78d2955d19f5b659513f23d8b3cfbbc0dff7fc1c0 +ghcr.io/cozystack/cozystack/objectstorage-sidecar:v1.0.0-beta.6@sha256:235b194a531b70e266a10ef78d2955d19f5b659513f23d8b3cfbbc0dff7fc1c0 diff --git a/packages/system/backup-controller/values.yaml b/packages/system/backup-controller/values.yaml index 63c49ed1..9ce9bac9 100644 --- a/packages/system/backup-controller/values.yaml +++ b/packages/system/backup-controller/values.yaml @@ -1,5 +1,5 @@ backupController: - image: "ghcr.io/cozystack/cozystack/backup-controller:v1.0.0-beta.5@sha256:c99ff8ab11c5c016841acd9dca72c7a643dba72a98b8f816ea67ba7fa3549f88" + image: "ghcr.io/cozystack/cozystack/backup-controller:v1.0.0-beta.6@sha256:365214a74ffc34a9314a62a7d4b491590051fc5486f6bae9913c0c1289983d43" replicas: 2 debug: false metrics: diff --git a/packages/system/backupstrategy-controller/values.yaml b/packages/system/backupstrategy-controller/values.yaml index 5ff8efd5..d74557a0 100644 --- a/packages/system/backupstrategy-controller/values.yaml +++ b/packages/system/backupstrategy-controller/values.yaml @@ -1,5 +1,5 @@ backupStrategyController: - image: "ghcr.io/cozystack/cozystack/backupstrategy-controller:v1.0.0-beta.5@sha256:cd92a2620c9965512977b57c2829d931e7ecb7a8afc0693d7c8ab39bd8ff77d8" + image: "ghcr.io/cozystack/cozystack/backupstrategy-controller:v1.0.0-beta.6@sha256:aa04ee61dce11950162606fc8db2d5cbc6f5b32ba700f790b3f1eee10d65efb1" replicas: 2 debug: false metrics: diff --git a/packages/system/cozystack-api/values.yaml b/packages/system/cozystack-api/values.yaml index 511d1f33..cc6279bc 100644 --- a/packages/system/cozystack-api/values.yaml +++ b/packages/system/cozystack-api/values.yaml @@ -1,3 +1,3 @@ cozystackAPI: - image: ghcr.io/cozystack/cozystack/cozystack-api:v1.0.0-beta.5@sha256:b217c3d1cca7e35b9e6ee32297ebe923fee12ca48d94062484a9dfcffda0e2e3 + image: ghcr.io/cozystack/cozystack/cozystack-api:v1.0.0-beta.6@sha256:d89cf68fb622d0dbef7db98db09d352efc93c2cce448d11f2d73dcd363e911b7 replicas: 2 diff --git a/packages/system/cozystack-controller/values.yaml b/packages/system/cozystack-controller/values.yaml index 5ead5eae..7a17c992 100644 --- a/packages/system/cozystack-controller/values.yaml +++ b/packages/system/cozystack-controller/values.yaml @@ -1,4 +1,4 @@ cozystackController: - image: ghcr.io/cozystack/cozystack/cozystack-controller:v1.0.0-beta.5@sha256:edeb1788395d650f5f14a935c8f6b95ede7ca548cba198c18c75cc5eafd89104 + image: ghcr.io/cozystack/cozystack/cozystack-controller:v1.0.0-beta.6@sha256:d55a3c288934b1f69a00321bc8a94776915556b5f882fe6ac615e9de2701c61f debug: false disableTelemetry: false diff --git a/packages/system/dashboard/templates/configmap.yaml b/packages/system/dashboard/templates/configmap.yaml index c32fc638..3ac5029e 100644 --- a/packages/system/dashboard/templates/configmap.yaml +++ b/packages/system/dashboard/templates/configmap.yaml @@ -1,6 +1,6 @@ {{- $brandingConfig := .Values._cluster.branding | default dict }} -{{- $tenantText := "v1.0.0-beta.5" }} +{{- $tenantText := "v1.0.0-beta.6" }} {{- $footerText := "Cozystack" }} {{- $titleText := "Cozystack Dashboard" }} {{- $logoText := "" }} diff --git a/packages/system/dashboard/values.yaml b/packages/system/dashboard/values.yaml index a2f9c642..32823845 100644 --- a/packages/system/dashboard/values.yaml +++ b/packages/system/dashboard/values.yaml @@ -1,6 +1,6 @@ openapiUI: - image: ghcr.io/cozystack/cozystack/openapi-ui:v1.0.0-beta.5@sha256:0d331986ac06de1fa5b660d69670cba0f2a59fcdab6cf43cb136827ecb8fcfe8 + image: ghcr.io/cozystack/cozystack/openapi-ui:v1.0.0-beta.6@sha256:c333637673a9e878f6c4ed0fc96db55967bbcf94b2434d075b0f0c6fcfcf9eff openapiUIK8sBff: - image: ghcr.io/cozystack/cozystack/openapi-ui-k8s-bff:v1.0.0-beta.5@sha256:1b3ea6d4c7dbbe6a8def3b2807fffdfab2ac4afc39d7a846e57dd491fa168f92 + image: ghcr.io/cozystack/cozystack/openapi-ui-k8s-bff:v1.0.0-beta.6@sha256:1b3ea6d4c7dbbe6a8def3b2807fffdfab2ac4afc39d7a846e57dd491fa168f92 tokenProxy: - image: ghcr.io/cozystack/cozystack/token-proxy:v1.0.0-beta.5@sha256:2e280991e07853ea48f97b0a42946afffa10d03d6a83d41099ed83e6ffc94fdc + image: ghcr.io/cozystack/cozystack/token-proxy:v1.0.0-beta.6@sha256:2e280991e07853ea48f97b0a42946afffa10d03d6a83d41099ed83e6ffc94fdc diff --git a/packages/system/grafana-operator/images/grafana-dashboards.tag b/packages/system/grafana-operator/images/grafana-dashboards.tag index 34111c9f..10837c35 100644 --- a/packages/system/grafana-operator/images/grafana-dashboards.tag +++ b/packages/system/grafana-operator/images/grafana-dashboards.tag @@ -1 +1 @@ -ghcr.io/cozystack/cozystack/grafana-dashboards:v1.0.0-beta.5@sha256:e866b5b3874b9d390b341183d2ee070e1387440c14cfe51af831695def6dc2ec +ghcr.io/cozystack/cozystack/grafana-dashboards:v1.0.0-beta.6@sha256:7a3c9af59f8d74d5a23750bbc845c7de64610dbd4d4f84011e10be037b3ce2a0 diff --git a/packages/system/kamaji/values.yaml b/packages/system/kamaji/values.yaml index cbf71457..334a5e92 100644 --- a/packages/system/kamaji/values.yaml +++ b/packages/system/kamaji/values.yaml @@ -3,7 +3,7 @@ kamaji: deploy: false image: pullPolicy: IfNotPresent - tag: v1.0.0-beta.5@sha256:05f8e166dc94aadb74cd6448f6f96fbd2167057a559097516a41b8f53e434918 + tag: v1.0.0-beta.6@sha256:05f8e166dc94aadb74cd6448f6f96fbd2167057a559097516a41b8f53e434918 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:v1.0.0-beta.5@sha256:05f8e166dc94aadb74cd6448f6f96fbd2167057a559097516a41b8f53e434918 + - --migrate-image=ghcr.io/cozystack/cozystack/kamaji:v1.0.0-beta.6@sha256:05f8e166dc94aadb74cd6448f6f96fbd2167057a559097516a41b8f53e434918 diff --git a/packages/system/kubeovn-plunger/values.yaml b/packages/system/kubeovn-plunger/values.yaml index 0abcc7db..c8357c90 100644 --- a/packages/system/kubeovn-plunger/values.yaml +++ b/packages/system/kubeovn-plunger/values.yaml @@ -1,4 +1,4 @@ portSecurity: true routes: "" -image: ghcr.io/cozystack/cozystack/kubeovn-plunger:v1.0.0-beta.5@sha256:67d615f86c3230e8c643b6bd347093cdb1e575c99f7c63599fd43e0c4ffc249a +image: ghcr.io/cozystack/cozystack/kubeovn-plunger:v1.0.0-beta.6@sha256:8e964605efe54e73a94c84abec7dbb5a011c02ccece282bef8ae7b70fce3d217 ovnCentralName: ovn-central diff --git a/packages/system/kubeovn-webhook/values.yaml b/packages/system/kubeovn-webhook/values.yaml index 26613989..b602e6df 100644 --- a/packages/system/kubeovn-webhook/values.yaml +++ b/packages/system/kubeovn-webhook/values.yaml @@ -1,3 +1,3 @@ portSecurity: true routes: "" -image: ghcr.io/cozystack/cozystack/kubeovn-webhook:v1.0.0-beta.5@sha256:e6334c29d3aaf0dea766c88e3e05b53ad623d1bb497b3c836e6f76adade45b29 +image: ghcr.io/cozystack/cozystack/kubeovn-webhook:v1.0.0-beta.6@sha256:e18f9fd679e38f65362a8d0042f25468272f6d081136ad47027168d8e7e07a4a diff --git a/packages/system/kubevirt-csi-node/values.yaml b/packages/system/kubevirt-csi-node/values.yaml index 521683be..ab26cc5a 100644 --- a/packages/system/kubevirt-csi-node/values.yaml +++ b/packages/system/kubevirt-csi-node/values.yaml @@ -1,3 +1,3 @@ storageClass: replicated csiDriver: - image: ghcr.io/cozystack/cozystack/kubevirt-csi-driver:0.0.0@sha256:1997d623bb60a5b540027a4e0716e7ca84f668ee09f31290e9c43324f0003137 + image: ghcr.io/cozystack/cozystack/kubevirt-csi-driver:0.0.0@sha256:604561e23df1b8eb25c24cf73fd93c7aaa6d1e7c56affbbda5c6f0f83424e4b1 diff --git a/packages/system/lineage-controller-webhook/values.yaml b/packages/system/lineage-controller-webhook/values.yaml index d5f28f89..2a9b2941 100644 --- a/packages/system/lineage-controller-webhook/values.yaml +++ b/packages/system/lineage-controller-webhook/values.yaml @@ -1,5 +1,5 @@ lineageControllerWebhook: - image: ghcr.io/cozystack/cozystack/lineage-controller-webhook:v1.0.0-beta.5@sha256:9dd5411d222fe7d7b0583a8e6807bf7745eb1412bf180827c5f1957e6ebbfeb4 + image: ghcr.io/cozystack/cozystack/lineage-controller-webhook:v1.0.0-beta.6@sha256:cf577e56ebc2b94205741d9c5b08f2983cec0811f0c2890edca8fdca22624de1 debug: false localK8sAPIEndpoint: enabled: true diff --git a/packages/system/objectstorage-controller/values.yaml b/packages/system/objectstorage-controller/values.yaml index b3c9d4c6..a270a7e8 100644 --- a/packages/system/objectstorage-controller/values.yaml +++ b/packages/system/objectstorage-controller/values.yaml @@ -1,3 +1,3 @@ objectstorage: controller: - image: "ghcr.io/cozystack/cozystack/objectstorage-controller:v1.0.0-beta.5@sha256:b4c972769afda76c48b58e7acf0ac66a0abf16a622f245c60338f432872f640a" + image: "ghcr.io/cozystack/cozystack/objectstorage-controller:v1.0.0-beta.6@sha256:b4c972769afda76c48b58e7acf0ac66a0abf16a622f245c60338f432872f640a" diff --git a/packages/system/seaweedfs/values.yaml b/packages/system/seaweedfs/values.yaml index 94aba339..e62cd357 100644 --- a/packages/system/seaweedfs/values.yaml +++ b/packages/system/seaweedfs/values.yaml @@ -177,7 +177,7 @@ seaweedfs: bucketClassName: "seaweedfs" region: "" sidecar: - image: "ghcr.io/cozystack/cozystack/objectstorage-sidecar:v1.0.0-beta.5@sha256:235b194a531b70e266a10ef78d2955d19f5b659513f23d8b3cfbbc0dff7fc1c0" + image: "ghcr.io/cozystack/cozystack/objectstorage-sidecar:v1.0.0-beta.6@sha256:235b194a531b70e266a10ef78d2955d19f5b659513f23d8b3cfbbc0dff7fc1c0" certificates: commonName: "SeaweedFS CA" ipAddresses: [] From 96467cdefd223a3ed140916ebfbfaa4f690c0c33 Mon Sep 17 00:00:00 2001 From: cozystack-bot <217169706+cozystack-bot@users.noreply.github.com> Date: Tue, 17 Feb 2026 22:44:01 +0000 Subject: [PATCH 104/666] docs: add changelog for v1.0.0-beta.6 Signed-off-by: cozystack-bot <217169706+cozystack-bot@users.noreply.github.com> --- docs/changelogs/v1.0.0-beta.6.md | 46 ++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 docs/changelogs/v1.0.0-beta.6.md diff --git a/docs/changelogs/v1.0.0-beta.6.md b/docs/changelogs/v1.0.0-beta.6.md new file mode 100644 index 00000000..99e565d7 --- /dev/null +++ b/docs/changelogs/v1.0.0-beta.6.md @@ -0,0 +1,46 @@ + + +> **⚠️ Beta Release Warning**: This is a pre-release version intended for testing and early adoption. Breaking changes may occur before the stable v1.0.0 release. + +## Features and Improvements + +* **[platform] Add cilium-kilo networking variant**: Added a new `cilium-kilo` networking variant that combines Cilium CNI with Kilo WireGuard mesh overlay. This variant enables `enable-ipip-termination` in Cilium for proper IPIP packet handling and deploys Kilo with `--compatibility=cilium` flag. Users can now select `cilium-kilo` as their networking variant during platform setup, simplifying the multi-location WireGuard setup compared to manually combining Cilium and standalone Kilo ([**@kvaps**](https://github.com/kvaps) in #2064). + +* **[nats] Add monitoring**: Added Grafana dashboards for NATS JetStream and server metrics monitoring, along with Prometheus monitoring support with TLS-aware endpoint configuration. Includes updated image customization options (digest and full image name) and component version upgrades for the NATS exporter and utilities. Users now have full observability into NATS message broker performance and health ([**@klinch0**](https://github.com/klinch0) in #1381). + +* **[platform] Add DNS-1035 validation for Application names**: Added dynamic DNS-1035 label validation for Application names in the Cozystack API, using `IsDNS1035Label` from `k8s.io/apimachinery`. Validation is performed at creation time and accounts for the root host length to prevent names that would exceed Kubernetes resource naming limits. This prevents creation of resources with invalid names that would fail downstream Kubernetes resource creation ([**@lexfrei**](https://github.com/lexfrei) in #1771). + +* **[operator] Add automatic CRD installation at startup**: Added `--install-crds` flag to the Cozystack operator that installs embedded CRD manifests at startup, ensuring CRDs exist before the operator begins reconciliation. CRD manifests are now embedded in the operator binary and verified for consistency with the Helm `crds/` directory via a new CI Makefile check. This eliminates ordering issues during initial cluster setup where CRDs might not yet be present ([**@lexfrei**](https://github.com/lexfrei) in #2060). + +## Fixes + +* **[platform] Adopt tenant-root into cozystack-basics during migration**: Added migration 31 to adopt existing `tenant-root` Namespace and HelmRelease into the `cozystack-basics` Helm release when upgrading from v0.41.x to v1.0. Previously these resources were applied via `kubectl apply` with no Helm release tracking, causing Helm to treat them as foreign resources and potentially delete them during reconciliation. This migration ensures a safe upgrade path by annotating and labeling these resources for Helm adoption ([**@kvaps**](https://github.com/kvaps) in #2065). + +* **[platform] Preserve tenant-root HelmRelease during migration**: Fixed a data-loss risk during migration from v0.41.x to v1.0.0-beta where the `tenant-root` HelmRelease (and the namespace it manages) could be deleted, causing tenant service outages. Added safety annotation to the HelmRelease and lookup logic to preserve current parameters during migration, preventing unwanted deletion of tenant-root resources ([**@sircthulhu**](https://github.com/sircthulhu) in #2063). + +* **[codegen] Add gen_client to update-codegen.sh and regenerate applyconfiguration**: Fixed a build error in `pkg/generated/applyconfiguration/utils.go` caused by a reference to `testing.TypeConverter` which was removed in client-go v0.34.1. The root cause was that `hack/update-codegen.sh` never called `gen_client`, leaving the generated applyconfiguration code stale. Running the full code generation now produces a consistent and compilable codebase ([**@lexfrei**](https://github.com/lexfrei) in #2061). + +* **[e2e] Make kubernetes test retries effective by cleaning up stale resources**: Fixed E2E test retries for the Kubernetes tenant test by adding pre-creation cleanup of backend deployment/service and NFS pod/PVC in `run-kubernetes.sh`. Previously, retries would fail immediately because stale resources from a failed attempt blocked re-creation. Also increased the tenant deployment wait timeout from 90s to 300s to handle CI resource pressure ([**@lexfrei**](https://github.com/lexfrei) in #2062). + +## Development, Testing, and CI/CD + +* **[e2e] Use helm install instead of kubectl apply for cozystack installation**: Replaced the pre-rendered static YAML application flow (`kubectl apply`) with direct `helm upgrade --install` of the `packages/core/installer` chart in E2E tests. Removed the CRD/operator artifact upload/download steps from the CI workflow, simplifying the pipeline. The chart with correct values is already present in the sandbox via workspace copy and `pr.patch` ([**@lexfrei**](https://github.com/lexfrei) in #2060). + +## Documentation + +* **[website] Improve Azure autoscaling troubleshooting guide**: Enhanced the Azure autoscaling troubleshooting documentation with serial console instructions for debugging VMSS worker nodes, a troubleshooting section for nodes stuck in maintenance mode due to invalid or missing machine config, `az vmss update --custom-data` instructions for updating machine config, and a warning that Azure does not support reading back `customData` ([**@kvaps**](https://github.com/kvaps) in cozystack/website#424). + +* **[website] Update multi-location documentation for cilium-kilo variant**: Updated multi-location networking documentation to reflect the new integrated `cilium-kilo` variant selection during platform setup, replacing the previous manual Kilo installation and Cilium configuration steps. Added explanation of `enable-ipip-termination` and updated the troubleshooting section ([**@kvaps**](https://github.com/kvaps) in cozystack/website@02d63f0). + +## Contributors + +We'd like to thank all contributors who made this release possible: + +* [**@klinch0**](https://github.com/klinch0) +* [**@kvaps**](https://github.com/kvaps) +* [**@lexfrei**](https://github.com/lexfrei) +* [**@sircthulhu**](https://github.com/sircthulhu) + +**Full Changelog**: https://github.com/cozystack/cozystack/compare/v1.0.0-beta.5...v1.0.0-beta.6 From 87d03902563171e49d7ad94cb1078b612a892db6 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Wed, 18 Feb 2026 02:07:38 +0300 Subject: [PATCH 105/666] fix(harbor): include tenant domain in default hostname and add E2E cleanup Use tenant base domain in default hostname construction (harbor.RELEASE.DOMAIN) to match the pattern used by other apps (kubernetes, vpn). Remove unused $ingress variable from harbor.yaml. Add cleanup of stale resources from previous failed E2E runs. Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- hack/e2e-apps/harbor.bats | 5 +++++ packages/apps/harbor/templates/harbor.yaml | 3 +-- packages/apps/harbor/templates/ingress.yaml | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/hack/e2e-apps/harbor.bats b/hack/e2e-apps/harbor.bats index 09215546..26f407ea 100644 --- a/hack/e2e-apps/harbor.bats +++ b/hack/e2e-apps/harbor.bats @@ -3,6 +3,11 @@ @test "Create Harbor" { name='test' release="harbor-$name" + + # Clean up stale resources from previous failed runs + kubectl -n tenant-test delete harbor.apps.cozystack.io $name 2>/dev/null || true + kubectl -n tenant-test wait hr $release --timeout=60s --for=delete 2>/dev/null || true + kubectl apply -f- < Date: Wed, 18 Feb 2026 02:12:11 +0300 Subject: [PATCH 106/666] fix(harbor): enable database TLS and fix token key/cert check Change sslmode from disable to require for CNPG PostgreSQL connection, as CNPG supports TLS out of the box. Fix token key/cert preservation to verify both values are present before passing to Harbor core. Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- packages/apps/harbor/templates/harbor.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/apps/harbor/templates/harbor.yaml b/packages/apps/harbor/templates/harbor.yaml index c3fab16a..94154ec9 100644 --- a/packages/apps/harbor/templates/harbor.yaml +++ b/packages/apps/harbor/templates/harbor.yaml @@ -112,7 +112,7 @@ spec: portal: resources: {{- include "cozy-lib.resources.defaultingSanitize" (list "nano" (dict) $) | nindent 10 }} core: - {{- if $tokenKey }} + {{- if and $tokenKey $tokenCert }} tokenKey: {{ $tokenKey | quote }} tokenCert: {{ $tokenCert | quote }} {{- end }} @@ -136,7 +136,7 @@ spec: port: "5432" username: app coreDatabase: app - sslmode: disable + sslmode: require existingSecret: "{{ .Release.Name }}-db-app" redis: type: external From 0198c9896a10675bddc06277c9510527a700549c Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Wed, 18 Feb 2026 02:17:59 +0300 Subject: [PATCH 107/666] fix(harbor): use standard hostname pattern without double prefix Follow the same hostname pattern as kubernetes and vpn apps: use Release.Name directly (which already includes the harbor- prefix) instead of adding an extra harbor. subdomain. Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- packages/apps/harbor/templates/harbor.yaml | 2 +- packages/apps/harbor/templates/ingress.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/apps/harbor/templates/harbor.yaml b/packages/apps/harbor/templates/harbor.yaml index 94154ec9..ff8e88ad 100644 --- a/packages/apps/harbor/templates/harbor.yaml +++ b/packages/apps/harbor/templates/harbor.yaml @@ -1,5 +1,5 @@ {{- $host := .Values._namespace.host }} -{{- $harborHost := .Values.host | default (printf "harbor.%s.%s" .Release.Name $host) }} +{{- $harborHost := .Values.host | default (printf "%s.%s" .Release.Name $host) }} {{- $existingSecret := lookup "v1" "Secret" .Release.Namespace (printf "%s-credentials" .Release.Name) }} {{- $adminPassword := randAlphaNum 16 }} diff --git a/packages/apps/harbor/templates/ingress.yaml b/packages/apps/harbor/templates/ingress.yaml index 07d73b8e..28691654 100644 --- a/packages/apps/harbor/templates/ingress.yaml +++ b/packages/apps/harbor/templates/ingress.yaml @@ -1,6 +1,6 @@ {{- $ingress := .Values._namespace.ingress }} {{- $host := .Values._namespace.host }} -{{- $harborHost := .Values.host | default (printf "harbor.%s.%s" .Release.Name $host) }} +{{- $harborHost := .Values.host | default (printf "%s.%s" .Release.Name $host) }} {{- $issuerType := (index .Values._cluster "clusterissuer") | default "http01" }} --- apiVersion: networking.k8s.io/v1 From daf1b71e7c97b2910453245c159d7f774efeef9c Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Wed, 18 Feb 2026 02:23:00 +0300 Subject: [PATCH 108/666] fix(harbor): add explicit CNPG bootstrap configuration Specify initdb bootstrap with database and owner names explicitly instead of relying on CNPG defaults which may change between versions. Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- packages/system/harbor/templates/database.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/system/harbor/templates/database.yaml b/packages/system/harbor/templates/database.yaml index 5d1827aa..68f047f8 100644 --- a/packages/system/harbor/templates/database.yaml +++ b/packages/system/harbor/templates/database.yaml @@ -10,6 +10,10 @@ spec: {{- with .Values.db.storageClass }} storageClass: {{ . }} {{- end }} + bootstrap: + initdb: + database: app + owner: app monitoring: enablePodMonitor: true resources: From 199ffe319a1740d52c929f48991d8ec5b8d8cd60 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Wed, 18 Feb 2026 11:26:58 +0300 Subject: [PATCH 109/666] fix(harbor): set UTF-8 encoding and locale for CNPG database Add encoding, localeCollate, and localeCType to initdb bootstrap configuration to ensure fulltext search (ilike) works correctly. Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- packages/system/harbor/templates/database.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/system/harbor/templates/database.yaml b/packages/system/harbor/templates/database.yaml index 68f047f8..b1221e1c 100644 --- a/packages/system/harbor/templates/database.yaml +++ b/packages/system/harbor/templates/database.yaml @@ -14,6 +14,9 @@ spec: initdb: database: app owner: app + encoding: UTF8 + localeCollate: en_US.UTF-8 + localeCType: en_US.UTF-8 monitoring: enablePodMonitor: true resources: From b5b2f95c3e6cf36dbb6b50e83e386c4c59ed785a Mon Sep 17 00:00:00 2001 From: Kirill Ilin Date: Wed, 18 Feb 2026 15:45:14 +0500 Subject: [PATCH 110/666] [cozystack-basics] Preserve existing HelmRelease values during reconciliations Signed-off-by: Kirill Ilin --- .../system/cozystack-basics/templates/tenant-root.yaml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/system/cozystack-basics/templates/tenant-root.yaml b/packages/system/cozystack-basics/templates/tenant-root.yaml index 95af9d7f..93f5129e 100644 --- a/packages/system/cozystack-basics/templates/tenant-root.yaml +++ b/packages/system/cozystack-basics/templates/tenant-root.yaml @@ -23,7 +23,6 @@ spec: namespace: cozy-system interval: 1m0s timeout: 5m0s - values: - _cluster: - oidc-enabled: {{ .Values.oidcEnabled | quote }} - root-host: {{ .Values.rootHost | quote }} + valuesFrom: + - kind: Secret + name: cozystack-values From db1425a8deef2ec3096a1a82759a81563f432a41 Mon Sep 17 00:00:00 2001 From: Kirill Ilin Date: Thu, 19 Feb 2026 14:33:20 +0500 Subject: [PATCH 111/666] feat(dashboard): add API-backed dropdown for VMInstance instanceType Override spec.instanceType field with listInput type in schema so the dashboard renders it as a select dropdown populated from VirtualMachineClusterInstancetype resources. Default value is read dynamically from the ApplicationDefinition's OpenAPI schema. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Kirill Ilin --- .../dashboard/customformsoverride.go | 71 +++++++- .../dashboard/customformsoverride_test.go | 164 ++++++++++++++++++ 2 files changed, 234 insertions(+), 1 deletion(-) diff --git a/internal/controller/dashboard/customformsoverride.go b/internal/controller/dashboard/customformsoverride.go index dfbccf5c..26afab50 100644 --- a/internal/controller/dashboard/customformsoverride.go +++ b/internal/controller/dashboard/customformsoverride.go @@ -46,8 +46,11 @@ func (m *Manager) ensureCustomFormsOverride(ctx context.Context, crd *cozyv1alph } } - // Build schema with multilineString for string fields without enum + // Parse OpenAPI schema once for reuse l := log.FromContext(ctx) + openAPIProps := parseOpenAPIProperties(crd.Spec.Application.OpenAPISchema) + + // Build schema with multilineString for string fields without enum schema, err := buildMultilineStringSchema(crd.Spec.Application.OpenAPISchema) if err != nil { // If schema parsing fails, log the error and use an empty schema @@ -55,6 +58,9 @@ func (m *Manager) ensureCustomFormsOverride(ctx context.Context, crd *cozyv1alph schema = map[string]any{} } + // Override specific fields with API-backed dropdowns (listInput type) + applyListInputOverrides(schema, kind, openAPIProps) + spec := map[string]any{ "customizationId": customizationID, "hidden": hidden, @@ -176,6 +182,69 @@ func buildMultilineStringSchema(openAPISchema string) (map[string]any, error) { return schema, nil } +// applyListInputOverrides injects listInput type overrides into the schema +// for fields that should be rendered as API-backed dropdowns in the dashboard. +// openAPIProps are the parsed top-level properties from the OpenAPI schema. +func applyListInputOverrides(schema map[string]any, kind string, openAPIProps map[string]any) { + switch kind { + case "VMInstance": + specProps := ensureSchemaPath(schema, "spec") + field := map[string]any{ + "type": "listInput", + "customProps": map[string]any{ + "valueUri": "/api/clusters/{cluster}/k8s/apis/instancetype.kubevirt.io/v1beta1/virtualmachineclusterinstancetypes", + "keysToValue": []any{"metadata", "name"}, + "keysToLabel": []any{"metadata", "name"}, + }, + } + if prop, _ := openAPIProps["instanceType"].(map[string]any); prop != nil { + if def := prop["default"]; def != nil { + field["default"] = def + } + } + specProps["instanceType"] = field + } +} + +// parseOpenAPIProperties parses the top-level properties from an OpenAPI schema JSON string. +func parseOpenAPIProperties(openAPISchema string) map[string]any { + if openAPISchema == "" { + return nil + } + var root map[string]any + if err := json.Unmarshal([]byte(openAPISchema), &root); err != nil { + return nil + } + props, _ := root["properties"].(map[string]any) + return props +} + +// ensureSchemaPath ensures the nested properties structure exists in a schema +// and returns the innermost properties map. +// e.g. ensureSchemaPath(schema, "spec") returns schema["properties"]["spec"]["properties"] +func ensureSchemaPath(schema map[string]any, segments ...string) map[string]any { + current := schema + for _, seg := range segments { + props, ok := current["properties"].(map[string]any) + if !ok { + props = map[string]any{} + current["properties"] = props + } + child, ok := props[seg].(map[string]any) + if !ok { + child = map[string]any{} + props[seg] = child + } + current = child + } + props, ok := current["properties"].(map[string]any) + if !ok { + props = map[string]any{} + current["properties"] = props + } + return props +} + // processSpecProperties recursively processes spec properties and adds multilineString type // for string fields without enum func processSpecProperties(props map[string]any, schemaProps map[string]any) { diff --git a/internal/controller/dashboard/customformsoverride_test.go b/internal/controller/dashboard/customformsoverride_test.go index 9f7babe9..3df24724 100644 --- a/internal/controller/dashboard/customformsoverride_test.go +++ b/internal/controller/dashboard/customformsoverride_test.go @@ -169,3 +169,167 @@ func TestBuildMultilineStringSchemaInvalidJSON(t *testing.T) { t.Errorf("Expected nil schema for invalid JSON, got %v", schema) } } + +func TestApplyListInputOverrides_VMInstance(t *testing.T) { + openAPIProps := map[string]any{ + "instanceType": map[string]any{"type": "string", "default": "u1.medium"}, + } + + schema := map[string]any{} + applyListInputOverrides(schema, "VMInstance", openAPIProps) + + specProps := schema["properties"].(map[string]any)["spec"].(map[string]any)["properties"].(map[string]any) + instanceType, ok := specProps["instanceType"].(map[string]any) + if !ok { + t.Fatal("instanceType not found in schema.properties.spec.properties") + } + + if instanceType["type"] != "listInput" { + t.Errorf("expected type listInput, got %v", instanceType["type"]) + } + + if instanceType["default"] != "u1.medium" { + t.Errorf("expected default u1.medium, got %v", instanceType["default"]) + } + + customProps, ok := instanceType["customProps"].(map[string]any) + if !ok { + t.Fatal("customProps not found") + } + + expectedURI := "/api/clusters/{cluster}/k8s/apis/instancetype.kubevirt.io/v1beta1/virtualmachineclusterinstancetypes" + if customProps["valueUri"] != expectedURI { + t.Errorf("expected valueUri %s, got %v", expectedURI, customProps["valueUri"]) + } +} + +func TestApplyListInputOverrides_UnknownKind(t *testing.T) { + schema := map[string]any{} + applyListInputOverrides(schema, "SomeOtherKind", map[string]any{}) + + if len(schema) != 0 { + t.Errorf("expected empty schema for unknown kind, got %v", schema) + } +} + +func TestApplyListInputOverrides_NoDefault(t *testing.T) { + openAPIProps := map[string]any{ + "instanceType": map[string]any{"type": "string"}, + } + + schema := map[string]any{} + applyListInputOverrides(schema, "VMInstance", openAPIProps) + + specProps := schema["properties"].(map[string]any)["spec"].(map[string]any)["properties"].(map[string]any) + instanceType := specProps["instanceType"].(map[string]any) + + if _, exists := instanceType["default"]; exists { + t.Errorf("expected no default key, got %v", instanceType["default"]) + } +} + +func TestApplyListInputOverrides_MergesWithExistingSchema(t *testing.T) { + openAPIProps := map[string]any{ + "instanceType": map[string]any{"type": "string", "default": "u1.medium"}, + } + + // Simulate schema that already has spec.properties from buildMultilineStringSchema + schema := map[string]any{ + "properties": map[string]any{ + "spec": map[string]any{ + "properties": map[string]any{ + "otherField": map[string]any{"type": "multilineString"}, + }, + }, + }, + } + applyListInputOverrides(schema, "VMInstance", openAPIProps) + + specProps := schema["properties"].(map[string]any)["spec"].(map[string]any)["properties"].(map[string]any) + + // instanceType should be added + if _, ok := specProps["instanceType"].(map[string]any); !ok { + t.Fatal("instanceType not found after override") + } + + // otherField should be preserved + otherField, ok := specProps["otherField"].(map[string]any) + if !ok { + t.Fatal("otherField was lost after override") + } + if otherField["type"] != "multilineString" { + t.Errorf("otherField type changed, got %v", otherField["type"]) + } +} + +func TestParseOpenAPIProperties(t *testing.T) { + t.Run("extracts properties", func(t *testing.T) { + props := parseOpenAPIProperties(`{"type":"object","properties":{"instanceType":{"type":"string","default":"u1.medium"}}}`) + field, _ := props["instanceType"].(map[string]any) + if field["default"] != "u1.medium" { + t.Errorf("expected default u1.medium, got %v", field["default"]) + } + }) + + t.Run("empty string", func(t *testing.T) { + if props := parseOpenAPIProperties(""); props != nil { + t.Errorf("expected nil, got %v", props) + } + }) + + t.Run("invalid JSON", func(t *testing.T) { + if props := parseOpenAPIProperties("{bad"); props != nil { + t.Errorf("expected nil, got %v", props) + } + }) + + t.Run("no properties key", func(t *testing.T) { + if props := parseOpenAPIProperties(`{"type":"object"}`); props != nil { + t.Errorf("expected nil, got %v", props) + } + }) +} + +func TestEnsureSchemaPath(t *testing.T) { + t.Run("creates path from empty schema", func(t *testing.T) { + schema := map[string]any{} + props := ensureSchemaPath(schema, "spec") + + props["field"] = "value" + + // Verify structure: schema.properties.spec.properties.field + got := schema["properties"].(map[string]any)["spec"].(map[string]any)["properties"].(map[string]any)["field"] + if got != "value" { + t.Errorf("expected value, got %v", got) + } + }) + + t.Run("preserves existing nested properties", func(t *testing.T) { + schema := map[string]any{ + "properties": map[string]any{ + "spec": map[string]any{ + "properties": map[string]any{ + "existing": "keep", + }, + }, + }, + } + props := ensureSchemaPath(schema, "spec") + + if props["existing"] != "keep" { + t.Errorf("existing property lost, got %v", props["existing"]) + } + }) + + t.Run("multi-level path", func(t *testing.T) { + schema := map[string]any{} + props := ensureSchemaPath(schema, "spec", "nested") + + props["deep"] = true + + got := schema["properties"].(map[string]any)["spec"].(map[string]any)["properties"].(map[string]any)["nested"].(map[string]any)["properties"].(map[string]any)["deep"] + if got != true { + t.Errorf("expected true, got %v", got) + } + }) +} From 4387a3e95f7304bf9c1952d21d4181bb314ecb67 Mon Sep 17 00:00:00 2001 From: Kirill Ilin Date: Thu, 19 Feb 2026 15:58:18 +0500 Subject: [PATCH 112/666] fix(dashboard): patch FormListInput to fix value binding The Flex wrapper between ResetedFormItem and Select prevented Ant Design's Form.Item from injecting value/onChange into the Select, causing the dropdown to appear empty even when the form store had a value. Move Flex outside ResetedFormItem so Select is its direct child. Signed-off-by: Kirill Ilin --- .../patches/formlistinput-value-binding.diff | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 packages/system/dashboard/images/openapi-ui/openapi-k8s-toolkit/patches/formlistinput-value-binding.diff diff --git a/packages/system/dashboard/images/openapi-ui/openapi-k8s-toolkit/patches/formlistinput-value-binding.diff b/packages/system/dashboard/images/openapi-ui/openapi-k8s-toolkit/patches/formlistinput-value-binding.diff new file mode 100644 index 00000000..0da12c00 --- /dev/null +++ b/packages/system/dashboard/images/openapi-ui/openapi-k8s-toolkit/patches/formlistinput-value-binding.diff @@ -0,0 +1,49 @@ +diff --git a/src/components/molecules/BlackholeForm/molecules/FormListInput/FormListInput.tsx b/src/components/molecules/BlackholeForm/molecules/FormListInput/FormListInput.tsx +index d5e5230..9038dbb 100644 +--- a/src/components/molecules/BlackholeForm/molecules/FormListInput/FormListInput.tsx ++++ b/src/components/molecules/BlackholeForm/molecules/FormListInput/FormListInput.tsx +@@ -259,14 +259,15 @@ export const FormListInput: FC = ({ + + + +- +- ++ ++ + = ({ - - - -- -- -+ -+ - ++ ++ ++ ++
++
++ lock ++ ++ ++
++
++
++
++ ++
++
++ ++ ++ ++ ++ ++ ++ ++{{ end }} From 175b3badd2642a21d249fcac590f807b66a6fa2c Mon Sep 17 00:00:00 2001 From: IvanHunters Date: Wed, 4 Mar 2026 18:30:49 +0300 Subject: [PATCH 212/666] feat(bucket): make credentials and basic auth conditional on users deployment.yaml: use s3._namespace.host for ENDPOINT instead of secret ref, inject ACCESS_KEY_ID/SECRET_ACCESS_KEY only when users exist. Without users, s3manager starts in login mode. ingress.yaml: nginx basic auth annotations only when users exist. Without users, s3manager handles authentication via its login form. Co-Authored-By: Claude Signed-off-by: IvanHunters --- packages/system/bucket/templates/deployment.yaml | 8 ++++---- packages/system/bucket/templates/ingress.yaml | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/system/bucket/templates/deployment.yaml b/packages/system/bucket/templates/deployment.yaml index 7e115de0..086a6934 100644 --- a/packages/system/bucket/templates/deployment.yaml +++ b/packages/system/bucket/templates/deployment.yaml @@ -1,3 +1,4 @@ +{{- $users := keys .Values.users | sortAlpha }} apiVersion: apps/v1 kind: Deployment metadata: @@ -17,12 +18,10 @@ spec: image: "{{ $.Files.Get "images/s3manager.tag" | trim }}" env: - name: ENDPOINT - valueFrom: - secretKeyRef: - name: {{ .Values.bucketName }}-credentials - key: endpoint + value: "s3.{{ .Values._namespace.host }}" - name: SKIP_SSL_VERIFICATION value: "true" + {{- if $users }} - name: ACCESS_KEY_ID valueFrom: secretKeyRef: @@ -33,3 +32,4 @@ spec: secretKeyRef: name: {{ .Values.bucketName }}-credentials key: secretKey + {{- end }} diff --git a/packages/system/bucket/templates/ingress.yaml b/packages/system/bucket/templates/ingress.yaml index 9ad9f46d..e54fede3 100644 --- a/packages/system/bucket/templates/ingress.yaml +++ b/packages/system/bucket/templates/ingress.yaml @@ -2,15 +2,18 @@ {{- $ingress := .Values._namespace.ingress }} {{- $solver := (index .Values._cluster "solver") | default "http01" }} {{- $clusterIssuer := (index .Values._cluster "issuer-name") | default "letsencrypt-prod" }} +{{- $users := keys .Values.users | sortAlpha }} apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: {{ .Values.bucketName }}-ui annotations: + {{- if $users }} nginx.ingress.kubernetes.io/auth-type: "basic" nginx.ingress.kubernetes.io/auth-secret: "{{ .Values.bucketName }}-ui-auth" nginx.ingress.kubernetes.io/auth-realm: "Authentication Required" + {{- end }} nginx.ingress.kubernetes.io/proxy-body-size: "0" nginx.ingress.kubernetes.io/proxy-read-timeout: "99999" nginx.ingress.kubernetes.io/proxy-send-timeout: "99999" From 84da47a2ce5494cab5ddefe6c3922a53201635a1 Mon Sep 17 00:00:00 2001 From: IvanHunters Date: Wed, 4 Mar 2026 18:47:51 +0300 Subject: [PATCH 213/666] refactor(bucket): replace basic auth with s3manager login page Remove nginx basic auth and credential secret injection from the bucket Helm chart. s3manager now always starts in login mode and handles authentication via its own login page with encrypted session cookies. This eliminates the dependency on the -credentials and -ui-auth secrets for the UI layer. Co-Authored-By: Claude Signed-off-by: IvanHunters --- .../bucket/images/s3manager/cozystack.patch | 648 ++++++++++-------- .../system/bucket/templates/deployment.yaml | 13 - packages/system/bucket/templates/ingress.yaml | 6 - packages/system/bucket/templates/secret.yaml | 32 +- 4 files changed, 353 insertions(+), 346 deletions(-) diff --git a/packages/system/bucket/images/s3manager/cozystack.patch b/packages/system/bucket/images/s3manager/cozystack.patch index 9e98edde..2f569042 100644 --- a/packages/system/bucket/images/s3manager/cozystack.patch +++ b/packages/system/bucket/images/s3manager/cozystack.patch @@ -23,187 +23,29 @@ index b5d8540..6ede8e8 100644 github.com/hashicorp/hcl v1.0.0 // indirect github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/cpuid/v2 v2.2.8 // indirect -diff --git a/internal/app/s3manager/auth.go b/internal/app/s3manager/auth.go -new file mode 100644 -index 0000000..a315cde ---- /dev/null -+++ b/internal/app/s3manager/auth.go -@@ -0,0 +1,173 @@ -+package s3manager -+ -+import ( -+ "context" -+ "crypto/rand" -+ "crypto/tls" -+ "fmt" -+ "html/template" -+ "io/fs" -+ "net/http" -+ -+ "github.com/gorilla/sessions" -+ "github.com/minio/minio-go/v7" -+ "github.com/minio/minio-go/v7/pkg/credentials" -+) -+ -+type contextKey string -+ -+const s3ContextKey contextKey = "s3client" -+ -+// S3FromContext retrieves the S3 client from the request context. -+func S3FromContext(ctx context.Context) S3 { -+ s3, _ := ctx.Value(s3ContextKey).(S3) -+ return s3 -+} -+ -+func contextWithS3(ctx context.Context, s3 S3) context.Context { -+ return context.WithValue(ctx, s3ContextKey, s3) -+} -+ -+// NewS3Client creates a new minio S3 client with the given credentials. -+func NewS3Client(endpoint, accessKey, secretKey string, useSSL, skipVerify bool) (*minio.Client, error) { -+ opts := &minio.Options{ -+ Creds: credentials.NewStaticV4(accessKey, secretKey, ""), -+ Secure: useSSL, -+ } -+ if useSSL && skipVerify { -+ opts.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} //nolint:gosec -+ } -+ return minio.New(endpoint, opts) -+} -+ -+// NewSessionStore creates a new encrypted cookie store with a random key. -+func NewSessionStore() *sessions.CookieStore { -+ key := make([]byte, 32) -+ if _, err := rand.Read(key); err != nil { -+ panic(fmt.Sprintf("failed to generate session key: %v", err)) -+ } -+ store := sessions.NewCookieStore(key) -+ store.Options = &sessions.Options{ -+ Path: "/", -+ MaxAge: 86400, // 24 hours -+ HttpOnly: true, -+ Secure: true, -+ SameSite: http.SameSiteLaxMode, -+ } -+ return store -+} -+ -+// RequireAuth is middleware that checks for valid S3 credentials in the session. -+func RequireAuth(store *sessions.CookieStore, endpoint string, useSSL, skipVerify bool) func(http.Handler) http.Handler { -+ return func(next http.Handler) http.Handler { -+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { -+ session, err := store.Get(r, "s3session") -+ if err != nil { -+ http.Redirect(w, r, "/login", http.StatusFound) -+ return -+ } -+ -+ accessKey, ok1 := session.Values["accessKey"].(string) -+ secretKey, ok2 := session.Values["secretKey"].(string) -+ if !ok1 || !ok2 || accessKey == "" || secretKey == "" { -+ http.Redirect(w, r, "/login", http.StatusFound) -+ return -+ } -+ -+ s3Client, err := NewS3Client(endpoint, accessKey, secretKey, useSSL, skipVerify) -+ if err != nil { -+ http.Redirect(w, r, "/login?error=invalid_credentials", http.StatusFound) -+ return -+ } -+ -+ ctx := contextWithS3(r.Context(), s3Client) -+ next.ServeHTTP(w, r.WithContext(ctx)) -+ }) -+ } -+} -+ -+// HandleLoginView renders the login page. -+func HandleLoginView(templates fs.FS) http.HandlerFunc { -+ return func(w http.ResponseWriter, r *http.Request) { -+ data := struct { -+ Error string -+ }{ -+ Error: r.URL.Query().Get("error"), -+ } -+ t, err := template.ParseFS(templates, "layout.html.tmpl", "login.html.tmpl") -+ if err != nil { -+ handleHTTPError(w, fmt.Errorf("error parsing login template: %w", err)) -+ return -+ } -+ err = t.ExecuteTemplate(w, "layout", data) -+ if err != nil { -+ handleHTTPError(w, fmt.Errorf("error executing login template: %w", err)) -+ return -+ } -+ } -+} -+ -+// HandleLogin processes login form submission. -+func HandleLogin(store *sessions.CookieStore, endpoint string, useSSL, skipVerify bool) http.HandlerFunc { -+ return func(w http.ResponseWriter, r *http.Request) { -+ accessKey := r.FormValue("accessKey") -+ secretKey := r.FormValue("secretKey") -+ -+ if accessKey == "" || secretKey == "" { -+ http.Redirect(w, r, "/login?error=credentials_required", http.StatusFound) -+ return -+ } -+ -+ // Validate credentials by attempting to list buckets -+ s3Client, err := NewS3Client(endpoint, accessKey, secretKey, useSSL, skipVerify) -+ if err != nil { -+ http.Redirect(w, r, "/login?error=invalid_credentials", http.StatusFound) -+ return -+ } -+ -+ _, err = s3Client.ListBuckets(r.Context()) -+ if err != nil { -+ http.Redirect(w, r, "/login?error=invalid_credentials", http.StatusFound) -+ return -+ } -+ -+ session, err := store.Get(r, "s3session") -+ if err != nil { -+ http.Redirect(w, r, "/login?error=session_error", http.StatusFound) -+ return -+ } -+ -+ session.Values["accessKey"] = accessKey -+ session.Values["secretKey"] = secretKey -+ if err := session.Save(r, w); err != nil { -+ http.Redirect(w, r, "/login?error=session_error", http.StatusFound) -+ return -+ } -+ -+ http.Redirect(w, r, "/buckets", http.StatusFound) -+ } -+} -+ -+// HandleLogout destroys the session and redirects to login. -+func HandleLogout(store *sessions.CookieStore) http.HandlerFunc { -+ return func(w http.ResponseWriter, r *http.Request) { -+ session, err := store.Get(r, "s3session") -+ if err == nil { -+ session.Options.MaxAge = -1 -+ _ = session.Save(r, w) -+ } -+ http.Redirect(w, r, "/login", http.StatusFound) -+ } -+} -+ -+// DynamicS3Handler wraps a handler factory that takes S3 as its only parameter. -+func DynamicS3Handler(factory func(S3) http.HandlerFunc) http.HandlerFunc { -+ return func(w http.ResponseWriter, r *http.Request) { -+ s3 := S3FromContext(r.Context()) -+ if s3 == nil { -+ http.Redirect(w, r, "/login", http.StatusFound) -+ return -+ } -+ factory(s3).ServeHTTP(w, r) -+ } -+} +diff --git a/go.sum b/go.sum +index 1ea1b16..d7866ce 100644 +--- a/go.sum ++++ b/go.sum +@@ -16,10 +16,16 @@ github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= + github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= + github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= + github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= ++github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= ++github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= + github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= + github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= + github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= + github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= ++github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= ++github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= ++github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= ++github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= + github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= + github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= + github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= diff --git a/main.go b/main.go -index 2ffe8ab..5fdc4cd 100644 +index 2ffe8ab..723a1b8 100644 --- a/main.go +++ b/main.go @@ -41,10 +41,12 @@ type configuration struct { @@ -219,7 +61,7 @@ index 2ffe8ab..5fdc4cd 100644 viper.AutomaticEnv() -@@ -57,13 +59,11 @@ func parseConfiguration() configuration { +@@ -57,13 +59,10 @@ func parseConfiguration() configuration { iamEndpoint = viper.GetString("IAM_ENDPOINT") } else { accessKeyID = viper.GetString("ACCESS_KEY_ID") @@ -230,14 +72,13 @@ index 2ffe8ab..5fdc4cd 100644 secretAccessKey = viper.GetString("SECRET_ACCESS_KEY") - if len(secretAccessKey) == 0 { - log.Fatal("please provide SECRET_ACCESS_KEY") -+ + if len(accessKeyID) == 0 || len(secretAccessKey) == 0 { + log.Println("ACCESS_KEY_ID or SECRET_ACCESS_KEY not set, starting in login mode") + loginMode = true } } -@@ -115,6 +115,7 @@ func parseConfiguration() configuration { +@@ -115,6 +114,7 @@ func parseConfiguration() configuration { Timeout: timeout, SseType: sseType, SseKey: sseKey, @@ -245,7 +86,7 @@ index 2ffe8ab..5fdc4cd 100644 } } -@@ -135,57 +136,103 @@ func main() { +@@ -135,57 +135,96 @@ func main() { log.Fatal(err) } @@ -255,54 +96,7 @@ index 2ffe8ab..5fdc4cd 100644 - } - if configuration.UseIam { - opts.Creds = credentials.NewIAM(configuration.IamEndpoint) -+ // Set up router -+ r := mux.NewRouter() -+ r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.FS(statics)))).Methods(http.MethodGet) -+ -+ if configuration.LoginMode { -+ // Login mode: no pre-configured S3 client, use per-session credentials -+ store := s3manager.NewSessionStore() -+ authMiddleware := s3manager.RequireAuth(store, configuration.Endpoint, configuration.UseSSL, configuration.SkipSSLVerification) -+ -+ // Public routes -+ r.Handle("/login", s3manager.HandleLoginView(templates)).Methods(http.MethodGet) -+ r.Handle("/login", s3manager.HandleLogin(store, configuration.Endpoint, configuration.UseSSL, configuration.SkipSSLVerification)).Methods(http.MethodPost) -+ r.Handle("/logout", s3manager.HandleLogout(store)).Methods(http.MethodPost) -+ -+ // Protected routes - use dynamic S3 client from session -+ protected := r.PathPrefix("/").Subrouter() -+ protected.Use(authMiddleware) -+ -+ protected.Handle("/", http.RedirectHandler("/buckets", http.StatusPermanentRedirect)).Methods(http.MethodGet) -+ protected.Handle("/buckets", s3manager.DynamicS3Handler(func(s3 s3manager.S3) http.HandlerFunc { -+ return s3manager.HandleBucketsView(s3, templates, configuration.AllowDelete) -+ })).Methods(http.MethodGet) -+ protected.PathPrefix("/buckets/").Handler(s3manager.DynamicS3Handler(func(s3 s3manager.S3) http.HandlerFunc { -+ return s3manager.HandleBucketView(s3, templates, configuration.AllowDelete, configuration.ListRecursive) -+ })).Methods(http.MethodGet) -+ protected.Handle("/api/buckets", s3manager.DynamicS3Handler(func(s3 s3manager.S3) http.HandlerFunc { -+ return s3manager.HandleCreateBucket(s3) -+ })).Methods(http.MethodPost) -+ if configuration.AllowDelete { -+ protected.Handle("/api/buckets/{bucketName}", s3manager.DynamicS3Handler(func(s3 s3manager.S3) http.HandlerFunc { -+ return s3manager.HandleDeleteBucket(s3) -+ })).Methods(http.MethodDelete) -+ } -+ protected.Handle("/api/buckets/{bucketName}/objects", s3manager.DynamicS3Handler(func(s3 s3manager.S3) http.HandlerFunc { -+ return s3manager.HandleCreateObject(s3, sseType) -+ })).Methods(http.MethodPost) -+ protected.Handle("/api/buckets/{bucketName}/objects/{objectName:.*}/url", s3manager.DynamicS3Handler(func(s3 s3manager.S3) http.HandlerFunc { -+ return s3manager.HandleGenerateUrl(s3) -+ })).Methods(http.MethodGet) -+ protected.Handle("/api/buckets/{bucketName}/objects/{objectName:.*}", s3manager.DynamicS3Handler(func(s3 s3manager.S3) http.HandlerFunc { -+ return s3manager.HandleGetObject(s3, configuration.ForceDownload) -+ })).Methods(http.MethodGet) -+ if configuration.AllowDelete { -+ protected.Handle("/api/buckets/{bucketName}/objects/{objectName:.*}", s3manager.DynamicS3Handler(func(s3 s3manager.S3) http.HandlerFunc { -+ return s3manager.HandleDeleteObject(s3) -+ })).Methods(http.MethodDelete) -+ } - } else { +- } else { - var signatureType credentials.SignatureType - - switch configuration.SignatureType { @@ -316,6 +110,59 @@ index 2ffe8ab..5fdc4cd 100644 - signatureType = credentials.SignatureAnonymous - default: - log.Fatalf("Invalid SIGNATURE_TYPE: %s", configuration.SignatureType) ++ // Set up router ++ r := mux.NewRouter() ++ r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.FS(statics)))).Methods(http.MethodGet) ++ ++ if configuration.LoginMode { ++ // Login mode: no pre-configured S3 client, per-session credentials ++ sessionCfg := &s3manager.SessionConfig{ ++ Store: s3manager.NewSessionStore(), ++ Endpoint: configuration.Endpoint, ++ UseSSL: configuration.UseSSL, ++ SkipSSLVerify: configuration.SkipSSLVerification, ++ AllowDelete: configuration.AllowDelete, ++ ForceDownload: configuration.ForceDownload, ++ ListRecursive: configuration.ListRecursive, ++ SseInfo: sseType, ++ Templates: templates, + } + +- opts.Creds = credentials.NewStatic(configuration.AccessKeyID, configuration.SecretAccessKey, "", signatureType) +- } ++ // Public routes (no auth required) ++ r.Handle("/login", s3manager.HandleLoginView(templates)).Methods(http.MethodGet) ++ r.Handle("/login", s3manager.HandleLogin(sessionCfg)).Methods(http.MethodPost) ++ r.Handle("/logout", s3manager.HandleLogout(sessionCfg)).Methods(http.MethodPost) ++ ++ // Protected routes (auth required via middleware) ++ protected := mux.NewRouter() ++ protected.Handle("/", http.RedirectHandler("/buckets", http.StatusPermanentRedirect)).Methods(http.MethodGet) ++ protected.Handle("/buckets", s3manager.HandleBucketsViewDynamic(templates, configuration.AllowDelete)).Methods(http.MethodGet) ++ protected.PathPrefix("/buckets/").Handler(s3manager.HandleBucketViewDynamic(templates, configuration.AllowDelete, configuration.ListRecursive)).Methods(http.MethodGet) ++ protected.Handle("/api/buckets", s3manager.HandleCreateBucketDynamic()).Methods(http.MethodPost) ++ if configuration.AllowDelete { ++ protected.Handle("/api/buckets/{bucketName}", s3manager.HandleDeleteBucketDynamic()).Methods(http.MethodDelete) ++ } ++ protected.Handle("/api/buckets/{bucketName}/objects", s3manager.HandleCreateObjectDynamic(sseType)).Methods(http.MethodPost) ++ protected.Handle("/api/buckets/{bucketName}/objects/{objectName:.*}/url", s3manager.HandleGenerateUrlDynamic()).Methods(http.MethodGet) ++ protected.Handle("/api/buckets/{bucketName}/objects/{objectName:.*}", s3manager.HandleGetObjectDynamic(configuration.ForceDownload)).Methods(http.MethodGet) ++ if configuration.AllowDelete { ++ protected.Handle("/api/buckets/{bucketName}/objects/{objectName:.*}", s3manager.HandleDeleteObjectDynamic()).Methods(http.MethodDelete) ++ } + +- if configuration.Region != "" { +- opts.Region = configuration.Region +- } +- if configuration.UseSSL && configuration.SkipSSLVerification { +- opts.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} //nolint:gosec +- } +- s3, err := minio.New(configuration.Endpoint, opts) +- if err != nil { +- log.Fatalln(fmt.Errorf("error creating s3 client: %w", err)) +- } ++ r.PathPrefix("/").Handler(s3manager.RequireAuth(sessionCfg, protected)) ++ } else { + // Pre-configured mode: existing behavior with static S3 client + opts := &minio.Options{ + Secure: configuration.UseSSL, @@ -339,30 +186,6 @@ index 2ffe8ab..5fdc4cd 100644 + } + + opts.Creds = credentials.NewStatic(configuration.AccessKeyID, configuration.SecretAccessKey, "", signatureType) - } - -- opts.Creds = credentials.NewStatic(configuration.AccessKeyID, configuration.SecretAccessKey, "", signatureType) -- } -- -- if configuration.Region != "" { -- opts.Region = configuration.Region -- } -- if configuration.UseSSL && configuration.SkipSSLVerification { -- opts.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} //nolint:gosec -- } -- s3, err := minio.New(configuration.Endpoint, opts) -- if err != nil { -- log.Fatalln(fmt.Errorf("error creating s3 client: %w", err)) -- } -+ if configuration.Region != "" { -+ opts.Region = configuration.Region -+ } -+ if configuration.UseSSL && configuration.SkipSSLVerification { -+ opts.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} //nolint:gosec -+ } -+ s3, err := minio.New(configuration.Endpoint, opts) -+ if err != nil { -+ log.Fatalln(fmt.Errorf("error creating s3 client: %w", err)) + } - // Set up router @@ -380,6 +203,17 @@ index 2ffe8ab..5fdc4cd 100644 - r.Handle("/api/buckets/{bucketName}/objects/{objectName:.*}", s3manager.HandleGetObject(s3, configuration.ForceDownload)).Methods(http.MethodGet) - if configuration.AllowDelete { - r.Handle("/api/buckets/{bucketName}/objects/{objectName:.*}", s3manager.HandleDeleteObject(s3)).Methods(http.MethodDelete) ++ if configuration.Region != "" { ++ opts.Region = configuration.Region ++ } ++ if configuration.UseSSL && configuration.SkipSSLVerification { ++ opts.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} //nolint:gosec ++ } ++ s3, err := minio.New(configuration.Endpoint, opts) ++ if err != nil { ++ log.Fatalln(fmt.Errorf("error creating s3 client: %w", err)) ++ } ++ + r.Handle("/", http.RedirectHandler("/buckets", http.StatusPermanentRedirect)).Methods(http.MethodGet) + r.Handle("/buckets", s3manager.HandleBucketsView(s3, templates, configuration.AllowDelete)).Methods(http.MethodGet) + r.PathPrefix("/buckets/").Handler(s3manager.HandleBucketView(s3, templates, configuration.AllowDelete, configuration.ListRecursive)).Methods(http.MethodGet) @@ -422,12 +256,255 @@ index c7ea184..fb1dce7 100644 +diff --git a/internal/app/s3manager/auth.go b/internal/app/s3manager/auth.go +new file mode 100644 +index 0000000..58589e2 +--- /dev/null ++++ b/internal/app/s3manager/auth.go +@@ -0,0 +1,237 @@ ++package s3manager ++ ++import ( ++ "context" ++ "crypto/rand" ++ "crypto/tls" ++ "fmt" ++ "html/template" ++ "io/fs" ++ "log" ++ "net/http" ++ ++ "github.com/gorilla/sessions" ++ "github.com/minio/minio-go/v7" ++ "github.com/minio/minio-go/v7/pkg/credentials" ++) ++ ++type contextKey string ++ ++const s3ContextKey contextKey = "s3client" ++ ++// SessionConfig holds session store and S3 connection settings for login mode. ++type SessionConfig struct { ++ Store *sessions.CookieStore ++ Endpoint string ++ UseSSL bool ++ SkipSSLVerify bool ++ AllowDelete bool ++ ForceDownload bool ++ ListRecursive bool ++ SseInfo SSEType ++ Templates fs.FS ++} ++ ++// NewSessionStore creates a CookieStore with a random encryption key. ++func NewSessionStore() *sessions.CookieStore { ++ key := make([]byte, 32) ++ if _, err := rand.Read(key); err != nil { ++ log.Fatal("failed to generate session key:", err) ++ } ++ store := sessions.NewCookieStore(key) ++ store.Options = &sessions.Options{ ++ Path: "/", ++ MaxAge: 86400, ++ HttpOnly: true, ++ Secure: true, ++ SameSite: http.SameSiteLaxMode, ++ } ++ return store ++} ++ ++// NewS3Client creates a minio client from user-provided credentials. ++func NewS3Client(endpoint, accessKey, secretKey string, useSSL, skipSSLVerify bool) (*minio.Client, error) { ++ opts := &minio.Options{ ++ Creds: credentials.NewStaticV4(accessKey, secretKey, ""), ++ Secure: useSSL, ++ } ++ if useSSL && skipSSLVerify { ++ opts.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} //nolint:gosec ++ } ++ return minio.New(endpoint, opts) ++} ++ ++// S3FromContext retrieves the S3 client stored in request context. ++func S3FromContext(ctx context.Context) S3 { ++ if s3, ok := ctx.Value(s3ContextKey).(S3); ok { ++ return s3 ++ } ++ return nil ++} ++ ++func contextWithS3(ctx context.Context, s3 S3) context.Context { ++ return context.WithValue(ctx, s3ContextKey, s3) ++} ++ ++// RequireAuth is middleware that validates session credentials and injects ++// an S3 client into the request context. Redirects to /login if no session. ++func RequireAuth(cfg *SessionConfig, next http.Handler) http.Handler { ++ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ++ session, _ := cfg.Store.Get(r, "s3session") ++ accessKey, ok1 := session.Values["accessKey"].(string) ++ secretKey, ok2 := session.Values["secretKey"].(string) ++ if !ok1 || !ok2 || accessKey == "" || secretKey == "" { ++ http.Redirect(w, r, "/login", http.StatusFound) ++ return ++ } ++ ++ s3, err := NewS3Client(cfg.Endpoint, accessKey, secretKey, cfg.UseSSL, cfg.SkipSSLVerify) ++ if err != nil { ++ // Session has bad credentials — clear and redirect to login ++ session.Options.MaxAge = -1 ++ _ = session.Save(r, w) ++ http.Redirect(w, r, "/login", http.StatusFound) ++ return ++ } ++ ++ ctx := contextWithS3(r.Context(), s3) ++ next.ServeHTTP(w, r.WithContext(ctx)) ++ }) ++} ++ ++// HandleLoginView renders the login page. ++func HandleLoginView(templates fs.FS) http.HandlerFunc { ++ return func(w http.ResponseWriter, r *http.Request) { ++ errorMsg := r.URL.Query().Get("error") ++ ++ data := struct { ++ Error string ++ }{ ++ Error: errorMsg, ++ } ++ ++ t, err := template.ParseFS(templates, "layout.html.tmpl", "login.html.tmpl") ++ if err != nil { ++ handleHTTPError(w, fmt.Errorf("error parsing login template: %w", err)) ++ return ++ } ++ err = t.ExecuteTemplate(w, "layout", data) ++ if err != nil { ++ handleHTTPError(w, fmt.Errorf("error executing login template: %w", err)) ++ return ++ } ++ } ++} ++ ++// HandleLogin processes the login form POST. ++func HandleLogin(cfg *SessionConfig) http.HandlerFunc { ++ return func(w http.ResponseWriter, r *http.Request) { ++ accessKey := r.FormValue("accessKey") ++ secretKey := r.FormValue("secretKey") ++ ++ if accessKey == "" || secretKey == "" { ++ http.Redirect(w, r, "/login?error=credentials+required", http.StatusFound) ++ return ++ } ++ ++ // Validate credentials by attempting ListBuckets ++ s3, err := NewS3Client(cfg.Endpoint, accessKey, secretKey, cfg.UseSSL, cfg.SkipSSLVerify) ++ if err != nil { ++ http.Redirect(w, r, "/login?error=connection+failed", http.StatusFound) ++ return ++ } ++ _, err = s3.ListBuckets(r.Context()) ++ if err != nil { ++ http.Redirect(w, r, "/login?error=invalid+credentials", http.StatusFound) ++ return ++ } ++ ++ // Save credentials to session ++ session, _ := cfg.Store.Get(r, "s3session") ++ session.Values["accessKey"] = accessKey ++ session.Values["secretKey"] = secretKey ++ err = session.Save(r, w) ++ if err != nil { ++ handleHTTPError(w, fmt.Errorf("error saving session: %w", err)) ++ return ++ } ++ ++ http.Redirect(w, r, "/buckets", http.StatusFound) ++ } ++} ++ ++// HandleLogout destroys the session and redirects to login. ++func HandleLogout(cfg *SessionConfig) http.HandlerFunc { ++ return func(w http.ResponseWriter, r *http.Request) { ++ session, _ := cfg.Store.Get(r, "s3session") ++ session.Options.MaxAge = -1 ++ _ = session.Save(r, w) ++ http.Redirect(w, r, "/login", http.StatusFound) ++ } ++} ++ ++// Dynamic handler wrappers — extract S3 from context, delegate to original handlers. ++ ++// HandleBucketsViewDynamic wraps HandleBucketsView for login mode. ++func HandleBucketsViewDynamic(templates fs.FS, allowDelete bool) http.HandlerFunc { ++ return func(w http.ResponseWriter, r *http.Request) { ++ s3 := S3FromContext(r.Context()) ++ HandleBucketsView(s3, templates, allowDelete).ServeHTTP(w, r) ++ } ++} ++ ++// HandleBucketViewDynamic wraps HandleBucketView for login mode. ++func HandleBucketViewDynamic(templates fs.FS, allowDelete bool, listRecursive bool) http.HandlerFunc { ++ return func(w http.ResponseWriter, r *http.Request) { ++ s3 := S3FromContext(r.Context()) ++ HandleBucketView(s3, templates, allowDelete, listRecursive).ServeHTTP(w, r) ++ } ++} ++ ++// HandleCreateBucketDynamic wraps HandleCreateBucket for login mode. ++func HandleCreateBucketDynamic() http.HandlerFunc { ++ return func(w http.ResponseWriter, r *http.Request) { ++ s3 := S3FromContext(r.Context()) ++ HandleCreateBucket(s3).ServeHTTP(w, r) ++ } ++} ++ ++// HandleDeleteBucketDynamic wraps HandleDeleteBucket for login mode. ++func HandleDeleteBucketDynamic() http.HandlerFunc { ++ return func(w http.ResponseWriter, r *http.Request) { ++ s3 := S3FromContext(r.Context()) ++ HandleDeleteBucket(s3).ServeHTTP(w, r) ++ } ++} ++ ++// HandleCreateObjectDynamic wraps HandleCreateObject for login mode. ++func HandleCreateObjectDynamic(sseInfo SSEType) http.HandlerFunc { ++ return func(w http.ResponseWriter, r *http.Request) { ++ s3 := S3FromContext(r.Context()) ++ HandleCreateObject(s3, sseInfo).ServeHTTP(w, r) ++ } ++} ++ ++// HandleGenerateUrlDynamic wraps HandleGenerateUrl for login mode. ++func HandleGenerateUrlDynamic() http.HandlerFunc { ++ return func(w http.ResponseWriter, r *http.Request) { ++ s3 := S3FromContext(r.Context()) ++ HandleGenerateUrl(s3).ServeHTTP(w, r) ++ } ++} ++ ++// HandleGetObjectDynamic wraps HandleGetObject for login mode. ++func HandleGetObjectDynamic(forceDownload bool) http.HandlerFunc { ++ return func(w http.ResponseWriter, r *http.Request) { ++ s3 := S3FromContext(r.Context()) ++ HandleGetObject(s3, forceDownload).ServeHTTP(w, r) ++ } ++} ++ ++// HandleDeleteObjectDynamic wraps HandleDeleteObject for login mode. ++func HandleDeleteObjectDynamic() http.HandlerFunc { ++ return func(w http.ResponseWriter, r *http.Request) { ++ s3 := S3FromContext(r.Context()) ++ HandleDeleteObject(s3).ServeHTTP(w, r) ++ } ++} diff --git a/web/template/login.html.tmpl b/web/template/login.html.tmpl new file mode 100644 -index 0000000..7a1730a +index 0000000..f153018 --- /dev/null +++ b/web/template/login.html.tmpl -@@ -0,0 +1,69 @@ +@@ -0,0 +1,46 @@ +{{ define "content" }} +