mirror of
https://github.com/cozystack/cozystack.git
synced 2026-03-04 22:18:54 +00:00
Compare commits
53 Commits
feat/node-
...
feat/cozyc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
488b1bf27b | ||
|
|
f4e9660b43 | ||
|
|
d9cfd5ac9e | ||
|
|
5762ac4139 | ||
|
|
38f446c0d3 | ||
|
|
4de8e91864 | ||
|
|
e3a5933f7b | ||
|
|
7dfb819a9c | ||
|
|
d95ea930b6 | ||
|
|
8dbd6d5167 | ||
|
|
2c372ae378 | ||
|
|
02064888a4 | ||
|
|
4c3766a555 | ||
|
|
7bc93c5045 | ||
|
|
d2f7c9ab82 | ||
|
|
d856775961 | ||
|
|
17c2ea0e9c | ||
|
|
c98b6203a7 | ||
|
|
376e4d1fd3 | ||
|
|
1c05999812 | ||
|
|
356070615c | ||
|
|
4aa1f03321 | ||
|
|
8f1e52690d | ||
|
|
00ab6e792c | ||
|
|
3d89d3732c | ||
|
|
e39ba9fb8c | ||
|
|
5c5a170589 | ||
|
|
a6b498d7ec | ||
|
|
d18e6d1c24 | ||
|
|
8162e3828e | ||
|
|
def8a5c835 | ||
|
|
4e5455c72c | ||
|
|
d4cb47b58b | ||
|
|
4843a617bc | ||
|
|
0738fae56d | ||
|
|
8b9a11360e | ||
|
|
d0a6ddd782 | ||
|
|
8c6c69cdab | ||
|
|
4821f025fc | ||
|
|
dbfdbc8298 | ||
|
|
58e2b646be | ||
|
|
8450830f06 | ||
|
|
0e8b6515af | ||
|
|
655133b81c | ||
|
|
668ddc552e | ||
|
|
7a5eb76b6a | ||
|
|
a6e66a021a | ||
|
|
6437abb35d | ||
|
|
989686624c | ||
|
|
4387a3e95f | ||
|
|
db1425a8de | ||
|
|
16db457536 | ||
|
|
b5b2f95c3e |
34
Makefile
34
Makefile
@@ -1,4 +1,4 @@
|
||||
.PHONY: manifests assets unit-tests helm-unit-tests verify-crds
|
||||
.PHONY: manifests assets unit-tests helm-unit-tests
|
||||
|
||||
include hack/common-envs.mk
|
||||
|
||||
@@ -38,30 +38,30 @@ build: build-deps
|
||||
|
||||
manifests:
|
||||
mkdir -p _out/assets
|
||||
cat packages/core/installer/crds/*.yaml > _out/assets/cozystack-crds.yaml
|
||||
cat internal/crdinstall/manifests/*.yaml > _out/assets/cozystack-crds.yaml
|
||||
# Talos variant (default)
|
||||
helm template installer packages/core/installer -n cozy-system \
|
||||
-s templates/cozystack-operator.yaml \
|
||||
-s templates/packagesource.yaml \
|
||||
--show-only templates/cozystack-operator.yaml \
|
||||
> _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 \
|
||||
--show-only templates/cozystack-operator.yaml \
|
||||
> _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 \
|
||||
--show-only templates/cozystack-operator.yaml \
|
||||
> _out/assets/cozystack-operator-hosted.yaml
|
||||
|
||||
cozypkg:
|
||||
go build -ldflags "-X github.com/cozystack/cozystack/cmd/cozypkg/cmd.Version=v$(COZYSTACK_VERSION)" -o _out/bin/cozypkg ./cmd/cozypkg
|
||||
|
||||
assets: assets-talos assets-cozypkg
|
||||
cozyctl:
|
||||
go build -ldflags "-X github.com/cozystack/cozystack/cmd/cozyctl/cmd.Version=v$(COZYSTACK_VERSION)" -o _out/bin/cozyctl ./cmd/cozyctl
|
||||
|
||||
assets: assets-talos assets-cozypkg assets-cozyctl
|
||||
|
||||
assets-talos:
|
||||
make -C packages/core/talos assets
|
||||
@@ -76,15 +76,21 @@ assets-cozypkg-%:
|
||||
cp LICENSE _out/bin/cozypkg-$*/LICENSE
|
||||
tar -C _out/bin/cozypkg-$* -czf _out/assets/cozypkg-$*.tar.gz LICENSE cozypkg$(EXT)
|
||||
|
||||
assets-cozyctl: assets-cozyctl-linux-amd64 assets-cozyctl-linux-arm64 assets-cozyctl-darwin-amd64 assets-cozyctl-darwin-arm64 assets-cozyctl-windows-amd64 assets-cozyctl-windows-arm64
|
||||
(cd _out/assets/ && sha256sum cozyctl-*.tar.gz) > _out/assets/cozyctl-checksums.txt
|
||||
|
||||
assets-cozyctl-%:
|
||||
$(eval EXT := $(if $(filter windows,$(firstword $(subst -, ,$*))),.exe,))
|
||||
mkdir -p _out/assets
|
||||
GOOS=$(firstword $(subst -, ,$*)) GOARCH=$(lastword $(subst -, ,$*)) go build -ldflags "-X github.com/cozystack/cozystack/cmd/cozyctl/cmd.Version=v$(COZYSTACK_VERSION)" -o _out/bin/cozyctl-$*/cozyctl$(EXT) ./cmd/cozyctl
|
||||
cp LICENSE _out/bin/cozyctl-$*/LICENSE
|
||||
tar -C _out/bin/cozyctl-$* -czf _out/assets/cozyctl-$*.tar.gz LICENSE cozyctl$(EXT)
|
||||
|
||||
test:
|
||||
make -C packages/core/testing apply
|
||||
make -C packages/core/testing test
|
||||
|
||||
verify-crds:
|
||||
@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
|
||||
unit-tests: helm-unit-tests
|
||||
|
||||
helm-unit-tests:
|
||||
hack/helm-unit-tests.sh
|
||||
|
||||
114
cmd/cozyctl/cmd/client.go
Normal file
114
cmd/cozyctl/cmd/client.go
Normal file
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
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 cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/client-go/dynamic"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
func buildRestConfig() (*rest.Config, error) {
|
||||
rules := clientcmd.NewDefaultClientConfigLoadingRules()
|
||||
if globalFlags.kubeconfig != "" {
|
||||
rules.ExplicitPath = globalFlags.kubeconfig
|
||||
}
|
||||
overrides := &clientcmd.ConfigOverrides{}
|
||||
if globalFlags.context != "" {
|
||||
overrides.CurrentContext = globalFlags.context
|
||||
}
|
||||
config, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, overrides).ClientConfig()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load kubeconfig: %w", err)
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func newScheme() *runtime.Scheme {
|
||||
scheme := runtime.NewScheme()
|
||||
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
|
||||
utilruntime.Must(cozyv1alpha1.AddToScheme(scheme))
|
||||
return scheme
|
||||
}
|
||||
|
||||
func newClients() (client.Client, dynamic.Interface, error) {
|
||||
config, err := buildRestConfig()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
scheme := newScheme()
|
||||
|
||||
typedClient, err := client.New(config, client.Options{Scheme: scheme})
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to create k8s client: %w", err)
|
||||
}
|
||||
|
||||
dynClient, err := dynamic.NewForConfig(config)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to create dynamic client: %w", err)
|
||||
}
|
||||
|
||||
return typedClient, dynClient, nil
|
||||
}
|
||||
|
||||
func getNamespace() (string, error) {
|
||||
if globalFlags.namespace != "" {
|
||||
return globalFlags.namespace, nil
|
||||
}
|
||||
|
||||
rules := clientcmd.NewDefaultClientConfigLoadingRules()
|
||||
if globalFlags.kubeconfig != "" {
|
||||
rules.ExplicitPath = globalFlags.kubeconfig
|
||||
}
|
||||
overrides := &clientcmd.ConfigOverrides{}
|
||||
if globalFlags.context != "" {
|
||||
overrides.CurrentContext = globalFlags.context
|
||||
}
|
||||
|
||||
clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, overrides)
|
||||
ns, _, err := clientConfig.Namespace()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to determine namespace: %w", err)
|
||||
}
|
||||
if ns == "" {
|
||||
ns = "default"
|
||||
}
|
||||
return ns, nil
|
||||
}
|
||||
|
||||
// getRestConfig is a convenience function when only the rest.Config is needed
|
||||
// (used by buildRestConfig but also available for other callers).
|
||||
func getRestConfig() (*rest.Config, error) {
|
||||
if globalFlags.kubeconfig != "" || globalFlags.context != "" {
|
||||
return buildRestConfig()
|
||||
}
|
||||
config, err := ctrl.GetConfig()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get kubeconfig: %w", err)
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
43
cmd/cozyctl/cmd/console.go
Normal file
43
cmd/cozyctl/cmd/console.go
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
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 cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var consoleCmd = &cobra.Command{
|
||||
Use: "console <type> <name>",
|
||||
Short: "Open a serial console to a VirtualMachine",
|
||||
Long: `Open a serial console to a VirtualMachine using virtctl. Only valid for VirtualMachine or VMInstance kinds.`,
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: runConsole,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(consoleCmd)
|
||||
}
|
||||
|
||||
func runConsole(cmd *cobra.Command, args []string) error {
|
||||
vmName, ns, err := resolveVMArgs(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
virtctlArgs := []string{"virtctl", "console", vmName, "-n", ns}
|
||||
return execVirtctl(virtctlArgs)
|
||||
}
|
||||
112
cmd/cozyctl/cmd/discovery.go
Normal file
112
cmd/cozyctl/cmd/discovery.go
Normal file
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
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 cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
// AppDefInfo holds resolved information about an ApplicationDefinition.
|
||||
type AppDefInfo struct {
|
||||
Name string // e.g. "postgres"
|
||||
Kind string // e.g. "Postgres"
|
||||
Plural string // e.g. "postgreses"
|
||||
Singular string // e.g. "postgres"
|
||||
Prefix string // e.g. "postgres-"
|
||||
IsModule bool
|
||||
}
|
||||
|
||||
// AppDefRegistry provides fast lookup of ApplicationDefinitions by plural, singular, or kind.
|
||||
type AppDefRegistry struct {
|
||||
byPlural map[string]*AppDefInfo
|
||||
bySingular map[string]*AppDefInfo
|
||||
byKind map[string]*AppDefInfo
|
||||
all []*AppDefInfo
|
||||
}
|
||||
|
||||
// discoverAppDefs lists all ApplicationDefinitions from the cluster and builds a registry.
|
||||
func discoverAppDefs(ctx context.Context, typedClient client.Client) (*AppDefRegistry, error) {
|
||||
var list cozyv1alpha1.ApplicationDefinitionList
|
||||
if err := typedClient.List(ctx, &list); err != nil {
|
||||
return nil, fmt.Errorf("failed to list ApplicationDefinitions: %w", err)
|
||||
}
|
||||
|
||||
reg := &AppDefRegistry{
|
||||
byPlural: make(map[string]*AppDefInfo),
|
||||
bySingular: make(map[string]*AppDefInfo),
|
||||
byKind: make(map[string]*AppDefInfo),
|
||||
}
|
||||
|
||||
for i := range list.Items {
|
||||
ad := &list.Items[i]
|
||||
info := &AppDefInfo{
|
||||
Name: ad.Name,
|
||||
Kind: ad.Spec.Application.Kind,
|
||||
Plural: ad.Spec.Application.Plural,
|
||||
Singular: ad.Spec.Application.Singular,
|
||||
Prefix: ad.Spec.Release.Prefix,
|
||||
IsModule: ad.Spec.Dashboard != nil && ad.Spec.Dashboard.Module,
|
||||
}
|
||||
reg.all = append(reg.all, info)
|
||||
reg.byPlural[strings.ToLower(info.Plural)] = info
|
||||
reg.bySingular[strings.ToLower(info.Singular)] = info
|
||||
reg.byKind[strings.ToLower(info.Kind)] = info
|
||||
}
|
||||
|
||||
return reg, nil
|
||||
}
|
||||
|
||||
// Resolve looks up an AppDefInfo by name (case-insensitive), checking plural, singular, then kind.
|
||||
func (r *AppDefRegistry) Resolve(name string) *AppDefInfo {
|
||||
lower := strings.ToLower(name)
|
||||
if info, ok := r.byPlural[lower]; ok {
|
||||
return info
|
||||
}
|
||||
if info, ok := r.bySingular[lower]; ok {
|
||||
return info
|
||||
}
|
||||
if info, ok := r.byKind[lower]; ok {
|
||||
return info
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResolveModule looks up an AppDefInfo among modules only.
|
||||
func (r *AppDefRegistry) ResolveModule(name string) *AppDefInfo {
|
||||
lower := strings.ToLower(name)
|
||||
for _, info := range r.all {
|
||||
if !info.IsModule {
|
||||
continue
|
||||
}
|
||||
if strings.ToLower(info.Plural) == lower ||
|
||||
strings.ToLower(info.Singular) == lower ||
|
||||
strings.ToLower(info.Kind) == lower {
|
||||
return info
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// All returns all discovered AppDefInfo entries.
|
||||
func (r *AppDefRegistry) All() []*AppDefInfo {
|
||||
return r.all
|
||||
}
|
||||
361
cmd/cozyctl/cmd/get.go
Normal file
361
cmd/cozyctl/cmd/get.go
Normal file
@@ -0,0 +1,361 @@
|
||||
/*
|
||||
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 cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
appsv1alpha1 "github.com/cozystack/cozystack/pkg/apis/apps/v1alpha1"
|
||||
corev1alpha1 "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
var getCmdFlags struct {
|
||||
target string
|
||||
}
|
||||
|
||||
var getCmd = &cobra.Command{
|
||||
Use: "get <type> [name]",
|
||||
Short: "Display one or many resources",
|
||||
Long: `Display one or many resources.
|
||||
|
||||
Built-in types:
|
||||
ns, namespaces Tenant namespaces (cluster-scoped)
|
||||
modules Tenant modules
|
||||
pvc, pvcs PersistentVolumeClaims
|
||||
|
||||
Sub-resource types (use -t to filter by parent application):
|
||||
secrets Secrets
|
||||
services, svc Services
|
||||
ingresses, ing Ingresses
|
||||
workloads WorkloadMonitors
|
||||
|
||||
Application types are discovered dynamically from ApplicationDefinitions.
|
||||
Use -t type/name to filter sub-resources by a specific application.`,
|
||||
Args: cobra.RangeArgs(1, 2),
|
||||
RunE: runGet,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(getCmd)
|
||||
getCmd.Flags().StringVarP(&getCmdFlags.target, "target", "t", "", "Filter sub-resources by application type/name")
|
||||
}
|
||||
|
||||
func runGet(cmd *cobra.Command, args []string) error {
|
||||
ctx := context.Background()
|
||||
resourceType := args[0]
|
||||
var resourceName string
|
||||
if len(args) > 1 {
|
||||
resourceName = args[1]
|
||||
}
|
||||
|
||||
switch strings.ToLower(resourceType) {
|
||||
case "ns", "namespace", "namespaces":
|
||||
return getNamespaces(ctx, resourceName)
|
||||
case "module", "modules":
|
||||
return getModules(ctx, resourceName)
|
||||
case "pvc", "pvcs", "persistentvolumeclaim", "persistentvolumeclaims":
|
||||
return getPVCs(ctx, resourceName)
|
||||
case "secret", "secrets":
|
||||
return getSubResources(ctx, "secrets", resourceName)
|
||||
case "service", "services", "svc":
|
||||
return getSubResources(ctx, "services", resourceName)
|
||||
case "ingress", "ingresses", "ing":
|
||||
return getSubResources(ctx, "ingresses", resourceName)
|
||||
case "workload", "workloads":
|
||||
return getSubResources(ctx, "workloads", resourceName)
|
||||
default:
|
||||
return getApplications(ctx, resourceType, resourceName)
|
||||
}
|
||||
}
|
||||
|
||||
func getNamespaces(ctx context.Context, name string) error {
|
||||
_, dynClient, err := newClients()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gvr := schema.GroupVersionResource{Group: "core.cozystack.io", Version: "v1alpha1", Resource: "tenantnamespaces"}
|
||||
|
||||
if name != "" {
|
||||
item, err := dynClient.Resource(gvr).Get(ctx, name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get namespace %q: %w", name, err)
|
||||
}
|
||||
printNamespaces([]unstructured.Unstructured{*item})
|
||||
return nil
|
||||
}
|
||||
|
||||
list, err := dynClient.Resource(gvr).List(ctx, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list namespaces: %w", err)
|
||||
}
|
||||
if len(list.Items) == 0 {
|
||||
printNoResources(os.Stderr, "namespaces")
|
||||
return nil
|
||||
}
|
||||
printNamespaces(list.Items)
|
||||
return nil
|
||||
}
|
||||
|
||||
func getModules(ctx context.Context, name string) error {
|
||||
_, dynClient, err := newClients()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ns, err := getNamespace()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gvr := schema.GroupVersionResource{Group: "core.cozystack.io", Version: "v1alpha1", Resource: "tenantmodules"}
|
||||
|
||||
if name != "" {
|
||||
item, err := dynClient.Resource(gvr).Namespace(ns).Get(ctx, name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get module %q: %w", name, err)
|
||||
}
|
||||
printModules([]unstructured.Unstructured{*item})
|
||||
return nil
|
||||
}
|
||||
|
||||
list, err := dynClient.Resource(gvr).Namespace(ns).List(ctx, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list modules: %w", err)
|
||||
}
|
||||
if len(list.Items) == 0 {
|
||||
printNoResources(os.Stderr, "modules")
|
||||
return nil
|
||||
}
|
||||
printModules(list.Items)
|
||||
return nil
|
||||
}
|
||||
|
||||
func getPVCs(ctx context.Context, name string) error {
|
||||
_, dynClient, err := newClients()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ns, err := getNamespace()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gvr := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "persistentvolumeclaims"}
|
||||
|
||||
if name != "" {
|
||||
item, err := dynClient.Resource(gvr).Namespace(ns).Get(ctx, name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get PVC %q: %w", name, err)
|
||||
}
|
||||
printPVCs([]unstructured.Unstructured{*item})
|
||||
return nil
|
||||
}
|
||||
|
||||
list, err := dynClient.Resource(gvr).Namespace(ns).List(ctx, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list PVCs: %w", err)
|
||||
}
|
||||
if len(list.Items) == 0 {
|
||||
printNoResources(os.Stderr, "PVCs")
|
||||
return nil
|
||||
}
|
||||
printPVCs(list.Items)
|
||||
return nil
|
||||
}
|
||||
|
||||
func getSubResources(ctx context.Context, subType string, name string) error {
|
||||
typedClient, dynClient, err := newClients()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ns, err := getNamespace()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
labelSelector, err := buildSubResourceSelector(ctx, typedClient, getCmdFlags.target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch subType {
|
||||
case "secrets":
|
||||
return getFilteredSecrets(ctx, dynClient, ns, name, labelSelector)
|
||||
case "services":
|
||||
return getFilteredServices(ctx, dynClient, ns, name, labelSelector)
|
||||
case "ingresses":
|
||||
return getFilteredIngresses(ctx, dynClient, ns, name, labelSelector)
|
||||
case "workloads":
|
||||
return getFilteredWorkloads(ctx, dynClient, ns, name, labelSelector)
|
||||
default:
|
||||
return fmt.Errorf("unknown sub-resource type: %s", subType)
|
||||
}
|
||||
}
|
||||
|
||||
func buildSubResourceSelector(ctx context.Context, typedClient client.Client, target string) (string, error) {
|
||||
var selectors []string
|
||||
|
||||
if target == "" {
|
||||
selectors = append(selectors, corev1alpha1.TenantResourceLabelKey+"="+corev1alpha1.TenantResourceLabelValue)
|
||||
return strings.Join(selectors, ","), nil
|
||||
}
|
||||
|
||||
parts := strings.SplitN(target, "/", 2)
|
||||
if len(parts) != 2 {
|
||||
return "", fmt.Errorf("invalid target format %q, expected type/name", target)
|
||||
}
|
||||
targetType, targetName := parts[0], parts[1]
|
||||
|
||||
// Discover ApplicationDefinitions to resolve the target type
|
||||
registry, err := discoverAppDefs(ctx, typedClient)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Check if this is a module reference
|
||||
if strings.ToLower(targetType) == "module" {
|
||||
info := registry.ResolveModule(targetName)
|
||||
if info == nil {
|
||||
return "", fmt.Errorf("unknown module %q", targetName)
|
||||
}
|
||||
selectors = append(selectors,
|
||||
appsv1alpha1.ApplicationKindLabel+"="+info.Kind,
|
||||
appsv1alpha1.ApplicationNameLabel+"="+targetName,
|
||||
corev1alpha1.TenantResourceLabelKey+"="+corev1alpha1.TenantResourceLabelValue,
|
||||
)
|
||||
return strings.Join(selectors, ","), nil
|
||||
}
|
||||
|
||||
info := registry.Resolve(targetType)
|
||||
if info == nil {
|
||||
return "", fmt.Errorf("unknown application type %q", targetType)
|
||||
}
|
||||
|
||||
selectors = append(selectors,
|
||||
appsv1alpha1.ApplicationKindLabel+"="+info.Kind,
|
||||
appsv1alpha1.ApplicationNameLabel+"="+targetName,
|
||||
corev1alpha1.TenantResourceLabelKey+"="+corev1alpha1.TenantResourceLabelValue,
|
||||
)
|
||||
return strings.Join(selectors, ","), nil
|
||||
}
|
||||
|
||||
func getFilteredSecrets(ctx context.Context, dynClient dynamic.Interface, ns, name, labelSelector string) error {
|
||||
gvr := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "secrets"}
|
||||
return getFilteredResources(ctx, dynClient, gvr, ns, name, labelSelector, "secrets", printSecrets)
|
||||
}
|
||||
|
||||
func getFilteredServices(ctx context.Context, dynClient dynamic.Interface, ns, name, labelSelector string) error {
|
||||
gvr := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "services"}
|
||||
return getFilteredResources(ctx, dynClient, gvr, ns, name, labelSelector, "services", printServices)
|
||||
}
|
||||
|
||||
func getFilteredIngresses(ctx context.Context, dynClient dynamic.Interface, ns, name, labelSelector string) error {
|
||||
gvr := schema.GroupVersionResource{Group: "networking.k8s.io", Version: "v1", Resource: "ingresses"}
|
||||
return getFilteredResources(ctx, dynClient, gvr, ns, name, labelSelector, "ingresses", printIngresses)
|
||||
}
|
||||
|
||||
func getFilteredWorkloads(ctx context.Context, dynClient dynamic.Interface, ns, name, labelSelector string) error {
|
||||
gvr := schema.GroupVersionResource{Group: "cozystack.io", Version: "v1alpha1", Resource: "workloadmonitors"}
|
||||
return getFilteredResources(ctx, dynClient, gvr, ns, name, labelSelector, "workloads", printWorkloads)
|
||||
}
|
||||
|
||||
func getFilteredResources(
|
||||
ctx context.Context,
|
||||
dynClient dynamic.Interface,
|
||||
gvr schema.GroupVersionResource,
|
||||
ns, name, labelSelector string,
|
||||
typeName string,
|
||||
printer func([]unstructured.Unstructured),
|
||||
) error {
|
||||
if name != "" {
|
||||
item, err := dynClient.Resource(gvr).Namespace(ns).Get(ctx, name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get %s %q: %w", typeName, name, err)
|
||||
}
|
||||
printer([]unstructured.Unstructured{*item})
|
||||
return nil
|
||||
}
|
||||
|
||||
list, err := dynClient.Resource(gvr).Namespace(ns).List(ctx, metav1.ListOptions{
|
||||
LabelSelector: labelSelector,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list %s: %w", typeName, err)
|
||||
}
|
||||
if len(list.Items) == 0 {
|
||||
printNoResources(os.Stderr, typeName)
|
||||
return nil
|
||||
}
|
||||
printer(list.Items)
|
||||
return nil
|
||||
}
|
||||
|
||||
func getApplications(ctx context.Context, resourceType, name string) error {
|
||||
typedClient, dynClient, err := newClients()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ns, err := getNamespace()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
registry, err := discoverAppDefs(ctx, typedClient)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
info := registry.Resolve(resourceType)
|
||||
if info == nil {
|
||||
return fmt.Errorf("unknown resource type %q\nUse 'cozyctl get --help' for available types", resourceType)
|
||||
}
|
||||
|
||||
gvr := schema.GroupVersionResource{Group: "apps.cozystack.io", Version: "v1alpha1", Resource: info.Plural}
|
||||
|
||||
if name != "" {
|
||||
item, err := dynClient.Resource(gvr).Namespace(ns).Get(ctx, name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get %s %q: %w", info.Singular, name, err)
|
||||
}
|
||||
printApplications([]unstructured.Unstructured{*item})
|
||||
return nil
|
||||
}
|
||||
|
||||
list, err := dynClient.Resource(gvr).Namespace(ns).List(ctx, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list %s: %w", info.Plural, err)
|
||||
}
|
||||
if len(list.Items) == 0 {
|
||||
printNoResources(os.Stderr, info.Plural)
|
||||
return nil
|
||||
}
|
||||
printApplications(list.Items)
|
||||
return nil
|
||||
}
|
||||
43
cmd/cozyctl/cmd/migrate.go
Normal file
43
cmd/cozyctl/cmd/migrate.go
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
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 cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var migrateCmd = &cobra.Command{
|
||||
Use: "migrate <type> <name>",
|
||||
Short: "Live-migrate a VirtualMachine to another node",
|
||||
Long: `Live-migrate a VirtualMachine to another node using virtctl. Only valid for VirtualMachine or VMInstance kinds.`,
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: runMigrate,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(migrateCmd)
|
||||
}
|
||||
|
||||
func runMigrate(cmd *cobra.Command, args []string) error {
|
||||
vmName, ns, err := resolveVMArgs(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
virtctlArgs := []string{"virtctl", "migrate", vmName, "-n", ns}
|
||||
return execVirtctl(virtctlArgs)
|
||||
}
|
||||
51
cmd/cozyctl/cmd/portforward.go
Normal file
51
cmd/cozyctl/cmd/portforward.go
Normal file
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
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 cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var portForwardCmd = &cobra.Command{
|
||||
Use: "port-forward <type/name> [ports...]",
|
||||
Short: "Forward ports to a VirtualMachineInstance",
|
||||
Long: `Forward ports to a VirtualMachineInstance using virtctl. Only valid for VirtualMachine or VMInstance kinds.`,
|
||||
Args: cobra.MinimumNArgs(2),
|
||||
RunE: runPortForward,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(portForwardCmd)
|
||||
}
|
||||
|
||||
func runPortForward(cmd *cobra.Command, args []string) error {
|
||||
vmName, ns, err := resolveVMArgs(args[:1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ports := args[1:]
|
||||
if len(ports) == 0 {
|
||||
return fmt.Errorf("at least one port is required")
|
||||
}
|
||||
|
||||
virtctlArgs := []string{"virtctl", "port-forward", "vmi/" + vmName, "-n", ns}
|
||||
virtctlArgs = append(virtctlArgs, ports...)
|
||||
return execVirtctl(virtctlArgs)
|
||||
}
|
||||
250
cmd/cozyctl/cmd/printer.go
Normal file
250
cmd/cozyctl/cmd/printer.go
Normal file
@@ -0,0 +1,250 @@
|
||||
/*
|
||||
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 cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
func newTabWriter() *tabwriter.Writer {
|
||||
return tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
|
||||
}
|
||||
|
||||
func printApplications(items []unstructured.Unstructured) {
|
||||
w := newTabWriter()
|
||||
defer w.Flush()
|
||||
|
||||
fmt.Fprintln(w, "NAME\tVERSION\tREADY\tSTATUS")
|
||||
for _, item := range items {
|
||||
name := item.GetName()
|
||||
version, _, _ := unstructured.NestedString(item.Object, "appVersion")
|
||||
ready, status := extractCondition(item)
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", name, version, ready, truncate(status, 48))
|
||||
}
|
||||
}
|
||||
|
||||
func printNamespaces(items []unstructured.Unstructured) {
|
||||
w := newTabWriter()
|
||||
defer w.Flush()
|
||||
|
||||
fmt.Fprintln(w, "NAME")
|
||||
for _, item := range items {
|
||||
fmt.Fprintln(w, item.GetName())
|
||||
}
|
||||
}
|
||||
|
||||
func printModules(items []unstructured.Unstructured) {
|
||||
w := newTabWriter()
|
||||
defer w.Flush()
|
||||
|
||||
fmt.Fprintln(w, "NAME\tVERSION\tREADY\tSTATUS")
|
||||
for _, item := range items {
|
||||
name := item.GetName()
|
||||
version, _, _ := unstructured.NestedString(item.Object, "appVersion")
|
||||
ready, status := extractCondition(item)
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", name, version, ready, truncate(status, 48))
|
||||
}
|
||||
}
|
||||
|
||||
func printPVCs(items []unstructured.Unstructured) {
|
||||
w := newTabWriter()
|
||||
defer w.Flush()
|
||||
|
||||
fmt.Fprintln(w, "NAME\tSTATUS\tVOLUME\tCAPACITY\tSTORAGECLASS")
|
||||
for _, item := range items {
|
||||
name := item.GetName()
|
||||
phase, _, _ := unstructured.NestedString(item.Object, "status", "phase")
|
||||
volume, _, _ := unstructured.NestedString(item.Object, "spec", "volumeName")
|
||||
capacity := ""
|
||||
if cap, ok, _ := unstructured.NestedStringMap(item.Object, "status", "capacity"); ok {
|
||||
capacity = cap["storage"]
|
||||
}
|
||||
sc, _, _ := unstructured.NestedString(item.Object, "spec", "storageClassName")
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", name, phase, volume, capacity, sc)
|
||||
}
|
||||
}
|
||||
|
||||
func printSecrets(items []unstructured.Unstructured) {
|
||||
w := newTabWriter()
|
||||
defer w.Flush()
|
||||
|
||||
fmt.Fprintln(w, "NAME\tTYPE\tDATA")
|
||||
for _, item := range items {
|
||||
name := item.GetName()
|
||||
secretType, _, _ := unstructured.NestedString(item.Object, "type")
|
||||
data, _, _ := unstructured.NestedMap(item.Object, "data")
|
||||
fmt.Fprintf(w, "%s\t%s\t%d\n", name, secretType, len(data))
|
||||
}
|
||||
}
|
||||
|
||||
func printServices(items []unstructured.Unstructured) {
|
||||
w := newTabWriter()
|
||||
defer w.Flush()
|
||||
|
||||
fmt.Fprintln(w, "NAME\tTYPE\tCLUSTER-IP\tEXTERNAL-IP\tPORTS")
|
||||
for _, item := range items {
|
||||
name := item.GetName()
|
||||
svcType, _, _ := unstructured.NestedString(item.Object, "spec", "type")
|
||||
clusterIP, _, _ := unstructured.NestedString(item.Object, "spec", "clusterIP")
|
||||
|
||||
externalIP := "<none>"
|
||||
if lbIngress, ok, _ := unstructured.NestedSlice(item.Object, "status", "loadBalancer", "ingress"); ok && len(lbIngress) > 0 {
|
||||
var ips []string
|
||||
for _, ingress := range lbIngress {
|
||||
if m, ok := ingress.(map[string]interface{}); ok {
|
||||
if ip, ok := m["ip"].(string); ok {
|
||||
ips = append(ips, ip)
|
||||
} else if hostname, ok := m["hostname"].(string); ok {
|
||||
ips = append(ips, hostname)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(ips) > 0 {
|
||||
externalIP = strings.Join(ips, ",")
|
||||
}
|
||||
}
|
||||
|
||||
ports := formatPorts(item)
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", name, svcType, clusterIP, externalIP, ports)
|
||||
}
|
||||
}
|
||||
|
||||
func printIngresses(items []unstructured.Unstructured) {
|
||||
w := newTabWriter()
|
||||
defer w.Flush()
|
||||
|
||||
fmt.Fprintln(w, "NAME\tCLASS\tHOSTS\tADDRESS")
|
||||
for _, item := range items {
|
||||
name := item.GetName()
|
||||
class, _, _ := unstructured.NestedString(item.Object, "spec", "ingressClassName")
|
||||
|
||||
var hosts []string
|
||||
if rules, ok, _ := unstructured.NestedSlice(item.Object, "spec", "rules"); ok {
|
||||
for _, rule := range rules {
|
||||
if m, ok := rule.(map[string]interface{}); ok {
|
||||
if host, ok := m["host"].(string); ok {
|
||||
hosts = append(hosts, host)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
hostsStr := "<none>"
|
||||
if len(hosts) > 0 {
|
||||
hostsStr = strings.Join(hosts, ",")
|
||||
}
|
||||
|
||||
address := ""
|
||||
if lbIngress, ok, _ := unstructured.NestedSlice(item.Object, "status", "loadBalancer", "ingress"); ok && len(lbIngress) > 0 {
|
||||
var addrs []string
|
||||
for _, ingress := range lbIngress {
|
||||
if m, ok := ingress.(map[string]interface{}); ok {
|
||||
if ip, ok := m["ip"].(string); ok {
|
||||
addrs = append(addrs, ip)
|
||||
} else if hostname, ok := m["hostname"].(string); ok {
|
||||
addrs = append(addrs, hostname)
|
||||
}
|
||||
}
|
||||
}
|
||||
address = strings.Join(addrs, ",")
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", name, class, hostsStr, address)
|
||||
}
|
||||
}
|
||||
|
||||
func printWorkloads(items []unstructured.Unstructured) {
|
||||
w := newTabWriter()
|
||||
defer w.Flush()
|
||||
|
||||
fmt.Fprintln(w, "NAME\tKIND\tTYPE\tVERSION\tAVAILABLE\tOBSERVED\tOPERATIONAL")
|
||||
for _, item := range items {
|
||||
name := item.GetName()
|
||||
kind, _, _ := unstructured.NestedString(item.Object, "spec", "kind")
|
||||
wType, _, _ := unstructured.NestedString(item.Object, "spec", "type")
|
||||
version, _, _ := unstructured.NestedString(item.Object, "spec", "version")
|
||||
available, _, _ := unstructured.NestedInt64(item.Object, "status", "availableReplicas")
|
||||
observed, _, _ := unstructured.NestedInt64(item.Object, "status", "observedReplicas")
|
||||
operational, ok, _ := unstructured.NestedBool(item.Object, "status", "operational")
|
||||
opStr := ""
|
||||
if ok {
|
||||
opStr = fmt.Sprintf("%t", operational)
|
||||
}
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%d\t%d\t%s\n", name, kind, wType, version, available, observed, opStr)
|
||||
}
|
||||
}
|
||||
|
||||
func printNoResources(w io.Writer, resourceType string) {
|
||||
fmt.Fprintf(w, "No %s found\n", resourceType)
|
||||
}
|
||||
|
||||
func extractCondition(item unstructured.Unstructured) (string, string) {
|
||||
conditions, ok, _ := unstructured.NestedSlice(item.Object, "status", "conditions")
|
||||
if !ok {
|
||||
return "Unknown", ""
|
||||
}
|
||||
for _, c := range conditions {
|
||||
cond, ok := c.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if cond["type"] == "Ready" {
|
||||
ready, _ := cond["status"].(string)
|
||||
message, _ := cond["message"].(string)
|
||||
return ready, message
|
||||
}
|
||||
}
|
||||
return "Unknown", ""
|
||||
}
|
||||
|
||||
func truncate(s string, maxLen int) string {
|
||||
if len(s) <= maxLen {
|
||||
return s
|
||||
}
|
||||
return s[:maxLen-3] + "..."
|
||||
}
|
||||
|
||||
func formatPorts(item unstructured.Unstructured) string {
|
||||
ports, ok, _ := unstructured.NestedSlice(item.Object, "spec", "ports")
|
||||
if !ok || len(ports) == 0 {
|
||||
return "<none>"
|
||||
}
|
||||
var parts []string
|
||||
for _, p := range ports {
|
||||
port, ok := p.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
portNum, _, _ := unstructured.NestedInt64(port, "port")
|
||||
protocol, _, _ := unstructured.NestedString(port, "protocol")
|
||||
if protocol == "" {
|
||||
protocol = "TCP"
|
||||
}
|
||||
nodePort, _, _ := unstructured.NestedInt64(port, "nodePort")
|
||||
if nodePort > 0 {
|
||||
parts = append(parts, fmt.Sprintf("%d:%d/%s", portNum, nodePort, protocol))
|
||||
} else {
|
||||
parts = append(parts, fmt.Sprintf("%d/%s", portNum, protocol))
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
57
cmd/cozyctl/cmd/root.go
Normal file
57
cmd/cozyctl/cmd/root.go
Normal file
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
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 cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Version is set at build time via -ldflags.
|
||||
var Version = "dev"
|
||||
|
||||
var globalFlags struct {
|
||||
kubeconfig string
|
||||
context string
|
||||
namespace string
|
||||
}
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "cozyctl",
|
||||
Short: "A CLI for managing Cozystack applications",
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
DisableAutoGenTag: true,
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
func Execute() error {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.Version = Version
|
||||
rootCmd.PersistentFlags().StringVar(&globalFlags.kubeconfig, "kubeconfig", "", "Path to kubeconfig file")
|
||||
rootCmd.PersistentFlags().StringVar(&globalFlags.context, "context", "", "Kubernetes context to use")
|
||||
rootCmd.PersistentFlags().StringVarP(&globalFlags.namespace, "namespace", "n", "", "Kubernetes namespace")
|
||||
}
|
||||
106
cmd/cozyctl/cmd/vm.go
Normal file
106
cmd/cozyctl/cmd/vm.go
Normal file
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
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 cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// vmKindPrefix maps application Kind to the release prefix used by KubeVirt VMs.
|
||||
func vmKindPrefix(kind string) (string, bool) {
|
||||
switch kind {
|
||||
case "VirtualMachine":
|
||||
return "virtual-machine", true
|
||||
case "VMInstance":
|
||||
return "vm-instance", true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
// resolveVMArgs takes CLI args (type, name or type/name), resolves the application type
|
||||
// via discovery, validates it's a VM kind, and returns the full VM name and namespace.
|
||||
func resolveVMArgs(args []string) (string, string, error) {
|
||||
var resourceType, resourceName string
|
||||
|
||||
if len(args) == 1 {
|
||||
// type/name format
|
||||
parts := strings.SplitN(args[0], "/", 2)
|
||||
if len(parts) != 2 {
|
||||
return "", "", fmt.Errorf("expected type/name format, got %q", args[0])
|
||||
}
|
||||
resourceType, resourceName = parts[0], parts[1]
|
||||
} else {
|
||||
resourceType = args[0]
|
||||
resourceName = args[1]
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
typedClient, _, err := newClients()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
registry, err := discoverAppDefs(ctx, typedClient)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
info := registry.Resolve(resourceType)
|
||||
if info == nil {
|
||||
return "", "", fmt.Errorf("unknown application type %q", resourceType)
|
||||
}
|
||||
|
||||
prefix, ok := vmKindPrefix(info.Kind)
|
||||
if !ok {
|
||||
return "", "", fmt.Errorf("resource type %q (Kind=%s) is not a VirtualMachine or VMInstance", resourceType, info.Kind)
|
||||
}
|
||||
|
||||
ns, err := getNamespace()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
vmName := prefix + "-" + resourceName
|
||||
return vmName, ns, nil
|
||||
}
|
||||
|
||||
// execVirtctl replaces the current process with virtctl.
|
||||
func execVirtctl(args []string) error {
|
||||
virtctlPath, err := exec.LookPath("virtctl")
|
||||
if err != nil {
|
||||
return fmt.Errorf("virtctl not found in PATH: %w", err)
|
||||
}
|
||||
|
||||
// Append kubeconfig/context flags if set
|
||||
if globalFlags.kubeconfig != "" {
|
||||
args = append(args, "--kubeconfig", globalFlags.kubeconfig)
|
||||
}
|
||||
if globalFlags.context != "" {
|
||||
args = append(args, "--context", globalFlags.context)
|
||||
}
|
||||
|
||||
if err := syscall.Exec(virtctlPath, args, os.Environ()); err != nil {
|
||||
return fmt.Errorf("failed to exec virtctl: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
43
cmd/cozyctl/cmd/vnc.go
Normal file
43
cmd/cozyctl/cmd/vnc.go
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
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 cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var vncCmd = &cobra.Command{
|
||||
Use: "vnc <type> <name>",
|
||||
Short: "Open a VNC connection to a VirtualMachine",
|
||||
Long: `Open a VNC connection to a VirtualMachine using virtctl. Only valid for VirtualMachine or VMInstance kinds.`,
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: runVNC,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(vncCmd)
|
||||
}
|
||||
|
||||
func runVNC(cmd *cobra.Command, args []string) error {
|
||||
vmName, ns, err := resolveVMArgs(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
virtctlArgs := []string{"virtctl", "vnc", vmName, "-n", ns}
|
||||
return execVirtctl(virtctlArgs)
|
||||
}
|
||||
29
cmd/cozyctl/main.go
Normal file
29
cmd/cozyctl/main.go
Normal file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
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 main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/cozystack/cozystack/cmd/cozyctl/cmd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := cmd.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -108,7 +108,7 @@ func main() {
|
||||
flag.StringVar(&telemetryInterval, "telemetry-interval", "15m",
|
||||
"Interval between telemetry data collection (e.g. 15m, 1h)")
|
||||
flag.StringVar(&platformSourceURL, "platform-source-url", "", "Platform source URL (oci:// or https://). If specified, generates OCIRepository or GitRepository resource.")
|
||||
flag.StringVar(&platformSourceName, "platform-source-name", "cozystack-packages", "Name for the generated platform source resource (default: cozystack-packages)")
|
||||
flag.StringVar(&platformSourceName, "platform-source-name", "cozystack-platform", "Name for the generated platform source resource and PackageSource")
|
||||
flag.StringVar(&platformSourceRef, "platform-source-ref", "", "Reference specification as key=value pairs (e.g., 'branch=main' or 'digest=sha256:...,tag=v1.0'). For OCI: digest, semver, semverFilter, tag. For Git: branch, tag, semver, name, commit.")
|
||||
flag.StringVar(&cozyValuesSecretName, "cozy-values-secret-name", "cozystack-values", "The name of the secret containing cluster-wide configuration values.")
|
||||
flag.StringVar(&cozyValuesSecretNamespace, "cozy-values-secret-namespace", "cozy-system", "The namespace of the secret containing cluster-wide configuration values.")
|
||||
@@ -224,6 +224,29 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Create platform PackageSource when CRDs are managed by the operator and
|
||||
// a platform source URL is configured. Without a URL there is no Flux source
|
||||
// resource to reference, so creating a PackageSource would leave a dangling SourceRef.
|
||||
if installCRDs && platformSourceURL != "" {
|
||||
sourceRefKind := "OCIRepository"
|
||||
sourceType, _, err := parsePlatformSourceURL(platformSourceURL)
|
||||
if err != nil {
|
||||
setupLog.Error(err, "failed to parse platform source URL for PackageSource")
|
||||
os.Exit(1)
|
||||
}
|
||||
if sourceType == "git" {
|
||||
sourceRefKind = "GitRepository"
|
||||
}
|
||||
setupLog.Info("Creating platform PackageSource", "platformSourceName", platformSourceName)
|
||||
psCtx, psCancel := context.WithTimeout(mgrCtx, 2*time.Minute)
|
||||
defer psCancel()
|
||||
if err := installPlatformPackageSource(psCtx, directClient, platformSourceName, sourceRefKind); err != nil {
|
||||
setupLog.Error(err, "failed to create platform PackageSource")
|
||||
os.Exit(1)
|
||||
}
|
||||
setupLog.Info("Platform PackageSource creation completed successfully")
|
||||
}
|
||||
|
||||
// Setup PackageSource reconciler
|
||||
if err := (&operator.PackageSourceReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
@@ -552,3 +575,79 @@ func generateGitRepository(name, repoURL string, refMap map[string]string) (*sou
|
||||
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
// installPlatformPackageSource creates the platform PackageSource resource
|
||||
// that references the Flux source resource (OCIRepository or GitRepository).
|
||||
//
|
||||
// The variant list is intentionally hardcoded here. These are platform-defined
|
||||
// deployment profiles (not user-extensible), matching what was previously in
|
||||
// the Helm template. Changes require a new operator build and release.
|
||||
func installPlatformPackageSource(ctx context.Context, k8sClient client.Client, platformSourceName, sourceRefKind string) error {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
packageSourceName := "cozystack." + platformSourceName
|
||||
|
||||
ps := &cozyv1alpha1.PackageSource{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: cozyv1alpha1.GroupVersion.String(),
|
||||
Kind: "PackageSource",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: packageSourceName,
|
||||
Annotations: map[string]string{
|
||||
"operator.cozystack.io/skip-cozystack-values": "true",
|
||||
},
|
||||
},
|
||||
Spec: cozyv1alpha1.PackageSourceSpec{
|
||||
SourceRef: &cozyv1alpha1.PackageSourceRef{
|
||||
Kind: sourceRefKind,
|
||||
Name: platformSourceName,
|
||||
Namespace: "cozy-system",
|
||||
Path: "/",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
variantData := []struct {
|
||||
name string
|
||||
valuesFiles []string
|
||||
}{
|
||||
{"default", []string{"values.yaml"}},
|
||||
{"isp-full", []string{"values.yaml", "values-isp-full.yaml"}},
|
||||
{"isp-hosted", []string{"values.yaml", "values-isp-hosted.yaml"}},
|
||||
{"isp-full-generic", []string{"values.yaml", "values-isp-full-generic.yaml"}},
|
||||
}
|
||||
|
||||
variants := make([]cozyv1alpha1.Variant, len(variantData))
|
||||
for i, v := range variantData {
|
||||
variants[i] = cozyv1alpha1.Variant{
|
||||
Name: v.name,
|
||||
Components: []cozyv1alpha1.Component{
|
||||
{
|
||||
Name: "platform",
|
||||
Path: "core/platform",
|
||||
Install: &cozyv1alpha1.ComponentInstall{
|
||||
Namespace: "cozy-system",
|
||||
ReleaseName: "cozystack-platform",
|
||||
},
|
||||
ValuesFiles: v.valuesFiles,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
ps.Spec.Variants = variants
|
||||
|
||||
logger.Info("Applying platform PackageSource", "name", packageSourceName)
|
||||
|
||||
patchOptions := &client.PatchOptions{
|
||||
FieldManager: "cozystack-operator",
|
||||
Force: func() *bool { b := true; return &b }(),
|
||||
}
|
||||
|
||||
if err := k8sClient.Patch(ctx, ps, client.Apply, patchOptions); err != nil {
|
||||
return fmt.Errorf("failed to apply PackageSource %s: %w", packageSourceName, err)
|
||||
}
|
||||
|
||||
logger.Info("Applied platform PackageSource", "name", packageSourceName)
|
||||
return nil
|
||||
}
|
||||
|
||||
574
cmd/cozystack-operator/main_test.go
Normal file
574
cmd/cozystack-operator/main_test.go
Normal file
@@ -0,0 +1,574 @@
|
||||
/*
|
||||
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 main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
)
|
||||
|
||||
func newTestScheme() *runtime.Scheme {
|
||||
s := runtime.NewScheme()
|
||||
_ = cozyv1alpha1.AddToScheme(s)
|
||||
return s
|
||||
}
|
||||
|
||||
func TestInstallPlatformPackageSource_Creates(t *testing.T) {
|
||||
s := newTestScheme()
|
||||
k8sClient := fake.NewClientBuilder().WithScheme(s).Build()
|
||||
|
||||
err := installPlatformPackageSource(context.Background(), k8sClient, "cozystack-platform", "OCIRepository")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
ps := &cozyv1alpha1.PackageSource{}
|
||||
if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: "cozystack.cozystack-platform"}, ps); err != nil {
|
||||
t.Fatalf("PackageSource not found: %v", err)
|
||||
}
|
||||
|
||||
// Verify name
|
||||
if ps.Name != "cozystack.cozystack-platform" {
|
||||
t.Errorf("expected name %q, got %q", "cozystack.cozystack-platform", ps.Name)
|
||||
}
|
||||
|
||||
// Verify annotation
|
||||
if ps.Annotations["operator.cozystack.io/skip-cozystack-values"] != "true" {
|
||||
t.Errorf("expected skip-cozystack-values annotation to be 'true', got %q", ps.Annotations["operator.cozystack.io/skip-cozystack-values"])
|
||||
}
|
||||
|
||||
// Verify sourceRef
|
||||
if ps.Spec.SourceRef == nil {
|
||||
t.Fatal("expected SourceRef to be set")
|
||||
}
|
||||
if ps.Spec.SourceRef.Kind != "OCIRepository" {
|
||||
t.Errorf("expected sourceRef.kind %q, got %q", "OCIRepository", ps.Spec.SourceRef.Kind)
|
||||
}
|
||||
if ps.Spec.SourceRef.Name != "cozystack-platform" {
|
||||
t.Errorf("expected sourceRef.name %q, got %q", "cozystack-platform", ps.Spec.SourceRef.Name)
|
||||
}
|
||||
if ps.Spec.SourceRef.Namespace != "cozy-system" {
|
||||
t.Errorf("expected sourceRef.namespace %q, got %q", "cozy-system", ps.Spec.SourceRef.Namespace)
|
||||
}
|
||||
if ps.Spec.SourceRef.Path != "/" {
|
||||
t.Errorf("expected sourceRef.path %q, got %q", "/", ps.Spec.SourceRef.Path)
|
||||
}
|
||||
|
||||
// Verify variants
|
||||
expectedVariants := []string{"default", "isp-full", "isp-hosted", "isp-full-generic"}
|
||||
if len(ps.Spec.Variants) != len(expectedVariants) {
|
||||
t.Fatalf("expected %d variants, got %d", len(expectedVariants), len(ps.Spec.Variants))
|
||||
}
|
||||
for i, name := range expectedVariants {
|
||||
if ps.Spec.Variants[i].Name != name {
|
||||
t.Errorf("expected variant[%d].name %q, got %q", i, name, ps.Spec.Variants[i].Name)
|
||||
}
|
||||
if len(ps.Spec.Variants[i].Components) != 1 {
|
||||
t.Errorf("expected variant[%d] to have 1 component, got %d", i, len(ps.Spec.Variants[i].Components))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallPlatformPackageSource_Updates(t *testing.T) {
|
||||
s := newTestScheme()
|
||||
|
||||
existing := &cozyv1alpha1.PackageSource{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "cozystack.cozystack-platform",
|
||||
ResourceVersion: "1",
|
||||
Labels: map[string]string{
|
||||
"custom-label": "should-be-preserved",
|
||||
},
|
||||
},
|
||||
Spec: cozyv1alpha1.PackageSourceSpec{
|
||||
SourceRef: &cozyv1alpha1.PackageSourceRef{
|
||||
Kind: "OCIRepository",
|
||||
Name: "old-name",
|
||||
Namespace: "cozy-system",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
k8sClient := fake.NewClientBuilder().WithScheme(s).WithObjects(existing).Build()
|
||||
|
||||
err := installPlatformPackageSource(context.Background(), k8sClient, "cozystack-platform", "OCIRepository")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
ps := &cozyv1alpha1.PackageSource{}
|
||||
if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: "cozystack.cozystack-platform"}, ps); err != nil {
|
||||
t.Fatalf("PackageSource not found: %v", err)
|
||||
}
|
||||
|
||||
// Verify sourceRef was updated
|
||||
if ps.Spec.SourceRef.Name != "cozystack-platform" {
|
||||
t.Errorf("expected updated sourceRef.name %q, got %q", "cozystack-platform", ps.Spec.SourceRef.Name)
|
||||
}
|
||||
|
||||
// Verify all 4 variants are present after update
|
||||
if len(ps.Spec.Variants) != 4 {
|
||||
t.Errorf("expected 4 variants after update, got %d", len(ps.Spec.Variants))
|
||||
}
|
||||
|
||||
// Verify that labels set by other controllers are preserved (SSA does not overwrite unmanaged fields)
|
||||
if ps.Labels["custom-label"] != "should-be-preserved" {
|
||||
t.Errorf("expected custom-label to be preserved, got %q", ps.Labels["custom-label"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePlatformSourceURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
wantType string
|
||||
wantURL string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "OCI URL",
|
||||
url: "oci://ghcr.io/cozystack/cozystack/cozystack-packages",
|
||||
wantType: "oci",
|
||||
wantURL: "oci://ghcr.io/cozystack/cozystack/cozystack-packages",
|
||||
},
|
||||
{
|
||||
name: "HTTPS URL",
|
||||
url: "https://github.com/cozystack/cozystack",
|
||||
wantType: "git",
|
||||
wantURL: "https://github.com/cozystack/cozystack",
|
||||
},
|
||||
{
|
||||
name: "SSH URL",
|
||||
url: "ssh://git@github.com/cozystack/cozystack",
|
||||
wantType: "git",
|
||||
wantURL: "ssh://git@github.com/cozystack/cozystack",
|
||||
},
|
||||
{
|
||||
name: "empty URL",
|
||||
url: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "unsupported scheme",
|
||||
url: "ftp://example.com/repo",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
sourceType, repoURL, err := parsePlatformSourceURL(tt.url)
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for URL %q, got nil", tt.url)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if sourceType != tt.wantType {
|
||||
t.Errorf("expected type %q, got %q", tt.wantType, sourceType)
|
||||
}
|
||||
if repoURL != tt.wantURL {
|
||||
t.Errorf("expected URL %q, got %q", tt.wantURL, repoURL)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallPlatformPackageSource_VariantValuesFiles(t *testing.T) {
|
||||
s := newTestScheme()
|
||||
k8sClient := fake.NewClientBuilder().WithScheme(s).Build()
|
||||
|
||||
err := installPlatformPackageSource(context.Background(), k8sClient, "cozystack-platform", "OCIRepository")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
ps := &cozyv1alpha1.PackageSource{}
|
||||
if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: "cozystack.cozystack-platform"}, ps); err != nil {
|
||||
t.Fatalf("PackageSource not found: %v", err)
|
||||
}
|
||||
|
||||
expectedValuesFiles := map[string][]string{
|
||||
"default": {"values.yaml"},
|
||||
"isp-full": {"values.yaml", "values-isp-full.yaml"},
|
||||
"isp-hosted": {"values.yaml", "values-isp-hosted.yaml"},
|
||||
"isp-full-generic": {"values.yaml", "values-isp-full-generic.yaml"},
|
||||
}
|
||||
|
||||
for _, v := range ps.Spec.Variants {
|
||||
expected, ok := expectedValuesFiles[v.Name]
|
||||
if !ok {
|
||||
t.Errorf("unexpected variant %q", v.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(v.Components) != 1 {
|
||||
t.Errorf("variant %q: expected 1 component, got %d", v.Name, len(v.Components))
|
||||
continue
|
||||
}
|
||||
|
||||
comp := v.Components[0]
|
||||
if comp.Name != "platform" {
|
||||
t.Errorf("variant %q: expected component name %q, got %q", v.Name, "platform", comp.Name)
|
||||
}
|
||||
if comp.Path != "core/platform" {
|
||||
t.Errorf("variant %q: expected component path %q, got %q", v.Name, "core/platform", comp.Path)
|
||||
}
|
||||
if comp.Install == nil {
|
||||
t.Errorf("variant %q: expected Install to be set", v.Name)
|
||||
} else {
|
||||
if comp.Install.Namespace != "cozy-system" {
|
||||
t.Errorf("variant %q: expected install namespace %q, got %q", v.Name, "cozy-system", comp.Install.Namespace)
|
||||
}
|
||||
if comp.Install.ReleaseName != "cozystack-platform" {
|
||||
t.Errorf("variant %q: expected install releaseName %q, got %q", v.Name, "cozystack-platform", comp.Install.ReleaseName)
|
||||
}
|
||||
}
|
||||
|
||||
if len(comp.ValuesFiles) != len(expected) {
|
||||
t.Errorf("variant %q: expected %d valuesFiles, got %d", v.Name, len(expected), len(comp.ValuesFiles))
|
||||
continue
|
||||
}
|
||||
for i, f := range expected {
|
||||
if comp.ValuesFiles[i] != f {
|
||||
t.Errorf("variant %q: expected valuesFiles[%d] %q, got %q", v.Name, i, f, comp.ValuesFiles[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallPlatformPackageSource_CustomName(t *testing.T) {
|
||||
s := newTestScheme()
|
||||
k8sClient := fake.NewClientBuilder().WithScheme(s).Build()
|
||||
|
||||
err := installPlatformPackageSource(context.Background(), k8sClient, "custom-source", "OCIRepository")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
ps := &cozyv1alpha1.PackageSource{}
|
||||
if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: "cozystack.custom-source"}, ps); err != nil {
|
||||
t.Fatalf("PackageSource not found: %v", err)
|
||||
}
|
||||
|
||||
if ps.Name != "cozystack.custom-source" {
|
||||
t.Errorf("expected name %q, got %q", "cozystack.custom-source", ps.Name)
|
||||
}
|
||||
if ps.Spec.SourceRef.Name != "custom-source" {
|
||||
t.Errorf("expected sourceRef.name %q, got %q", "custom-source", ps.Spec.SourceRef.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallPlatformPackageSource_GitRepository(t *testing.T) {
|
||||
s := newTestScheme()
|
||||
k8sClient := fake.NewClientBuilder().WithScheme(s).Build()
|
||||
|
||||
err := installPlatformPackageSource(context.Background(), k8sClient, "my-source", "GitRepository")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
ps := &cozyv1alpha1.PackageSource{}
|
||||
if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: "cozystack.my-source"}, ps); err != nil {
|
||||
t.Fatalf("PackageSource not found: %v", err)
|
||||
}
|
||||
|
||||
if ps.Spec.SourceRef.Kind != "GitRepository" {
|
||||
t.Errorf("expected sourceRef.kind %q, got %q", "GitRepository", ps.Spec.SourceRef.Kind)
|
||||
}
|
||||
if ps.Spec.SourceRef.Name != "my-source" {
|
||||
t.Errorf("expected sourceRef.name %q, got %q", "my-source", ps.Spec.SourceRef.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRefSpec(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want map[string]string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty string",
|
||||
input: "",
|
||||
want: map[string]string{},
|
||||
},
|
||||
{
|
||||
name: "single key-value",
|
||||
input: "tag=v1.0",
|
||||
want: map[string]string{"tag": "v1.0"},
|
||||
},
|
||||
{
|
||||
name: "multiple key-values",
|
||||
input: "digest=sha256:abc123,tag=v1.0",
|
||||
want: map[string]string{"digest": "sha256:abc123", "tag": "v1.0"},
|
||||
},
|
||||
{
|
||||
name: "whitespace around pairs",
|
||||
input: " tag=v1.0 , branch=main ",
|
||||
want: map[string]string{"tag": "v1.0", "branch": "main"},
|
||||
},
|
||||
{
|
||||
name: "equals sign in value",
|
||||
input: "digest=sha256:abc=123",
|
||||
want: map[string]string{"digest": "sha256:abc=123"},
|
||||
},
|
||||
{
|
||||
name: "missing equals sign",
|
||||
input: "tag",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty key",
|
||||
input: "=value",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty value",
|
||||
input: "tag=",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "trailing comma",
|
||||
input: "tag=v1.0,",
|
||||
want: map[string]string{"tag": "v1.0"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := parseRefSpec(tt.input)
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for input %q, got nil", tt.input)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(got) != len(tt.want) {
|
||||
t.Fatalf("expected %d entries, got %d: %v", len(tt.want), len(got), got)
|
||||
}
|
||||
for k, v := range tt.want {
|
||||
if got[k] != v {
|
||||
t.Errorf("expected %q=%q, got %q=%q", k, v, k, got[k])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateOCIRef(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
refMap map[string]string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid tag",
|
||||
refMap: map[string]string{"tag": "v1.0"},
|
||||
},
|
||||
{
|
||||
name: "valid digest",
|
||||
refMap: map[string]string{"digest": "sha256:abc123def456"},
|
||||
},
|
||||
{
|
||||
name: "valid semver",
|
||||
refMap: map[string]string{"semver": ">=1.0.0"},
|
||||
},
|
||||
{
|
||||
name: "multiple valid keys",
|
||||
refMap: map[string]string{"tag": "v1.0", "digest": "sha256:abc"},
|
||||
},
|
||||
{
|
||||
name: "empty map",
|
||||
refMap: map[string]string{},
|
||||
},
|
||||
{
|
||||
name: "invalid key",
|
||||
refMap: map[string]string{"branch": "main"},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid digest format",
|
||||
refMap: map[string]string{"digest": "md5:abc"},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validateOCIRef(tt.refMap)
|
||||
if tt.wantErr && err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if !tt.wantErr && err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateGitRef(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
refMap map[string]string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid branch",
|
||||
refMap: map[string]string{"branch": "main"},
|
||||
},
|
||||
{
|
||||
name: "valid commit",
|
||||
refMap: map[string]string{"commit": "abc1234"},
|
||||
},
|
||||
{
|
||||
name: "valid tag and branch",
|
||||
refMap: map[string]string{"tag": "v1.0", "branch": "release"},
|
||||
},
|
||||
{
|
||||
name: "empty map",
|
||||
refMap: map[string]string{},
|
||||
},
|
||||
{
|
||||
name: "invalid key",
|
||||
refMap: map[string]string{"digest": "sha256:abc"},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "commit too short",
|
||||
refMap: map[string]string{"commit": "abc"},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "commit not hex",
|
||||
refMap: map[string]string{"commit": "zzzzzzz"},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validateGitRef(tt.refMap)
|
||||
if tt.wantErr && err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if !tt.wantErr && err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateOCIRepository(t *testing.T) {
|
||||
refMap := map[string]string{"tag": "v1.0", "digest": "sha256:abc123"}
|
||||
obj, err := generateOCIRepository("my-repo", "oci://registry.example.com/repo", refMap)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if obj.Name != "my-repo" {
|
||||
t.Errorf("expected name %q, got %q", "my-repo", obj.Name)
|
||||
}
|
||||
if obj.Namespace != "cozy-system" {
|
||||
t.Errorf("expected namespace %q, got %q", "cozy-system", obj.Namespace)
|
||||
}
|
||||
if obj.Spec.URL != "oci://registry.example.com/repo" {
|
||||
t.Errorf("expected URL %q, got %q", "oci://registry.example.com/repo", obj.Spec.URL)
|
||||
}
|
||||
if obj.Spec.Reference == nil {
|
||||
t.Fatal("expected Reference to be set")
|
||||
}
|
||||
if obj.Spec.Reference.Tag != "v1.0" {
|
||||
t.Errorf("expected tag %q, got %q", "v1.0", obj.Spec.Reference.Tag)
|
||||
}
|
||||
if obj.Spec.Reference.Digest != "sha256:abc123" {
|
||||
t.Errorf("expected digest %q, got %q", "sha256:abc123", obj.Spec.Reference.Digest)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateOCIRepository_NoRef(t *testing.T) {
|
||||
obj, err := generateOCIRepository("my-repo", "oci://registry.example.com/repo", map[string]string{})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if obj.Spec.Reference != nil {
|
||||
t.Error("expected Reference to be nil for empty refMap")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateOCIRepository_InvalidRef(t *testing.T) {
|
||||
_, err := generateOCIRepository("my-repo", "oci://registry.example.com/repo", map[string]string{"branch": "main"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for invalid OCI ref key, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateGitRepository(t *testing.T) {
|
||||
refMap := map[string]string{"branch": "main", "commit": "abc1234def5678"}
|
||||
obj, err := generateGitRepository("my-repo", "https://github.com/user/repo", refMap)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if obj.Name != "my-repo" {
|
||||
t.Errorf("expected name %q, got %q", "my-repo", obj.Name)
|
||||
}
|
||||
if obj.Namespace != "cozy-system" {
|
||||
t.Errorf("expected namespace %q, got %q", "cozy-system", obj.Namespace)
|
||||
}
|
||||
if obj.Spec.URL != "https://github.com/user/repo" {
|
||||
t.Errorf("expected URL %q, got %q", "https://github.com/user/repo", obj.Spec.URL)
|
||||
}
|
||||
if obj.Spec.Reference == nil {
|
||||
t.Fatal("expected Reference to be set")
|
||||
}
|
||||
if obj.Spec.Reference.Branch != "main" {
|
||||
t.Errorf("expected branch %q, got %q", "main", obj.Spec.Reference.Branch)
|
||||
}
|
||||
if obj.Spec.Reference.Commit != "abc1234def5678" {
|
||||
t.Errorf("expected commit %q, got %q", "abc1234def5678", obj.Spec.Reference.Commit)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateGitRepository_NoRef(t *testing.T) {
|
||||
obj, err := generateGitRepository("my-repo", "https://github.com/user/repo", map[string]string{})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if obj.Spec.Reference != nil {
|
||||
t.Error("expected Reference to be nil for empty refMap")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateGitRepository_InvalidRef(t *testing.T) {
|
||||
_, err := generateGitRepository("my-repo", "https://github.com/user/repo", map[string]string{"digest": "sha256:abc"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for invalid Git ref key, got nil")
|
||||
}
|
||||
}
|
||||
@@ -86,14 +86,18 @@ EOF
|
||||
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
|
||||
# Kill any stale port-forward on this port from a previous retry
|
||||
pkill -f "port-forward.*${port}:" 2>/dev/null || true
|
||||
sleep 1
|
||||
|
||||
# Set up port forwarding to the Kubernetes API server
|
||||
bash -c 'timeout 500s kubectl port-forward service/kubernetes-'"${test_name}"' -n tenant-test '"${port}"':6443 > /dev/null 2>&1 &'
|
||||
# Verify the Kubernetes version matches what we expect (retry for up to 20 seconds)
|
||||
timeout 20 sh -ec 'until kubectl --kubeconfig tenantkubeconfig-'"${test_name}"' version 2>/dev/null | grep -Fq "Server Version: ${k8s_version}"; do sleep 5; done'
|
||||
|
||||
# Wait for the nodes to be ready (timeout after 2 minutes)
|
||||
timeout 3m bash -c '
|
||||
until [ "$(kubectl --kubeconfig tenantkubeconfig-'"${test_name}"' get nodes -o jsonpath="{.items[*].metadata.name}" | wc -w)" -eq 2 ]; do
|
||||
# Wait for at least 2 nodes to join (timeout after 8 minutes)
|
||||
timeout 8m bash -c '
|
||||
until [ "$(kubectl --kubeconfig tenantkubeconfig-'"${test_name}"' get nodes -o jsonpath="{.items[*].metadata.name}" | wc -w)" -ge 2 ]; do
|
||||
sleep 2
|
||||
done
|
||||
'
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
}
|
||||
|
||||
@test "Install Cozystack" {
|
||||
# Install cozy-installer chart (CRDs from crds/ are applied automatically)
|
||||
# Install cozy-installer chart (operator installs CRDs on startup via --install-crds)
|
||||
helm upgrade installer packages/core/installer \
|
||||
--install \
|
||||
--namespace cozy-system \
|
||||
@@ -19,6 +19,14 @@
|
||||
# Verify the operator deployment is available
|
||||
kubectl wait deployment/cozystack-operator -n cozy-system --timeout=1m --for=condition=Available
|
||||
|
||||
# Wait for operator to install CRDs (happens at startup before reconcile loop).
|
||||
# kubectl wait fails immediately if the CRD does not exist yet, so poll until it appears first.
|
||||
timeout 120 sh -ec 'until kubectl wait crd/packages.cozystack.io --for=condition=Established --timeout=10s 2>/dev/null; do sleep 2; done'
|
||||
timeout 120 sh -ec 'until kubectl wait crd/packagesources.cozystack.io --for=condition=Established --timeout=10s 2>/dev/null; do sleep 2; done'
|
||||
|
||||
# Wait for operator to create the platform PackageSource
|
||||
timeout 120 sh -ec 'until kubectl get packagesource cozystack.cozystack-platform >/dev/null 2>&1; do sleep 2; done'
|
||||
|
||||
# Create platform Package with isp-full variant
|
||||
kubectl apply -f - <<EOF
|
||||
apiVersion: cozystack.io/v1alpha1
|
||||
|
||||
@@ -53,6 +53,39 @@ KEYCLOAK_REDIRECTS=$(echo "$COZYSTACK_CM" | jq -r '.data["extra-keycloak-redirec
|
||||
TELEMETRY_ENABLED=$(echo "$COZYSTACK_CM" | jq -r '.data["telemetry-enabled"] // "true"')
|
||||
BUNDLE_NAME=$(echo "$COZYSTACK_CM" | jq -r '.data["bundle-name"] // "paas-full"')
|
||||
|
||||
# Certificate issuer configuration (old undocumented field: clusterissuer)
|
||||
OLD_CLUSTER_ISSUER=$(echo "$COZYSTACK_CM" | jq -r '.data["clusterissuer"] // ""')
|
||||
|
||||
# Convert old clusterissuer value to new solver/issuerName fields
|
||||
SOLVER=""
|
||||
ISSUER_NAME=""
|
||||
case "$OLD_CLUSTER_ISSUER" in
|
||||
cloudflare)
|
||||
SOLVER="dns01"
|
||||
ISSUER_NAME="letsencrypt-prod"
|
||||
;;
|
||||
http01)
|
||||
SOLVER="http01"
|
||||
ISSUER_NAME="letsencrypt-prod"
|
||||
;;
|
||||
"")
|
||||
# Field not set; omit from Package so chart defaults apply
|
||||
;;
|
||||
*)
|
||||
# Unrecognised value — treat as custom ClusterIssuer name with no solver override
|
||||
ISSUER_NAME="$OLD_CLUSTER_ISSUER"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Build certificates YAML block (empty string when no override needed)
|
||||
if [ -n "$SOLVER" ] || [ -n "$ISSUER_NAME" ]; then
|
||||
CERTIFICATES_SECTION=" certificates:
|
||||
solver: \"${SOLVER}\"
|
||||
issuerName: \"${ISSUER_NAME}\""
|
||||
else
|
||||
CERTIFICATES_SECTION=""
|
||||
fi
|
||||
|
||||
# Network configuration
|
||||
POD_CIDR=$(echo "$COZYSTACK_CM" | jq -r '.data["ipv4-pod-cidr"] // "10.244.0.0/16"')
|
||||
POD_GATEWAY=$(echo "$COZYSTACK_CM" | jq -r '.data["ipv4-pod-gateway"] // "10.244.0.1"')
|
||||
@@ -110,6 +143,8 @@ echo " OIDC Enabled: $OIDC_ENABLED"
|
||||
echo " Bundle Name: $BUNDLE_NAME"
|
||||
echo " System Enabled: $SYSTEM_ENABLED"
|
||||
echo " System Type: $SYSTEM_TYPE"
|
||||
echo " Certificate Solver: ${SOLVER:-http01 (default)}"
|
||||
echo " Issuer Name: ${ISSUER_NAME:-letsencrypt-prod (default)}"
|
||||
echo ""
|
||||
|
||||
# Generate Package YAML
|
||||
@@ -144,6 +179,7 @@ spec:
|
||||
host: "$ROOT_HOST"
|
||||
apiServerEndpoint: "$API_SERVER_ENDPOINT"
|
||||
externalIPs: $EXTERNAL_IPS
|
||||
${CERTIFICATES_SECTION}
|
||||
authentication:
|
||||
oidc:
|
||||
enabled: $OIDC_ENABLED
|
||||
|
||||
@@ -24,8 +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/crds
|
||||
OPERATOR_EMBEDDIR=internal/crdinstall/manifests
|
||||
OPERATOR_CRDDIR=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
|
||||
@@ -74,9 +73,6 @@ $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
|
||||
|
||||
|
||||
@@ -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,101 @@ 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
|
||||
|
||||
// Override disks[].name to be an API-backed dropdown listing VMDisk resources
|
||||
disksItemProps := ensureArrayItemProps(specProps, "disks")
|
||||
disksItemProps["name"] = map[string]any{
|
||||
"type": "listInput",
|
||||
"customProps": map[string]any{
|
||||
"valueUri": "/api/clusters/{cluster}/k8s/apis/apps.cozystack.io/v1alpha1/namespaces/{namespace}/vmdisks",
|
||||
"keysToValue": []any{"metadata", "name"},
|
||||
"keysToLabel": []any{"metadata", "name"},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ensureArrayItemProps ensures that parentProps[fieldName].items.properties exists
|
||||
// and returns the items properties map. Used for overriding fields inside array items.
|
||||
func ensureArrayItemProps(parentProps map[string]any, fieldName string) map[string]any {
|
||||
field, ok := parentProps[fieldName].(map[string]any)
|
||||
if !ok {
|
||||
field = map[string]any{}
|
||||
parentProps[fieldName] = field
|
||||
}
|
||||
items, ok := field["items"].(map[string]any)
|
||||
if !ok {
|
||||
items = map[string]any{}
|
||||
field["items"] = items
|
||||
}
|
||||
props, ok := items["properties"].(map[string]any)
|
||||
if !ok {
|
||||
props = map[string]any{}
|
||||
items["properties"] = props
|
||||
}
|
||||
return props
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
||||
@@ -169,3 +169,231 @@ 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"])
|
||||
}
|
||||
|
||||
// Check disks[].name is a listInput
|
||||
disks, ok := specProps["disks"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("disks not found in schema.properties.spec.properties")
|
||||
}
|
||||
items, ok := disks["items"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("disks.items not found")
|
||||
}
|
||||
itemProps, ok := items["properties"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("disks.items.properties not found")
|
||||
}
|
||||
diskName, ok := itemProps["name"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("disks.items.properties.name not found")
|
||||
}
|
||||
if diskName["type"] != "listInput" {
|
||||
t.Errorf("expected disks name type listInput, got %v", diskName["type"])
|
||||
}
|
||||
diskCustomProps, ok := diskName["customProps"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("disks name customProps not found")
|
||||
}
|
||||
expectedDiskURI := "/api/clusters/{cluster}/k8s/apis/apps.cozystack.io/v1alpha1/namespaces/{namespace}/vmdisks"
|
||||
if diskCustomProps["valueUri"] != expectedDiskURI {
|
||||
t.Errorf("expected disks valueUri %s, got %v", expectedDiskURI, diskCustomProps["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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestEnsureArrayItemProps(t *testing.T) {
|
||||
t.Run("creates from empty parent", func(t *testing.T) {
|
||||
parent := map[string]any{}
|
||||
props := ensureArrayItemProps(parent, "disks")
|
||||
|
||||
props["name"] = map[string]any{"type": "listInput"}
|
||||
|
||||
got := parent["disks"].(map[string]any)["items"].(map[string]any)["properties"].(map[string]any)["name"].(map[string]any)["type"]
|
||||
if got != "listInput" {
|
||||
t.Errorf("expected listInput, got %v", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("preserves existing item properties", func(t *testing.T) {
|
||||
parent := map[string]any{
|
||||
"disks": map[string]any{
|
||||
"items": map[string]any{
|
||||
"properties": map[string]any{
|
||||
"bus": map[string]any{"type": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
props := ensureArrayItemProps(parent, "disks")
|
||||
props["name"] = map[string]any{"type": "listInput"}
|
||||
|
||||
if props["bus"].(map[string]any)["type"] != "string" {
|
||||
t.Error("existing bus property was lost")
|
||||
}
|
||||
if props["name"].(map[string]any)["type"] != "listInput" {
|
||||
t.Error("name property was not added")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{{- $ingress := .Values._namespace.ingress }}
|
||||
{{- $host := .Values._namespace.host }}
|
||||
{{- $harborHost := .Values.host | default (printf "%s.%s" .Release.Name $host) }}
|
||||
{{- $issuerType := (index .Values._cluster "clusterissuer") | default "http01" }}
|
||||
{{- $solver := (index .Values._cluster "solver") | default "http01" }}
|
||||
{{- $clusterIssuer := (index .Values._cluster "issuer-name") | default "letsencrypt-prod" }}
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
@@ -13,10 +14,10 @@ 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" }}
|
||||
{{- if eq $solver "http01" }}
|
||||
acme.cert-manager.io/http01-ingress-class: {{ $ingress }}
|
||||
{{- end }}
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
cert-manager.io/cluster-issuer: {{ $clusterIssuer }}
|
||||
spec:
|
||||
ingressClassName: {{ $ingress }}
|
||||
tls:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
KUBERNETES_VERSION = v1.33
|
||||
KUBERNETES_VERSION = v1.35
|
||||
KUBERNETES_PKG_TAG = $(shell awk '$$1 == "version:" {print $$2}' Chart.yaml)
|
||||
|
||||
include ../../../hack/common-envs.mk
|
||||
|
||||
@@ -104,7 +104,7 @@ See the reference for components utilized in this service:
|
||||
| `nodeGroups[name].resources.memory` | Memory (RAM) available. | `quantity` | `""` |
|
||||
| `nodeGroups[name].gpus` | List of GPUs to attach (NVIDIA driver requires at least 4 GiB RAM). | `[]object` | `[]` |
|
||||
| `nodeGroups[name].gpus[i].name` | Name of GPU, such as "nvidia.com/AD102GL_L40S". | `string` | `""` |
|
||||
| `version` | Kubernetes major.minor version to deploy | `string` | `v1.33` |
|
||||
| `version` | Kubernetes major.minor version to deploy | `string` | `v1.35` |
|
||||
| `host` | External hostname for Kubernetes cluster. Defaults to `<cluster-name>.<tenant-host>` if empty. | `string` | `""` |
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# Konnectivity proxy version overrides per Kubernetes minor version.
|
||||
# When empty or absent, Kamaji auto-derives v0.{minor}.0 from the Kubernetes version.
|
||||
# Add entries here only when the auto-derived image tag does not exist in the registry.
|
||||
"v1.35": "v0.34.0"
|
||||
@@ -1,6 +1,6 @@
|
||||
"v1.33": "v1.33.0"
|
||||
"v1.32": "v1.32.10"
|
||||
"v1.35": "v1.35.1"
|
||||
"v1.34": "v1.34.4"
|
||||
"v1.33": "v1.33.8"
|
||||
"v1.32": "v1.32.12"
|
||||
"v1.31": "v1.31.14"
|
||||
"v1.30": "v1.30.14"
|
||||
"v1.29": "v1.29.15"
|
||||
"v1.28": "v1.28.15"
|
||||
|
||||
@@ -5,3 +5,10 @@
|
||||
{{- end }}
|
||||
{{- index $versionMap .Values.version }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "kubernetes.konnectivityVersion" }}
|
||||
{{- $konnVersionMap := .Files.Get "files/konnectivity-versions.yaml" | fromYaml }}
|
||||
{{- if hasKey $konnVersionMap .Values.version }}
|
||||
{{- index $konnVersionMap .Values.version }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
@@ -126,8 +126,16 @@ spec:
|
||||
dataStoreName: "{{ $etcd }}"
|
||||
addons:
|
||||
konnectivity:
|
||||
{{- $konnVersion := include "kubernetes.konnectivityVersion" $ | trim }}
|
||||
{{- if $konnVersion }}
|
||||
agent:
|
||||
version: {{ $konnVersion }}
|
||||
{{- end }}
|
||||
server:
|
||||
port: 8132
|
||||
{{- if $konnVersion }}
|
||||
version: {{ $konnVersion }}
|
||||
{{- end }}
|
||||
resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.controlPlane.konnectivity.server.resourcesPreset .Values.controlPlane.konnectivity.server.resources $) | nindent 10 }}
|
||||
kubelet:
|
||||
cgroupfs: systemd
|
||||
|
||||
@@ -621,14 +621,14 @@
|
||||
"version": {
|
||||
"description": "Kubernetes major.minor version to deploy",
|
||||
"type": "string",
|
||||
"default": "v1.33",
|
||||
"default": "v1.35",
|
||||
"enum": [
|
||||
"v1.35",
|
||||
"v1.34",
|
||||
"v1.33",
|
||||
"v1.32",
|
||||
"v1.31",
|
||||
"v1.30",
|
||||
"v1.29",
|
||||
"v1.28"
|
||||
"v1.30"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,15 +48,15 @@ nodeGroups:
|
||||
|
||||
##
|
||||
## @enum {string} Version
|
||||
## @value v1.35
|
||||
## @value v1.34
|
||||
## @value v1.33
|
||||
## @value v1.32
|
||||
## @value v1.31
|
||||
## @value v1.30
|
||||
## @value v1.29
|
||||
## @value v1.28
|
||||
|
||||
## @param {Version} version - Kubernetes major.minor version to deploy
|
||||
version: "v1.33"
|
||||
version: "v1.35"
|
||||
|
||||
|
||||
## @param {string} host - External hostname for Kubernetes cluster. Defaults to `<cluster-name>.<tenant-host>` if empty.
|
||||
|
||||
1
packages/core/installer/crds/.gitattributes
vendored
1
packages/core/installer/crds/.gitattributes
vendored
@@ -1 +0,0 @@
|
||||
*.yaml linguist-generated
|
||||
@@ -1,171 +0,0 @@
|
||||
---
|
||||
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: {}
|
||||
@@ -1,250 +0,0 @@
|
||||
---
|
||||
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: {}
|
||||
@@ -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: v1
|
||||
kind: Namespace
|
||||
@@ -53,10 +57,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 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.
|
||||
# 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=0
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
{{- $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
|
||||
metadata:
|
||||
name: cozystack.cozystack-platform
|
||||
annotations:
|
||||
operator.cozystack.io/skip-cozystack-values: "true"
|
||||
spec:
|
||||
sourceRef:
|
||||
kind: OCIRepository
|
||||
name: cozystack-platform
|
||||
namespace: cozy-system
|
||||
path: /
|
||||
variants:
|
||||
- name: default
|
||||
components:
|
||||
- install:
|
||||
namespace: cozy-system
|
||||
releaseName: cozystack-platform
|
||||
name: platform
|
||||
path: core/platform
|
||||
valuesFiles:
|
||||
- values.yaml
|
||||
- name: isp-full
|
||||
components:
|
||||
- install:
|
||||
namespace: cozy-system
|
||||
releaseName: cozystack-platform
|
||||
name: platform
|
||||
path: core/platform
|
||||
valuesFiles:
|
||||
- values.yaml
|
||||
- values-isp-full.yaml
|
||||
- name: isp-hosted
|
||||
components:
|
||||
- install:
|
||||
namespace: cozy-system
|
||||
releaseName: cozystack-platform
|
||||
name: platform
|
||||
path: core/platform
|
||||
valuesFiles:
|
||||
- values.yaml
|
||||
- values-isp-hosted.yaml
|
||||
- name: isp-full-generic
|
||||
components:
|
||||
- install:
|
||||
namespace: cozy-system
|
||||
releaseName: cozystack-platform
|
||||
name: platform
|
||||
path: core/platform
|
||||
valuesFiles:
|
||||
- values.yaml
|
||||
- values-isp-full-generic.yaml
|
||||
92
packages/core/platform/images/migrations/migrations/32
Executable file
92
packages/core/platform/images/migrations/migrations/32
Executable file
@@ -0,0 +1,92 @@
|
||||
#!/bin/sh
|
||||
# Migration 32 --> 33
|
||||
# Convert publishing.certificates.issuerType to solver + issuerName in
|
||||
# the cozystack-platform Package resource.
|
||||
#
|
||||
# Old field (pre-refactor schema):
|
||||
# publishing.certificates.issuerType: "http01" | "cloudflare"
|
||||
# New fields:
|
||||
# publishing.certificates.solver: "http01" | "dns01"
|
||||
# publishing.certificates.issuerName: "letsencrypt-prod" (or custom)
|
||||
#
|
||||
# Conversion table:
|
||||
# cloudflare -> solver: dns01, issuerName: letsencrypt-prod
|
||||
# http01 -> solver: http01, issuerName: letsencrypt-prod
|
||||
# <custom> -> issuerName: <custom> (solver left at chart default)
|
||||
# <absent> -> no-op
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PACKAGE_NAME="cozystack.cozystack-platform"
|
||||
|
||||
# Check if Package exists
|
||||
if ! kubectl get package "$PACKAGE_NAME" >/dev/null 2>&1; then
|
||||
echo "Package $PACKAGE_NAME not found, skipping migration"
|
||||
kubectl create configmap -n cozy-system cozystack-version \
|
||||
--from-literal=version=33 --dry-run=client -o yaml | kubectl apply -f -
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Read current issuerType value
|
||||
ISSUER_TYPE=$(kubectl get package "$PACKAGE_NAME" -o json | \
|
||||
jq -r '.spec.components.platform.values.publishing.certificates.issuerType // ""')
|
||||
|
||||
if [ -z "$ISSUER_TYPE" ]; then
|
||||
echo "No issuerType found in Package $PACKAGE_NAME, nothing to migrate"
|
||||
kubectl create configmap -n cozy-system cozystack-version \
|
||||
--from-literal=version=33 --dry-run=client -o yaml | kubectl apply -f -
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Found issuerType: $ISSUER_TYPE"
|
||||
|
||||
# Convert old issuerType to new solver/issuerName
|
||||
SOLVER=""
|
||||
ISSUER_NAME=""
|
||||
case "$ISSUER_TYPE" in
|
||||
cloudflare)
|
||||
SOLVER="dns01"
|
||||
ISSUER_NAME="letsencrypt-prod"
|
||||
;;
|
||||
http01)
|
||||
SOLVER="http01"
|
||||
ISSUER_NAME="letsencrypt-prod"
|
||||
;;
|
||||
*)
|
||||
# Unrecognised value — treat as custom ClusterIssuer name, no solver override
|
||||
ISSUER_NAME="$ISSUER_TYPE"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Converting to: solver=${SOLVER:-<chart default>}, issuerName=${ISSUER_NAME:-<chart default>}"
|
||||
|
||||
# Build the certificates patch:
|
||||
# - null removes issuerType (JSON merge patch semantics)
|
||||
# - solver and issuerName are included only when non-empty
|
||||
CERTS_PATCH=$(jq -n --arg solver "$SOLVER" --arg issuerName "$ISSUER_NAME" '
|
||||
{"issuerType": null}
|
||||
+ (if $solver != "" then {"solver": $solver} else {} end)
|
||||
+ (if $issuerName != "" then {"issuerName": $issuerName} else {} end)
|
||||
')
|
||||
|
||||
PATCH_JSON=$(jq -n --argjson certs "$CERTS_PATCH" '{
|
||||
"spec": {
|
||||
"components": {
|
||||
"platform": {
|
||||
"values": {
|
||||
"publishing": {
|
||||
"certificates": $certs
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}')
|
||||
|
||||
kubectl patch package "$PACKAGE_NAME" --type=merge --patch "$PATCH_JSON"
|
||||
|
||||
echo "Migration complete: issuerType=$ISSUER_TYPE -> solver=${SOLVER:-<unset>} issuerName=${ISSUER_NAME:-<unset>}"
|
||||
|
||||
# Stamp version
|
||||
kubectl create configmap -n cozy-system cozystack-version \
|
||||
--from-literal=version=33 --dry-run=client -o yaml | kubectl apply -f -
|
||||
@@ -21,7 +21,8 @@ stringData:
|
||||
_cluster:
|
||||
root-host: {{ $rootHost | quote }}
|
||||
bundle-name: {{ .Values.bundles.system.variant | quote }}
|
||||
clusterissuer: {{ .Values.publishing.certificates.issuerType | quote }}
|
||||
solver: {{ .Values.publishing.certificates.solver | quote }}
|
||||
issuer-name: {{ .Values.publishing.certificates.issuerName | quote }}
|
||||
oidc-enabled: {{ .Values.authentication.oidc.enabled | quote }}
|
||||
oidc-insecure-skip-verify: {{ .Values.authentication.oidc.insecureSkipVerify | quote }}
|
||||
extra-keycloak-redirect-uri-for-dashboard: {{ index .Values.authentication.oidc.keycloakExtraRedirectUri | quote }}
|
||||
|
||||
@@ -6,7 +6,7 @@ sourceRef:
|
||||
migrations:
|
||||
enabled: false
|
||||
image: ghcr.io/cozystack/cozystack/platform-migrations:v1.0.0-beta.6@sha256:37c78dafcedbdad94acd9912550db0b4875897150666b8a06edfa894de99064e
|
||||
targetVersion: 32
|
||||
targetVersion: 33
|
||||
# Bundle deployment configuration
|
||||
bundles:
|
||||
system:
|
||||
@@ -46,7 +46,8 @@ publishing:
|
||||
apiServerEndpoint: "" # example: "https://api.example.org"
|
||||
externalIPs: []
|
||||
certificates:
|
||||
issuerType: http01 # "http01" or "cloudflare"
|
||||
solver: http01 # "http01" or "dns01"
|
||||
issuerName: letsencrypt-prod
|
||||
# Authentication configuration
|
||||
authentication:
|
||||
oidc:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{{- $issuerType := (index .Values._cluster "clusterissuer") | default "http01" }}
|
||||
{{- $solver := (index .Values._cluster "solver") | default "http01" }}
|
||||
{{- $clusterIssuer := (index .Values._cluster "issuer-name") | default "letsencrypt-prod" }}
|
||||
{{- $ingress := .Values._namespace.ingress }}
|
||||
{{- $host := .Values._namespace.host }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
@@ -8,10 +9,10 @@ metadata:
|
||||
labels:
|
||||
app: bootbox
|
||||
annotations:
|
||||
{{- if ne $issuerType "cloudflare" }}
|
||||
{{- if eq $solver "http01" }}
|
||||
acme.cert-manager.io/http01-ingress-class: {{ $ingress }}
|
||||
{{- end }}
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
cert-manager.io/cluster-issuer: {{ $clusterIssuer }}
|
||||
{{- if .Values.whitelistHTTP }}
|
||||
nginx.ingress.kubernetes.io/whitelist-source-range: "{{ join "," (.Values.whitelist | default "0.0.0.0/32") }}"
|
||||
{{- end }}
|
||||
|
||||
@@ -36,6 +36,8 @@
|
||||
{{- if not (eq .Values.topology "Client") }}
|
||||
{{- $ingress := .Values._namespace.ingress }}
|
||||
{{- $host := .Values._namespace.host }}
|
||||
{{- $solver := (index .Values._cluster "solver") | default "http01" }}
|
||||
{{- $clusterIssuer := (index .Values._cluster "issuer-name") | default "letsencrypt-prod" }}
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
@@ -134,8 +136,10 @@ spec:
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: "0"
|
||||
nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
|
||||
{{- if eq $solver "http01" }}
|
||||
acme.cert-manager.io/http01-ingress-class: {{ $ingress }}
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
{{- end }}
|
||||
cert-manager.io/cluster-issuer: {{ $clusterIssuer }}
|
||||
tls:
|
||||
- hosts:
|
||||
- {{ .Values.host | default (printf "s3.%s" $host) }}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{{- $host := .Values._namespace.host }}
|
||||
{{- $ingress := .Values._namespace.ingress }}
|
||||
{{- $issuerType := (index .Values._cluster "clusterissuer") | default "http01" }}
|
||||
{{- $solver := (index .Values._cluster "solver") | default "http01" }}
|
||||
{{- $clusterIssuer := (index .Values._cluster "issuer-name") | default "letsencrypt-prod" }}
|
||||
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
@@ -13,10 +14,10 @@ metadata:
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: "0"
|
||||
nginx.ingress.kubernetes.io/proxy-read-timeout: "99999"
|
||||
nginx.ingress.kubernetes.io/proxy-send-timeout: "99999"
|
||||
{{- if ne $issuerType "cloudflare" }}
|
||||
{{- if eq $solver "http01" }}
|
||||
acme.cert-manager.io/http01-ingress-class: {{ $ingress }}
|
||||
{{- end }}
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
cert-manager.io/cluster-issuer: {{ $clusterIssuer }}
|
||||
spec:
|
||||
ingressClassName: {{ $ingress }}
|
||||
tls:
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,9 @@
|
||||
# update this file only when a new major or minor version is released
|
||||
apiVersion: clusterctl.cluster.x-k8s.io/v1alpha3
|
||||
releaseSeries:
|
||||
- major: 0
|
||||
minor: 16
|
||||
contract: v1beta1
|
||||
- major: 0
|
||||
minor: 15
|
||||
contract: v1beta1
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: v0.15.1-cp
|
||||
name: v0.16.0-cp
|
||||
labels:
|
||||
cp-components: cozy
|
||||
annotations:
|
||||
|
||||
@@ -4,7 +4,7 @@ metadata:
|
||||
name: kamaji
|
||||
spec:
|
||||
# https://github.com/clastix/cluster-api-control-plane-provider-kamaji
|
||||
version: v0.15.1-cp
|
||||
version: v0.16.0-cp
|
||||
fetchConfig:
|
||||
selector:
|
||||
matchLabels:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{{- $issuerType := (index .Values._cluster "clusterissuer") | default "http01" }}
|
||||
{{- $solver := (index .Values._cluster "solver") | default "http01" }}
|
||||
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: ClusterIssuer
|
||||
@@ -10,7 +10,7 @@ spec:
|
||||
name: letsencrypt-prod
|
||||
server: https://acme-v02.api.letsencrypt.org/directory
|
||||
solvers:
|
||||
- {{- if eq $issuerType "cloudflare" }}
|
||||
- {{- if eq $solver "dns01" }}
|
||||
dns01:
|
||||
cloudflare:
|
||||
apiTokenSecretRef:
|
||||
@@ -34,7 +34,7 @@ spec:
|
||||
name: letsencrypt-stage
|
||||
server: https://acme-staging-v02.api.letsencrypt.org/directory
|
||||
solvers:
|
||||
- {{- if eq $issuerType "cloudflare" }}
|
||||
- {{- if eq $solver "dns01" }}
|
||||
dns01:
|
||||
cloudflare:
|
||||
apiTokenSecretRef:
|
||||
|
||||
@@ -181,7 +181,6 @@ rules:
|
||||
- persistentvolumes
|
||||
- endpoints
|
||||
- events
|
||||
- resourcequotas
|
||||
verbs:
|
||||
- delete
|
||||
- apiGroups: ["kubevirt.io"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<TFormListInputProps> = ({
|
||||
<PersistedCheckbox formName={persistName || name} persistedControls={persistedControls} type="arr" />
|
||||
</Flex>
|
||||
</Flex>
|
||||
- <ResetedFormItem
|
||||
- key={arrKey !== undefined ? arrKey : Array.isArray(name) ? name.slice(-1)[0] : name}
|
||||
- name={arrName || fixedName}
|
||||
- rules={[getRequiredRule(forceNonRequired === false && !!required?.includes(getStringByName(name)), name)]}
|
||||
- validateTrigger="onBlur"
|
||||
- hasFeedback={designNewLayout ? { icons: feedbackIcons } : true}
|
||||
- >
|
||||
- <Flex gap={8} align="center">
|
||||
+ <Flex gap={8} align="center">
|
||||
+ <ResetedFormItem
|
||||
+ key={arrKey !== undefined ? arrKey : Array.isArray(name) ? name.slice(-1)[0] : name}
|
||||
+ name={arrName || fixedName}
|
||||
+ rules={[getRequiredRule(forceNonRequired === false && !!required?.includes(getStringByName(name)), name)]}
|
||||
+ validateTrigger="onBlur"
|
||||
+ hasFeedback={designNewLayout ? { icons: feedbackIcons } : true}
|
||||
+ style={{ flex: 1 }}
|
||||
+ >
|
||||
<Select
|
||||
mode={customProps.mode}
|
||||
placeholder="Select"
|
||||
@@ -277,13 +278,13 @@ export const FormListInput: FC<TFormListInputProps> = ({
|
||||
showSearch
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
- {relatedValueTooltip && (
|
||||
- <Tooltip title={relatedValueTooltip}>
|
||||
- <QuestionCircleOutlined />
|
||||
- </Tooltip>
|
||||
- )}
|
||||
- </Flex>
|
||||
- </ResetedFormItem>
|
||||
+ </ResetedFormItem>
|
||||
+ {relatedValueTooltip && (
|
||||
+ <Tooltip title={relatedValueTooltip}>
|
||||
+ <QuestionCircleOutlined />
|
||||
+ </Tooltip>
|
||||
+ )}
|
||||
+ </Flex>
|
||||
</HiddenContainer>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
{{- $issuerType := (index .Values._cluster "clusterissuer") | default "http01" }}
|
||||
{{- $solver := (index .Values._cluster "solver") | default "http01" }}
|
||||
{{- $clusterIssuer := (index .Values._cluster "issuer-name") | default "letsencrypt-prod" }}
|
||||
{{- $host := index .Values._cluster "root-host" }}
|
||||
{{- $exposeServices := splitList "," ((index .Values._cluster "expose-services") | default "") }}
|
||||
{{- $exposeIngress := (index .Values._cluster "expose-ingress") | default "tenant-root" }}
|
||||
@@ -8,9 +9,8 @@ apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
{{- if eq $issuerType "cloudflare" }}
|
||||
{{- else }}
|
||||
cert-manager.io/cluster-issuer: {{ $clusterIssuer }}
|
||||
{{- if eq $solver "http01" }}
|
||||
acme.cert-manager.io/http01-ingress-class: {{ $exposeIngress }}
|
||||
{{- end }}
|
||||
nginx.ingress.kubernetes.io/rewrite-target: /
|
||||
|
||||
28
packages/system/kamaji/charts/kamaji-crds/.helmignore
Normal file
28
packages/system/kamaji/charts/kamaji-crds/.helmignore
Normal file
@@ -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
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
# Helm source files
|
||||
README.md.gotmpl
|
||||
.helmignore
|
||||
# Build tools
|
||||
Makefile
|
||||
39
packages/system/kamaji/charts/kamaji-crds/Chart.yaml
Normal file
39
packages/system/kamaji/charts/kamaji-crds/Chart.yaml
Normal file
@@ -0,0 +1,39 @@
|
||||
apiVersion: v2
|
||||
appVersion: latest
|
||||
description: Kamaji is the Hosted Control Plane Manager for Kubernetes.
|
||||
home: https://github.com/clastix/kamaji
|
||||
icon: https://github.com/clastix/kamaji/raw/master/assets/logo-colored.png
|
||||
maintainers:
|
||||
- email: dario@tranchitella.eu
|
||||
name: Dario Tranchitella
|
||||
url: https://clastix.io
|
||||
- email: me@bsctl.io
|
||||
name: Adriano Pezzuto
|
||||
url: https://clastix.io
|
||||
name: kamaji-crds
|
||||
sources:
|
||||
- https://github.com/clastix/kamaji
|
||||
type: application
|
||||
version: 0.0.0+latest
|
||||
annotations:
|
||||
artifacthub.io/crds: |
|
||||
- kind: TenantControlPlane
|
||||
version: v1alpha1
|
||||
name: tenantcontrolplanes.kamaji.clastix.io
|
||||
displayName: TenantControlPlane
|
||||
description: TenantControlPlane defines the desired state for a Control Plane backed by Kamaji.
|
||||
- kind: DataStore
|
||||
version: v1alpha1
|
||||
name: datastores.kamaji.clastix.io
|
||||
displayName: DataStore
|
||||
description: DataStores is holding all the required details to communicate with a Datastore, such as etcd, MySQL, PostgreSQL, and NATS.
|
||||
artifacthub.io/links: |
|
||||
- name: CLASTIX
|
||||
url: https://clastix.io
|
||||
- name: support
|
||||
url: https://clastix.io/support
|
||||
artifacthub.io/changes: |
|
||||
- kind: changed
|
||||
description: Upgrading support to Kubernetes v1.35
|
||||
- kind: added
|
||||
description: Supporting multiple Datastore via etcd overrides
|
||||
9
packages/system/kamaji/charts/kamaji-crds/Makefile
Normal file
9
packages/system/kamaji/charts/kamaji-crds/Makefile
Normal file
@@ -0,0 +1,9 @@
|
||||
docs: HELMDOCS_VERSION := v1.8.1
|
||||
docs: docker
|
||||
@docker run --rm -v "$$(pwd):/helm-docs" -u $$(id -u) jnorwood/helm-docs:$(HELMDOCS_VERSION)
|
||||
|
||||
docker:
|
||||
@hash docker 2>/dev/null || {\
|
||||
echo "You need docker" &&\
|
||||
exit 1;\
|
||||
}
|
||||
2
packages/system/kamaji/charts/kamaji-crds/NOTES.txt
Normal file
2
packages/system/kamaji/charts/kamaji-crds/NOTES.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Kamaji Custom Resource Definitions have been installed properly:
|
||||
you can proceed to upgrade your Kamaji operator instance.
|
||||
66
packages/system/kamaji/charts/kamaji-crds/README.md
Normal file
66
packages/system/kamaji/charts/kamaji-crds/README.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# kamaji-crds
|
||||
|
||||
  
|
||||
|
||||
Kamaji is the Hosted Control Plane Manager for Kubernetes.
|
||||
|
||||
## Maintainers
|
||||
|
||||
| Name | Email | Url |
|
||||
| ---- | ------ | --- |
|
||||
| Dario Tranchitella | <dario@tranchitella.eu> | <https://clastix.io> |
|
||||
| Adriano Pezzuto | <me@bsctl.io> | <https://clastix.io> |
|
||||
|
||||
## Source Code
|
||||
|
||||
* <https://github.com/clastix/kamaji>
|
||||
|
||||
[Kamaji](https://github.com/clastix/kamaji) Custom Resource Definitions packaged as Helm Charts.
|
||||
|
||||
## How to use this chart
|
||||
|
||||
Add `clastix` Helm repository:
|
||||
|
||||
helm repo add clastix https://clastix.github.io/charts
|
||||
|
||||
Install the Chart with the release name `kamaji-crds`:
|
||||
|
||||
helm upgrade --install --namespace kamaji-system --create-namespace kamaji-crds clastix/kamaji-crds
|
||||
|
||||
Show the status:
|
||||
|
||||
helm status kamaji-crds -n kamaji-system
|
||||
|
||||
Upgrade the Chart
|
||||
|
||||
helm upgrade kamaji-crds -n kamaji-system clastix/kamaji-crds
|
||||
|
||||
Uninstall the Chart
|
||||
|
||||
helm uninstall kamaji-crds -n kamaji-system
|
||||
|
||||
## Customize the installation
|
||||
|
||||
There are two methods for specifying overrides of values during Chart installation: `--values` and `--set`.
|
||||
|
||||
The `--values` option is the preferred method because it allows you to keep your overrides in a YAML file, rather than specifying them all on the command line. Create a copy of the YAML file `values.yaml` and add your overrides to it.
|
||||
|
||||
Specify your overrides file when you install the Chart:
|
||||
|
||||
helm upgrade kamaji-crds --install --namespace kamaji-system --create-namespace clastix/kamaji-crds --values myvalues.yaml
|
||||
|
||||
The values in your overrides file `myvalues.yaml` will override their counterparts in the Chart's values.yaml file. Any values in `values.yaml` that weren’t overridden will keep their defaults.
|
||||
|
||||
If you only need to make minor customizations, you can specify them on the command line by using the `--set` option. For example:
|
||||
|
||||
helm upgrade kamaji-crds --install --namespace kamaji-system --create-namespace clastix/kamaji-crds --set kamajiCertificateName=kamaji
|
||||
|
||||
## Values
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| fullnameOverride | string | `""` | Overrides the full name of the resources created by the chart. |
|
||||
| kamajiCertificateName | string | `"kamaji-serving-cert"` | The cert-manager Certificate resource name, holding the Certificate Authority for webhooks. |
|
||||
| kamajiNamespace | string | `"kamaji-system"` | The namespace where Kamaji has been installed: required to inject the Certificate Authority for cert-manager. |
|
||||
| kamajiService | string | `"kamaji-webhook-service"` | The Kamaji webhook Service name. |
|
||||
| nameOverride | string | `""` | Overrides the name of the chart for resource naming purposes. |
|
||||
54
packages/system/kamaji/charts/kamaji-crds/README.md.gotmpl
Normal file
54
packages/system/kamaji/charts/kamaji-crds/README.md.gotmpl
Normal file
@@ -0,0 +1,54 @@
|
||||
{{ template "chart.header" . }}
|
||||
{{ template "chart.deprecationWarning" . }}
|
||||
|
||||
{{ template "chart.badgesSection" . }}
|
||||
|
||||
{{ template "chart.description" . }}
|
||||
|
||||
{{ template "chart.maintainersSection" . }}
|
||||
|
||||
{{ template "chart.sourcesSection" . }}
|
||||
|
||||
{{ template "chart.requirementsSection" . }}
|
||||
|
||||
[Kamaji](https://github.com/clastix/kamaji) Custom Resource Definitions packaged as Helm Charts.
|
||||
|
||||
## How to use this chart
|
||||
|
||||
Add `clastix` Helm repository:
|
||||
|
||||
helm repo add clastix https://clastix.github.io/charts
|
||||
|
||||
Install the Chart with the release name `kamaji-crds`:
|
||||
|
||||
helm upgrade --install --namespace kamaji-system --create-namespace kamaji-crds clastix/kamaji-crds
|
||||
|
||||
Show the status:
|
||||
|
||||
helm status kamaji-crds -n kamaji-system
|
||||
|
||||
Upgrade the Chart
|
||||
|
||||
helm upgrade kamaji-crds -n kamaji-system clastix/kamaji-crds
|
||||
|
||||
Uninstall the Chart
|
||||
|
||||
helm uninstall kamaji-crds -n kamaji-system
|
||||
|
||||
## Customize the installation
|
||||
|
||||
There are two methods for specifying overrides of values during Chart installation: `--values` and `--set`.
|
||||
|
||||
The `--values` option is the preferred method because it allows you to keep your overrides in a YAML file, rather than specifying them all on the command line. Create a copy of the YAML file `values.yaml` and add your overrides to it.
|
||||
|
||||
Specify your overrides file when you install the Chart:
|
||||
|
||||
helm upgrade kamaji-crds --install --namespace kamaji-system --create-namespace clastix/kamaji-crds --values myvalues.yaml
|
||||
|
||||
The values in your overrides file `myvalues.yaml` will override their counterparts in the Chart's values.yaml file. Any values in `values.yaml` that weren’t overridden will keep their defaults.
|
||||
|
||||
If you only need to make minor customizations, you can specify them on the command line by using the `--set` option. For example:
|
||||
|
||||
helm upgrade kamaji-crds --install --namespace kamaji-system --create-namespace clastix/kamaji-crds --set kamajiCertificateName=kamaji
|
||||
|
||||
{{ template "chart.valuesSection" . }}
|
||||
@@ -0,0 +1,11 @@
|
||||
spec:
|
||||
conversion:
|
||||
strategy: Webhook
|
||||
webhook:
|
||||
clientConfig:
|
||||
service:
|
||||
name: kamaji-webhook-service
|
||||
namespace: kamaji-system
|
||||
path: /convert
|
||||
conversionReviewVersions:
|
||||
- v1
|
||||
@@ -0,0 +1,292 @@
|
||||
group: kamaji.clastix.io
|
||||
names:
|
||||
kind: DataStore
|
||||
listKind: DataStoreList
|
||||
plural: datastores
|
||||
singular: datastore
|
||||
scope: Cluster
|
||||
versions:
|
||||
- additionalPrinterColumns:
|
||||
- description: Kamaji data store driver
|
||||
jsonPath: .spec.driver
|
||||
name: Driver
|
||||
type: string
|
||||
- description: Age
|
||||
jsonPath: .metadata.creationTimestamp
|
||||
name: Age
|
||||
type: date
|
||||
name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: DataStore is the Schema for the datastores 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: DataStoreSpec defines the desired state of DataStore.
|
||||
properties:
|
||||
basicAuth:
|
||||
description: |-
|
||||
In case of authentication enabled for the given data store, specifies the username and password pair.
|
||||
This value is optional.
|
||||
properties:
|
||||
password:
|
||||
properties:
|
||||
content:
|
||||
description: |-
|
||||
Bare content of the file, base64 encoded.
|
||||
It has precedence over the SecretReference value.
|
||||
format: byte
|
||||
type: string
|
||||
secretReference:
|
||||
properties:
|
||||
keyPath:
|
||||
description: |-
|
||||
Name of the key for the given Secret reference where the content is stored.
|
||||
This value is mandatory.
|
||||
minLength: 1
|
||||
type: string
|
||||
name:
|
||||
description: name is unique within a namespace to reference a secret resource.
|
||||
type: string
|
||||
namespace:
|
||||
description: namespace defines the space within which the secret name must be unique.
|
||||
type: string
|
||||
required:
|
||||
- keyPath
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
type: object
|
||||
username:
|
||||
properties:
|
||||
content:
|
||||
description: |-
|
||||
Bare content of the file, base64 encoded.
|
||||
It has precedence over the SecretReference value.
|
||||
format: byte
|
||||
type: string
|
||||
secretReference:
|
||||
properties:
|
||||
keyPath:
|
||||
description: |-
|
||||
Name of the key for the given Secret reference where the content is stored.
|
||||
This value is mandatory.
|
||||
minLength: 1
|
||||
type: string
|
||||
name:
|
||||
description: name is unique within a namespace to reference a secret resource.
|
||||
type: string
|
||||
namespace:
|
||||
description: namespace defines the space within which the secret name must be unique.
|
||||
type: string
|
||||
required:
|
||||
- keyPath
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
type: object
|
||||
required:
|
||||
- password
|
||||
- username
|
||||
type: object
|
||||
driver:
|
||||
description: The driver to use to connect to the shared datastore.
|
||||
enum:
|
||||
- etcd
|
||||
- MySQL
|
||||
- PostgreSQL
|
||||
- NATS
|
||||
type: string
|
||||
x-kubernetes-validations:
|
||||
- message: Datastore driver is immutable
|
||||
rule: self == oldSelf
|
||||
endpoints:
|
||||
description: |-
|
||||
List of the endpoints to connect to the shared datastore.
|
||||
No need for protocol, just bare IP/FQDN and port.
|
||||
items:
|
||||
type: string
|
||||
minItems: 1
|
||||
type: array
|
||||
tlsConfig:
|
||||
description: |-
|
||||
Defines the TLS/SSL configuration required to connect to the data store in a secure way.
|
||||
This value is optional.
|
||||
properties:
|
||||
certificateAuthority:
|
||||
description: |-
|
||||
Retrieve the Certificate Authority certificate and private key, such as bare content of the file, or a SecretReference.
|
||||
The key reference is required since etcd authentication is based on certificates, and Kamaji is responsible in creating this.
|
||||
properties:
|
||||
certificate:
|
||||
properties:
|
||||
content:
|
||||
description: |-
|
||||
Bare content of the file, base64 encoded.
|
||||
It has precedence over the SecretReference value.
|
||||
format: byte
|
||||
type: string
|
||||
secretReference:
|
||||
properties:
|
||||
keyPath:
|
||||
description: |-
|
||||
Name of the key for the given Secret reference where the content is stored.
|
||||
This value is mandatory.
|
||||
minLength: 1
|
||||
type: string
|
||||
name:
|
||||
description: name is unique within a namespace to reference a secret resource.
|
||||
type: string
|
||||
namespace:
|
||||
description: namespace defines the space within which the secret name must be unique.
|
||||
type: string
|
||||
required:
|
||||
- keyPath
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
type: object
|
||||
privateKey:
|
||||
properties:
|
||||
content:
|
||||
description: |-
|
||||
Bare content of the file, base64 encoded.
|
||||
It has precedence over the SecretReference value.
|
||||
format: byte
|
||||
type: string
|
||||
secretReference:
|
||||
properties:
|
||||
keyPath:
|
||||
description: |-
|
||||
Name of the key for the given Secret reference where the content is stored.
|
||||
This value is mandatory.
|
||||
minLength: 1
|
||||
type: string
|
||||
name:
|
||||
description: name is unique within a namespace to reference a secret resource.
|
||||
type: string
|
||||
namespace:
|
||||
description: namespace defines the space within which the secret name must be unique.
|
||||
type: string
|
||||
required:
|
||||
- keyPath
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
type: object
|
||||
required:
|
||||
- certificate
|
||||
type: object
|
||||
clientCertificate:
|
||||
description: Specifies the SSL/TLS key and private key pair used to connect to the data store.
|
||||
properties:
|
||||
certificate:
|
||||
properties:
|
||||
content:
|
||||
description: |-
|
||||
Bare content of the file, base64 encoded.
|
||||
It has precedence over the SecretReference value.
|
||||
format: byte
|
||||
type: string
|
||||
secretReference:
|
||||
properties:
|
||||
keyPath:
|
||||
description: |-
|
||||
Name of the key for the given Secret reference where the content is stored.
|
||||
This value is mandatory.
|
||||
minLength: 1
|
||||
type: string
|
||||
name:
|
||||
description: name is unique within a namespace to reference a secret resource.
|
||||
type: string
|
||||
namespace:
|
||||
description: namespace defines the space within which the secret name must be unique.
|
||||
type: string
|
||||
required:
|
||||
- keyPath
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
type: object
|
||||
privateKey:
|
||||
properties:
|
||||
content:
|
||||
description: |-
|
||||
Bare content of the file, base64 encoded.
|
||||
It has precedence over the SecretReference value.
|
||||
format: byte
|
||||
type: string
|
||||
secretReference:
|
||||
properties:
|
||||
keyPath:
|
||||
description: |-
|
||||
Name of the key for the given Secret reference where the content is stored.
|
||||
This value is mandatory.
|
||||
minLength: 1
|
||||
type: string
|
||||
name:
|
||||
description: name is unique within a namespace to reference a secret resource.
|
||||
type: string
|
||||
namespace:
|
||||
description: namespace defines the space within which the secret name must be unique.
|
||||
type: string
|
||||
required:
|
||||
- keyPath
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
type: object
|
||||
required:
|
||||
- certificate
|
||||
- privateKey
|
||||
type: object
|
||||
required:
|
||||
- certificateAuthority
|
||||
type: object
|
||||
required:
|
||||
- driver
|
||||
- endpoints
|
||||
type: object
|
||||
x-kubernetes-validations:
|
||||
- message: certificateAuthority privateKey must have secretReference or content when driver is etcd
|
||||
rule: '(self.driver == "etcd") ? (self.tlsConfig != null && (has(self.tlsConfig.certificateAuthority.privateKey.secretReference) || has(self.tlsConfig.certificateAuthority.privateKey.content))) : true'
|
||||
- message: clientCertificate must have secretReference or content when driver is etcd
|
||||
rule: '(self.driver == "etcd") ? (self.tlsConfig != null && (has(self.tlsConfig.clientCertificate.certificate.secretReference) || has(self.tlsConfig.clientCertificate.certificate.content))) : true'
|
||||
- message: clientCertificate privateKey must have secretReference or content when driver is etcd
|
||||
rule: '(self.driver == "etcd") ? (self.tlsConfig != null && (has(self.tlsConfig.clientCertificate.privateKey.secretReference) || has(self.tlsConfig.clientCertificate.privateKey.content))) : true'
|
||||
- message: When driver is not etcd and tlsConfig exists, clientCertificate must be null or contain valid content
|
||||
rule: '(self.driver != "etcd" && has(self.tlsConfig) && has(self.tlsConfig.clientCertificate)) ? (((has(self.tlsConfig.clientCertificate.certificate.secretReference) || has(self.tlsConfig.clientCertificate.certificate.content)))) : true'
|
||||
- message: When driver is not etcd and basicAuth exists, username must have secretReference or content
|
||||
rule: '(self.driver != "etcd" && has(self.basicAuth)) ? ((has(self.basicAuth.username.secretReference) || has(self.basicAuth.username.content))) : true'
|
||||
- message: When driver is not etcd and basicAuth exists, password must have secretReference or content
|
||||
rule: '(self.driver != "etcd" && has(self.basicAuth)) ? ((has(self.basicAuth.password.secretReference) || has(self.basicAuth.password.content))) : true'
|
||||
- message: When driver is not etcd, either tlsConfig or basicAuth must be provided
|
||||
rule: '(self.driver != "etcd") ? (has(self.tlsConfig) || has(self.basicAuth)) : true'
|
||||
status:
|
||||
description: DataStoreStatus defines the observed state of DataStore.
|
||||
properties:
|
||||
observedGeneration:
|
||||
description: ObservedGeneration represents the .metadata.generation that was last reconciled.
|
||||
format: int64
|
||||
type: integer
|
||||
usedBy:
|
||||
description: List of the Tenant Control Planes, namespaced named, using this data store.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
@@ -0,0 +1,218 @@
|
||||
group: kamaji.clastix.io
|
||||
names:
|
||||
categories:
|
||||
- kamaji
|
||||
kind: KubeconfigGenerator
|
||||
listKind: KubeconfigGeneratorList
|
||||
plural: kubeconfiggenerators
|
||||
shortNames:
|
||||
- kc
|
||||
singular: kubeconfiggenerator
|
||||
scope: Cluster
|
||||
versions:
|
||||
- additionalPrinterColumns:
|
||||
- description: Age
|
||||
jsonPath: .metadata.creationTimestamp
|
||||
name: Age
|
||||
type: date
|
||||
name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: KubeconfigGenerator is the Schema for the kubeconfiggenerators 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:
|
||||
controlPlaneEndpointFrom:
|
||||
default: admin.svc
|
||||
description: |-
|
||||
ControlPlaneEndpointFrom is the key used to extract the Tenant Control Plane endpoint that must be used by the generator.
|
||||
The targeted Secret is the `${TCP}-admin-kubeconfig` one, default to `admin.svc`.
|
||||
type: string
|
||||
groups:
|
||||
description: |-
|
||||
Groups is resolved a set of strings used to assign the x509 organisations field.
|
||||
It will be recognised by Kubernetes as user groups.
|
||||
items:
|
||||
description: |-
|
||||
CompoundValue allows defining a static, or a dynamic value.
|
||||
Options are mutually exclusive, just one should be picked up.
|
||||
properties:
|
||||
fromDefinition:
|
||||
description: |-
|
||||
FromDefinition is used to generate a dynamic value,
|
||||
it uses the dot notation to access fields from the referenced TenantControlPlane object:
|
||||
e.g.: metadata.name
|
||||
type: string
|
||||
stringValue:
|
||||
description: StringValue is a static string value.
|
||||
type: string
|
||||
type: object
|
||||
x-kubernetes-validations:
|
||||
- message: Either stringValue or fromDefinition must be set, but not both.
|
||||
rule: (has(self.stringValue) || has(self.fromDefinition)) && !(has(self.stringValue) && has(self.fromDefinition))
|
||||
type: array
|
||||
namespaceSelector:
|
||||
description: NamespaceSelector is used to filter Namespaces from which the generator should extract TenantControlPlane objects.
|
||||
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
|
||||
tenantControlPlaneSelector:
|
||||
description: TenantControlPlaneSelector is used to filter the TenantControlPlane objects that should be address by the generator.
|
||||
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
|
||||
user:
|
||||
description: User resolves to a string to identify the client, assigned to the x509 Common Name field.
|
||||
properties:
|
||||
fromDefinition:
|
||||
description: |-
|
||||
FromDefinition is used to generate a dynamic value,
|
||||
it uses the dot notation to access fields from the referenced TenantControlPlane object:
|
||||
e.g.: metadata.name
|
||||
type: string
|
||||
stringValue:
|
||||
description: StringValue is a static string value.
|
||||
type: string
|
||||
type: object
|
||||
x-kubernetes-validations:
|
||||
- message: Either stringValue or fromDefinition must be set, but not both.
|
||||
rule: (has(self.stringValue) || has(self.fromDefinition)) && !(has(self.stringValue) && has(self.fromDefinition))
|
||||
required:
|
||||
- user
|
||||
type: object
|
||||
status:
|
||||
description: KubeconfigGeneratorStatus defines the observed state of KubeconfigGenerator.
|
||||
properties:
|
||||
availableResources:
|
||||
default: 0
|
||||
description: |-
|
||||
AvailableResources is the sum of successfully generated resources.
|
||||
In case of a different value compared to Resources, check the field errors.
|
||||
type: integer
|
||||
errors:
|
||||
description: Errors is the list of failed kubeconfig generations.
|
||||
items:
|
||||
properties:
|
||||
message:
|
||||
description: Message is the error message recorded upon the last generator run.
|
||||
type: string
|
||||
resource:
|
||||
description: Resource is the Namespaced name of the errored resource.
|
||||
type: string
|
||||
required:
|
||||
- message
|
||||
- resource
|
||||
type: object
|
||||
type: array
|
||||
observedGeneration:
|
||||
description: ObservedGeneration represents the .metadata.generation that was last reconciled.
|
||||
format: int64
|
||||
type: integer
|
||||
resources:
|
||||
default: 0
|
||||
description: Resources is the sum of targeted TenantControlPlane objects.
|
||||
type: integer
|
||||
required:
|
||||
- availableResources
|
||||
- resources
|
||||
type: object
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,49 @@
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "kamaji-crds.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 "kamaji.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 "kamaji-crds.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the cert-manager annotation to inject Certificate CA.
|
||||
*/}}
|
||||
{{- define "kamaji-crds.certManagerAnnotation" -}}
|
||||
{{- printf "%s/%s" (required "A valid .Values.kamajiNamespace is required" .Values.kamajiNamespace) (required "A valid .Values.kamajiCertificateName is required" .Values.kamajiCertificateName) }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "kamaji-crds.labels" -}}
|
||||
helm.sh/chart: {{ include "kamaji-crds.chart" . }}
|
||||
app.kubernetes.io/name: {{ include "kamaji-crds.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/component: "crds"
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,10 @@
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
cert-manager.io/inject-ca-from: {{ include "kamaji-crds.certManagerAnnotation" . }}
|
||||
labels:
|
||||
{{- include "kamaji-crds.labels" . | nindent 4 }}
|
||||
name: datastores.kamaji.clastix.io
|
||||
spec:
|
||||
{{ tpl (.Files.Get "hack/kamaji.clastix.io_datastores_spec.yaml") . | nindent 2}}
|
||||
@@ -0,0 +1,10 @@
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
cert-manager.io/inject-ca-from: {{ include "kamaji-crds.certManagerAnnotation" . }}
|
||||
labels:
|
||||
{{- include "kamaji-crds.labels" . | nindent 4 }}
|
||||
name: kubeconfiggenerators.kamaji.clastix.io
|
||||
spec:
|
||||
{{ tpl (.Files.Get "hack/kamaji.clastix.io_kubeconfiggenerators_spec.yaml") . | nindent 2 }}
|
||||
@@ -0,0 +1,10 @@
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
cert-manager.io/inject-ca-from: {{ include "kamaji-crds.certManagerAnnotation" . }}
|
||||
labels:
|
||||
{{- include "kamaji-crds.labels" . | nindent 4 }}
|
||||
name: tenantcontrolplanes.kamaji.clastix.io
|
||||
spec:
|
||||
{{ tpl (.Files.Get "hack/kamaji.clastix.io_tenantcontrolplanes_spec.yaml") . | nindent 2 }}
|
||||
15
packages/system/kamaji/charts/kamaji-crds/values.yaml
Normal file
15
packages/system/kamaji/charts/kamaji-crds/values.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
# Default values for kamaji-crds.
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
|
||||
# -- Overrides the name of the chart for resource naming purposes.
|
||||
nameOverride: ""
|
||||
# -- Overrides the full name of the resources created by the chart.
|
||||
fullnameOverride: ""
|
||||
|
||||
# -- The namespace where Kamaji has been installed: required to inject the Certificate Authority for cert-manager.
|
||||
kamajiNamespace: kamaji-system
|
||||
# -- The Kamaji webhook Service name.
|
||||
kamajiService: kamaji-webhook-service
|
||||
# -- The cert-manager Certificate resource name, holding the Certificate Authority for webhooks.
|
||||
kamajiCertificateName: kamaji-serving-cert
|
||||
@@ -21,3 +21,8 @@
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
# Helm source files
|
||||
README.md.gotmpl
|
||||
.helmignore
|
||||
# Build tools
|
||||
Makefile
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
dependencies:
|
||||
- name: kamaji-etcd
|
||||
repository: https://clastix.github.io/charts
|
||||
version: 0.9.2
|
||||
digest: sha256:ba76d3a30e5e20dbbbbcc36a0e7465d4b1adacc956061e7f6ea47b99fc8f08a6
|
||||
generated: "2025-03-14T21:23:30.421915+09:00"
|
||||
version: 0.11.0
|
||||
digest: sha256:96b4115b8c02f771f809ec1bed3be3a3903e7e8315d6966aa54b0f73230ea421
|
||||
generated: "2025-07-03T09:19:19.835421461+02:00"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
appVersion: v0.0.0
|
||||
appVersion: latest
|
||||
description: Kamaji is the Hosted Control Plane Manager for Kubernetes.
|
||||
home: https://github.com/clastix/kamaji
|
||||
icon: https://github.com/clastix/kamaji/raw/master/assets/logo-colored.png
|
||||
@@ -17,11 +17,11 @@ name: kamaji
|
||||
sources:
|
||||
- https://github.com/clastix/kamaji
|
||||
type: application
|
||||
version: 0.0.0
|
||||
version: 0.0.0+latest
|
||||
dependencies:
|
||||
- name: kamaji-etcd
|
||||
repository: https://clastix.github.io/charts
|
||||
version: ">=0.9.2"
|
||||
version: ">=0.11.0"
|
||||
condition: kamaji-etcd.deploy
|
||||
annotations:
|
||||
catalog.cattle.io/certified: partner
|
||||
@@ -46,4 +46,5 @@ annotations:
|
||||
artifacthub.io/operator: "true"
|
||||
artifacthub.io/operatorCapabilities: "full lifecycle"
|
||||
artifacthub.io/changes: |
|
||||
- Using dependency chart `kamaji-etcd` as a default DataStore.
|
||||
- kind: added
|
||||
description: Releasing latest chart at every push
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# kamaji
|
||||
|
||||
  
|
||||
  
|
||||
|
||||
Kamaji is the Hosted Control Plane Manager for Kubernetes.
|
||||
|
||||
@@ -22,7 +22,7 @@ Kubernetes: `>=1.21.0-0`
|
||||
|
||||
| Repository | Name | Version |
|
||||
|------------|------|---------|
|
||||
| https://clastix.github.io/charts | kamaji-etcd | >=0.9.2 |
|
||||
| https://clastix.github.io/charts | kamaji-etcd | >=0.11.0 |
|
||||
|
||||
[Kamaji](https://github.com/clastix/kamaji) requires a [multi-tenant `etcd`](https://github.com/clastix/kamaji-internal/blob/master/deploy/getting-started-with-kamaji.md#setup-internal-multi-tenant-etcd) cluster.
|
||||
This Helm Chart starting from v0.1.1 provides the installation of an internal `etcd` in order to streamline the local test. If you'd like to use an externally managed etcd instance, you can specify the overrides and by setting the value `etcd.deploy=false`.
|
||||
@@ -82,10 +82,25 @@ Here the values you can override:
|
||||
| image.repository | string | `"clastix/kamaji"` | The container image of the Kamaji controller. |
|
||||
| image.tag | string | `nil` | Overrides the image tag whose default is the chart appVersion. |
|
||||
| imagePullSecrets | list | `[]` | |
|
||||
| kamaji-etcd.datastore.enabled | bool | `true` | |
|
||||
| kamaji-etcd.datastore.name | string | `"default"` | |
|
||||
| kamaji-etcd.deploy | bool | `true` | |
|
||||
| kamaji-etcd.fullnameOverride | string | `"kamaji-etcd"` | |
|
||||
| kamaji-etcd | object | `{"clusterDomain":"cluster.local","datastore":{"enabled":true,"name":"default"},"deploy":true,"fullnameOverride":"kamaji-etcd"}` | Subchart: See https://github.com/clastix/kamaji-etcd/blob/master/charts/kamaji-etcd/values.yaml |
|
||||
| kubeconfigGenerator.affinity | object | `{}` | Kubernetes affinity rules to apply to Kubeconfig Generator controller pods |
|
||||
| kubeconfigGenerator.enableLeaderElect | bool | `true` | Enables the leader election. |
|
||||
| kubeconfigGenerator.enabled | bool | `false` | Toggle to deploy the Kubeconfig Generator Deployment. |
|
||||
| kubeconfigGenerator.extraArgs | list | `[]` | A list of extra arguments to add to the Kubeconfig Generator controller default ones. |
|
||||
| kubeconfigGenerator.fullnameOverride | string | `""` | |
|
||||
| kubeconfigGenerator.healthProbeBindAddress | string | `":8081"` | The address the probe endpoint binds to. |
|
||||
| kubeconfigGenerator.loggingDevel.enable | bool | `false` | Development Mode defaults(encoder=consoleEncoder,logLevel=Debug,stackTraceLevel=Warn). Production Mode defaults(encoder=jsonEncoder,logLevel=Info,stackTraceLevel=Error) |
|
||||
| kubeconfigGenerator.nodeSelector | object | `{}` | Kubernetes node selector rules to schedule Kubeconfig Generator controller |
|
||||
| kubeconfigGenerator.podAnnotations | object | `{}` | The annotations to apply to the Kubeconfig Generator controller pods. |
|
||||
| kubeconfigGenerator.podSecurityContext | object | `{"runAsNonRoot":true}` | The securityContext to apply to the Kubeconfig Generator controller pods. |
|
||||
| kubeconfigGenerator.replicaCount | int | `2` | The number of the pod replicas for the Kubeconfig Generator controller. |
|
||||
| kubeconfigGenerator.resources.limits.cpu | string | `"200m"` | |
|
||||
| kubeconfigGenerator.resources.limits.memory | string | `"512Mi"` | |
|
||||
| kubeconfigGenerator.resources.requests.cpu | string | `"200m"` | |
|
||||
| kubeconfigGenerator.resources.requests.memory | string | `"512Mi"` | |
|
||||
| kubeconfigGenerator.securityContext | object | `{"allowPrivilegeEscalation":false}` | The securityContext to apply to the Kubeconfig Generator controller container only. |
|
||||
| kubeconfigGenerator.serviceAccountOverride | string | `""` | The name of the service account to use. If not set, the root Kamaji one will be used. |
|
||||
| kubeconfigGenerator.tolerations | list | `[]` | Kubernetes node taints that the Kubeconfig Generator controller pods would tolerate |
|
||||
| livenessProbe | object | `{"httpGet":{"path":"/healthz","port":"healthcheck"},"initialDelaySeconds":15,"periodSeconds":20}` | The livenessProbe for the controller container |
|
||||
| loggingDevel.enable | bool | `false` | Development Mode defaults(encoder=consoleEncoder,logLevel=Debug,stackTraceLevel=Warn). Production Mode defaults(encoder=jsonEncoder,logLevel=Info,stackTraceLevel=Error) (default false) |
|
||||
| metricsBindAddress | string | `":8080"` | The address the metric endpoint binds to. (default ":8080") |
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
# Kamaji
|
||||
|
||||
Kamaji deploys and operates Kubernetes at scale with a fraction of the operational burden.
|
||||
|
||||
Useful links:
|
||||
- [Kamaji Github repository](https://github.com/clastix/kamaji)
|
||||
- [Kamaji Documentation](https://kamaji.clastix.io)
|
||||
|
||||
## Requirements
|
||||
|
||||
* Kubernetes v1.22+
|
||||
* Helm v3
|
||||
@@ -1,3 +1,25 @@
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- configmaps
|
||||
- secrets
|
||||
- services
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- namespaces
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
@@ -21,11 +43,19 @@
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- ""
|
||||
- gateway.networking.k8s.io
|
||||
resources:
|
||||
- configmaps
|
||||
- secrets
|
||||
- services
|
||||
- gateways
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- gateway.networking.k8s.io
|
||||
resources:
|
||||
- grpcroutes
|
||||
- httproutes
|
||||
- tlsroutes
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
@@ -51,6 +81,7 @@
|
||||
- kamaji.clastix.io
|
||||
resources:
|
||||
- datastores/status
|
||||
- kubeconfiggenerators/status
|
||||
- tenantcontrolplanes/status
|
||||
verbs:
|
||||
- get
|
||||
@@ -59,6 +90,18 @@
|
||||
- apiGroups:
|
||||
- kamaji.clastix.io
|
||||
resources:
|
||||
- kubeconfiggenerators
|
||||
verbs:
|
||||
- create
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- kamaji.clastix.io
|
||||
resources:
|
||||
- kubeconfiggenerators/finalizers
|
||||
- tenantcontrolplanes/finalizers
|
||||
verbs:
|
||||
- update
|
||||
|
||||
@@ -4,7 +4,7 @@ kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
cert-manager.io/inject-ca-from: kamaji-system/kamaji-serving-cert
|
||||
controller-gen.kubebuilder.io/version: v0.16.1
|
||||
controller-gen.kubebuilder.io/version: v0.20.0
|
||||
name: datastores.kamaji.clastix.io
|
||||
spec:
|
||||
group: kamaji.clastix.io
|
||||
@@ -284,6 +284,10 @@ spec:
|
||||
status:
|
||||
description: DataStoreStatus defines the observed state of DataStore.
|
||||
properties:
|
||||
observedGeneration:
|
||||
description: ObservedGeneration represents the .metadata.generation that was last reconciled.
|
||||
format: int64
|
||||
type: integer
|
||||
usedBy:
|
||||
description: List of the Tenant Control Planes, namespaced named, using this data store.
|
||||
items:
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
cert-manager.io/inject-ca-from: kamaji-system/kamaji-serving-cert
|
||||
controller-gen.kubebuilder.io/version: v0.20.0
|
||||
name: kubeconfiggenerators.kamaji.clastix.io
|
||||
spec:
|
||||
group: kamaji.clastix.io
|
||||
names:
|
||||
categories:
|
||||
- kamaji
|
||||
kind: KubeconfigGenerator
|
||||
listKind: KubeconfigGeneratorList
|
||||
plural: kubeconfiggenerators
|
||||
shortNames:
|
||||
- kc
|
||||
singular: kubeconfiggenerator
|
||||
scope: Cluster
|
||||
versions:
|
||||
- additionalPrinterColumns:
|
||||
- description: Age
|
||||
jsonPath: .metadata.creationTimestamp
|
||||
name: Age
|
||||
type: date
|
||||
name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: KubeconfigGenerator is the Schema for the kubeconfiggenerators 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:
|
||||
controlPlaneEndpointFrom:
|
||||
default: admin.svc
|
||||
description: |-
|
||||
ControlPlaneEndpointFrom is the key used to extract the Tenant Control Plane endpoint that must be used by the generator.
|
||||
The targeted Secret is the `${TCP}-admin-kubeconfig` one, default to `admin.svc`.
|
||||
type: string
|
||||
groups:
|
||||
description: |-
|
||||
Groups is resolved a set of strings used to assign the x509 organisations field.
|
||||
It will be recognised by Kubernetes as user groups.
|
||||
items:
|
||||
description: |-
|
||||
CompoundValue allows defining a static, or a dynamic value.
|
||||
Options are mutually exclusive, just one should be picked up.
|
||||
properties:
|
||||
fromDefinition:
|
||||
description: |-
|
||||
FromDefinition is used to generate a dynamic value,
|
||||
it uses the dot notation to access fields from the referenced TenantControlPlane object:
|
||||
e.g.: metadata.name
|
||||
type: string
|
||||
stringValue:
|
||||
description: StringValue is a static string value.
|
||||
type: string
|
||||
type: object
|
||||
x-kubernetes-validations:
|
||||
- message: Either stringValue or fromDefinition must be set, but not both.
|
||||
rule: (has(self.stringValue) || has(self.fromDefinition)) && !(has(self.stringValue) && has(self.fromDefinition))
|
||||
type: array
|
||||
namespaceSelector:
|
||||
description: NamespaceSelector is used to filter Namespaces from which the generator should extract TenantControlPlane objects.
|
||||
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
|
||||
tenantControlPlaneSelector:
|
||||
description: TenantControlPlaneSelector is used to filter the TenantControlPlane objects that should be address by the generator.
|
||||
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
|
||||
user:
|
||||
description: User resolves to a string to identify the client, assigned to the x509 Common Name field.
|
||||
properties:
|
||||
fromDefinition:
|
||||
description: |-
|
||||
FromDefinition is used to generate a dynamic value,
|
||||
it uses the dot notation to access fields from the referenced TenantControlPlane object:
|
||||
e.g.: metadata.name
|
||||
type: string
|
||||
stringValue:
|
||||
description: StringValue is a static string value.
|
||||
type: string
|
||||
type: object
|
||||
x-kubernetes-validations:
|
||||
- message: Either stringValue or fromDefinition must be set, but not both.
|
||||
rule: (has(self.stringValue) || has(self.fromDefinition)) && !(has(self.stringValue) && has(self.fromDefinition))
|
||||
required:
|
||||
- user
|
||||
type: object
|
||||
status:
|
||||
description: KubeconfigGeneratorStatus defines the observed state of KubeconfigGenerator.
|
||||
properties:
|
||||
availableResources:
|
||||
default: 0
|
||||
description: |-
|
||||
AvailableResources is the sum of successfully generated resources.
|
||||
In case of a different value compared to Resources, check the field errors.
|
||||
type: integer
|
||||
errors:
|
||||
description: Errors is the list of failed kubeconfig generations.
|
||||
items:
|
||||
properties:
|
||||
message:
|
||||
description: Message is the error message recorded upon the last generator run.
|
||||
type: string
|
||||
resource:
|
||||
description: Resource is the Namespaced name of the errored resource.
|
||||
type: string
|
||||
required:
|
||||
- message
|
||||
- resource
|
||||
type: object
|
||||
type: array
|
||||
observedGeneration:
|
||||
description: ObservedGeneration represents the .metadata.generation that was last reconciled.
|
||||
format: int64
|
||||
type: integer
|
||||
resources:
|
||||
default: 0
|
||||
description: Resources is the sum of targeted TenantControlPlane objects.
|
||||
type: integer
|
||||
required:
|
||||
- availableResources
|
||||
- resources
|
||||
type: object
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -89,3 +89,15 @@ Create the name of the cert-manager Certificate
|
||||
{{- define "kamaji.certificateName" -}}
|
||||
{{- printf "%s-serving-cert" (include "kamaji.fullname" .) }}
|
||||
{{- end }}
|
||||
|
||||
|
||||
{{/*
|
||||
Kubeconfig Generator Deployment name.
|
||||
*/}}
|
||||
{{- define "kamaji.kubeconfigGeneratorName" -}}
|
||||
{{- if .Values.kubeconfigGenerator.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name "kubeconfig-generator" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
{{- if .Values.kubeconfigGenerator.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "kamaji.labels" . | nindent 4 }}
|
||||
name: {{ include "kamaji.kubeconfigGeneratorName" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
spec:
|
||||
replicas: {{ .Values.kubeconfigGenerator.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "kamaji.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
{{- with .Values.kubeconfigGenerator.podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "kamaji.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
securityContext:
|
||||
{{- toYaml .Values.kubeconfigGenerator.podSecurityContext | nindent 8 }}
|
||||
serviceAccountName: {{ default .Values.kubeconfigGenerator.serviceAccountOverride (include "kamaji.serviceAccountName" .) }}
|
||||
containers:
|
||||
- args:
|
||||
- kubeconfig-generator
|
||||
- --health-probe-bind-address={{ .Values.kubeconfigGenerator.healthProbeBindAddress }}
|
||||
- --leader-elect={{ .Values.kubeconfigGenerator.enableLeaderElect }}
|
||||
{{- if .Values.kubeconfigGenerator.loggingDevel.enable }}- --zap-devel{{- end }}
|
||||
{{- with .Values.kubeconfigGenerator.extraArgs }}
|
||||
{{- toYaml . | nindent 10 }}
|
||||
{{- end }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
name: controller
|
||||
resources:
|
||||
{{- toYaml .Values.kubeconfigGenerator.resources | nindent 12 }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.kubeconfigGenerator.securityContext | nindent 12 }}
|
||||
{{- with .Values.kubeconfigGenerator.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.kubeconfigGenerator.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.kubeconfigGenerator.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -98,9 +98,12 @@ loggingDevel:
|
||||
# -- If specified, all the Kamaji instances with an unassigned DataStore will inherit this default value.
|
||||
defaultDatastoreName: default
|
||||
|
||||
# -- Subchart: See https://github.com/clastix/kamaji-etcd/blob/master/charts/kamaji-etcd/values.yaml
|
||||
kamaji-etcd:
|
||||
deploy: true
|
||||
fullnameOverride: kamaji-etcd
|
||||
## -- Important, this must match your management cluster's clusterDomain, otherwise the init jobs will fail
|
||||
clusterDomain: "cluster.local"
|
||||
datastore:
|
||||
enabled: true
|
||||
name: default
|
||||
@@ -108,4 +111,48 @@ kamaji-etcd:
|
||||
# -- Disable the analytics traces collection
|
||||
telemetry:
|
||||
disabled: false
|
||||
|
||||
|
||||
kubeconfigGenerator:
|
||||
# -- Toggle to deploy the Kubeconfig Generator Deployment.
|
||||
enabled: false
|
||||
fullnameOverride: ""
|
||||
# -- The number of the pod replicas for the Kubeconfig Generator controller.
|
||||
replicaCount: 2
|
||||
# -- The annotations to apply to the Kubeconfig Generator controller pods.
|
||||
podAnnotations: {}
|
||||
# -- The securityContext to apply to the Kubeconfig Generator controller pods.
|
||||
podSecurityContext:
|
||||
runAsNonRoot: true
|
||||
# -- The name of the service account to use. If not set, the root Kamaji one will be used.
|
||||
serviceAccountOverride: ""
|
||||
# -- The address the probe endpoint binds to.
|
||||
healthProbeBindAddress: ":8081"
|
||||
# -- Enables the leader election.
|
||||
enableLeaderElect: true
|
||||
loggingDevel:
|
||||
# -- Development Mode defaults(encoder=consoleEncoder,logLevel=Debug,stackTraceLevel=Warn). Production Mode defaults(encoder=jsonEncoder,logLevel=Info,stackTraceLevel=Error)
|
||||
enable: false
|
||||
# -- A list of extra arguments to add to the Kubeconfig Generator controller default ones.
|
||||
extraArgs: []
|
||||
resources:
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 512Mi
|
||||
requests:
|
||||
cpu: 200m
|
||||
memory: 512Mi
|
||||
# -- The securityContext to apply to the Kubeconfig Generator controller container only.
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
# capabilities:
|
||||
# drop:
|
||||
# - ALL
|
||||
# readOnlyRootFilesystem: true
|
||||
# runAsNonRoot: true
|
||||
# runAsUser: 1000
|
||||
# -- Kubernetes node selector rules to schedule Kubeconfig Generator controller
|
||||
nodeSelector: {}
|
||||
# -- Kubernetes node taints that the Kubeconfig Generator controller pods would tolerate
|
||||
tolerations: []
|
||||
# -- Kubernetes affinity rules to apply to Kubeconfig Generator controller pods
|
||||
affinity: {}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Build the manager binary
|
||||
FROM golang:1.24 as builder
|
||||
FROM golang:1.26 AS builder
|
||||
|
||||
ARG VERSION=edge-25.4.1
|
||||
ARG VERSION=edge-26.2.4
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
diff --git a/internal/resources/api_server_certificate.go b/internal/resources/api_server_certificate.go
|
||||
index 436cdf9..4702b6c 100644
|
||||
--- a/internal/resources/api_server_certificate.go
|
||||
+++ b/internal/resources/api_server_certificate.go
|
||||
@@ -108,6 +108,7 @@ func (r *APIServerCertificate) mutate(ctx context.Context, tenantControlPlane *k
|
||||
}
|
||||
|
||||
r.resource.SetLabels(utilities.MergeMaps(
|
||||
+ r.resource.GetLabels(),
|
||||
utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()),
|
||||
map[string]string{
|
||||
constants.ControllerLabelResource: "x509",
|
||||
diff --git a/internal/resources/api_server_kubelet_client_certificate.go b/internal/resources/api_server_kubelet_client_certificate.go
|
||||
index 85b4d42..da18db4 100644
|
||||
--- a/internal/resources/api_server_kubelet_client_certificate.go
|
||||
+++ b/internal/resources/api_server_kubelet_client_certificate.go
|
||||
@@ -95,6 +95,7 @@ func (r *APIServerKubeletClientCertificate) mutate(ctx context.Context, tenantCo
|
||||
}
|
||||
|
||||
r.resource.SetLabels(utilities.MergeMaps(
|
||||
+ r.resource.GetLabels(),
|
||||
utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()),
|
||||
map[string]string{
|
||||
constants.ControllerLabelResource: "x509",
|
||||
diff --git a/internal/resources/ca_certificate.go b/internal/resources/ca_certificate.go
|
||||
index 5425b0b..625273f 100644
|
||||
--- a/internal/resources/ca_certificate.go
|
||||
+++ b/internal/resources/ca_certificate.go
|
||||
@@ -137,7 +137,7 @@ func (r *CACertificate) mutate(ctx context.Context, tenantControlPlane *kamajiv1
|
||||
corev1.TLSPrivateKeyKey: ca.PrivateKey,
|
||||
}
|
||||
|
||||
- r.resource.SetLabels(utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()))
|
||||
+ r.resource.SetLabels(utilities.MergeMaps(r.resource.GetLabels(), utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName())))
|
||||
|
||||
utilities.SetObjectChecksum(r.resource, r.resource.Data)
|
||||
|
||||
diff --git a/internal/resources/datastore/datastore_certificate.go b/internal/resources/datastore/datastore_certificate.go
|
||||
index dea45ae..8492a5e 100644
|
||||
--- a/internal/resources/datastore/datastore_certificate.go
|
||||
+++ b/internal/resources/datastore/datastore_certificate.go
|
||||
@@ -94,6 +94,7 @@ func (r *Certificate) mutate(ctx context.Context, tenantControlPlane *kamajiv1al
|
||||
r.resource.Data["ca.crt"] = ca
|
||||
|
||||
r.resource.SetLabels(utilities.MergeMaps(
|
||||
+ r.resource.GetLabels(),
|
||||
utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()),
|
||||
map[string]string{
|
||||
constants.ControllerLabelResource: "x509",
|
||||
diff --git a/internal/resources/datastore/datastore_storage_config.go b/internal/resources/datastore/datastore_storage_config.go
|
||||
index 7d03420..4ea9e64 100644
|
||||
--- a/internal/resources/datastore/datastore_storage_config.go
|
||||
+++ b/internal/resources/datastore/datastore_storage_config.go
|
||||
@@ -181,7 +181,7 @@ func (r *Config) mutate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.
|
||||
|
||||
utilities.SetObjectChecksum(r.resource, r.resource.Data)
|
||||
|
||||
- r.resource.SetLabels(utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()))
|
||||
+ r.resource.SetLabels(utilities.MergeMaps(r.resource.GetLabels(), utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName())))
|
||||
|
||||
return ctrl.SetControllerReference(tenantControlPlane, r.resource, r.Client.Scheme())
|
||||
}
|
||||
diff --git a/internal/resources/front-proxy-client-certificate.go b/internal/resources/front-proxy-client-certificate.go
|
||||
index f5ed67c..2dd4eda 100644
|
||||
--- a/internal/resources/front-proxy-client-certificate.go
|
||||
+++ b/internal/resources/front-proxy-client-certificate.go
|
||||
@@ -95,6 +95,7 @@ func (r *FrontProxyClientCertificate) mutate(ctx context.Context, tenantControlP
|
||||
}
|
||||
|
||||
r.resource.SetLabels(utilities.MergeMaps(
|
||||
+ r.resource.GetLabels(),
|
||||
utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()),
|
||||
map[string]string{
|
||||
constants.ControllerLabelResource: "x509",
|
||||
diff --git a/internal/resources/front_proxy_ca_certificate.go b/internal/resources/front_proxy_ca_certificate.go
|
||||
index d410720..ccadc70 100644
|
||||
--- a/internal/resources/front_proxy_ca_certificate.go
|
||||
+++ b/internal/resources/front_proxy_ca_certificate.go
|
||||
@@ -114,7 +114,7 @@ func (r *FrontProxyCACertificate) mutate(ctx context.Context, tenantControlPlane
|
||||
kubeadmconstants.FrontProxyCAKeyName: ca.PrivateKey,
|
||||
}
|
||||
|
||||
- r.resource.SetLabels(utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()))
|
||||
+ r.resource.SetLabels(utilities.MergeMaps(r.resource.GetLabels(), utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName())))
|
||||
|
||||
utilities.SetObjectChecksum(r.resource, r.resource.Data)
|
||||
|
||||
diff --git a/internal/resources/k8s_ingress_resource.go b/internal/resources/k8s_ingress_resource.go
|
||||
index f2e014f..e1aef59 100644
|
||||
--- a/internal/resources/k8s_ingress_resource.go
|
||||
+++ b/internal/resources/k8s_ingress_resource.go
|
||||
@@ -147,7 +147,7 @@ func (r *KubernetesIngressResource) Define(_ context.Context, tenantControlPlane
|
||||
|
||||
func (r *KubernetesIngressResource) mutate(tenantControlPlane *kamajiv1alpha1.TenantControlPlane) controllerutil.MutateFn {
|
||||
return func() error {
|
||||
- labels := utilities.MergeMaps(utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()), tenantControlPlane.Spec.ControlPlane.Ingress.AdditionalMetadata.Labels)
|
||||
+ labels := utilities.MergeMaps(r.resource.GetLabels(), utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()), tenantControlPlane.Spec.ControlPlane.Ingress.AdditionalMetadata.Labels)
|
||||
r.resource.SetLabels(labels)
|
||||
|
||||
annotations := utilities.MergeMaps(r.resource.GetAnnotations(), tenantControlPlane.Spec.ControlPlane.Ingress.AdditionalMetadata.Annotations)
|
||||
diff --git a/internal/resources/k8s_service_resource.go b/internal/resources/k8s_service_resource.go
|
||||
index 7e7f11f..9c30145 100644
|
||||
--- a/internal/resources/k8s_service_resource.go
|
||||
+++ b/internal/resources/k8s_service_resource.go
|
||||
@@ -76,7 +76,12 @@ func (r *KubernetesServiceResource) mutate(ctx context.Context, tenantControlPla
|
||||
address, _ := tenantControlPlane.DeclaredControlPlaneAddress(ctx, r.Client)
|
||||
|
||||
return func() error {
|
||||
- labels := utilities.MergeMaps(utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()), tenantControlPlane.Spec.ControlPlane.Service.AdditionalMetadata.Labels)
|
||||
+ labels := utilities.MergeMaps(
|
||||
+ r.resource.GetLabels(),
|
||||
+ utilities.KamajiLabels(
|
||||
+ tenantControlPlane.GetName(), r.GetName()),
|
||||
+ tenantControlPlane.Spec.ControlPlane.Service.AdditionalMetadata.Labels,
|
||||
+ )
|
||||
r.resource.SetLabels(labels)
|
||||
|
||||
annotations := utilities.MergeMaps(r.resource.GetAnnotations(), tenantControlPlane.Spec.ControlPlane.Service.AdditionalMetadata.Annotations)
|
||||
diff --git a/internal/resources/kubeadm_config.go b/internal/resources/kubeadm_config.go
|
||||
index ae4cfc0..98dc36d 100644
|
||||
--- a/internal/resources/kubeadm_config.go
|
||||
+++ b/internal/resources/kubeadm_config.go
|
||||
@@ -89,7 +89,7 @@ func (r *KubeadmConfigResource) mutate(ctx context.Context, tenantControlPlane *
|
||||
return err
|
||||
}
|
||||
|
||||
- r.resource.SetLabels(utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()))
|
||||
+ r.resource.SetLabels(utilities.MergeMaps(r.resource.GetLabels(), utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName())))
|
||||
|
||||
params := kubeadm.Parameters{
|
||||
TenantControlPlaneAddress: address,
|
||||
diff --git a/internal/resources/kubeconfig.go b/internal/resources/kubeconfig.go
|
||||
index a87da7f..bd77676 100644
|
||||
--- a/internal/resources/kubeconfig.go
|
||||
+++ b/internal/resources/kubeconfig.go
|
||||
@@ -163,6 +163,7 @@ func (r *KubeconfigResource) mutate(ctx context.Context, tenantControlPlane *kam
|
||||
}
|
||||
|
||||
r.resource.SetLabels(utilities.MergeMaps(
|
||||
+ r.resource.GetLabels(),
|
||||
utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()),
|
||||
map[string]string{
|
||||
constants.ControllerLabelResource: "kubeconfig",
|
||||
diff --git a/internal/resources/sa_certificate.go b/internal/resources/sa_certificate.go
|
||||
index b53c7b0..4001eca 100644
|
||||
--- a/internal/resources/sa_certificate.go
|
||||
+++ b/internal/resources/sa_certificate.go
|
||||
@@ -113,7 +113,7 @@ func (r *SACertificate) mutate(ctx context.Context, tenantControlPlane *kamajiv1
|
||||
kubeadmconstants.ServiceAccountPrivateKeyName: sa.PrivateKey,
|
||||
}
|
||||
|
||||
- r.resource.SetLabels(utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()))
|
||||
+ r.resource.SetLabels(utilities.MergeMaps(r.resource.GetLabels(), utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName())))
|
||||
|
||||
utilities.SetObjectChecksum(r.resource, r.resource.Data)
|
||||
|
||||
@@ -1,23 +1,30 @@
|
||||
diff --git a/cmd/manager/cmd.go b/cmd/manager/cmd.go
|
||||
index 9a24d4e..a03a4e0 100644
|
||||
--- a/cmd/manager/cmd.go
|
||||
+++ b/cmd/manager/cmd.go
|
||||
@@ -31,7 +31,6 @@ import (
|
||||
@@ -4,7 +4,6 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
- "context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -34,7 +33,6 @@
|
||||
"github.com/clastix/kamaji/controllers/soot"
|
||||
"github.com/clastix/kamaji/internal"
|
||||
"github.com/clastix/kamaji/internal/builders/controlplane"
|
||||
- datastoreutils "github.com/clastix/kamaji/internal/datastore/utils"
|
||||
"github.com/clastix/kamaji/internal/utilities"
|
||||
"github.com/clastix/kamaji/internal/webhook"
|
||||
"github.com/clastix/kamaji/internal/webhook/handlers"
|
||||
"github.com/clastix/kamaji/internal/webhook/routes"
|
||||
@@ -80,10 +79,6 @@ func NewCmd(scheme *runtime.Scheme) *cobra.Command {
|
||||
@@ -85,10 +83,6 @@
|
||||
|
||||
if webhookCABundle, err = os.ReadFile(webhookCAPath); err != nil {
|
||||
return fmt.Errorf("unable to read webhook CA: %w", err)
|
||||
}
|
||||
|
||||
- if err = datastoreutils.CheckExists(ctx, scheme, datastore); err != nil {
|
||||
- return err
|
||||
- }
|
||||
-
|
||||
if controllerReconcileTimeout.Seconds() == 0 {
|
||||
return fmt.Errorf("the controller reconcile timeout must be greater than zero")
|
||||
- if err = datastoreutils.CheckExists(context.Background(), scheme, datastore); err != nil {
|
||||
- return err
|
||||
}
|
||||
|
||||
if controllerReconcileTimeout.Seconds() == 0 {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
diff --git a/internal/kubeadm/uploadconfig.go b/internal/kubeadm/uploadconfig.go
|
||||
index 89c9b54..1ee38cd 100644
|
||||
--- a/internal/kubeadm/uploadconfig.go
|
||||
+++ b/internal/kubeadm/uploadconfig.go
|
||||
@@ -41,7 +41,7 @@ func UploadKubeletConfig(client kubernetes.Interface, config *Configuration, pat
|
||||
TenantControlPlaneCgroupDriver: config.Parameters.TenantControlPlaneCGroupDriver,
|
||||
}
|
||||
|
||||
- content, err := getKubeletConfigmapContent(kubeletConfiguration, patches)
|
||||
+ content, err := getKubeletConfigmapContent(kubeletConfiguration, patches, config.Parameters.TenantControlPlaneVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -72,7 +72,13 @@ func UploadKubeletConfig(client kubernetes.Interface, config *Configuration, pat
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
-func getKubeletConfigmapContent(kubeletConfiguration KubeletConfiguration, patch jsonpatchv5.Patch) ([]byte, error) {
|
||||
+// minVerKubeletNewDefaults is the minimum Kubernetes version that supports the
|
||||
+// CrashLoopBackOff and ImagePullCredentialsVerificationPolicy kubelet
|
||||
+// configuration fields (gated by KubeletCrashLoopBackOffMax and
|
||||
+// KubeletEnsureSecretPulledImages feature gates respectively).
|
||||
+var minVerKubeletNewDefaults = semver.MustParse("1.35.0")
|
||||
+
|
||||
+func getKubeletConfigmapContent(kubeletConfiguration KubeletConfiguration, patch jsonpatchv5.Patch, version string) ([]byte, error) {
|
||||
var kc kubelettypes.KubeletConfiguration
|
||||
|
||||
kubeletv1beta1.SetDefaults_KubeletConfiguration(&kc)
|
||||
@@ -94,6 +100,20 @@ func getKubeletConfigmapContent(kubeletConfiguration KubeletConfiguration, patch
|
||||
// determine the resolvConf location, as reported in clastix/kamaji#581.
|
||||
kc.ResolverConfig = nil
|
||||
|
||||
+ // Clear fields set by SetDefaults_KubeletConfiguration from Kubernetes >= 1.35.
|
||||
+ // Older kubelets reject these fields because the corresponding feature gates
|
||||
+ // (KubeletCrashLoopBackOffMax, KubeletEnsureSecretPulledImages) are not enabled.
|
||||
+ // See: https://github.com/clastix/kamaji/issues/1062
|
||||
+ parsedVer, parseErr := semver.ParseTolerant(version)
|
||||
+ if parseErr != nil {
|
||||
+ return nil, fmt.Errorf("failed to parse kubernetes version %q for kubelet config: %w", version, parseErr)
|
||||
+ }
|
||||
+
|
||||
+ if parsedVer.LT(minVerKubeletNewDefaults) {
|
||||
+ kc.CrashLoopBackOff = kubelettypes.CrashLoopBackOffConfig{}
|
||||
+ kc.ImagePullCredentialsVerificationPolicy = ""
|
||||
+ }
|
||||
+
|
||||
if len(patch) > 0 {
|
||||
kubeletConfig, patchErr := utilities.EncodeToJSON(&kc)
|
||||
if patchErr != nil {
|
||||
@@ -1,5 +1,6 @@
|
||||
{{- $host := index .Values._cluster "root-host" }}
|
||||
{{- $issuerType := (index .Values._cluster "clusterissuer") | default "http01" }}
|
||||
{{- $solver := (index .Values._cluster "solver") | default "http01" }}
|
||||
{{- $clusterIssuer := (index .Values._cluster "issuer-name") | default "letsencrypt-prod" }}
|
||||
{{- $exposeIngress := (index .Values._cluster "expose-ingress") | default "tenant-root" }}
|
||||
|
||||
apiVersion: networking.k8s.io/v1
|
||||
@@ -8,10 +9,10 @@ metadata:
|
||||
name: keycloak-ingress
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- if ne $issuerType "cloudflare" }}
|
||||
{{- if eq $solver "http01" }}
|
||||
acme.cert-manager.io/http01-ingress-class: {{ $exposeIngress }}
|
||||
{{- end }}
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
cert-manager.io/cluster-issuer: {{ $clusterIssuer }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -124,7 +124,7 @@ spec:
|
||||
filesystemOverhead:
|
||||
description: FilesystemOverhead describes the space reserved for
|
||||
overhead when using Filesystem volumes. A value is between 0
|
||||
and 1, if not defined it is 0.055 (5.5% overhead)
|
||||
and 1, if not defined it is 0.06 (6% overhead)
|
||||
properties:
|
||||
global:
|
||||
description: Global is how much space of a Filesystem volume
|
||||
@@ -2656,7 +2656,7 @@ spec:
|
||||
filesystemOverhead:
|
||||
description: FilesystemOverhead describes the space reserved for
|
||||
overhead when using Filesystem volumes. A value is between 0
|
||||
and 1, if not defined it is 0.055 (5.5% overhead)
|
||||
and 1, if not defined it is 0.06 (6% overhead)
|
||||
properties:
|
||||
global:
|
||||
description: Global is how much space of a Filesystem volume
|
||||
@@ -5164,6 +5164,14 @@ rules:
|
||||
- get
|
||||
- update
|
||||
- delete
|
||||
- apiGroups:
|
||||
- admissionregistration.k8s.io
|
||||
resourceNames:
|
||||
- cdi-api-dataimportcron-mutate
|
||||
resources:
|
||||
- mutatingwebhookconfigurations
|
||||
verbs:
|
||||
- delete
|
||||
- apiGroups:
|
||||
- apiregistration.k8s.io
|
||||
resources:
|
||||
@@ -5175,6 +5183,17 @@ rules:
|
||||
- create
|
||||
- update
|
||||
- delete
|
||||
- apiGroups:
|
||||
- populator.storage.k8s.io
|
||||
resources:
|
||||
- volumepopulators
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- create
|
||||
- update
|
||||
- delete
|
||||
- apiGroups:
|
||||
- authorization.k8s.io
|
||||
resources:
|
||||
@@ -5285,6 +5304,9 @@ rules:
|
||||
verbs:
|
||||
- create
|
||||
- patch
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
@@ -5325,6 +5347,14 @@ rules:
|
||||
- watch
|
||||
- create
|
||||
- delete
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- namespaces
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
@@ -5358,6 +5388,7 @@ rules:
|
||||
- get
|
||||
- apiGroups:
|
||||
- cdi.kubevirt.io
|
||||
- forklift.cdi.kubevirt.io
|
||||
resources:
|
||||
- '*'
|
||||
verbs:
|
||||
@@ -5418,14 +5449,11 @@ rules:
|
||||
verbs:
|
||||
- update
|
||||
- apiGroups:
|
||||
- forklift.cdi.kubevirt.io
|
||||
- authorization.k8s.io
|
||||
resources:
|
||||
- ovirtvolumepopulators
|
||||
- openstackvolumepopulators
|
||||
- subjectaccessreviews
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- create
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
@@ -5670,6 +5698,7 @@ metadata:
|
||||
labels:
|
||||
cdi.kubevirt.io: cdi-operator
|
||||
name: cdi-operator
|
||||
np.kubevirt.io/allow-access-cluster-services: "true"
|
||||
operator.cdi.kubevirt.io: ""
|
||||
prometheus.cdi.kubevirt.io: "true"
|
||||
name: cdi-operator
|
||||
@@ -5688,6 +5717,7 @@ spec:
|
||||
labels:
|
||||
cdi.kubevirt.io: cdi-operator
|
||||
name: cdi-operator
|
||||
np.kubevirt.io/allow-access-cluster-services: "true"
|
||||
operator.cdi.kubevirt.io: ""
|
||||
prometheus.cdi.kubevirt.io: "true"
|
||||
spec:
|
||||
@@ -5708,33 +5738,50 @@ spec:
|
||||
- name: DEPLOY_CLUSTER_RESOURCES
|
||||
value: "true"
|
||||
- name: OPERATOR_VERSION
|
||||
value: v1.62.0
|
||||
value: v1.64.0
|
||||
- name: CONTROLLER_IMAGE
|
||||
value: quay.io/kubevirt/cdi-controller:v1.62.0
|
||||
value: quay.io/kubevirt/cdi-controller:v1.64.0
|
||||
- name: IMPORTER_IMAGE
|
||||
value: quay.io/kubevirt/cdi-importer:v1.62.0
|
||||
value: quay.io/kubevirt/cdi-importer:v1.64.0
|
||||
- name: CLONER_IMAGE
|
||||
value: quay.io/kubevirt/cdi-cloner:v1.62.0
|
||||
value: quay.io/kubevirt/cdi-cloner:v1.64.0
|
||||
- name: OVIRT_POPULATOR_IMAGE
|
||||
value: quay.io/kubevirt/cdi-importer:v1.62.0
|
||||
value: quay.io/kubevirt/cdi-importer:v1.64.0
|
||||
- name: APISERVER_IMAGE
|
||||
value: quay.io/kubevirt/cdi-apiserver:v1.62.0
|
||||
value: quay.io/kubevirt/cdi-apiserver:v1.64.0
|
||||
- name: UPLOAD_SERVER_IMAGE
|
||||
value: quay.io/kubevirt/cdi-uploadserver:v1.62.0
|
||||
value: quay.io/kubevirt/cdi-uploadserver:v1.64.0
|
||||
- name: UPLOAD_PROXY_IMAGE
|
||||
value: quay.io/kubevirt/cdi-uploadproxy:v1.62.0
|
||||
value: quay.io/kubevirt/cdi-uploadproxy:v1.64.0
|
||||
- name: VERBOSITY
|
||||
value: "1"
|
||||
- name: PULL_POLICY
|
||||
value: IfNotPresent
|
||||
- name: MONITORING_NAMESPACE
|
||||
image: quay.io/kubevirt/cdi-operator:v1.62.0
|
||||
image: quay.io/kubevirt/cdi-operator:v1.64.0
|
||||
imagePullPolicy: IfNotPresent
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 8444
|
||||
scheme: HTTP
|
||||
initialDelaySeconds: 5
|
||||
timeoutSeconds: 10
|
||||
name: cdi-operator
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
- containerPort: 8443
|
||||
name: metrics
|
||||
protocol: TCP
|
||||
- containerPort: 8444
|
||||
name: health
|
||||
protocol: TCP
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /readyz
|
||||
port: 8444
|
||||
scheme: HTTP
|
||||
initialDelaySeconds: 5
|
||||
timeoutSeconds: 10
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
@@ -5748,13 +5795,19 @@ spec:
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
terminationMessagePath: /dev/termination-log
|
||||
terminationMessagePolicy: File
|
||||
terminationMessagePolicy: FallbackToLogsOnError
|
||||
nodeSelector:
|
||||
kubernetes.io/os: linux
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
serviceAccountName: cdi-operator
|
||||
tolerations:
|
||||
- key: CriticalAddonsOnly
|
||||
operator: Exists
|
||||
---
|
||||
- key: CriticalAddonsOnly
|
||||
operator: Exists
|
||||
- effect: NoSchedule
|
||||
key: node-role.kubernetes.io/control-plane
|
||||
operator: Exists
|
||||
- effect: NoSchedule
|
||||
key: node-role.kubernetes.io/master
|
||||
operator: Exists
|
||||
---
|
||||
|
||||
@@ -6,7 +6,8 @@ include ../../../hack/package.mk
|
||||
update:
|
||||
rm -rf templates
|
||||
mkdir templates
|
||||
export RELEASE=$$(curl https://storage.googleapis.com/kubevirt-prow/release/kubevirt/kubevirt/stable.txt) && \
|
||||
# v1.7.0 blocked by https://github.com/kubevirt/kubevirt/issues/16386
|
||||
export RELEASE=v1.6.3 && \
|
||||
wget https://github.com/kubevirt/kubevirt/releases/download/$${RELEASE}/kubevirt-operator.yaml -O templates/kubevirt-operator.yaml && \
|
||||
sed -i 's/namespace: kubevirt/namespace: $(NAMESPACE)/g' templates/kubevirt-operator.yaml
|
||||
awk -i inplace -v RS="---" '!/kind: Namespace/{printf "%s", $$0 RS}' templates/kubevirt-operator.yaml
|
||||
|
||||
@@ -288,6 +288,10 @@ spec:
|
||||
developerConfiguration:
|
||||
description: DeveloperConfiguration holds developer options
|
||||
properties:
|
||||
clusterProfiler:
|
||||
description: Enable the ability to pprof profile KubeVirt
|
||||
control plane
|
||||
type: boolean
|
||||
cpuAllocationRatio:
|
||||
description: |-
|
||||
For each requested virtual CPU, CPUAllocationRatio defines how much physical CPU to request per VMI
|
||||
@@ -337,6 +341,8 @@ spec:
|
||||
type: integer
|
||||
virtOperator:
|
||||
type: integer
|
||||
virtSynchronizationController:
|
||||
type: integer
|
||||
type: object
|
||||
memoryOvercommit:
|
||||
description: |-
|
||||
@@ -2132,6 +2138,10 @@ spec:
|
||||
When ServiceMonitorNamespace is set, then we'll install the service monitor object in that namespace
|
||||
otherwise we will use the monitoring namespace.
|
||||
type: string
|
||||
synchronizationPort:
|
||||
description: Specify the port to listen on for VMI status synchronization
|
||||
traffic. Default is 9185
|
||||
type: string
|
||||
uninstallStrategy:
|
||||
description: |-
|
||||
Specifies if kubevirt can be deleted if workloads are still present.
|
||||
@@ -3263,6 +3273,11 @@ spec:
|
||||
description: KubeVirtPhase is a label for the phase of a KubeVirt
|
||||
deployment at the current time.
|
||||
type: string
|
||||
synchronizationAddresses:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
x-kubernetes-list-type: atomic
|
||||
targetDeploymentConfig:
|
||||
type: string
|
||||
targetDeploymentID:
|
||||
@@ -3552,6 +3567,10 @@ spec:
|
||||
developerConfiguration:
|
||||
description: DeveloperConfiguration holds developer options
|
||||
properties:
|
||||
clusterProfiler:
|
||||
description: Enable the ability to pprof profile KubeVirt
|
||||
control plane
|
||||
type: boolean
|
||||
cpuAllocationRatio:
|
||||
description: |-
|
||||
For each requested virtual CPU, CPUAllocationRatio defines how much physical CPU to request per VMI
|
||||
@@ -3601,6 +3620,8 @@ spec:
|
||||
type: integer
|
||||
virtOperator:
|
||||
type: integer
|
||||
virtSynchronizationController:
|
||||
type: integer
|
||||
type: object
|
||||
memoryOvercommit:
|
||||
description: |-
|
||||
@@ -5396,6 +5417,10 @@ spec:
|
||||
When ServiceMonitorNamespace is set, then we'll install the service monitor object in that namespace
|
||||
otherwise we will use the monitoring namespace.
|
||||
type: string
|
||||
synchronizationPort:
|
||||
description: Specify the port to listen on for VMI status synchronization
|
||||
traffic. Default is 9185
|
||||
type: string
|
||||
uninstallStrategy:
|
||||
description: |-
|
||||
Specifies if kubevirt can be deleted if workloads are still present.
|
||||
@@ -6527,6 +6552,11 @@ spec:
|
||||
description: KubeVirtPhase is a label for the phase of a KubeVirt
|
||||
deployment at the current time.
|
||||
type: string
|
||||
synchronizationAddresses:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
x-kubernetes-list-type: atomic
|
||||
targetDeploymentConfig:
|
||||
type: string
|
||||
targetDeploymentID:
|
||||
@@ -6602,6 +6632,8 @@ rules:
|
||||
- kubevirt-virt-api-certs
|
||||
- kubevirt-controller-certs
|
||||
- kubevirt-exportproxy-certs
|
||||
- kubevirt-synchronization-controller-certs
|
||||
- kubevirt-synchronization-controller-server-certs
|
||||
resources:
|
||||
- secrets
|
||||
verbs:
|
||||
@@ -6713,6 +6745,28 @@ rules:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- ""
|
||||
resourceNames:
|
||||
- kubevirt-ca
|
||||
resources:
|
||||
- configmaps
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- coordination.k8s.io
|
||||
resources:
|
||||
- leases
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- delete
|
||||
- update
|
||||
- create
|
||||
- patch
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
@@ -6936,6 +6990,7 @@ rules:
|
||||
- persistentvolumeclaims
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- apiGroups:
|
||||
- kubevirt.io
|
||||
resources:
|
||||
@@ -7331,6 +7386,15 @@ rules:
|
||||
- create
|
||||
- get
|
||||
- delete
|
||||
- apiGroups:
|
||||
- resource.k8s.io
|
||||
resources:
|
||||
- resourceslices
|
||||
- resourceclaims
|
||||
verbs:
|
||||
- list
|
||||
- watch
|
||||
- get
|
||||
- apiGroups:
|
||||
- kubevirt.io
|
||||
resources:
|
||||
@@ -7402,6 +7466,50 @@ rules:
|
||||
verbs:
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- kubevirt.io
|
||||
resources:
|
||||
- virtualmachineinstances
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- update
|
||||
- patch
|
||||
- apiGroups:
|
||||
- kubevirt.io
|
||||
resources:
|
||||
- virtualmachineinstancemigrations
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- patch
|
||||
- delete
|
||||
- apiGroups:
|
||||
- kubevirt.io
|
||||
resources:
|
||||
- kubevirts
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- events
|
||||
verbs:
|
||||
- update
|
||||
- create
|
||||
- patch
|
||||
- apiGroups:
|
||||
- apiextensions.k8s.io
|
||||
resources:
|
||||
- customresourcedefinitions
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- kubevirt.io
|
||||
resources:
|
||||
@@ -7430,6 +7538,8 @@ rules:
|
||||
- virtualmachineinstances/sev/fetchcertchain
|
||||
- virtualmachineinstances/sev/querylaunchmeasurement
|
||||
- virtualmachineinstances/usbredir
|
||||
- virtualmachines/objectgraph
|
||||
- virtualmachineinstances/objectgraph
|
||||
verbs:
|
||||
- get
|
||||
- apiGroups:
|
||||
@@ -7586,6 +7696,8 @@ rules:
|
||||
- virtualmachineinstances/sev/fetchcertchain
|
||||
- virtualmachineinstances/sev/querylaunchmeasurement
|
||||
- virtualmachineinstances/usbredir
|
||||
- virtualmachines/objectgraph
|
||||
- virtualmachineinstances/objectgraph
|
||||
verbs:
|
||||
- get
|
||||
- apiGroups:
|
||||
@@ -7746,6 +7858,8 @@ rules:
|
||||
- virtualmachineinstances/userlist
|
||||
- virtualmachineinstances/sev/fetchcertchain
|
||||
- virtualmachineinstances/sev/querylaunchmeasurement
|
||||
- virtualmachines/objectgraph
|
||||
- virtualmachineinstances/objectgraph
|
||||
verbs:
|
||||
- get
|
||||
- apiGroups:
|
||||
@@ -7922,15 +8036,22 @@ spec:
|
||||
- virt-operator
|
||||
env:
|
||||
- name: VIRT_OPERATOR_IMAGE
|
||||
value: quay.io/kubevirt/virt-operator:v1.5.2
|
||||
value: quay.io/kubevirt/virt-operator:v1.6.3
|
||||
- name: WATCH_NAMESPACE
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.annotations['olm.targetNamespaces']
|
||||
- name: KUBEVIRT_VERSION
|
||||
value: v1.5.2
|
||||
image: quay.io/kubevirt/virt-operator:v1.5.2
|
||||
value: v1.6.3
|
||||
image: quay.io/kubevirt/virt-operator:v1.6.3
|
||||
imagePullPolicy: IfNotPresent
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /metrics
|
||||
port: 8443
|
||||
scheme: HTTPS
|
||||
initialDelaySeconds: 5
|
||||
timeoutSeconds: 10
|
||||
name: virt-operator
|
||||
ports:
|
||||
- containerPort: 8443
|
||||
@@ -7957,6 +8078,7 @@ spec:
|
||||
- ALL
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
terminationMessagePolicy: FallbackToLogsOnError
|
||||
volumeMounts:
|
||||
- mountPath: /etc/virt-operator/certificates
|
||||
name: kubevirt-operator-certs
|
||||
@@ -7981,4 +8103,4 @@ spec:
|
||||
secretName: kubevirt-operator-certs
|
||||
- emptyDir: {}
|
||||
name: profile-data
|
||||
---
|
||||
---
|
||||
|
||||
@@ -22,6 +22,8 @@ spec:
|
||||
- GPU
|
||||
- VMExport
|
||||
evictionStrategy: LiveMigrate
|
||||
virtualMachineOptions:
|
||||
disableSerialConsoleLog: {}
|
||||
vmRolloutStrategy: LiveUpdate
|
||||
workloadUpdateStrategy:
|
||||
workloadUpdateMethods:
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
diff --git a/satellite/src/main/java/com/linbit/linstor/core/devmgr/DeviceHandlerImpl.java b/satellite/src/main/java/com/linbit/linstor/core/devmgr/DeviceHandlerImpl.java
|
||||
index 49138a8fd..2f768ca0d 100644
|
||||
--- a/satellite/src/main/java/com/linbit/linstor/core/devmgr/DeviceHandlerImpl.java
|
||||
+++ b/satellite/src/main/java/com/linbit/linstor/core/devmgr/DeviceHandlerImpl.java
|
||||
@@ -83,6 +83,8 @@ import java.util.TreeMap;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Function;
|
||||
+import java.nio.file.Files;
|
||||
+import java.nio.file.Paths;
|
||||
|
||||
@Singleton
|
||||
public class DeviceHandlerImpl implements DeviceHandler
|
||||
@@ -1646,7 +1648,10 @@ public class DeviceHandlerImpl implements DeviceHandler
|
||||
private void updateDiscGran(VlmProviderObject<Resource> vlmData) throws DatabaseException, StorageException
|
||||
{
|
||||
String devicePath = vlmData.getDevicePath();
|
||||
- if (devicePath != null && vlmData.exists())
|
||||
+ // Check if device path physically exists before calling lsblk
|
||||
+ // This is important for DRBD devices which might be temporarily unavailable during adjust
|
||||
+ // (drbdadm adjust brings devices down/up, and kernel might not have created the device node yet)
|
||||
+ if (devicePath != null && vlmData.exists() && Files.exists(Paths.get(devicePath)))
|
||||
{
|
||||
if (vlmData.getDiscGran() == VlmProviderObject.UNINITIALIZED_SIZE)
|
||||
{
|
||||
diff --git a/satellite/src/main/java/com/linbit/linstor/layer/drbd/DrbdLayer.java b/satellite/src/main/java/com/linbit/linstor/layer/drbd/DrbdLayer.java
|
||||
index 01967a31f..78b8195a4 100644
|
||||
--- a/satellite/src/main/java/com/linbit/linstor/layer/drbd/DrbdLayer.java
|
||||
+++ b/satellite/src/main/java/com/linbit/linstor/layer/drbd/DrbdLayer.java
|
||||
@@ -592,7 +592,29 @@ public class DrbdLayer implements DeviceLayer
|
||||
// The .res file might not have been generated in the prepare method since it was
|
||||
// missing information from the child-layers. Now that we have processed them, we
|
||||
// need to make sure the .res file exists in all circumstances.
|
||||
- regenerateResFile(drbdRscData);
|
||||
+ // However, if the underlying devices are not accessible (e.g., LUKS device is closed
|
||||
+ // during resource deletion), we skip regenerating the res file to avoid errors
|
||||
+ boolean canRegenerateResFile = true;
|
||||
+ if (!skipDisk && !drbdRscData.getAbsResource().isDrbdDiskless(workerCtx))
|
||||
+ {
|
||||
+ AbsRscLayerObject<Resource> dataChild = drbdRscData.getChildBySuffix(RscLayerSuffixes.SUFFIX_DATA);
|
||||
+ if (dataChild != null)
|
||||
+ {
|
||||
+ for (DrbdVlmData<Resource> drbdVlmData : drbdRscData.getVlmLayerObjects().values())
|
||||
+ {
|
||||
+ VlmProviderObject<Resource> childVlm = dataChild.getVlmProviderObject(drbdVlmData.getVlmNr());
|
||||
+ if (childVlm == null || !childVlm.exists() || childVlm.getDevicePath() == null)
|
||||
+ {
|
||||
+ canRegenerateResFile = false;
|
||||
+ break;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ if (canRegenerateResFile)
|
||||
+ {
|
||||
+ regenerateResFile(drbdRscData);
|
||||
+ }
|
||||
|
||||
// createMetaData needs rendered resFile
|
||||
for (DrbdVlmData<Resource> drbdVlmData : createMetaData)
|
||||
@@ -766,19 +788,72 @@ public class DrbdLayer implements DeviceLayer
|
||||
|
||||
if (drbdRscData.isAdjustRequired())
|
||||
{
|
||||
- try
|
||||
+ // Check if underlying devices are accessible before adjusting
|
||||
+ // This is important for encrypted resources (LUKS) where the device
|
||||
+ // might be closed during deletion
|
||||
+ boolean canAdjust = true;
|
||||
+
|
||||
+ // IMPORTANT: Check child volumes only when disk access is actually needed.
|
||||
+ // For network reconnect (StandAlone -> Connected), disk access is not required.
|
||||
+ boolean needsDiskAccess = false;
|
||||
+
|
||||
+ // Check if there are pending operations that require disk access
|
||||
+ for (DrbdVlmData<Resource> drbdVlmData : drbdRscData.getVlmLayerObjects().values())
|
||||
{
|
||||
- drbdUtils.adjust(
|
||||
- drbdRscData,
|
||||
- false,
|
||||
- skipDisk,
|
||||
- false
|
||||
- );
|
||||
+ Volume vlm = (Volume) drbdVlmData.getVolume();
|
||||
+ StateFlags<Volume.Flags> vlmFlags = vlm.getFlags();
|
||||
+
|
||||
+ // Disk access is needed if:
|
||||
+ // - creating a new volume
|
||||
+ // - resizing
|
||||
+ // - checking/creating metadata
|
||||
+ if (!drbdVlmData.exists() ||
|
||||
+ drbdVlmData.checkMetaData() ||
|
||||
+ vlmFlags.isSomeSet(workerCtx, Volume.Flags.RESIZE, Volume.Flags.DRBD_RESIZE))
|
||||
+ {
|
||||
+ needsDiskAccess = true;
|
||||
+ break;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ // Check child volumes only if disk access is actually needed
|
||||
+ if (needsDiskAccess && !skipDisk && !drbdRscData.getAbsResource().isDrbdDiskless(workerCtx))
|
||||
+ {
|
||||
+ AbsRscLayerObject<Resource> dataChild = drbdRscData.getChildBySuffix(RscLayerSuffixes.SUFFIX_DATA);
|
||||
+ if (dataChild != null)
|
||||
+ {
|
||||
+ for (DrbdVlmData<Resource> drbdVlmData : drbdRscData.getVlmLayerObjects().values())
|
||||
+ {
|
||||
+ VlmProviderObject<Resource> childVlm = dataChild.getVlmProviderObject(drbdVlmData.getVlmNr());
|
||||
+ if (childVlm == null || !childVlm.exists() || childVlm.getDevicePath() == null)
|
||||
+ {
|
||||
+ canAdjust = false;
|
||||
+ break;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
}
|
||||
- catch (ExtCmdFailedException extCmdExc)
|
||||
+
|
||||
+ if (canAdjust)
|
||||
+ {
|
||||
+ try
|
||||
+ {
|
||||
+ drbdUtils.adjust(
|
||||
+ drbdRscData,
|
||||
+ false,
|
||||
+ skipDisk,
|
||||
+ false
|
||||
+ );
|
||||
+ }
|
||||
+ catch (ExtCmdFailedException extCmdExc)
|
||||
+ {
|
||||
+ restoreBackupResFile(drbdRscData);
|
||||
+ throw extCmdExc;
|
||||
+ }
|
||||
+ }
|
||||
+ else
|
||||
{
|
||||
- restoreBackupResFile(drbdRscData);
|
||||
- throw extCmdExc;
|
||||
+ drbdRscData.setAdjustRequired(false);
|
||||
}
|
||||
}
|
||||
|
||||
diff --git a/satellite/src/main/java/com/linbit/linstor/layer/luks/LuksLayer.java b/satellite/src/main/java/com/linbit/linstor/layer/luks/LuksLayer.java
|
||||
index cdca0b6d2..89c8be9da 100644
|
||||
--- a/satellite/src/main/java/com/linbit/linstor/layer/luks/LuksLayer.java
|
||||
+++ b/satellite/src/main/java/com/linbit/linstor/layer/luks/LuksLayer.java
|
||||
@@ -383,6 +383,7 @@ public class LuksLayer implements DeviceLayer
|
||||
vlmData.setSizeState(Size.AS_EXPECTED);
|
||||
|
||||
vlmData.setOpened(true);
|
||||
+ vlmData.setExists(true);
|
||||
vlmData.setFailed(false);
|
||||
}
|
||||
}
|
||||
@@ -280,8 +280,8 @@ vmagent:
|
||||
cluster: cozystack
|
||||
remoteWrite:
|
||||
urls:
|
||||
- http://vminsert-shortterm.{{ .Values.global.target }}.svc:8480/insert/0/prometheus
|
||||
- http://vminsert-longterm.{{ .Values.global.target }}.svc:8480/insert/0/prometheus
|
||||
- http://vminsert-shortterm.{{ .Values.global.target }}.svc.{{ (index .Values._cluster "cluster-domain") | default "cluster.local" }}:8480/insert/0/prometheus
|
||||
- http://vminsert-longterm.{{ .Values.global.target }}.svc.{{ (index .Values._cluster "cluster-domain") | default "cluster.local" }}:8480/insert/0/prometheus
|
||||
extraArgs: {}
|
||||
|
||||
fluent-bit:
|
||||
@@ -344,7 +344,7 @@ fluent-bit:
|
||||
[OUTPUT]
|
||||
Name http
|
||||
Match kube.*
|
||||
Host vlogs-generic.{{ .Values.global.target }}.svc
|
||||
Host vlogs-generic.{{ .Values.global.target }}.svc.{{ (index .Values._cluster "cluster-domain") | default "cluster.local" }}
|
||||
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
|
||||
@@ -355,7 +355,7 @@ fluent-bit:
|
||||
[OUTPUT]
|
||||
Name http
|
||||
Match events.*
|
||||
Host vlogs-generic.{{ .Values.global.target }}.svc
|
||||
Host vlogs-generic.{{ .Values.global.target }}.svc.{{ (index .Values._cluster "cluster-domain") | default "cluster.local" }}
|
||||
port 9428
|
||||
compress gzip
|
||||
uri /insert/jsonline?_stream_fields=log_source,reason,meatdata_namespace,metadata_name&_msg_field=message&_time_field=date
|
||||
@@ -366,7 +366,7 @@ fluent-bit:
|
||||
[OUTPUT]
|
||||
Name http
|
||||
Match audit.*
|
||||
Host vlogs-generic.{{ .Values.global.target }}.svc
|
||||
Host vlogs-generic.{{ .Values.global.target }}.svc.{{ (index .Values._cluster "cluster-domain") | default "cluster.local" }}
|
||||
port 9428
|
||||
compress gzip
|
||||
uri /insert/jsonline?_stream_fields=log_source,stage,user_username,verb,requestUri&_msg_field=requestURI&_time_field=date
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{{- $issuerType := (index .Values._cluster "clusterissuer") | default "http01" }}
|
||||
{{- $solver := (index .Values._cluster "solver") | default "http01" }}
|
||||
{{- $clusterIssuer := (index .Values._cluster "issuer-name") | default "letsencrypt-prod" }}
|
||||
{{- $ingress := .Values._namespace.ingress }}
|
||||
{{- $host := .Values._namespace.host }}
|
||||
|
||||
@@ -171,10 +172,10 @@ metadata:
|
||||
labels:
|
||||
app: alerta
|
||||
annotations:
|
||||
{{- if ne $issuerType "cloudflare" }}
|
||||
{{- if eq $solver "http01" }}
|
||||
acme.cert-manager.io/http01-ingress-class: {{ $ingress }}
|
||||
{{- end }}
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
cert-manager.io/cluster-issuer: {{ $clusterIssuer }}
|
||||
spec:
|
||||
ingressClassName: {{ $ingress }}
|
||||
tls:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{{- $issuerType := (index .Values._cluster "clusterissuer") | default "http01" }}
|
||||
{{- $solver := (index .Values._cluster "solver") | default "http01" }}
|
||||
{{- $clusterIssuer := (index .Values._cluster "issuer-name") | default "letsencrypt-prod" }}
|
||||
{{- $ingress := .Values._namespace.ingress }}
|
||||
{{- $host := .Values._namespace.host }}
|
||||
---
|
||||
@@ -72,10 +73,10 @@ spec:
|
||||
ingress:
|
||||
metadata:
|
||||
annotations:
|
||||
{{- if ne $issuerType "cloudflare" }}
|
||||
{{- if eq $solver "http01" }}
|
||||
acme.cert-manager.io/http01-ingress-class: "{{ $ingress }}"
|
||||
{{- end }}
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
cert-manager.io/cluster-issuer: {{ $clusterIssuer }}
|
||||
spec:
|
||||
ingressClassName: "{{ $ingress }}"
|
||||
rules:
|
||||
|
||||
Reference in New Issue
Block a user