Compare commits

...

30 Commits

Author SHA1 Message Date
Andrei Kvapil
80ad9965e5 [cozystack-api] Fix List kinds
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-11-12 10:32:51 -05:00
Andrei Kvapil
523510469c [cozystack-controller] improve API tests (#1617)
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>

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

## What this PR does

Adds check also for core.cozystack.io group

### Release note

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

```release-note
[cozystack-controller] improve API tests
```

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

## Summary by CodeRabbit

# Release Notes

* **Tests**
* Enhanced validation during installation to verify multiple API
services
* Expanded OpenAPI endpoint verification to include additional services

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-07 23:44:27 +01:00
Andrei Kvapil
cf5b2f2bbb [cozystack-controller] improve API tests
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-11-07 22:57:28 +01:00
Andrei Kvapil
4e5343e36c [dashboard-controller] Fix static resources reconciliation and showing secrets (#1615)
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>

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

## What this PR does


### Release note

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

```release-note
[dashboard-controller] Fix static resources reconciliation and showing secrets
```
2025-11-07 17:31:09 +01:00
Andrei Kvapil
d8237b4321 [dashboard-controller] Fix static resources reconciliation and showing
secrets

Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-11-07 17:30:26 +01:00
Andrei Kvapil
83c3b0ca12 [virtual-machine] Revert per-vm network policies (#1611)
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>

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

## What this PR does

Revert per-vm network policies functionality introduced by
https://github.com/cozystack/cozystack/pull/1611
As it is not working as expected any way.

This is temporary solution before implementing full-fledged security
groups in Cozystack

fixes https://github.com/cozystack/cozystack/issues/1601
alternative solution: https://github.com/cozystack/cozystack/pull/1602

### Release note

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

```release-note
[virtual-machine] Revert per-vm network policies
```
2025-11-07 15:48:02 +01:00
Andrei Kvapil
e1590aad1b [cozystack-api][dashboard] Fix filtering for application services/ingresses/secrets (#1612)
<!-- Thank you for making a contribution! Here are some tips for you:
- Start the PR title with the [label] of Cozystack component:
- For system components: [platform], [system], [linstor], [cilium],
[kube-ovn], [dashboard], [cluster-api], etc.
- For managed apps: [apps], [tenant], [kubernetes], [postgres],
[virtual-machine] etc.
- For development and maintenance: [tests], [ci], [docs], [maintenance].
- If it's a work in progress, consider creating this PR as a draft.
- Don't hesistate to ask for opinion and review in the community chats,
even if it's still a draft.
- Add the label `backport` if it's a bugfix that needs to be backported
to a previous version.
-->

## What this PR does

- **[dashboard-controller] Fix labelSelectors**
- **[cozystack-api] Enhance TenantSecrets filtering**
- **[cozystack-api] Fix sorting for TenantSecrets**

### Release note

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

```release-note
[cozystack-api][dashboard] Fix filtering for application services/ingresses/secrets
```

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

## Summary by CodeRabbit

* **Refactor**
* Standardized internal configuration naming conventions across
dashboard components.
* Enhanced tenant secret validation and filtering logic with improved
label-based operations for consistency and correctness.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-07 15:36:24 +01:00
Andrei Kvapil
304338d697 Apply review suggestions
Co-authored-by: Timofei Larkin <lllamnyp@gmail.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-11-07 15:35:56 +01:00
Andrei Kvapil
b65d639ecb [cozystack-api] Fix sorting for TenantSecrets
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-11-07 14:56:23 +01:00
Andrei Kvapil
339e71331f [cozystack-api] Enhance TenantSecrets filtering
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-11-07 14:56:23 +01:00
Andrei Kvapil
08be385665 [dashboard-controller] Fix labelSelectors
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-11-07 14:56:23 +01:00
Andrei Kvapil
2f0657f8ba [virtual-machine] Revert per-vm network policies
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-11-07 11:36:59 +01:00
Andrei Kvapil
a64ba184ce [cozy-lib] Fix: handling resources=nil (#1607)
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>

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

## What this PR does


Fixes issue:

```
error: template: tcp-balancer/templates/deployment.yaml:37:23: executing "tcp-balancer/templates/deployment.yaml" at <include "cozy-lib.resources.defaultingSanitize" (list .Values.resourcesP
reset .Values.resources $)>: error calling include: template: tcp-balancer/charts/cozy-lib/templates/_resources.tpl:157:20: executing "cozy-lib.resources.defaultingSanitize" at <deepCopy $re
sources>: error calling deepCopy: reflect: call of reflect.Value.Type on zero Value
```

### Release note

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

```release-note
[cozy-lib] Fix: handling resources=nil
```

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

## Summary by CodeRabbit

* **Bug Fixes**
* Improved resource handling to ensure proper behavior when resources
are not provided, enhancing system reliability and consistency in
resource merging operations.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-07 11:03:32 +01:00
Andrei Kvapil
00328c8a31 [cozy-lib] Fix: handling resources=nil
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
Co-authored-by: Timofei Larkin <lllamnyp@gmail.com>
2025-11-07 11:02:02 +01:00
Timofei Larkin
7009c8da37 [kubernetes] Helm hooks for cleanup (#1606)
## What this PR does

When deleting a Kubernetes, some resources may linger post deletion
because of a race to remove HelmReleases deployed inside the tenant
cluster and the removal of the cluster and its controlplane itself. This
patch modifies the existing pre-delete hook to remove those helmreleases
instead of simply suspending them. Similarly, datavolumes may also
remain. These are now delete with a post-delete hook.

### Release note

```release-note
[kubernetes] Use Helm hooks to clean up HelmReleases deployed in tenant
clusters and DataVolumes backing the tenant clusters' PVCs when deleting
a tenant Kubernetes.
```

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

* **Chores**
* Added an automated post-delete cleanup job to remove persistent data
volumes scoped to the release namespace when a release is deleted.
* Updated Helm release teardown to actively delete lingering release
resources (rather than only suspending them) for cleaner uninstall
behavior.
* Broadened lifecycle hooks to run on successful completions and
expanded teardown permissions to list and delete related release
artifacts, including gateway CRDs.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-07 13:50:55 +04:00
Timofei Larkin
63db8ca009 [kubernetes] Helm hooks for cleanup
## What this PR does

When deleting a Kubernetes, some resources may linger post deletion
because of a race to remove HelmReleases deployed inside the tenant
cluster and the removal of the cluster and its controlplane itself. This
patch modifies the existing pre-delete hook to remove those helmreleases
instead of simply suspending them. Similarly, datavolumes may also
remain. These are now delete with a post-delete hook.

### Release note

```release-note
[kubernetes] Use Helm hooks to clean up HelmReleases deployed in tenant
clusters and DataVolumes backing the tenant clusters' PVCs when deleting
a tenant Kubernetes.
```

Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2025-11-07 12:01:00 +03:00
Andrei Kvapil
369384f5ec [dashboard] sync with upstream & enhancements (#1603)
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>

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

## What this PR does

- Move patches to upstream: `namespaces` and `hide inside`
- Introduce flatMap logic
- Remove `tenantsecretstables` resource
- Extend dashboard-controller to specify `multilineString` for any
string without enum in spec (previusly it was for all strings)

### Release note

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

```release-note
[dashboard] sync with upstream & enhancements
```

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

* **New Features**
* Enhanced OpenAPI form handling: string fields now better support
multiline input.

* **Improvements**
* Secrets UI and API alignment: secrets display and data keys updated
for consistency.
  * Form generation improved for nested objects and arrays.
* Deployment defaults adjusted (logger flags normalized; inside feature
hidden via env).

* **Removed**
* Removed the "Inside" header menu item and the legacy secrets-table
API/resource.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-06 16:23:39 +01:00
Timofei Larkin
4278692763 Revert "[kubernetes] Helm hooks for cleanup"
This reverts commit edc942b6c1.

Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2025-11-06 15:06:42 +03:00
Timofei Larkin
edc942b6c1 [kubernetes] Helm hooks for cleanup
## What this PR does

When deleting a Kubernetes, some resources may linger post deletion
because of a race to remove HelmReleases deployed inside the tenant
cluster and the removal of the cluster and its controlplane itself. This
patch modifies the existing pre-delete hook to remove those helmreleases
instead of simply suspending them. Similarly, datavolumes may also
remain. These are now delete with a post-delete hook.

### Release note

```release-note
[kubernetes] Use Helm hooks to clean up HelmReleases deployed in tenant
clusters and DataVolumes backing the tenant clusters' PVCs when deleting
a tenant Kubernetes.
```

Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2025-11-06 15:01:11 +03:00
Timofei Larkin
4c71e7fe57 [nats] Fix NATS app chart to use existing secret credentials when present (#1599)
<!-- Thank you for making a contribution! Here are some tips for you:
- Start the PR title with the [label] of Cozystack component:
- For system components: [platform], [system], [linstor], [cilium],
[kube-ovn], [dashboard], [cluster-api], etc.
- For managed apps: [apps], [tenant], [kubernetes], [postgres],
[virtual-machine] etc.
- For development and maintenance: [tests], [ci], [docs], [maintenance].
- If it's a work in progress, consider creating this PR as a draft.
- Don't hesistate to ask for opinion and review in the community chats,
even if it's still a draft.
- Add the label `backport` if it's a bugfix that needs to be backported
to a previous version.
-->

## What this PR does

This PR fixes an issue where NATS user credentials were being
regenerated on every helm release update, rather than reusing existing
secrets. The fix implements the same secret reuse pattern that is
already used in the postgres app.

### Changes:
- Added `lookup` call to fetch existing credentials secret before
generating passwords
- Pre-populate passwords from existing secret data (base64 decoded)
- Only generate new random passwords for users that don't have existing
credentials

### Behavior:
- **Before**: Every helm upgrade would regenerate credentials for users
without explicit passwords, breaking existing connections
- **After**: Existing credentials are preserved across helm upgrades,
matching postgres app behavior

### Release note

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

```release-note
[nats] Fix credential regeneration on helm release updates by implementing existing secret lookup pattern
```

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

* **New Features**
* NATS deployments can now read and reuse existing release credentials,
reducing unnecessary credential rotation and keeping logins consistent
across updates.
* When credentials are missing, the system still auto-generates
passwords; when users are defined it emits the computed credentials for
use by the deployment.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-06 14:11:04 +04:00
Isaiah Olson
627022972d Use dig function to check for existing secret in NATS app template and prevent nil indexing
Signed-off-by: Isaiah Olson <isaiah@olson-network.com>
2025-11-05 18:12:23 -06:00
Isaiah Olson
1e8a9ee980 Fix NATS app chart to use existing secret credentials when present
Signed-off-by: Isaiah Olson <isaiah@olson-network.com>
2025-11-05 18:12:22 -06:00
Andrei Kvapil
b45f4a6545 [dashboard] sync with upstream & enhancements
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-11-05 18:22:22 +01:00
Timofei Larkin
5b96190be8 [vpc] Entry per subnet in the subnets configmap (#1600)
### Release note

```release-note
[vpc] Change the subnets configmap structure from
.data.subnets==[]Subnet to .data==map[SubnetName]Subnet for simpler
representation in the dashboard.
```

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

## Summary by CodeRabbit

* **Refactor**
* Restructured VPC subnet data organization in configuration from a
static list format to a dynamic map structure, where each subnet is now
stored with its own key containing subnet name, ID, and CIDR
information.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-05 14:38:23 +04:00
Timofei Larkin
8849570f74 [system] Tune kubevirt rollout and eviction settings (#1544)
<!-- Thank you for making a contribution! Here are some tips for you:
- Start the PR title with the [label] of Cozystack component:
- For system components: [platform], [system], [linstor], [cilium],
[kube-ovn], [dashboard], [cluster-api], etc.
- For managed apps: [apps], [tenant], [kubernetes], [postgres],
[virtual-machine] etc.
- For development and maintenance: [tests], [ci], [docs], [maintenance].
- If it's a work in progress, consider creating this PR as a draft.
- Don't hesistate to ask for opinion and review in the community chats,
even if it's still a draft.
- Add the label `backport` if it's a bugfix that needs to be backported
to a previous version.
-->

## What this PR does
Adds kubevirt settings:
`vmRolloutStrategy`: how changes to a manifest are propagated to a vm:
changes will be applied on-the-fly if possible (such as guest memory)
`workloadUpdateStrategy`: how vms will react to an eviction, less
disruptive method will be used.

### Release note

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

```release-note
Kubevirt rollout and eviction settings tuned
```

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

## Summary by CodeRabbit

## Release Notes

* **New Features**
  * Enhanced VM rollout strategy with Live Update support
* Introduced configurable workload update strategy with Live Migration
and Eviction options
  * Added batch eviction controls for optimized resource management

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-05 14:30:25 +04:00
Nikita
b6958320b2 [apps] vpc: more docs (#1594)
## What this PR does
Adds VPC details about bundles and required components for it to work.

### Release note
```release-note
More docs for VPC
```
2025-11-05 13:01:49 +03:00
Timofei Larkin
0a210bf5d3 [vpc] Entry per subnet in the subnets configmap
### Release note

```release-note
[vpc] Change the subnets configmap structure from
.data.subnets==[]Subnet to .data==map[SubnetName]Subnet for simpler
representation in the dashboard.
```

Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2025-11-05 13:01:15 +03:00
nbykov0
90d50fef48 [apps] vpc: more docs
Signed-off-by: nbykov0 <166552198+nbykov0@users.noreply.github.com>
2025-11-05 12:21:00 +03:00
nbykov0
ea74d7d59a [system] kubevirt: restore evictionStrategy
Signed-off-by: nbykov0 <166552198+nbykov0@users.noreply.github.com>
2025-10-21 19:29:50 +03:00
nbykov0
74262977f6 [system] tune kubevirt rollout and eviction
Signed-off-by: nbykov0 <166552198+nbykov0@users.noreply.github.com>
2025-10-21 19:18:57 +03:00
36 changed files with 522 additions and 1049 deletions

View File

@@ -118,7 +118,7 @@ EOF
}
@test "Check Cozystack API service" {
kubectl wait --for=condition=Available apiservices/v1alpha1.apps.cozystack.io --timeout=2m
kubectl wait --for=condition=Available apiservices/v1alpha1.apps.cozystack.io apiservices/v1alpha1.core.cozystack.io --timeout=2m
}
@test "Configure Tenant and wait for applications" {

View File

@@ -9,6 +9,7 @@
@test "Test OpenAPI v3 endpoint" {
kubectl get -v7 --raw '/openapi/v3/apis/apps.cozystack.io/v1alpha1' > /dev/null
kubectl get -v7 --raw '/openapi/v3/apis/core.cozystack.io/v1alpha1' > /dev/null
}
@test "Test OpenAPI v2 endpoint (protobuf)" {
@@ -18,3 +19,26 @@
curl -sS --fail 'http://localhost:21234/openapi/v2?timeout=32s' -H 'Accept: application/com.github.proto-openapi.spec.v2@v1.0+protobuf' > /dev/null
)
}
@test "Test kinds" {
val=$(kubectl get --raw /apis/apps.cozystack.io/v1alpha1/tenants | jq -r '.kind')
if [ "$val" != "TenantList" ]; then
echo "Expected kind to be TenantList, got $val"
exit 1
fi
val=$(kubectl get --raw /apis/apps.cozystack.io/v1alpha1/tenants | jq -r '.items[0].kind')
if [ "$val" != "Tenant" ]; then
echo "Expected kind to be Tenant, got $val"
exit 1
fi
val=$(kubectl get --raw /apis/apps.cozystack.io/v1alpha1/ingresses | jq -r '.kind')
if [ "$val" != "IngressList" ]; then
echo "Expected kind to be IngressList, got $val"
exit 1
fi
val=$(kubectl get --raw /apis/apps.cozystack.io/v1alpha1/ingresses | jq -r '.items[0].kind')
if [ "$val" != "Ingress" ]; then
echo "Expected kind to be Ingress, got $val"
exit 1
fi
}

View File

@@ -11,6 +11,7 @@ import (
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/log"
)
// ensureCustomFormsOverride creates or updates a CustomFormsOverride resource for the given CRD
@@ -45,15 +46,24 @@ func (m *Manager) ensureCustomFormsOverride(ctx context.Context, crd *cozyv1alph
}
}
// Build schema with multilineString for string fields without enum
l := log.FromContext(ctx)
schema, err := buildMultilineStringSchema(crd.Spec.Application.OpenAPISchema)
if err != nil {
// If schema parsing fails, log the error and use an empty schema
l.Error(err, "failed to build multiline string schema, using empty schema", "crd", crd.Name)
schema = map[string]any{}
}
spec := map[string]any{
"customizationId": customizationID,
"hidden": hidden,
"sort": sort,
"schema": map[string]any{}, // {}
"schema": schema,
"strategy": "merge",
}
_, err := controllerutil.CreateOrUpdate(ctx, m.Client, obj, func() error {
_, err = controllerutil.CreateOrUpdate(ctx, m.Client, obj, func() error {
if err := controllerutil.SetOwnerReference(crd, obj, m.Scheme); err != nil {
return err
}
@@ -73,3 +83,94 @@ func (m *Manager) ensureCustomFormsOverride(ctx context.Context, crd *cozyv1alph
})
return err
}
// buildMultilineStringSchema parses OpenAPI schema and creates schema with multilineString
// for all string fields inside spec that don't have enum
func buildMultilineStringSchema(openAPISchema string) (map[string]any, error) {
if openAPISchema == "" {
return map[string]any{}, nil
}
var root map[string]any
if err := json.Unmarshal([]byte(openAPISchema), &root); err != nil {
return nil, fmt.Errorf("cannot parse openAPISchema: %w", err)
}
props, _ := root["properties"].(map[string]any)
if props == nil {
return map[string]any{}, nil
}
schema := map[string]any{
"properties": map[string]any{},
}
// Process spec properties recursively
processSpecProperties(props, schema["properties"].(map[string]any))
return schema, nil
}
// processSpecProperties recursively processes spec properties and adds multilineString type
// for string fields without enum
func processSpecProperties(props map[string]any, schemaProps map[string]any) {
for pname, raw := range props {
sub, ok := raw.(map[string]any)
if !ok {
continue
}
typ, _ := sub["type"].(string)
switch typ {
case "string":
// Check if this string field has enum
if !hasEnum(sub) {
// Add multilineString type for this field
if schemaProps[pname] == nil {
schemaProps[pname] = map[string]any{}
}
fieldSchema := schemaProps[pname].(map[string]any)
fieldSchema["type"] = "multilineString"
}
case "object":
// Recursively process nested objects
if childProps, ok := sub["properties"].(map[string]any); ok {
fieldSchema, ok := schemaProps[pname].(map[string]any)
if !ok {
fieldSchema = map[string]any{}
schemaProps[pname] = fieldSchema
}
nestedSchemaProps, ok := fieldSchema["properties"].(map[string]any)
if !ok {
nestedSchemaProps = map[string]any{}
fieldSchema["properties"] = nestedSchemaProps
}
processSpecProperties(childProps, nestedSchemaProps)
}
case "array":
// Check if array items are objects with properties
if items, ok := sub["items"].(map[string]any); ok {
if itemProps, ok := items["properties"].(map[string]any); ok {
// Create array item schema
fieldSchema, ok := schemaProps[pname].(map[string]any)
if !ok {
fieldSchema = map[string]any{}
schemaProps[pname] = fieldSchema
}
itemSchema, ok := fieldSchema["items"].(map[string]any)
if !ok {
itemSchema = map[string]any{}
fieldSchema["items"] = itemSchema
}
itemSchemaProps, ok := itemSchema["properties"].(map[string]any)
if !ok {
itemSchemaProps = map[string]any{}
itemSchema["properties"] = itemSchemaProps
}
processSpecProperties(itemProps, itemSchemaProps)
}
}
}
}
}

View File

@@ -0,0 +1,155 @@
package dashboard
import (
"encoding/json"
"testing"
)
func TestBuildMultilineStringSchema(t *testing.T) {
// Test OpenAPI schema with various field types
openAPISchema := `{
"properties": {
"simpleString": {
"type": "string",
"description": "A simple string field"
},
"stringWithEnum": {
"type": "string",
"enum": ["option1", "option2"],
"description": "String with enum should be skipped"
},
"numberField": {
"type": "number",
"description": "Number field should be skipped"
},
"nestedObject": {
"type": "object",
"properties": {
"nestedString": {
"type": "string",
"description": "Nested string should get multilineString"
},
"nestedStringWithEnum": {
"type": "string",
"enum": ["a", "b"],
"description": "Nested string with enum should be skipped"
}
}
},
"arrayOfObjects": {
"type": "array",
"items": {
"type": "object",
"properties": {
"itemString": {
"type": "string",
"description": "String in array item"
}
}
}
}
}
}`
schema, err := buildMultilineStringSchema(openAPISchema)
if err != nil {
t.Fatalf("buildMultilineStringSchema failed: %v", err)
}
// Marshal to JSON for easier inspection
schemaJSON, err := json.MarshalIndent(schema, "", " ")
if err != nil {
t.Fatalf("Failed to marshal schema: %v", err)
}
t.Logf("Generated schema:\n%s", schemaJSON)
// Verify that simpleString has multilineString type
props, ok := schema["properties"].(map[string]any)
if !ok {
t.Fatal("schema.properties is not a map")
}
// Check simpleString
simpleString, ok := props["simpleString"].(map[string]any)
if !ok {
t.Fatal("simpleString not found in properties")
}
if simpleString["type"] != "multilineString" {
t.Errorf("simpleString should have type multilineString, got %v", simpleString["type"])
}
// Check stringWithEnum should not be present (or should not have multilineString)
if stringWithEnum, ok := props["stringWithEnum"].(map[string]any); ok {
if stringWithEnum["type"] == "multilineString" {
t.Error("stringWithEnum should not have multilineString type")
}
}
// Check numberField should not be present
if numberField, ok := props["numberField"].(map[string]any); ok {
if numberField["type"] != nil {
t.Error("numberField should not have any type override")
}
}
// Check nested object
nestedObject, ok := props["nestedObject"].(map[string]any)
if !ok {
t.Fatal("nestedObject not found in properties")
}
nestedProps, ok := nestedObject["properties"].(map[string]any)
if !ok {
t.Fatal("nestedObject.properties is not a map")
}
// Check nestedString
nestedString, ok := nestedProps["nestedString"].(map[string]any)
if !ok {
t.Fatal("nestedString not found in nestedObject.properties")
}
if nestedString["type"] != "multilineString" {
t.Errorf("nestedString should have type multilineString, got %v", nestedString["type"])
}
// Check array of objects
arrayOfObjects, ok := props["arrayOfObjects"].(map[string]any)
if !ok {
t.Fatal("arrayOfObjects not found in properties")
}
items, ok := arrayOfObjects["items"].(map[string]any)
if !ok {
t.Fatal("arrayOfObjects.items is not a map")
}
itemProps, ok := items["properties"].(map[string]any)
if !ok {
t.Fatal("arrayOfObjects.items.properties is not a map")
}
itemString, ok := itemProps["itemString"].(map[string]any)
if !ok {
t.Fatal("itemString not found in arrayOfObjects.items.properties")
}
if itemString["type"] != "multilineString" {
t.Errorf("itemString should have type multilineString, got %v", itemString["type"])
}
}
func TestBuildMultilineStringSchemaEmpty(t *testing.T) {
schema, err := buildMultilineStringSchema("")
if err != nil {
t.Fatalf("buildMultilineStringSchema failed on empty string: %v", err)
}
if len(schema) != 0 {
t.Errorf("Expected empty schema for empty input, got %v", schema)
}
}
func TestBuildMultilineStringSchemaInvalidJSON(t *testing.T) {
schema, err := buildMultilineStringSchema("{invalid json")
if err == nil {
t.Error("Expected error for invalid JSON")
}
if schema != nil {
t.Errorf("Expected nil schema for invalid JSON, got %v", schema)
}
}

View File

@@ -221,7 +221,7 @@ func workloadsTab(kind string) map[string]any {
"baseprefix": "/openapi-ui",
"customizationId": "factory-details-v1alpha1.cozystack.io.workloadmonitors",
"pathToItems": []any{"items"},
"labelsSelector": map[string]any{
"labelSelector": map[string]any{
"apps.cozystack.io/application.group": "apps.cozystack.io",
"apps.cozystack.io/application.kind": kind,
"apps.cozystack.io/application.name": "{reqs[0]['metadata','name']}",
@@ -246,7 +246,7 @@ func servicesTab(kind string) map[string]any {
"baseprefix": "/openapi-ui",
"customizationId": "factory-details-v1.services",
"pathToItems": []any{"items"},
"labelsSelector": map[string]any{
"labelSelector": map[string]any{
"apps.cozystack.io/application.group": "apps.cozystack.io",
"apps.cozystack.io/application.kind": kind,
"apps.cozystack.io/application.name": "{reqs[0]['metadata','name']}",
@@ -272,7 +272,7 @@ func ingressesTab(kind string) map[string]any {
"baseprefix": "/openapi-ui",
"customizationId": "factory-details-networking.k8s.io.v1.ingresses",
"pathToItems": []any{"items"},
"labelsSelector": map[string]any{
"labelSelector": map[string]any{
"apps.cozystack.io/application.group": "apps.cozystack.io",
"apps.cozystack.io/application.kind": kind,
"apps.cozystack.io/application.name": "{reqs[0]['metadata','name']}",
@@ -293,12 +293,12 @@ func secretsTab(kind string) map[string]any {
"type": "EnrichedTable",
"data": map[string]any{
"id": "secrets-table",
"fetchUrl": "/api/clusters/{2}/k8s/apis/core.cozystack.io/v1alpha1/namespaces/{3}/tenantsecretstables",
"fetchUrl": "/api/clusters/{2}/k8s/apis/core.cozystack.io/v1alpha1/namespaces/{3}/tenantsecrets",
"clusterNamePartOfUrl": "{2}",
"baseprefix": "/openapi-ui",
"customizationId": "factory-details-v1alpha1.core.cozystack.io.tenantsecretstables",
"customizationId": "factory-details-v1alpha1.core.cozystack.io.tenantsecrets",
"pathToItems": []any{"items"},
"labelsSelector": map[string]any{
"labelSelector": map[string]any{
"apps.cozystack.io/application.group": "apps.cozystack.io",
"apps.cozystack.io/application.kind": kind,
"apps.cozystack.io/application.name": "{reqs[0]['metadata','name']}",

View File

@@ -15,6 +15,7 @@ import (
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
managerpkg "sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
@@ -53,10 +54,19 @@ func NewManager(c client.Client, scheme *runtime.Scheme) *Manager {
}
func (m *Manager) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
if err := ctrl.NewControllerManagedBy(mgr).
Named("dashboard-reconciler").
For(&cozyv1alpha1.CozystackResourceDefinition{}).
Complete(m)
Complete(m); err != nil {
return err
}
return mgr.Add(managerpkg.RunnableFunc(func(ctx context.Context) error {
if !mgr.GetCache().WaitForCacheSync(ctx) {
return fmt.Errorf("dashboard static resources cache sync failed")
}
return m.ensureStaticResources(ctx)
}))
}
func (m *Manager) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {

View File

@@ -122,7 +122,7 @@ func createCustomColumnsOverride(id string, additionalPrinterColumns []any) *das
}
}
if name == "factory-details-v1alpha1.core.cozystack.io.tenantsecretstables" {
if name == "factory-details-v1alpha1.core.cozystack.io.tenantsecrets" {
data["additionalPrinterColumnsTrimLengths"] = []any{
map[string]any{
"key": "Name",
@@ -1046,6 +1046,15 @@ func createConverterBytesColumn(name, jsonPath string) map[string]any {
}
}
// createFlatMapColumn creates a flatMap column that expands a map into separate rows
func createFlatMapColumn(name, jsonPath string) map[string]any {
return map[string]any{
"name": name,
"type": "flatMap",
"jsonPath": jsonPath,
}
}
// ---------------- Factory UI helper functions ----------------
// labelsEditor creates a Labels editor component

View File

@@ -173,11 +173,12 @@ func CreateAllCustomColumnsOverrides() []*dashboardv1alpha1.CustomColumnsOverrid
createStringColumn("OBSERVED", ".status.observedReplicas"),
}),
// Factory details v1alpha1 core cozystack io tenantsecretstables
createCustomColumnsOverride("factory-details-v1alpha1.core.cozystack.io.tenantsecretstables", []any{
// Factory details v1alpha1 core cozystack io tenantsecrets
createCustomColumnsOverride("factory-details-v1alpha1.core.cozystack.io.tenantsecrets", []any{
createCustomColumnWithJsonPath("Name", ".metadata.name", "Secret", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/kube-secret-details/{reqsJsonPath[0]['.metadata.name']['-']}"),
createStringColumn("Key", ".data.key"),
createSecretBase64Column("Value", ".data.value"),
createFlatMapColumn("Data", ".data"),
createStringColumn("Key", "_flatMapData_Key"),
createSecretBase64Column("Value", "._flatMapData_Value"),
createTimestampColumn("Created", ".metadata.creationTimestamp"),
}),
@@ -1055,7 +1056,7 @@ func CreateAllFactories() []*dashboardv1alpha1.Factory {
"clusterNamePartOfUrl": "{2}",
"customizationId": "factory-kube-service-details-endpointslice",
"fetchUrl": "/api/clusters/{2}/k8s/apis/discovery.k8s.io/v1/namespaces/{3}/endpointslices",
"labelsSelector": map[string]any{
"labelSelector": map[string]any{
"kubernetes.io/service-name": "{reqsJsonPath[0]['.metadata.name']['-']}",
},
"pathToItems": ".items[*].endpoints",
@@ -1396,7 +1397,7 @@ func CreateAllFactories() []*dashboardv1alpha1.Factory {
"clusterNamePartOfUrl": "{2}",
"customizationId": "factory-details-v1alpha1.cozystack.io.workloads",
"fetchUrl": "/api/clusters/{2}/k8s/apis/cozystack.io/v1alpha1/namespaces/{3}/workloads",
"labelsSelector": map[string]any{
"labelSelector": map[string]any{
"workloads.cozystack.io/monitor": "{reqs[0]['metadata','name']}",
},
"pathToItems": []any{"items"},

View File

@@ -0,0 +1,76 @@
---
apiVersion: batch/v1
kind: Job
metadata:
annotations:
"helm.sh/hook": post-delete
"helm.sh/hook-weight": "10"
"helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation,hook-failed
name: {{ .Release.Name }}-datavolume-cleanup
spec:
template:
spec:
serviceAccountName: {{ .Release.Name }}-datavolume-cleanup
restartPolicy: Never
tolerations:
- key: CriticalAddonsOnly
operator: Exists
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: "NoSchedule"
containers:
- name: kubectl
image: docker.io/clastix/kubectl:v1.32
command:
- /bin/sh
- -c
- kubectl -n {{ .Release.Namespace }} delete datavolumes
-l "cluster.x-k8s.io/cluster-name={{ .Release.Name }}"
--ignore-not-found=true
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ .Release.Name }}-datavolume-cleanup
annotations:
helm.sh/hook: post-delete
helm.sh/hook-delete-policy: before-hook-creation,hook-failed,hook-succeeded
helm.sh/hook-weight: "0"
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
annotations:
"helm.sh/hook": post-delete
"helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation,hook-failed
"helm.sh/hook-weight": "5"
name: {{ .Release.Name }}-datavolume-cleanup
rules:
- apiGroups:
- "cdi.kubevirt.io"
resources:
- datavolumes
verbs:
- get
- list
- delete
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
annotations:
"helm.sh/hook": post-delete
"helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation,hook-failed
"helm.sh/hook-weight": "5"
name: {{ .Release.Name }}-datavolume-cleanup
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: {{ .Release.Name }}-datavolume-cleanup
subjects:
- kind: ServiceAccount
name: {{ .Release.Name }}-datavolume-cleanup
namespace: {{ .Release.Namespace }}

View File

@@ -24,26 +24,26 @@ spec:
command:
- /bin/sh
- -c
- |
kubectl
--namespace={{ .Release.Namespace }}
patch
helmrelease
{{ .Release.Name }}-cilium
{{ .Release.Name }}-gateway-api-crds
{{ .Release.Name }}-csi
{{ .Release.Name }}-cert-manager
{{ .Release.Name }}-cert-manager-crds
{{ .Release.Name }}-vertical-pod-autoscaler
{{ .Release.Name }}-vertical-pod-autoscaler-crds
{{ .Release.Name }}-ingress-nginx
{{ .Release.Name }}-fluxcd-operator
{{ .Release.Name }}-fluxcd
{{ .Release.Name }}-gpu-operator
{{ .Release.Name }}-velero
{{ .Release.Name }}-coredns
-p '{"spec": {"suspend": true}}'
--type=merge --field-manager=flux-client-side-apply || true
- >-
kubectl
--namespace={{ .Release.Namespace }}
patch
helmrelease
{{ .Release.Name }}-cilium
{{ .Release.Name }}-gateway-api-crds
{{ .Release.Name }}-csi
{{ .Release.Name }}-cert-manager
{{ .Release.Name }}-cert-manager-crds
{{ .Release.Name }}-vertical-pod-autoscaler
{{ .Release.Name }}-vertical-pod-autoscaler-crds
{{ .Release.Name }}-ingress-nginx
{{ .Release.Name }}-fluxcd-operator
{{ .Release.Name }}-fluxcd
{{ .Release.Name }}-gpu-operator
{{ .Release.Name }}-velero
{{ .Release.Name }}-coredns
-p '{"spec": {"suspend": true}}'
--type=merge --field-manager=flux-client-side-apply || true
---
apiVersion: v1
kind: ServiceAccount
@@ -51,7 +51,7 @@ metadata:
name: {{ .Release.Name }}-flux-teardown
annotations:
helm.sh/hook: pre-delete
helm.sh/hook-delete-policy: before-hook-creation,hook-failed
helm.sh/hook-delete-policy: before-hook-creation,hook-failed,hook-succeeded
helm.sh/hook-weight: "0"
---
apiVersion: rbac.authorization.k8s.io/v1
@@ -75,6 +75,7 @@ rules:
- {{ .Release.Name }}-csi
- {{ .Release.Name }}-cert-manager
- {{ .Release.Name }}-cert-manager-crds
- {{ .Release.Name }}-gateway-api-crds
- {{ .Release.Name }}-vertical-pod-autoscaler
- {{ .Release.Name }}-vertical-pod-autoscaler-crds
- {{ .Release.Name }}-ingress-nginx

View File

@@ -1,6 +1,14 @@
{{- $cozyConfig := lookup "v1" "ConfigMap" "cozy-system" "cozystack" }}
{{- $clusterDomain := (index $cozyConfig.data "cluster-domain") | default "cozy.local" }}
{{- $existingSecret := lookup "v1" "Secret" .Release.Namespace (printf "%s-credentials" .Release.Name) }}
{{- $passwords := dict }}
{{- with (dig "data" (dict) $existingSecret) }}
{{- range $k, $v := . }}
{{- $_ := set $passwords $k (b64dec $v) }}
{{- end }}
{{- end }}
{{- range $user, $u := .Values.users }}
{{- if $u.password }}
{{- $_ := set $passwords $user $u.password }}

View File

@@ -20,11 +20,7 @@ metadata:
name: allow-external-communication
namespace: {{ include "tenant.name" . }}
spec:
endpointSelector:
matchExpressions:
- key: policy.cozystack.io/allow-external-communication
operator: NotIn
values: ["false"]
endpointSelector: {}
ingress:
- fromEntities:
- world

View File

@@ -35,7 +35,6 @@ rules:
resources:
- tenantmodules
- tenantsecrets
- tenantsecretstables
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
@@ -193,7 +192,6 @@ rules:
resources:
- tenantmodules
- tenantsecrets
- tenantsecretstables
verbs: ["get", "list", "watch"]
---
kind: RoleBinding
@@ -293,7 +291,6 @@ rules:
resources:
- tenantmodules
- tenantsecrets
- tenantsecretstables
verbs: ["get", "list", "watch"]
---
kind: RoleBinding
@@ -368,7 +365,6 @@ rules:
resources:
- tenantmodules
- tenantsecrets
- tenantsecretstables
verbs: ["get", "list", "watch"]
---
kind: RoleBinding

View File

@@ -28,27 +28,3 @@ spec:
{{- end }}
{{- end }}
{{- end }}
---
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: {{ include "virtual-machine.fullname" . }}
spec:
endpointSelector:
matchLabels:
{{- include "virtual-machine.selectorLabels" . | nindent 6 }}
ingress:
- fromEntities:
- cluster
- fromEntities:
- world
{{- if eq .Values.externalMethod "PortList" }}
toPorts:
- ports:
{{- range .Values.externalPorts }}
- port: {{ quote . }}
{{- end }}
{{- end }}
egress:
- toEntities:
- world

View File

@@ -62,7 +62,6 @@ spec:
template:
metadata:
annotations:
policy.cozystack.io/allow-external-communication: "false"
kubevirt.io/allow-pod-bridge-network-live-migration: "true"
labels:
{{- include "virtual-machine.labels" . | nindent 8 }}

View File

@@ -28,27 +28,3 @@ spec:
{{- end }}
{{- end }}
{{- end }}
---
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: {{ include "virtual-machine.fullname" . }}
spec:
endpointSelector:
matchLabels:
{{- include "virtual-machine.selectorLabels" . | nindent 6 }}
ingress:
- fromEntities:
- cluster
- fromEntities:
- world
{{- if eq .Values.externalMethod "PortList" }}
toPorts:
- ports:
{{- range .Values.externalPorts }}
- port: {{ quote . }}
{{- end }}
{{- end }}
egress:
- toEntities:
- world

View File

@@ -26,7 +26,6 @@ spec:
template:
metadata:
annotations:
policy.cozystack.io/allow-external-communication: "false"
kubevirt.io/allow-pod-bridge-network-live-migration: "true"
labels:
{{- include "virtual-machine.labels" . | nindent 8 }}

View File

@@ -5,12 +5,12 @@ As the service evolves, it will provide more ways to isolate your workloads.
## Service details
The service utilizes kube-ovn VPC and Subnet resources, which use ovn logical routers and logical switches under the hood.
Currently every workload will have a connection to a default management network which will also have a default gateway, and the majority of traffic will be going through it.
VPC subnets are for now an additional dedicated networking spaces.
To function, the service requires kube-ovn and multus CNI to be present, so by default it will only work on `paas-full` bundle.
Kube-ovn provides VPC and Subnet resources and performs isolation and networking maintenance such as DHCP. Under the hood it uses ovn virtual routers and virtual switches.
Multus enables a multi-nic capability, so a pod or a VM could have two or more network interfaces.
A VM or a pod may be connected to multiple secondary Subnets at once.
Each secondary connection will be represented as an additional network interface.
Currently every workload will have a connection to a default management network which will also have a default gateway, and the majority of traffic will go through it.
VPC subnets are for now an additional dedicated networking spaces.
## Deployment notes
@@ -21,6 +21,8 @@ Currently there are no fail-safe checks, however they are planned for the future
Different VPCs may have subnets with ovelapping ip address ranges.
A VM or a pod may be connected to multiple secondary Subnets at once. Each secondary connection will be represented as an additional network interface.
## Parameters
### Common parameters

View File

@@ -63,10 +63,10 @@ metadata:
cozystack.io/vpcId: {{ $vpcId }}
cozystack.io/tenantName: {{ $.Release.Namespace }}
data:
subnets: |
{{- range $subnetName, $subnetConfig := .Values.subnets }}
- subnetName: {{ $subnetName }}
subnetId: {{ print "subnet-" (print $.Release.Namespace "/" $vpcId "/" $subnetName | sha256sum | trunc 8) }}
subnetCIDR: {{ $subnetConfig.cidr }}
{{- end }}
{{- range $subnetName, $subnetConfig := .Values.subnets }}
{{ $subnetName }}: |-
subnetName: {{ $subnetName }}
subnetId: {{ print "subnet-" (print $.Release.Namespace "/" $vpcId "/" $subnetName | sha256sum | trunc 8) }}
subnetCIDR: {{ $subnetConfig.cidr }}
{{- end }}

View File

@@ -154,7 +154,7 @@
{{- $resources := index . 1 }}
{{- $global := index . 2 }}
{{- $presetMap := include "cozy-lib.resources.unsanitizedPreset" $preset | fromYaml }}
{{- $mergedMap := deepCopy $resources | mergeOverwrite $presetMap }}
{{- $mergedMap := deepCopy (default (dict) $resources) | mergeOverwrite $presetMap }}
{{- include "cozy-lib.resources.sanitize" (list $mergedMap $global) }}
{{- end }}

View File

@@ -3,7 +3,7 @@ ARG NODE_VERSION=20.18.1
FROM node:${NODE_VERSION}-alpine AS builder
WORKDIR /src
ARG COMMIT_REF=92906a7f21050cfb8e352f98d36b209c57844f63
ARG COMMIT_REF=ba56271739505284aee569f914fc90e6a9c670da
RUN wget -O- https://github.com/PRO-Robotech/openapi-ui-k8s-bff/archive/${COMMIT_REF}.tar.gz | tar xzf - --strip-components=1
ENV PATH=/src/node_modules/.bin:$PATH

View File

@@ -5,7 +5,7 @@ ARG NODE_VERSION=20.18.1
FROM node:${NODE_VERSION}-alpine AS openapi-k8s-toolkit-builder
RUN apk add git
WORKDIR /src
ARG COMMIT=7086a2d8a07dcf6a94bb4276433db5d84acfcf3b
ARG COMMIT=7bd5380c6c4606640dd3bac68bf9dce469470518
RUN wget -O- https://github.com/cozystack/openapi-k8s-toolkit/archive/${COMMIT}.tar.gz | tar -xzvf- --strip-components=1
COPY openapi-k8s-toolkit/patches /patches
@@ -19,14 +19,14 @@ RUN npm run build
# openapi-ui
# imported from https://github.com/cozystack/openapi-ui
FROM node:${NODE_VERSION}-alpine AS builder
RUN apk add git
#RUN apk add git
WORKDIR /src
ARG COMMIT_REF=fe237518348e94cead6d4f3283b2fce27f26aa12
ARG COMMIT_REF=0c3629b2ce8545e81f7ece4d65372a188c802dfc
RUN wget -O- https://github.com/PRO-Robotech/openapi-ui/archive/${COMMIT_REF}.tar.gz | tar xzf - --strip-components=1
COPY openapi-ui/patches /patches
RUN git apply /patches/*.diff
#COPY openapi-ui/patches /patches
#RUN git apply /patches/*.diff
ENV PATH=/src/node_modules/.bin:$PATH

View File

@@ -1,230 +0,0 @@
diff --git a/src/components/molecules/BlackholeForm/molecules/FormObjectFromSwagger/FormObjectFromSwagger.tsx b/src/components/molecules/BlackholeForm/molecules/FormObjectFromSwagger/FormObjectFromSwagger.tsx
index a7135d4..2fea0bb 100644
--- a/src/components/molecules/BlackholeForm/molecules/FormObjectFromSwagger/FormObjectFromSwagger.tsx
+++ b/src/components/molecules/BlackholeForm/molecules/FormObjectFromSwagger/FormObjectFromSwagger.tsx
@@ -68,13 +68,60 @@ export const FormObjectFromSwagger: FC<TFormObjectFromSwaggerProps> = ({
properties?: OpenAPIV2.SchemaObject['properties']
required?: string
}
+
+ // Check if the field name exists in additionalProperties.properties
+ // If so, use the type from that property definition
+ const nestedProp = addProps?.properties?.[additionalPropValue] as OpenAPIV2.SchemaObject | undefined
+ let fieldType: string = addProps.type
+ let fieldItems: { type: string } | undefined = addProps.items
+ let fieldNestedProperties = addProps.properties || {}
+ let fieldRequired: string | undefined = addProps.required
+
+ if (nestedProp) {
+ // Use the nested property definition if it exists
+ // Handle type - it can be string or string[] in OpenAPI v2
+ if (nestedProp.type) {
+ if (Array.isArray(nestedProp.type)) {
+ fieldType = nestedProp.type[0] || addProps.type
+ } else if (typeof nestedProp.type === 'string') {
+ fieldType = nestedProp.type
+ } else {
+ fieldType = addProps.type
+ }
+ } else {
+ fieldType = addProps.type
+ }
+
+ // Handle items - it can be ItemsObject or ReferenceObject
+ if (nestedProp.items) {
+ // Check if it's a valid ItemsObject with type property
+ if ('type' in nestedProp.items && typeof nestedProp.items.type === 'string') {
+ fieldItems = { type: nestedProp.items.type }
+ } else {
+ fieldItems = addProps.items
+ }
+ } else {
+ fieldItems = addProps.items
+ }
+
+ fieldNestedProperties = nestedProp.properties || {}
+ // Handle required field - it can be string[] in OpenAPI schema
+ if (Array.isArray(nestedProp.required)) {
+ fieldRequired = nestedProp.required.join(',')
+ } else if (typeof nestedProp.required === 'string') {
+ fieldRequired = nestedProp.required
+ } else {
+ fieldRequired = addProps.required
+ }
+ }
+
inputProps?.addField({
path: Array.isArray(name) ? [...name, String(collapseTitle)] : [name, String(collapseTitle)],
name: additionalPropValue,
- type: addProps.type,
- items: addProps.items,
- nestedProperties: addProps.properties || {},
- required: addProps.required,
+ type: fieldType,
+ items: fieldItems,
+ nestedProperties: fieldNestedProperties,
+ required: fieldRequired,
})
setAddditionalPropValue(undefined)
}
diff --git a/src/components/molecules/BlackholeForm/molecules/FormStringInput/FormStringInput.tsx b/src/components/molecules/BlackholeForm/molecules/FormStringInput/FormStringInput.tsx
index 487d480..3ca46c1 100644
--- a/src/components/molecules/BlackholeForm/molecules/FormStringInput/FormStringInput.tsx
+++ b/src/components/molecules/BlackholeForm/molecules/FormStringInput/FormStringInput.tsx
@@ -42,7 +42,11 @@ export const FormStringInput: FC<TFormStringInputProps> = ({
const formValue = Form.useWatch(formFieldName)
// Derive multiline based on current local value
- const isMultiline = useMemo(() => isMultilineString(formValue), [formValue])
+ const isMultiline = useMemo(() => {
+ // Normalize value for multiline check
+ const value = typeof formValue === 'string' ? formValue : (formValue === null || formValue === undefined ? '' : String(formValue))
+ return isMultilineString(value)
+ }, [formValue])
const title = (
<>
@@ -77,6 +81,23 @@ export const FormStringInput: FC<TFormStringInputProps> = ({
rules={[{ required: forceNonRequired === false && required?.includes(getStringByName(name)) }]}
validateTrigger="onBlur"
hasFeedback={designNewLayout ? { icons: feedbackIcons } : true}
+ normalize={(value) => {
+ // Normalize value to string - prevent "[object Object]" display
+ if (value === undefined || value === null) {
+ return ''
+ }
+ if (typeof value === 'string') {
+ return value
+ }
+ if (typeof value === 'number' || typeof value === 'boolean') {
+ return String(value)
+ }
+ // If it's an object or array, it shouldn't be in a string field - return empty string
+ if (typeof value === 'object') {
+ return ''
+ }
+ return String(value)
+ }}
>
<Input.TextArea
placeholder={getStringByName(name)}
diff --git a/src/components/molecules/BlackholeForm/organisms/BlackholeForm/helpers/casts.ts b/src/components/molecules/BlackholeForm/organisms/BlackholeForm/helpers/casts.ts
index 6f9eb39..835224c 100644
--- a/src/components/molecules/BlackholeForm/organisms/BlackholeForm/helpers/casts.ts
+++ b/src/components/molecules/BlackholeForm/organisms/BlackholeForm/helpers/casts.ts
@@ -124,8 +124,26 @@ export const materializeAdditionalFromValues = (
*
* This is used when a new field appears in the data but doesn't yet exist in the schema.
*/
- const makeChildFromAP = (ap: any): OpenAPIV2.SchemaObject => {
- const t = ap?.type ?? 'object'
+ const makeChildFromAP = (ap: any, value?: unknown): OpenAPIV2.SchemaObject => {
+ // Determine type based on actual value if not explicitly defined in additionalProperties
+ let t = ap?.type
+ if (!t && value !== undefined && value !== null) {
+ if (Array.isArray(value)) {
+ t = 'array'
+ } else if (typeof value === 'object') {
+ t = 'object'
+ } else if (typeof value === 'string') {
+ t = 'string'
+ } else if (typeof value === 'number') {
+ t = 'number'
+ } else if (typeof value === 'boolean') {
+ t = 'boolean'
+ } else {
+ t = 'object'
+ }
+ }
+ t = t ?? 'object'
+
const child: OpenAPIV2.SchemaObject = { type: t } as any
// Copy common schema details (if present)
@@ -134,6 +152,20 @@ export const materializeAdditionalFromValues = (
if (ap?.required)
(child as any).required = _.cloneDeep(ap.required)
+ // If value is an array and items type is not defined, infer it from the first item
+ if (t === 'array' && Array.isArray(value) && value.length > 0 && !ap?.items) {
+ const firstItem = value[0]
+ if (typeof firstItem === 'string') {
+ ;(child as any).items = { type: 'string' }
+ } else if (typeof firstItem === 'number') {
+ ;(child as any).items = { type: 'number' }
+ } else if (typeof firstItem === 'boolean') {
+ ;(child as any).items = { type: 'boolean' }
+ } else if (typeof firstItem === 'object') {
+ ;(child as any).items = { type: 'object' }
+ }
+ }
+
// Mark as originating from `additionalProperties`
;(child as any).isAdditionalProperties = true
return child
@@ -177,7 +209,16 @@ export const materializeAdditionalFromValues = (
// If the key doesn't exist in schema, create it from `additionalProperties`
if (!schemaNode.properties![k]) {
- schemaNode.properties![k] = makeChildFromAP(ap)
+ // Check if there's a nested property definition in additionalProperties
+ const nestedProp = ap?.properties?.[k]
+ if (nestedProp) {
+ // Use the nested property definition from additionalProperties
+ schemaNode.properties![k] = _.cloneDeep(nestedProp) as any
+ ;(schemaNode.properties![k] as any).isAdditionalProperties = true
+ } else {
+ // Create from additionalProperties with value-based type inference
+ schemaNode.properties![k] = makeChildFromAP(ap, vo[k])
+ }
// If it's an existing additional property, merge any nested structure
} else if ((schemaNode.properties![k] as any).isAdditionalProperties && ap?.properties) {
;(schemaNode.properties![k] as any).properties ??= _.cloneDeep(ap.properties)
diff --git a/src/components/molecules/BlackholeForm/organisms/BlackholeForm/utils.tsx b/src/components/molecules/BlackholeForm/organisms/BlackholeForm/utils.tsx
index 2d887c7..d69d711 100644
--- a/src/components/molecules/BlackholeForm/organisms/BlackholeForm/utils.tsx
+++ b/src/components/molecules/BlackholeForm/organisms/BlackholeForm/utils.tsx
@@ -394,9 +394,11 @@ export const getArrayFormItemFromSwagger = ({
{(fields, { add, remove }, { errors }) => (
<>
{fields.map(field => {
- const fieldType = (
+ const rawFieldType = (
schema.items as (OpenAPIV2.ItemsObject & { properties?: OpenAPIV2.SchemaObject }) | undefined
)?.type
+ // Handle type as string or string[] (OpenAPI v2 allows both)
+ const fieldType = Array.isArray(rawFieldType) ? rawFieldType[0] : rawFieldType
const description = (schema.items as (OpenAPIV2.ItemsObject & { description?: string }) | undefined)
?.description
const entry = schema.items as
@@ -577,7 +579,29 @@ export const getArrayFormItemFromSwagger = ({
type="text"
size="small"
onClick={() => {
- add()
+ // Determine initial value based on item type
+ const fieldType = (
+ schema.items as (OpenAPIV2.ItemsObject & { properties?: OpenAPIV2.SchemaObject }) | undefined
+ )?.type
+
+ let initialValue: unknown
+ // Handle type as string or string[] (OpenAPI v2 allows both)
+ const typeStr = Array.isArray(fieldType) ? fieldType[0] : fieldType
+ if (typeStr === 'string') {
+ initialValue = ''
+ } else if (typeStr === 'number' || typeStr === 'integer') {
+ initialValue = 0
+ } else if (typeStr === 'boolean') {
+ initialValue = false
+ } else if (typeStr === 'array') {
+ initialValue = []
+ } else if (typeStr === 'object') {
+ initialValue = {}
+ } else {
+ initialValue = ''
+ }
+
+ add(initialValue)
}}
>
<PlusIcon />

View File

@@ -1,91 +0,0 @@
diff --git a/src/components/organisms/ListInsideClusterAndNs/ListInsideClusterAndNs.tsx b/src/components/organisms/ListInsideClusterAndNs/ListInsideClusterAndNs.tsx
index ac56e5f..c6e2350 100644
--- a/src/components/organisms/ListInsideClusterAndNs/ListInsideClusterAndNs.tsx
+++ b/src/components/organisms/ListInsideClusterAndNs/ListInsideClusterAndNs.tsx
@@ -1,6 +1,6 @@
import React, { FC, useState } from 'react'
import { Button, Alert, Spin, Typography } from 'antd'
-import { filterSelectOptions, Spacer, useBuiltinResources, useApiResources } from '@prorobotech/openapi-k8s-toolkit'
+import { filterSelectOptions, Spacer, useApiResources } from '@prorobotech/openapi-k8s-toolkit'
import { useNavigate } from 'react-router-dom'
import { useSelector, useDispatch } from 'react-redux'
import { RootState } from 'store/store'
@@ -11,6 +11,11 @@ import {
CUSTOM_NAMESPACE_API_RESOURCE_RESOURCE_NAME,
} from 'constants/customizationApiGroupAndVersion'
import { Styled } from './styled'
+import {
+ BASE_PROJECTS_API_GROUP,
+ BASE_PROJECTS_VERSION,
+ BASE_PROJECTS_RESOURCE_NAME,
+} from 'constants/customizationApiGroupAndVersion'
export const ListInsideClusterAndNs: FC = () => {
const clusterList = useSelector((state: RootState) => state.clusterList.clusterList)
@@ -33,9 +38,11 @@ export const ListInsideClusterAndNs: FC = () => {
typeof CUSTOM_NAMESPACE_API_RESOURCE_RESOURCE_NAME === 'string' &&
CUSTOM_NAMESPACE_API_RESOURCE_RESOURCE_NAME.length > 0
- const namespacesData = useBuiltinResources({
+ const namespacesData = useApiResources({
clusterName: selectedCluster || '',
- typeName: 'namespaces',
+ apiGroup: BASE_PROJECTS_API_GROUP,
+ apiVersion: BASE_PROJECTS_VERSION,
+ typeName: BASE_PROJECTS_RESOURCE_NAME,
limit: null,
isEnabled: selectedCluster !== undefined && !isCustomNamespaceResource,
})
diff --git a/src/hooks/useNavSelectorInside.ts b/src/hooks/useNavSelectorInside.ts
index 5736e2b..1ec0f71 100644
--- a/src/hooks/useNavSelectorInside.ts
+++ b/src/hooks/useNavSelectorInside.ts
@@ -1,6 +1,11 @@
-import { TClusterList, TSingleResource, useBuiltinResources } from '@prorobotech/openapi-k8s-toolkit'
+import { TClusterList, TSingleResource, useApiResources } from '@prorobotech/openapi-k8s-toolkit'
import { useSelector } from 'react-redux'
import { RootState } from 'store/store'
+import {
+ BASE_PROJECTS_API_GROUP,
+ BASE_PROJECTS_VERSION,
+ BASE_PROJECTS_RESOURCE_NAME,
+} from 'constants/customizationApiGroupAndVersion'
const mappedClusterToOptionInSidebar = ({ name }: TClusterList[number]): { value: string; label: string } => ({
value: name,
@@ -15,9 +20,11 @@ const mappedNamespaceToOptionInSidebar = ({ metadata }: TSingleResource): { valu
export const useNavSelectorInside = (clusterName?: string) => {
const clusterList = useSelector((state: RootState) => state.clusterList.clusterList)
- const { data: namespaces } = useBuiltinResources({
+ const { data: namespaces } = useApiResources({
clusterName: clusterName || '',
- typeName: 'namespaces',
+ apiGroup: BASE_PROJECTS_API_GROUP,
+ apiVersion: BASE_PROJECTS_VERSION,
+ typeName: BASE_PROJECTS_RESOURCE_NAME,
limit: null,
isEnabled: Boolean(clusterName),
})
diff --git a/src/utils/getBacklink.ts b/src/utils/getBacklink.ts
index a862354..f24e2bc 100644
--- a/src/utils/getBacklink.ts
+++ b/src/utils/getBacklink.ts
@@ -28,7 +28,7 @@ export const getFormsBackLink = ({
}
if (namespacesMode) {
- return `${baseprefix}/${clusterName}/builtin-table/namespaces`
+ return `${baseprefix}/${clusterName}/api-table/core.cozystack.io/v1alpha1/tenantnamespaces`
}
if (possibleProject) {
@@ -64,7 +64,7 @@ export const getTablesBackLink = ({
}
if (namespacesMode) {
- return `${baseprefix}/${clusterName}/builtin-table/namespaces`
+ return `${baseprefix}/${clusterName}/api-table/core.cozystack.io/v1alpha1/tenantnamespaces`
}
if (possibleProject) {

View File

@@ -1,15 +0,0 @@
diff --git a/src/components/organisms/Header/organisms/User/User.tsx b/src/components/organisms/Header/organisms/User/User.tsx
index efe7ac3..80b715c 100644
--- a/src/components/organisms/Header/organisms/User/User.tsx
+++ b/src/components/organisms/Header/organisms/User/User.tsx
@@ -23,10 +23,6 @@ export const User: FC = () => {
// key: '1',
// label: <ThemeSelector />,
// },
- {
- key: '2',
- label: <div onClick={() => navigate(`${baseprefix}/inside/clusters`)}>Inside</div>,
- },
{
key: '3',
label: (

View File

@@ -45,9 +45,9 @@ spec:
- name: BASE_NAMESPACE_FULL_PATH
value: "/apis/core.cozystack.io/v1alpha1/tenantnamespaces"
- name: LOGGER
value: "TRUE"
value: "true"
- name: LOGGER_WITH_HEADERS
value: "TRUE"
value: "false"
- name: PORT
value: "64231"
image: {{ .Values.openapiUIK8sBff.image | quote }}
@@ -94,6 +94,8 @@ spec:
- env:
- name: BASEPREFIX
value: /openapi-ui
- name: HIDE_INSIDE
value: "true"
- name: CUSTOMIZATION_API_GROUP
value: dashboard.cozystack.io
- name: CUSTOMIZATION_API_VERSION

View File

@@ -1,6 +1,6 @@
openapiUI:
image: ghcr.io/cozystack/cozystack/openapi-ui:latest@sha256:b942d98ff0ea36e3c6e864b6459b404d37ed68bc2b0ebc5d3007a1be4faf60c5
image: ghcr.io/cozystack/cozystack/openapi-ui:latest@sha256:77991f2482c0026d082582b22a8ffb191f3ba6fc948b2f125ef9b1081538f865
openapiUIK8sBff:
image: ghcr.io/cozystack/cozystack/openapi-ui-k8s-bff:latest@sha256:5ddc6546baf3acdb8e0572536665fe73053a7f985b05e51366454efa11c201d2
image: ghcr.io/cozystack/cozystack/openapi-ui-k8s-bff:latest@sha256:8386f0747266726afb2b30db662092d66b0af0370e3becd8bee9684125fa9cc9
tokenProxy:
image: ghcr.io/cozystack/cozystack/token-proxy:latest@sha256:fad27112617bb17816702571e1f39d0ac3fe5283468d25eb12f79906cdab566b

View File

@@ -22,7 +22,13 @@ spec:
- GPU
- VMExport
evictionStrategy: LiveMigrate
vmRolloutStrategy: LiveUpdate
workloadUpdateStrategy:
workloadUpdateMethods:
- LiveMigrate
- Evict
batchEvictionInterval: 1m
batchEvictionSize: 10
customizeComponents: {}
imagePullPolicy: IfNotPresent
monitorNamespace: tenant-root
workloadUpdateStrategy: {}

View File

@@ -59,11 +59,9 @@ func RegisterStaticTypes(scheme *runtime.Scheme) {
&TenantNamespaceList{},
&TenantSecret{},
&TenantSecretList{},
&TenantSecretsTable{},
&TenantSecretsTableList{},
&TenantModule{},
&TenantModuleList{},
)
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
klog.V(1).Info("Registered static kinds: TenantNamespace, TenantSecret, TenantSecretsTable, TenantModule")
klog.V(1).Info("Registered static kinds: TenantNamespace, TenantSecret, TenantModule")
}

View File

@@ -1,34 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
// TenantSecretEntry represents a single key from a Secret's data.
type TenantSecretEntry struct {
Name string `json:"name,omitempty"`
Key string `json:"key,omitempty"`
Value string `json:"value,omitempty"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// TenantSecretsTable is a virtual, namespaced resource that exposes every key
// of Secrets labelled cozystack.io/ui=true as a separate object.
type TenantSecretsTable struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Data TenantSecretEntry `json:"data,omitempty"`
}
// DeepCopy methods are generated by deepcopy-gen
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type TenantSecretsTableList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []TenantSecretsTable `json:"items"`
}
// DeepCopy methods are generated by deepcopy-gen

View File

@@ -216,22 +216,6 @@ func (in *TenantSecret) DeepCopyObject() runtime.Object {
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TenantSecretEntry) DeepCopyInto(out *TenantSecretEntry) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantSecretEntry.
func (in *TenantSecretEntry) DeepCopy() *TenantSecretEntry {
if in == nil {
return nil
}
out := new(TenantSecretEntry)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TenantSecretList) DeepCopyInto(out *TenantSecretList) {
*out = *in
@@ -264,63 +248,3 @@ func (in *TenantSecretList) DeepCopyObject() runtime.Object {
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TenantSecretsTable) DeepCopyInto(out *TenantSecretsTable) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
out.Data = in.Data
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantSecretsTable.
func (in *TenantSecretsTable) DeepCopy() *TenantSecretsTable {
if in == nil {
return nil
}
out := new(TenantSecretsTable)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *TenantSecretsTable) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TenantSecretsTableList) DeepCopyInto(out *TenantSecretsTableList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]TenantSecretsTable, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantSecretsTableList.
func (in *TenantSecretsTableList) DeepCopy() *TenantSecretsTableList {
if in == nil {
return nil
}
out := new(TenantSecretsTableList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *TenantSecretsTableList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}

View File

@@ -44,7 +44,6 @@ import (
tenantmodulestorage "github.com/cozystack/cozystack/pkg/registry/core/tenantmodule"
tenantnamespacestorage "github.com/cozystack/cozystack/pkg/registry/core/tenantnamespace"
tenantsecretstorage "github.com/cozystack/cozystack/pkg/registry/core/tenantsecret"
tenantsecretstablestorage "github.com/cozystack/cozystack/pkg/registry/core/tenantsecretstable"
)
var (
@@ -177,9 +176,6 @@ func (c completedConfig) New() (*CozyServer, error) {
coreV1alpha1Storage["tenantsecrets"] = cozyregistry.RESTInPeace(
tenantsecretstorage.NewREST(cli, watchCli),
)
coreV1alpha1Storage["tenantsecretstables"] = cozyregistry.RESTInPeace(
tenantsecretstablestorage.NewREST(cli, watchCli),
)
coreV1alpha1Storage["tenantmodules"] = cozyregistry.RESTInPeace(
tenantmodulestorage.NewREST(cli, watchCli),
)

View File

@@ -39,10 +39,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantNamespace": schema_pkg_apis_core_v1alpha1_TenantNamespace(ref),
"github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantNamespaceList": schema_pkg_apis_core_v1alpha1_TenantNamespaceList(ref),
"github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantSecret": schema_pkg_apis_core_v1alpha1_TenantSecret(ref),
"github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantSecretEntry": schema_pkg_apis_core_v1alpha1_TenantSecretEntry(ref),
"github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantSecretList": schema_pkg_apis_core_v1alpha1_TenantSecretList(ref),
"github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantSecretsTable": schema_pkg_apis_core_v1alpha1_TenantSecretsTable(ref),
"github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantSecretsTableList": schema_pkg_apis_core_v1alpha1_TenantSecretsTableList(ref),
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1.ConversionRequest": schema_pkg_apis_apiextensions_v1_ConversionRequest(ref),
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1.ConversionResponse": schema_pkg_apis_apiextensions_v1_ConversionResponse(ref),
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1.ConversionReview": schema_pkg_apis_apiextensions_v1_ConversionReview(ref),
@@ -557,37 +554,6 @@ func schema_pkg_apis_core_v1alpha1_TenantSecret(ref common.ReferenceCallback) co
}
}
func schema_pkg_apis_core_v1alpha1_TenantSecretEntry(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "TenantSecretEntry represents a single key from a Secret's data.",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"name": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "",
},
},
"key": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "",
},
},
"value": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "",
},
},
},
},
},
}
}
func schema_pkg_apis_core_v1alpha1_TenantSecretList(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
@@ -636,95 +602,6 @@ func schema_pkg_apis_core_v1alpha1_TenantSecretList(ref common.ReferenceCallback
}
}
func schema_pkg_apis_core_v1alpha1_TenantSecretsTable(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "TenantSecretsTable is a virtual, namespaced resource that exposes every key of Secrets labelled cozystack.io/ui=true as a separate object.",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
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{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
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{"string"},
Format: "",
},
},
"metadata": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"),
},
},
"data": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantSecretEntry"),
},
},
},
},
},
Dependencies: []string{
"github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantSecretEntry", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
}
}
func schema_pkg_apis_core_v1alpha1_TenantSecretsTableList(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
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{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
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{"string"},
Format: "",
},
},
"metadata": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"),
},
},
"items": {
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantSecretsTable"),
},
},
},
},
},
},
Required: []string{"items"},
},
},
Dependencies: []string{
"github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.TenantSecretsTable", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
}
}
func schema_pkg_apis_apiextensions_v1_ConversionRequest(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{

View File

@@ -1196,12 +1196,18 @@ func (r *REST) Destroy() {
// New creates a new instance of Application
func (r *REST) New() runtime.Object {
return &appsv1alpha1.Application{}
app := &unstructured.UnstructuredList{}
app.SetAPIVersion("apps.cozystack.io/v1alpha1")
app.SetKind(r.kindName)
return app
}
// NewList returns an empty list of Application objects
func (r *REST) NewList() runtime.Object {
return &appsv1alpha1.ApplicationList{}
appList := &unstructured.UnstructuredList{}
appList.SetAPIVersion("apps.cozystack.io/v1alpha1")
appList.SetKind(r.kindName + "List")
return appList
}
// Kind returns the resource kind used for API discovery

View File

@@ -9,7 +9,7 @@ import (
"encoding/base64"
"fmt"
"net/http"
"sort"
"slices"
"time"
corev1 "k8s.io/api/core/v1"
@@ -226,6 +226,9 @@ func (r *REST) Get(
if err != nil {
return nil, err
}
if sec.Labels == nil || sec.Labels[tsLabelKey] != tsLabelValue {
return nil, apierrors.NewNotFound(r.gvr.GroupResource(), name)
}
return secretToTenant(sec), nil
}
@@ -253,11 +256,13 @@ func (r *REST) List(ctx context.Context, opts *metainternal.ListOptions) (runtim
list := &corev1.SecretList{}
err = r.c.List(ctx, list,
&client.ListOptions{
Namespace: ns,
Namespace: ns,
LabelSelector: ls,
Raw: &metav1.ListOptions{
LabelSelector: ls.String(),
FieldSelector: fieldSel,
}})
},
})
if err != nil {
return nil, err
}
@@ -273,7 +278,17 @@ func (r *REST) List(ctx context.Context, opts *metainternal.ListOptions) (runtim
for i := range list.Items {
out.Items = append(out.Items, *secretToTenant(&list.Items[i]))
}
sort.Slice(out.Items, func(i, j int) bool { return out.Items[i].Name < out.Items[j].Name })
slices.SortFunc(out.Items, func(a, b corev1alpha1.TenantSecret) int {
aKey := fmt.Sprintf("%s/%s", a.Namespace, a.Name)
bKey := fmt.Sprintf("%s/%s", b.Namespace, b.Name)
switch {
case aKey < bKey:
return -1
case aKey > bKey:
return 1
}
return 0
})
return out, nil
}
@@ -291,10 +306,17 @@ func (r *REST) Update(
return nil, false, err
}
cur := &corev1.Secret{}
err = r.c.Get(ctx, types.NamespacedName{Namespace: ns, Name: name}, cur, &client.GetOptions{Raw: &metav1.GetOptions{}})
if err != nil && !apierrors.IsNotFound(err) {
return nil, false, err
var cur *corev1.Secret
previous := &corev1.Secret{}
if err := r.c.Get(ctx, types.NamespacedName{Namespace: ns, Name: name}, previous, &client.GetOptions{Raw: &metav1.GetOptions{}}); err != nil {
if !apierrors.IsNotFound(err) {
return nil, false, err
}
} else {
if previous.Labels == nil || previous.Labels[tsLabelKey] != tsLabelValue {
return nil, false, apierrors.NewNotFound(r.gvr.GroupResource(), name)
}
cur = previous
}
newObj, err := objInfo.UpdatedObject(ctx, nil)
@@ -306,7 +328,7 @@ func (r *REST) Update(
newSec := tenantToSecret(in, cur)
newSec.Namespace = ns
if cur == nil {
if !forceCreate && err == nil {
if !forceCreate {
return nil, false, apierrors.NewNotFound(r.gvr.GroupResource(), name)
}
err := r.c.Create(ctx, newSec, &client.CreateOptions{Raw: &metav1.CreateOptions{}})
@@ -328,6 +350,13 @@ func (r *REST) Delete(
if err != nil {
return nil, false, err
}
current := &corev1.Secret{}
if err := r.c.Get(ctx, types.NamespacedName{Namespace: ns, Name: name}, current, &client.GetOptions{Raw: &metav1.GetOptions{}}); err != nil {
return nil, false, err
}
if current.Labels == nil || current.Labels[tsLabelKey] != tsLabelValue {
return nil, false, apierrors.NewNotFound(r.gvr.GroupResource(), name)
}
err = r.c.Delete(ctx, &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Namespace: ns, Name: name}}, &client.DeleteOptions{Raw: opts})
return nil, err == nil, err
}
@@ -347,6 +376,13 @@ func (r *REST) Patch(
if err != nil {
return nil, err
}
current := &corev1.Secret{}
if err := r.c.Get(ctx, types.NamespacedName{Namespace: ns, Name: name}, current, &client.GetOptions{Raw: &metav1.GetOptions{}}); err != nil {
return nil, err
}
if current.Labels == nil || current.Labels[tsLabelKey] != tsLabelValue {
return nil, apierrors.NewNotFound(r.gvr.GroupResource(), name)
}
out := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: ns,
@@ -383,12 +419,16 @@ func (r *REST) Watch(ctx context.Context, opts *metainternal.ListOptions) (watch
}
secList := &corev1.SecretList{}
ls := labels.Set{tsLabelKey: tsLabelValue}.AsSelector().String()
base, err := r.w.Watch(ctx, secList, &client.ListOptions{Namespace: ns, Raw: &metav1.ListOptions{
Watch: true,
LabelSelector: ls,
ResourceVersion: opts.ResourceVersion,
}})
ls := labels.Set{tsLabelKey: tsLabelValue}.AsSelector()
base, err := r.w.Watch(ctx, secList, &client.ListOptions{
Namespace: ns,
LabelSelector: ls,
Raw: &metav1.ListOptions{
Watch: true,
LabelSelector: ls.String(),
ResourceVersion: opts.ResourceVersion,
},
})
if err != nil {
return nil, err
}

View File

@@ -1,335 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
// TenantSecretsTable registry namespaced, read-only flattened view over
// Secrets labelled "internal.cozystack.io/tenantresource=true". Each data key is a separate object.
package tenantsecretstable
import (
"context"
"encoding/base64"
"fmt"
"net/http"
"sort"
"time"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metainternal "k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/selection"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
corev1alpha1 "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1"
)
const (
tsLabelKey = corev1alpha1.TenantResourceLabelKey
tsLabelValue = corev1alpha1.TenantResourceLabelValue
kindObj = "TenantSecretsTable"
kindObjList = "TenantSecretsTableList"
singularName = "tenantsecretstable"
resourcePlural = "tenantsecretstables"
)
type REST struct {
c client.Client
w client.WithWatch
gvr schema.GroupVersionResource
}
func NewREST(c client.Client, w client.WithWatch) *REST {
return &REST{
c: c,
w: w,
gvr: schema.GroupVersionResource{
Group: corev1alpha1.GroupName,
Version: "v1alpha1",
Resource: resourcePlural,
},
}
}
var (
_ rest.Getter = &REST{}
_ rest.Lister = &REST{}
_ rest.Watcher = &REST{}
_ rest.TableConvertor = &REST{}
_ rest.Scoper = &REST{}
_ rest.SingularNameProvider = &REST{}
_ rest.Storage = &REST{}
)
func (*REST) NamespaceScoped() bool { return true }
func (*REST) New() runtime.Object { return &corev1alpha1.TenantSecretsTable{} }
func (*REST) NewList() runtime.Object {
return &corev1alpha1.TenantSecretsTableList{}
}
func (*REST) Kind() string { return kindObj }
func (r *REST) GroupVersionKind(_ schema.GroupVersion) schema.GroupVersionKind {
return r.gvr.GroupVersion().WithKind(kindObj)
}
func (*REST) GetSingularName() string { return singularName }
func (*REST) Destroy() {}
func nsFrom(ctx context.Context) (string, error) {
ns, ok := request.NamespaceFrom(ctx)
if !ok {
return "", fmt.Errorf("namespace required")
}
return ns, nil
}
// -----------------------
// Get/List
// -----------------------
func (r *REST) Get(ctx context.Context, name string, opts *metav1.GetOptions) (runtime.Object, error) {
ns, err := nsFrom(ctx)
if err != nil {
return nil, err
}
// We need to identify secret name and key. Iterate secrets in namespace with tenant secret label
// and return the matching composed object.
list := &corev1.SecretList{}
err = r.c.List(ctx, list,
&client.ListOptions{
Namespace: ns,
Raw: &metav1.ListOptions{
LabelSelector: labels.Set{tsLabelKey: tsLabelValue}.AsSelector().String(),
},
})
if err != nil {
return nil, err
}
for i := range list.Items {
sec := &list.Items[i]
for k, v := range sec.Data {
composed := composedName(sec.Name, k)
if composed == name {
return secretKeyToObj(sec, k, v), nil
}
}
}
return nil, apierrors.NewNotFound(r.gvr.GroupResource(), name)
}
func (r *REST) List(ctx context.Context, opts *metainternal.ListOptions) (runtime.Object, error) {
ns, err := nsFrom(ctx)
if err != nil {
return nil, err
}
sel := labels.NewSelector()
req, _ := labels.NewRequirement(tsLabelKey, selection.Equals, []string{tsLabelValue})
sel = sel.Add(*req)
if opts.LabelSelector != nil {
if reqs, _ := opts.LabelSelector.Requirements(); len(reqs) > 0 {
sel = sel.Add(reqs...)
}
}
fieldSel := ""
if opts.FieldSelector != nil {
fieldSel = opts.FieldSelector.String()
}
list := &corev1.SecretList{}
err = r.c.List(ctx, list,
&client.ListOptions{
Namespace: ns,
Raw: &metav1.ListOptions{
LabelSelector: labels.Set{tsLabelKey: tsLabelValue}.AsSelector().String(),
FieldSelector: fieldSel,
},
})
if err != nil {
return nil, err
}
out := &corev1alpha1.TenantSecretsTableList{
TypeMeta: metav1.TypeMeta{APIVersion: corev1alpha1.SchemeGroupVersion.String(), Kind: kindObjList},
ListMeta: list.ListMeta,
}
for i := range list.Items {
sec := &list.Items[i]
// Ensure stable ordering of keys
keys := make([]string, 0, len(sec.Data))
for k := range sec.Data {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
v := sec.Data[k]
o := secretKeyToObj(sec, k, v)
out.Items = append(out.Items, *o)
}
}
sort.Slice(out.Items, func(i, j int) bool { return out.Items[i].Name < out.Items[j].Name })
return out, nil
}
// -----------------------
// Watch
// -----------------------
func (r *REST) Watch(ctx context.Context, opts *metainternal.ListOptions) (watch.Interface, error) {
ns, err := nsFrom(ctx)
if err != nil {
return nil, err
}
secList := &corev1.SecretList{}
ls := labels.Set{tsLabelKey: tsLabelValue}.AsSelector().String()
base, err := r.w.Watch(ctx, secList, &client.ListOptions{Namespace: ns, Raw: &metav1.ListOptions{
Watch: true,
LabelSelector: ls,
ResourceVersion: opts.ResourceVersion,
}})
if err != nil {
return nil, err
}
ch := make(chan watch.Event)
proxy := watch.NewProxyWatcher(ch)
go func() {
defer proxy.Stop()
for ev := range base.ResultChan() {
sec, ok := ev.Object.(*corev1.Secret)
if !ok || sec == nil {
continue
}
// Emit an event per key
for k, v := range sec.Data {
obj := secretKeyToObj(sec, k, v)
ch <- watch.Event{Type: ev.Type, Object: obj}
}
}
}()
return proxy, nil
}
// -----------------------
// TableConvertor
// -----------------------
func (r *REST) ConvertToTable(_ context.Context, obj runtime.Object, _ runtime.Object) (*metav1.Table, error) {
now := time.Now()
row := func(o *corev1alpha1.TenantSecretsTable) metav1.TableRow {
return metav1.TableRow{
Cells: []interface{}{o.Name, o.Data.Name, o.Data.Key, humanAge(o.CreationTimestamp.Time, now)},
Object: runtime.RawExtension{Object: o},
}
}
tbl := &metav1.Table{
TypeMeta: metav1.TypeMeta{APIVersion: "meta.k8s.io/v1", Kind: "Table"},
ColumnDefinitions: []metav1.TableColumnDefinition{
{Name: "NAME", Type: "string"},
{Name: "SECRET", Type: "string"},
{Name: "KEY", Type: "string"},
{Name: "AGE", Type: "string"},
},
}
switch v := obj.(type) {
case *corev1alpha1.TenantSecretsTableList:
for i := range v.Items {
tbl.Rows = append(tbl.Rows, row(&v.Items[i]))
}
tbl.ListMeta.ResourceVersion = v.ListMeta.ResourceVersion
case *corev1alpha1.TenantSecretsTable:
tbl.Rows = append(tbl.Rows, row(v))
tbl.ListMeta.ResourceVersion = v.ResourceVersion
default:
return nil, notAcceptable{r.gvr.GroupResource(), fmt.Sprintf("unexpected %T", obj)}
}
return tbl, nil
}
// -----------------------
// Helpers
// -----------------------
func composedName(secretName, key string) string {
return secretName + "-" + key
}
func humanAge(t time.Time, now time.Time) string {
d := now.Sub(t)
// simple human duration
if d.Hours() >= 24 {
return fmt.Sprintf("%dd", int(d.Hours()/24))
}
if d.Hours() >= 1 {
return fmt.Sprintf("%dh", int(d.Hours()))
}
if d.Minutes() >= 1 {
return fmt.Sprintf("%dm", int(d.Minutes()))
}
return fmt.Sprintf("%ds", int(d.Seconds()))
}
func secretKeyToObj(sec *corev1.Secret, key string, val []byte) *corev1alpha1.TenantSecretsTable {
return &corev1alpha1.TenantSecretsTable{
TypeMeta: metav1.TypeMeta{APIVersion: corev1alpha1.SchemeGroupVersion.String(), Kind: kindObj},
ObjectMeta: metav1.ObjectMeta{
Name: sec.Name,
Namespace: sec.Namespace,
UID: sec.UID,
ResourceVersion: sec.ResourceVersion,
CreationTimestamp: sec.CreationTimestamp,
Labels: filterUserLabels(sec.Labels),
Annotations: sec.Annotations,
},
Data: corev1alpha1.TenantSecretEntry{
Name: sec.Name,
Key: key,
Value: toBase64String(val),
},
}
}
func filterUserLabels(m map[string]string) map[string]string {
if m == nil {
return nil
}
out := make(map[string]string, len(m))
for k, v := range m {
if k == tsLabelKey {
continue
}
out[k] = v
}
return out
}
func toBase64String(b []byte) string {
const enc = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
// Minimal base64 encoder to avoid extra deps; for readability we could use stdlib encoding/base64
// but keeping inline is fine; however using stdlib is clearer.
// Using stdlib:
return base64.StdEncoding.EncodeToString(b)
}
type notAcceptable struct {
resource schema.GroupResource
message string
}
func (e notAcceptable) Error() string { return e.message }
func (e notAcceptable) Status() metav1.Status {
return metav1.Status{
Status: metav1.StatusFailure,
Code: http.StatusNotAcceptable,
Reason: metav1.StatusReason("NotAcceptable"),
Message: e.message,
}
}