Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/d8/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import (
pluginscmd "github.com/deckhouse/deckhouse-cli/internal/plugins/cmd"
"github.com/deckhouse/deckhouse-cli/internal/plugins/flags"
selfupdatecmd "github.com/deckhouse/deckhouse-cli/internal/selfupdate/cmd"
snapshot "github.com/deckhouse/deckhouse-cli/internal/snapshot/cmd"
status "github.com/deckhouse/deckhouse-cli/internal/status/cmd"
system "github.com/deckhouse/deckhouse-cli/internal/system/cmd"
"github.com/deckhouse/deckhouse-cli/internal/tools"
Expand Down Expand Up @@ -104,6 +105,7 @@ func (r *RootCommand) registerCommands() {

r.cmd.AddCommand(backup.NewCommand())
r.cmd.AddCommand(data.NewCommand())
r.cmd.AddCommand(snapshot.NewCommand())
r.cmd.AddCommand(mirror.NewCommand())
r.cmd.AddCommand(cr.NewCommand())
r.cmd.AddCommand(status.NewCommand())
Expand Down
9 changes: 5 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -406,11 +406,12 @@ require (
github.com/json-iterator/go v1.1.12 // indirect
github.com/kelseyhightower/envconfig v1.4.0 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/compress v1.18.0
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/kubernetes-csi/external-snapshotter/client/v8 v8.2.0
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec // indirect
Expand Down Expand Up @@ -490,7 +491,7 @@ require (
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe // indirect
github.com/pierrec/lz4 v2.6.1+incompatible // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/pierrec/lz4/v4 v4.1.22
github.com/pires/go-proxyproto v0.6.1 // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
Expand Down Expand Up @@ -618,8 +619,8 @@ require (
golang.org/x/mod v0.37.0 // indirect
golang.org/x/net v0.57.0 // indirect
golang.org/x/oauth2 v0.35.0 // indirect
golang.org/x/sync v0.22.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/sync v0.22.0
golang.org/x/sys v0.47.0
golang.org/x/text v0.40.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.47.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1219,6 +1219,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kubernetes-csi/external-snapshotter/client/v8 v8.2.0 h1:Q3jQ1NkFqv5o+F8dMmHd8SfEmlcwNeo1immFApntEwE=
github.com/kubernetes-csi/external-snapshotter/client/v8 v8.2.0/go.mod h1:E3vdYxHj2C2q6qo8/Da4g7P+IcwqRZyy3gJBzYybV9Y=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
Expand Down
20 changes: 20 additions & 0 deletions internal/data/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,26 @@ func AskYesNoWithTimeout(prompt string, timeout time.Duration) bool {
}
}

// KindToGroup resolves the API group for a supported DataExport target kind. The kind is
// sent verbatim as targetRef.kind; only the group needs deriving here (the controller
// resolves the served version via its RESTMapper). These groups match the producer's
// DataExportTargetRefSpec contract in storage-volume-data-manager/api/v1alpha1/data_export.go.
// Returns an error for unrecognised kinds.
func KindToGroup(kind string) (string, error) {
switch kind {
case PersistentVolumeClaimKind:
return "", nil
case VolumeSnapshotKind:
return "snapshot.storage.k8s.io", nil
case VirtualDiskKind:
return "virtualization.deckhouse.io", nil
case VirtualDiskSnapshotKind:
return "virtualization.deckhouse.io", nil
default:
return "", fmt.Errorf("unsupported DataExport target kind %q", kind)
}
}

func ParseArgs(args []string) ( /*deName*/ string /*srcPath*/, string, error) {
var deName, srcPath string

Expand Down
12 changes: 11 additions & 1 deletion internal/data/dataexport/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
# DataExport
Subcommand for the command line client for Deckhouse.

The export target is referenced by **kind** (`targetRef.kind`, the API group is
derived from it). Supported target kinds and their CLI aliases:

| Kind | Aliases |
| --- | --- |
| `PersistentVolumeClaim` | `pvc`, `persistentvolumeclaim` |
| `VolumeSnapshot` | `vs`, `volumesnapshot` |
| `VirtualDisk` | `vd`, `virtualdisk` |
| `VirtualDiskSnapshot` | `vds`, `virtualdisksnapshot` |

### Available Commands:
* create - Create k8s DataExport object.

Expand Down Expand Up @@ -61,5 +71,5 @@ NAMESPACE NAME READYTOUSE SOURCEPVC

5. Create data export from that snapshot using d8 command as shown in the example below
```shell
d8 data create export-name snapshot/my-snapshot
d8 data create export-name vs/my-snapshot
```
15 changes: 15 additions & 0 deletions internal/data/dataexport/api/v1alpha1/data_export.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,23 @@ type DataExportStatus struct {
VolumeMode string `json:"volumeMode,omitempty"`
}

// TargetRefSpec references the export target by GroupKind + name (namespace is
// implicit = the DataExport's own namespace). The version is intentionally NOT pinned:
// the controller resolves the served version via the RESTMapper. Mirrors the producer's
// DataExportTargetRefSpec in storage-volume-data-manager/api/v1alpha1/data_export.go.
//
// +k8s:deepcopy-gen=true
type TargetRefSpec struct {
// Group is the API group of the target resource ("" = core group).
Group string `json:"group,omitempty"`
// Resource is the target resource plural (e.g. "volumesnapshots").
// TEMP REVERTME: required by the deployed storage-volume-data-manager (mr135)
// GVR-based CRD; the kind-based contract is not yet in SVDM main. Sending both
// resource and kind is safe because each CRD prunes the field it doesn't know.
Resource string `json:"resource,omitempty"`
// Kind is the target object kind (e.g. "VolumeSnapshot", "PersistentVolumeClaim").
// Required by the API server.
Kind string `json:"kind"`
// Name is the target object name.
Name string `json:"name"`
}
2 changes: 1 addition & 1 deletion internal/data/dataexport/api/v1alpha1/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import (
)

const (
APIGroup = "storage.deckhouse.io"
APIGroup = "storage-foundation.deckhouse.io"
APIVersion = "v1alpha1"
)

Expand Down
7 changes: 6 additions & 1 deletion internal/data/dataexport/cmd/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,12 @@ func Run(ctx context.Context, log *slog.Logger, cmd *cobra.Command, args []strin
return err
}

err = util.CreateDataExport(ctx, deName, namespace, ttl, volumeKind, volumeName, publish, rtClient)
group, err := dataio.KindToGroup(volumeKind)
if err != nil {
return err
}

err = util.CreateDataExport(ctx, deName, namespace, ttl, group, volumeKind, volumeName, publish, rtClient)
if err != nil {
return err
}
Expand Down
79 changes: 49 additions & 30 deletions internal/data/dataexport/util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,31 +81,37 @@ func GetDataExportWithRestart(ctx context.Context, deName, namespace string, rtC
}

for _, condition := range deObj.Status.Conditions {
// restart DataExport if Expired
if condition.Type == "Expired" {
if condition.Status == "True" {
if err := DeleteDataExport(ctx, deName, namespace, rtClient); err != nil {
return nil, err
}

if err := CreateDataExport(
ctx,
deName, namespace, "",
deObj.Spec.TargetRef.Kind,
deObj.Spec.TargetRef.Name,
deObj.Spec.Publish, rtClient,
); err != nil {
return nil, err
}
}
// The DataExport catalog no longer carries a standalone "Expired" condition; expiry is now the
// Ready condition with Status=False and Reason="Expired" (plus status.phase=Expired). Detect it
// on the Ready condition and auto-restart the export, rather than waiting for the producer's GC
// (which only deletes an expired DataExport after its retention TTL).
if condition.Type != "Ready" {
continue
}
// check DataExport is Ready
if condition.Type == "Ready" {
if condition.Status != "True" {
returnErr = fmt.Errorf("DataExport %s/%s is not Ready: %s (%s)",
deObj.ObjectMeta.Namespace, deObj.ObjectMeta.Name,
condition.Message, condition.Reason)

switch {
case condition.Status == "False" && condition.Reason == "Expired":
if err := DeleteDataExport(ctx, deName, namespace, rtClient); err != nil {
return nil, err
}

if err := CreateDataExport(
ctx,
deName, namespace, "",
deObj.Spec.TargetRef.Group,
deObj.Spec.TargetRef.Kind,
deObj.Spec.TargetRef.Name,
deObj.Spec.Publish, rtClient,
); err != nil {
return nil, err
}
// Recreated: keep retrying until the fresh export becomes Ready.
returnErr = fmt.Errorf("DataExport %s/%s expired; recreated, waiting for the new export to become Ready",
deObj.ObjectMeta.Namespace, deObj.ObjectMeta.Name)
case condition.Status != "True":
returnErr = fmt.Errorf("DataExport %s/%s is not Ready: %s (%s)",
deObj.ObjectMeta.Namespace, deObj.ObjectMeta.Name,
condition.Message, condition.Reason)
}
}
// check DataExport Url
Expand Down Expand Up @@ -194,7 +200,12 @@ func CreateDataExporterIfNeeded(ctx context.Context, log *slog.Logger, deName, n
return deName, nil
}

err := CreateDataExport(ctx, deName, namespace, ttl, volumeKind, volumeName, publish, rtClient)
group, err := dataio.KindToGroup(volumeKind)
if err != nil {
return deName, err
}

err = CreateDataExport(ctx, deName, namespace, ttl, group, volumeKind, volumeName, publish, rtClient)
if err != nil {
return deName, err
}
Expand All @@ -204,7 +215,10 @@ func CreateDataExporterIfNeeded(ctx context.Context, log *slog.Logger, deName, n
return deName, nil
}

func CreateDataExport(ctx context.Context, deName, namespace, ttl, volumeKind, volumeName string, publish bool, rtClient ctrlrtclient.Client) error {
// CreateDataExport creates a DataExport CR targeting the object identified by group, kind, and volumeName.
// group is the API group ("" for core, e.g. "snapshot.storage.k8s.io" for VolumeSnapshot).
// kind is the target object kind (e.g. "VolumeSnapshot", "PersistentVolumeClaim").
func CreateDataExport(ctx context.Context, deName, namespace, ttl, group, kind, volumeName string, publish bool, rtClient ctrlrtclient.Client) error {
if ttl == "" {
ttl = dataio.DefaultTTL
}
Expand All @@ -222,8 +236,9 @@ func CreateDataExport(ctx context.Context, deName, namespace, ttl, volumeKind, v
Spec: v1alpha1.DataexportSpec{
TTL: ttl,
TargetRef: v1alpha1.TargetRefSpec{
Kind: volumeKind,
Name: volumeName,
Group: group,
Kind: kind,
Name: volumeName,
},
Publish: publish,
},
Expand Down Expand Up @@ -291,9 +306,13 @@ func getExportStatus(ctx context.Context, log *slog.Logger, deName, namespace st
return "", "", "", fmt.Errorf("invalid URL")
}

volumeKind := deObj.Spec.TargetRef.Kind
if !slices.Contains([]string{dataio.PersistentVolumeClaimKind, dataio.VolumeSnapshotKind, dataio.VirtualDiskKind, dataio.VirtualDiskSnapshotKind}, volumeKind) {
return "", "", "", fmt.Errorf("invalid volume kind: %s", volumeKind)
if !slices.Contains([]string{
dataio.PersistentVolumeClaimKind,
dataio.VolumeSnapshotKind,
dataio.VirtualDiskKind,
dataio.VirtualDiskSnapshotKind,
}, deObj.Spec.TargetRef.Kind) {
return "", "", "", fmt.Errorf("invalid volume kind: %s", deObj.Spec.TargetRef.Kind)
}

volumeMode = deObj.Status.VolumeMode
Expand Down
Loading