Skip to content

Commit c46713d

Browse files
committed
feat(service): Introduce utility subcommand to drain services
Signed-off-by: Alexander Jung <alex@unikraft.io>
1 parent 418cfca commit c46713d

File tree

2 files changed

+166
-0
lines changed

2 files changed

+166
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// SPDX-License-Identifier: BSD-3-Clause
2+
// Copyright (c) 2025, Unikraft GmbH and The KraftKit Authors.
3+
// Licensed under the BSD-3-Clause License (the "License").
4+
// You may not use this file except in compliance with the License.
5+
6+
package drain
7+
8+
import (
9+
"context"
10+
"fmt"
11+
12+
"github.com/MakeNowJust/heredoc"
13+
"github.com/spf13/cobra"
14+
15+
kraftcloud "sdk.kraft.cloud"
16+
kcclient "sdk.kraft.cloud/client"
17+
18+
"kraftkit.sh/cmdfactory"
19+
"kraftkit.sh/config"
20+
"kraftkit.sh/internal/cli/kraft/cloud/utils"
21+
"kraftkit.sh/log"
22+
"kraftkit.sh/tui/processtree"
23+
)
24+
25+
type DrainOptions struct {
26+
Auth *config.AuthConfig `noattribute:"true"`
27+
Client kraftcloud.KraftCloud `noattribute:"true"`
28+
Metro string `noattribute:"true"`
29+
Token string `noattribute:"true"`
30+
}
31+
32+
func NewCmd() *cobra.Command {
33+
cmd, err := cmdfactory.New(&DrainOptions{}, cobra.Command{
34+
Short: "Drain instances in a service",
35+
Use: "drain [FLAGS] [NAME|UUID [NAME|UUID]...]",
36+
Args: cobra.ArbitraryArgs,
37+
Aliases: []string{"delete", "del", "rm"},
38+
Example: heredoc.Doc(`
39+
# Drain a service from your account by UUID.
40+
$ kraft cloud service drain fd1684ea-7970-4994-92d6-61dcc7905f2b
41+
42+
# Drain a service from your account by name.
43+
$ kraft cloud service drain my-service
44+
45+
# Drain multiple service from your account.
46+
$ kraft cloud service drain my-service my-other-service
47+
`),
48+
Annotations: map[string]string{
49+
cmdfactory.AnnotationHelpGroup: "kraftcloud-service",
50+
},
51+
})
52+
if err != nil {
53+
panic(err)
54+
}
55+
56+
return cmd
57+
}
58+
59+
func (opts *DrainOptions) Pre(cmd *cobra.Command, args []string) error {
60+
if err := utils.PopulateMetroToken(cmd, &opts.Metro, &opts.Token); err != nil {
61+
return fmt.Errorf("could not populate metro and token: %w", err)
62+
}
63+
64+
return nil
65+
}
66+
67+
func (opts *DrainOptions) Run(ctx context.Context, args []string) error {
68+
return Drain(ctx, opts, args...)
69+
}
70+
71+
func Drain(ctx context.Context, opts *DrainOptions, args ...string) error {
72+
var err error
73+
74+
if opts.Auth == nil {
75+
opts.Auth, err = config.GetKraftCloudAuthConfig(ctx, opts.Token)
76+
if err != nil {
77+
return fmt.Errorf("could not retrieve credentials: %w", err)
78+
}
79+
}
80+
81+
if opts.Client == nil {
82+
opts.Client = kraftcloud.NewClient(
83+
kraftcloud.WithToken(config.GetKraftCloudTokenAuthConfig(*opts.Auth)),
84+
)
85+
}
86+
87+
var processes []*processtree.ProcessTreeItem
88+
89+
for _, service := range args {
90+
processes = append(processes,
91+
processtree.NewProcessTreeItem(
92+
fmt.Sprintf("draining %s", service),
93+
"",
94+
func(ctx context.Context) error {
95+
serviceResp, err := opts.Client.Services().WithMetro(opts.Metro).Get(ctx, service)
96+
if err != nil {
97+
return fmt.Errorf("could not get service: %w", err)
98+
}
99+
100+
sg, err := serviceResp.FirstOrErr()
101+
if err != nil && *sg.Error == kcclient.APIHTTPErrorNotFound {
102+
return nil
103+
} else if err != nil {
104+
return err
105+
}
106+
107+
if len(sg.Instances) == 0 {
108+
return nil
109+
}
110+
111+
var instances []string
112+
for _, instance := range sg.Instances {
113+
instances = append(instances, instance.UUID)
114+
}
115+
116+
log.G(ctx).Infof("deleting %d instances...", len(instances))
117+
118+
if _, err := opts.Client.Instances().WithMetro(opts.Metro).Delete(ctx, instances...); err != nil {
119+
return err
120+
}
121+
122+
// Wait until the instances are deleted
123+
for {
124+
resp, err := opts.Client.Instances().WithMetro(opts.Metro).Get(ctx, instances...)
125+
if err != nil {
126+
break
127+
}
128+
129+
noError := true
130+
for _, entry := range resp.Data.Entries {
131+
if entry.Status != "error" {
132+
noError = false
133+
}
134+
}
135+
136+
if noError {
137+
break
138+
}
139+
}
140+
141+
return err
142+
},
143+
),
144+
)
145+
}
146+
147+
treemodel, err := processtree.NewProcessTree(
148+
ctx,
149+
[]processtree.ProcessTreeOption{
150+
processtree.IsParallel(true),
151+
processtree.WithRenderer(
152+
log.LoggerTypeFromString(config.G[config.KraftKit](ctx).Log.Type) != log.FANCY,
153+
),
154+
processtree.WithFailFast(true),
155+
processtree.WithHideOnSuccess(true),
156+
},
157+
processes...,
158+
)
159+
if err != nil {
160+
return err
161+
}
162+
163+
return treemodel.Start()
164+
}

internal/cli/kraft/cloud/service/service.go

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/spf13/pflag"
1414

1515
"kraftkit.sh/internal/cli/kraft/cloud/service/create"
16+
"kraftkit.sh/internal/cli/kraft/cloud/service/drain"
1617
"kraftkit.sh/internal/cli/kraft/cloud/service/get"
1718
"kraftkit.sh/internal/cli/kraft/cloud/service/list"
1819
"kraftkit.sh/internal/cli/kraft/cloud/service/logs"
@@ -43,6 +44,7 @@ func NewCmd() *cobra.Command {
4344
}
4445

4546
cmd.AddCommand(create.NewCmd())
47+
cmd.AddCommand(drain.NewCmd())
4648
cmd.AddCommand(list.NewCmd())
4749
cmd.AddCommand(get.NewCmd())
4850
cmd.AddCommand(logs.NewCmd())

0 commit comments

Comments
 (0)