Skip to content

Commit 3b97c67

Browse files
Added NAR cli commands
1 parent b810ffd commit 3b97c67

File tree

8 files changed

+1606
-1
lines changed

8 files changed

+1606
-1
lines changed

cmd/non-admin/nonadmin.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package nonadmin
1818

1919
import (
2020
"github.com/migtools/oadp-cli/cmd/non-admin/backup"
21+
"github.com/migtools/oadp-cli/cmd/non-admin/restore"
2122
"github.com/spf13/cobra"
2223
"github.com/vmware-tanzu/velero/pkg/client"
2324
)
@@ -27,13 +28,16 @@ func NewNonAdminCommand(f client.Factory) *cobra.Command {
2728
c := &cobra.Command{
2829
Use: "nonadmin",
2930
Short: "Work with non-admin resources",
30-
Long: "Work with non-admin resources like backups and backup storage locations",
31+
Long: "Work with non-admin resources like backups, restores, and backup storage locations",
3132
Aliases: []string{"na"},
3233
}
3334

3435
// Add backup subcommand
3536
c.AddCommand(backup.NewBackupCommand(f))
3637

38+
// Add restore subcommand
39+
c.AddCommand(restore.NewRestoreCommand(f))
40+
3741
// Add backup storage location subcommand
3842
//c.AddCommand(bsl.NewBSLCommand(f))
3943

cmd/non-admin/restore/create.go

Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
package restore
2+
3+
/*
4+
Copyright The Velero Contributors.
5+
6+
Licensed under the Apache License, Version 2.0 (the "License");
7+
you may not use this file except in compliance with the License.
8+
You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing, software
13+
distributed under the License is distributed on an "AS IS" BASIS,
14+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
See the License for the specific language governing permissions and
16+
limitations under the License.
17+
*/
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"time"
23+
24+
"github.com/spf13/cobra"
25+
"github.com/spf13/pflag"
26+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27+
kbclient "sigs.k8s.io/controller-runtime/pkg/client"
28+
29+
"github.com/migtools/oadp-cli/cmd/shared"
30+
nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1"
31+
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
32+
"github.com/vmware-tanzu/velero/pkg/client"
33+
"github.com/vmware-tanzu/velero/pkg/cmd"
34+
"github.com/vmware-tanzu/velero/pkg/cmd/util/flag"
35+
"github.com/vmware-tanzu/velero/pkg/cmd/util/output"
36+
)
37+
38+
func NewCreateCommand(f client.Factory, use string) *cobra.Command {
39+
o := NewCreateOptions()
40+
41+
c := &cobra.Command{
42+
Use: use + " NAME --from-backup BACKUP_NAME",
43+
Short: "Create a non-admin restore",
44+
Args: cobra.MaximumNArgs(1),
45+
Run: func(c *cobra.Command, args []string) {
46+
cmd.CheckError(o.Complete(args, f))
47+
cmd.CheckError(o.Validate(c, args, f))
48+
cmd.CheckError(o.Run(c, f))
49+
},
50+
Example: ` # Create a non-admin restore from a backup in the current namespace.
51+
kubectl oadp nonadmin restore create restore1 --from-backup backup1
52+
53+
# Create a non-admin restore with specific resource types.
54+
kubectl oadp nonadmin restore create restore2 --from-backup backup1 --include-resources deployments,services
55+
56+
# Create a non-admin restore excluding certain resources.
57+
kubectl oadp nonadmin restore create restore3 --from-backup backup1 --exclude-resources secrets
58+
59+
# View the YAML for a non-admin restore without sending it to the server.
60+
kubectl oadp nonadmin restore create restore4 --from-backup backup1 -o yaml
61+
62+
# Wait for a non-admin restore to complete before returning from the command.
63+
kubectl oadp nonadmin restore create restore5 --from-backup backup1 --wait`,
64+
}
65+
66+
o.BindFlags(c.Flags())
67+
o.BindWait(c.Flags())
68+
output.BindFlags(c.Flags())
69+
output.ClearOutputFlagDefault(c)
70+
71+
return c
72+
}
73+
74+
type CreateOptions struct {
75+
Name string
76+
FromBackup string
77+
IncludeResources flag.StringArray
78+
ExcludeResources flag.StringArray
79+
IncludeClusterScopedResources flag.StringArray
80+
ExcludeClusterScopedResources flag.StringArray
81+
IncludeNamespaceScopedResources flag.StringArray
82+
ExcludeNamespaceScopedResources flag.StringArray
83+
NamespaceMapping flag.Map
84+
Labels flag.Map
85+
Annotations flag.Map
86+
Selector flag.LabelSelector
87+
OrSelector flag.OrLabelSelector
88+
IncludeClusterResources flag.OptionalBool
89+
Wait bool
90+
RestorePVs flag.OptionalBool
91+
PreserveNodePorts flag.OptionalBool
92+
ItemOperationTimeout time.Duration
93+
ExistingResourcePolicy string
94+
UploaderConfig flag.Map
95+
client kbclient.WithWatch
96+
currentNamespace string
97+
}
98+
99+
func NewCreateOptions() *CreateOptions {
100+
return &CreateOptions{
101+
IncludeResources: flag.NewStringArray("*"),
102+
Labels: flag.NewMap(),
103+
Annotations: flag.NewMap(),
104+
NamespaceMapping: flag.NewMap(),
105+
UploaderConfig: flag.NewMap(),
106+
IncludeClusterResources: flag.NewOptionalBool(nil),
107+
RestorePVs: flag.NewOptionalBool(nil),
108+
PreserveNodePorts: flag.NewOptionalBool(nil),
109+
}
110+
}
111+
112+
func (o *CreateOptions) BindFlags(flags *pflag.FlagSet) {
113+
flags.StringVar(&o.FromBackup, "from-backup", o.FromBackup, "Backup to restore from (required).")
114+
flags.Var(&o.IncludeResources, "include-resources", "Resources to include in the restore, formatted as resource.group, such as storageclasses.storage.k8s.io (use '*' for all resources). Cannot work with include-cluster-scoped-resources, exclude-cluster-scoped-resources, include-namespace-scoped-resources and exclude-namespace-scoped-resources.")
115+
flags.Var(&o.ExcludeResources, "exclude-resources", "Resources to exclude from the restore, formatted as resource.group, such as storageclasses.storage.k8s.io. Cannot work with include-cluster-scoped-resources, exclude-cluster-scoped-resources, include-namespace-scoped-resources and exclude-namespace-scoped-resources.")
116+
flags.Var(&o.IncludeClusterScopedResources, "include-cluster-scoped-resources", "Cluster-scoped resources to include in the restore, formatted as resource.group, such as storageclasses.storage.k8s.io(use '*' for all resources). Cannot work with include-resources, exclude-resources and include-cluster-resources.")
117+
flags.Var(&o.ExcludeClusterScopedResources, "exclude-cluster-scoped-resources", "Cluster-scoped resources to exclude from the restore, formatted as resource.group, such as storageclasses.storage.k8s.io(use '*' for all resources). Cannot work with include-resources, exclude-resources and include-cluster-resources.")
118+
flags.Var(&o.IncludeNamespaceScopedResources, "include-namespace-scoped-resources", "Namespaced resources to include in the restore, formatted as resource.group, such as deployments.apps(use '*' for all resources). Cannot work with include-resources, exclude-resources and include-cluster-resources.")
119+
flags.Var(&o.ExcludeNamespaceScopedResources, "exclude-namespace-scoped-resources", "Namespaced resources to exclude from the restore, formatted as resource.group, such as deployments.apps(use '*' for all resources). Cannot work with include-resources, exclude-resources and include-cluster-resources.")
120+
flags.Var(&o.Labels, "labels", "Labels to apply to the restore.")
121+
flags.Var(&o.Annotations, "annotations", "Annotations to apply to the restore.")
122+
// Note: namespace-mappings are restricted for non-admin users and therefore not exposed
123+
flags.VarP(&o.Selector, "selector", "l", "Only restore resources matching this label selector.")
124+
flags.Var(&o.OrSelector, "or-selector", "Restore resources matching at least one of the label selector from the list. Label selectors should be separated by ' or '. For example, foo=bar or app=nginx")
125+
flags.DurationVar(&o.ItemOperationTimeout, "item-operation-timeout", o.ItemOperationTimeout, "How long to wait for async plugin operations before timeout.")
126+
flags.StringVar(&o.ExistingResourcePolicy, "existing-resource-policy", "", "Policy to handle restore collisions (none, update)")
127+
flags.Var(&o.UploaderConfig, "uploader-config", "Configuration for the uploader in form key1=value1,key2=value2")
128+
129+
f := flags.VarPF(&o.IncludeClusterResources, "include-cluster-resources", "", "Include cluster-scoped resources.")
130+
f.NoOptDefVal = cmd.TRUE
131+
132+
f = flags.VarPF(&o.RestorePVs, "restore-volumes", "", "Whether to restore volumes from snapshots.")
133+
f.NoOptDefVal = cmd.TRUE
134+
135+
f = flags.VarPF(&o.PreserveNodePorts, "preserve-nodeports", "", "Whether to restore NodePort services as NodePort.")
136+
f.NoOptDefVal = cmd.TRUE
137+
}
138+
139+
func (o *CreateOptions) BindWait(flags *pflag.FlagSet) {
140+
flags.BoolVarP(&o.Wait, "wait", "w", o.Wait, "Wait for the operation to complete.")
141+
}
142+
143+
func (o *CreateOptions) Complete(args []string, f client.Factory) error {
144+
// If an explicit name is specified, use that name
145+
if len(args) > 0 {
146+
o.Name = args[0]
147+
}
148+
149+
// Create client with NonAdmin scheme
150+
client, err := shared.NewClientWithScheme(f, shared.ClientOptions{
151+
IncludeNonAdminTypes: true,
152+
})
153+
if err != nil {
154+
return err
155+
}
156+
157+
// Get the current namespace from kubeconfig instead of using factory namespace
158+
currentNS, err := shared.GetCurrentNamespace()
159+
if err != nil {
160+
return fmt.Errorf("failed to determine current namespace: %w", err)
161+
}
162+
163+
o.client = client
164+
o.currentNamespace = currentNS
165+
return nil
166+
}
167+
168+
func (o *CreateOptions) Validate(c *cobra.Command, args []string, f client.Factory) error {
169+
if len(args) < 1 {
170+
return fmt.Errorf("restore name is required")
171+
}
172+
173+
if o.FromBackup == "" {
174+
return fmt.Errorf("--from-backup is required")
175+
}
176+
177+
if o.Name == "" {
178+
o.Name = args[0]
179+
}
180+
181+
return nil
182+
}
183+
184+
func (o *CreateOptions) Run(c *cobra.Command, f client.Factory) error {
185+
if printed, err := output.PrintWithFormat(c, o.buildRestore()); printed || err != nil {
186+
return err
187+
}
188+
189+
restore := o.buildRestore()
190+
191+
if err := o.client.Create(context.Background(), restore); err != nil {
192+
return err
193+
}
194+
195+
fmt.Printf("NonAdminRestore %q created successfully.\n", restore.Name)
196+
197+
if o.Wait {
198+
return o.waitForRestore(restore)
199+
}
200+
201+
return nil
202+
}
203+
204+
func (o *CreateOptions) buildRestore() *nacv1alpha1.NonAdminRestore {
205+
// Create a Velero RestoreSpec
206+
restoreSpec := &velerov1api.RestoreSpec{
207+
BackupName: o.FromBackup,
208+
}
209+
210+
// Add resource filters
211+
if len(o.IncludeResources) > 0 {
212+
restoreSpec.IncludedResources = o.IncludeResources
213+
}
214+
if len(o.ExcludeResources) > 0 {
215+
restoreSpec.ExcludedResources = o.ExcludeResources
216+
}
217+
218+
// Note: Namespace mappings are restricted for non-admin users and therefore not processed
219+
220+
// Add selectors
221+
if o.Selector.LabelSelector != nil {
222+
restoreSpec.LabelSelector = o.Selector.LabelSelector
223+
}
224+
if len(o.OrSelector.OrLabelSelectors) > 0 {
225+
restoreSpec.OrLabelSelectors = o.OrSelector.OrLabelSelectors
226+
}
227+
228+
// Add optional settings
229+
if o.IncludeClusterResources.Value != nil {
230+
restoreSpec.IncludeClusterResources = o.IncludeClusterResources.Value
231+
}
232+
if o.RestorePVs.Value != nil {
233+
restoreSpec.RestorePVs = o.RestorePVs.Value
234+
}
235+
if o.PreserveNodePorts.Value != nil {
236+
restoreSpec.PreserveNodePorts = o.PreserveNodePorts.Value
237+
}
238+
if o.ItemOperationTimeout > 0 {
239+
restoreSpec.ItemOperationTimeout = metav1.Duration{Duration: o.ItemOperationTimeout}
240+
}
241+
if o.ExistingResourcePolicy != "" {
242+
policy := velerov1api.PolicyType(o.ExistingResourcePolicy)
243+
restoreSpec.ExistingResourcePolicy = policy
244+
}
245+
if o.UploaderConfig.Data() != nil && len(o.UploaderConfig.Data()) > 0 {
246+
restoreSpec.UploaderConfig = &velerov1api.UploaderConfigForRestore{}
247+
// Note: UploaderConfigForRestore fields would be set here based on the map values
248+
// The exact field structure depends on the Velero version being used
249+
}
250+
251+
// Create NonAdminRestore using the builder
252+
restore := ForNonAdminRestore(o.currentNamespace, o.Name).
253+
ObjectMeta(
254+
WithLabelsMap(o.Labels.Data()),
255+
WithAnnotationsMap(o.Annotations.Data()),
256+
).
257+
RestoreSpec(nacv1alpha1.NonAdminRestoreSpec{
258+
RestoreSpec: restoreSpec,
259+
}).
260+
Result()
261+
262+
return restore
263+
}
264+
265+
func (o *CreateOptions) waitForRestore(restore *nacv1alpha1.NonAdminRestore) error {
266+
fmt.Printf("Waiting for restore %s to complete...\n", restore.Name)
267+
268+
// TODO: Implement proper wait functionality
269+
// For now, just poll the restore status periodically
270+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
271+
defer cancel()
272+
273+
ticker := time.NewTicker(5 * time.Second)
274+
defer ticker.Stop()
275+
276+
for {
277+
select {
278+
case <-ctx.Done():
279+
return fmt.Errorf("timeout waiting for restore to complete")
280+
case <-ticker.C:
281+
// Get current restore status
282+
currentRestore := &nacv1alpha1.NonAdminRestore{}
283+
err := o.client.Get(ctx, kbclient.ObjectKey{
284+
Namespace: restore.Namespace,
285+
Name: restore.Name,
286+
}, currentRestore)
287+
if err != nil {
288+
return fmt.Errorf("failed to get restore status: %w", err)
289+
}
290+
291+
phase := currentRestore.Status.Phase
292+
fmt.Printf("Restore %s status: %s\n", restore.Name, phase)
293+
294+
// Check if completed (using generic NonAdminPhase constants)
295+
if phase == nacv1alpha1.NonAdminPhaseCreated {
296+
fmt.Printf("Restore %s completed successfully.\n", restore.Name)
297+
return nil
298+
}
299+
// Add other phase checks as needed
300+
}
301+
}
302+
}

0 commit comments

Comments
 (0)