Skip to content

Commit 988d136

Browse files
authored
Merge pull request #4500 from Fedosin/gc_config_cli
Allow to configure garbage collector using clusterawsadm
2 parents 4a8a3ca + 3f9bd5b commit 988d136

File tree

5 files changed

+241
-8
lines changed

5 files changed

+241
-8
lines changed

cmd/clusterawsadm/cmd/gc/configure.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
Copyright 2023 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package gc
18+
19+
import (
20+
"fmt"
21+
"path/filepath"
22+
23+
"github.com/spf13/cobra"
24+
"k8s.io/client-go/util/homedir"
25+
26+
gcproc "sigs.k8s.io/cluster-api-provider-aws/v2/cmd/clusterawsadm/gc"
27+
"sigs.k8s.io/cluster-api/cmd/clusterctl/cmd"
28+
)
29+
30+
func newConfigureCmd() *cobra.Command {
31+
var (
32+
clusterName string
33+
namespace string
34+
kubeConfig string
35+
kubeConfigDefault string
36+
gcTasks []string
37+
)
38+
39+
if home := homedir.HomeDir(); home != "" {
40+
kubeConfigDefault = filepath.Join(home, ".kube", "config")
41+
}
42+
43+
newCmd := &cobra.Command{
44+
Use: "configure",
45+
Short: "Specify what cleanup tasks will be executed on a given cluster",
46+
Long: cmd.LongDesc(`
47+
This command will set what cleanup tasks to execute on the given cluster
48+
during garbage collection (i.e. deleting) when the cluster is
49+
requested to be deleted. Supported values: load-balancer, security-group, target-group.
50+
`),
51+
Example: cmd.Examples(`
52+
# Configure GC for a cluster to delete only load balancers and security groups using existing k8s context
53+
clusterawsadm gc configure --cluster-name=test-cluster --gc-task load-balancer --gc-task security-group
54+
55+
# Reset GC configuration for a cluster using kubeconfig
56+
clusterawsadm gc configure --cluster-name=test-cluster --kubeconfig=test.kubeconfig
57+
`),
58+
Args: cobra.NoArgs,
59+
RunE: func(cmd *cobra.Command, args []string) error {
60+
proc, err := gcproc.New(gcproc.GCInput{
61+
ClusterName: clusterName,
62+
Namespace: namespace,
63+
KubeconfigPath: kubeConfig,
64+
})
65+
if err != nil {
66+
return fmt.Errorf("creating command processor: %w", err)
67+
}
68+
69+
if err := proc.Configure(cmd.Context(), gcTasks); err != nil {
70+
return fmt.Errorf("configuring garbage collection: %w", err)
71+
}
72+
fmt.Printf("Configuring garbage collection for cluster %s/%s\n", namespace, clusterName)
73+
74+
return nil
75+
},
76+
}
77+
78+
newCmd.Flags().StringVar(&clusterName, "cluster-name", "", "The name of the CAPA cluster")
79+
newCmd.Flags().StringVarP(&namespace, "namespace", "n", "default", "The namespace for the cluster definition")
80+
newCmd.Flags().StringVar(&kubeConfig, "kubeconfig", kubeConfigDefault, "Path to the kubeconfig file to use")
81+
newCmd.Flags().StringSliceVar(&gcTasks, "gc-task", []string{}, "Garbage collection tasks to execute during cluster deletion")
82+
83+
newCmd.MarkFlagRequired("cluster-name") //nolint: errcheck
84+
85+
return newCmd
86+
}

cmd/clusterawsadm/cmd/gc/gc.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ func RootCmd() *cobra.Command {
3636

3737
newCmd.AddCommand(newEnableCmd())
3838
newCmd.AddCommand(newDisableCmd())
39+
newCmd.AddCommand(newConfigureCmd())
3940

4041
return newCmd
4142
}

cmd/clusterawsadm/gc/gc.go

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package gc
1919
import (
2020
"context"
2121
"fmt"
22+
"strings"
2223

2324
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2425
"k8s.io/apimachinery/pkg/runtime"
@@ -104,7 +105,7 @@ func New(input GCInput, opts ...CmdProcessorOption) (*CmdProcessor, error) {
104105

105106
// Enable is used to enable external resource garbage collection for a cluster.
106107
func (c *CmdProcessor) Enable(ctx context.Context) error {
107-
if err := c.setAnnotationAndPatch(ctx, "true"); err != nil {
108+
if err := c.setAnnotationAndPatch(ctx, infrav1.ExternalResourceGCAnnotation, "true"); err != nil {
108109
return fmt.Errorf("setting gc annotation to true: %w", err)
109110
}
110111

@@ -113,14 +114,42 @@ func (c *CmdProcessor) Enable(ctx context.Context) error {
113114

114115
// Disable is used to disable external resource garbage collection for a cluster.
115116
func (c *CmdProcessor) Disable(ctx context.Context) error {
116-
if err := c.setAnnotationAndPatch(ctx, "false"); err != nil {
117+
if err := c.setAnnotationAndPatch(ctx, infrav1.ExternalResourceGCAnnotation, "false"); err != nil {
117118
return fmt.Errorf("setting gc annotation to false: %w", err)
118119
}
119120

120121
return nil
121122
}
122123

123-
func (c *CmdProcessor) setAnnotationAndPatch(ctx context.Context, annotationValue string) error {
124+
// Configure is used to configure external resource garbage collection for a cluster.
125+
func (c *CmdProcessor) Configure(ctx context.Context, gcTasks []string) error {
126+
supportedGCTasks := []infrav1.GCTask{infrav1.GCTaskLoadBalancer, infrav1.GCTaskTargetGroup, infrav1.GCTaskSecurityGroup}
127+
128+
for _, gcTask := range gcTasks {
129+
found := false
130+
131+
for _, supportedGCTask := range supportedGCTasks {
132+
if gcTask == string(supportedGCTask) {
133+
found = true
134+
break
135+
}
136+
}
137+
138+
if !found {
139+
return fmt.Errorf("unsupported gc task: %s", gcTask)
140+
}
141+
}
142+
143+
annotationValue := strings.Join(gcTasks, ",")
144+
145+
if err := c.setAnnotationAndPatch(ctx, infrav1.ExternalResourceGCTasksAnnotation, annotationValue); err != nil {
146+
return fmt.Errorf("setting gc tasks annotation to %s: %w", annotationValue, err)
147+
}
148+
149+
return nil
150+
}
151+
152+
func (c *CmdProcessor) setAnnotationAndPatch(ctx context.Context, annotationName, annotationValue string) error {
124153
infraObj, err := c.getInfraCluster(ctx)
125154
if err != nil {
126155
return err
@@ -131,7 +160,11 @@ func (c *CmdProcessor) setAnnotationAndPatch(ctx context.Context, annotationValu
131160
return fmt.Errorf("creating patch helper: %w", err)
132161
}
133162

134-
annotations.Set(infraObj, infrav1.ExternalResourceGCAnnotation, annotationValue)
163+
if annotationValue != "" {
164+
annotations.Set(infraObj, annotationName, annotationValue)
165+
} else {
166+
annotations.Delete(infraObj, annotationName)
167+
}
135168

136169
if err := patchHelper.Patch(ctx, infraObj); err != nil {
137170
return fmt.Errorf("patching infra cluster with gc annotation: %w", err)

cmd/clusterawsadm/gc/gc_test.go

Lines changed: 106 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package gc
1818

1919
import (
2020
"context"
21+
"strings"
2122
"testing"
2223

2324
. "github.com/onsi/gomega"
@@ -34,11 +35,13 @@ import (
3435
"sigs.k8s.io/cluster-api/controllers/external"
3536
)
3637

38+
const (
39+
testClusterName = "test-cluster"
40+
)
41+
3742
func TestEnableGC(t *testing.T) {
3843
RegisterTestingT(t)
3944

40-
testClusterName := "test-cluster"
41-
4245
testCases := []struct {
4346
name string
4447
clusterName string
@@ -116,8 +119,6 @@ func TestEnableGC(t *testing.T) {
116119
func TestDisableGC(t *testing.T) {
117120
RegisterTestingT(t)
118121

119-
testClusterName := "test-cluster"
120-
121122
testCases := []struct {
122123
name string
123124
clusterName string
@@ -186,6 +187,107 @@ func TestDisableGC(t *testing.T) {
186187
}
187188
}
188189

190+
func TestConfigureGC(t *testing.T) {
191+
RegisterTestingT(t)
192+
193+
testCases := []struct {
194+
name string
195+
clusterName string
196+
gcTasks []string
197+
existingObjs []client.Object
198+
expectError bool
199+
}{
200+
{
201+
name: "no capi cluster",
202+
clusterName: testClusterName,
203+
existingObjs: []client.Object{},
204+
expectError: true,
205+
},
206+
{
207+
name: "no infra cluster",
208+
clusterName: testClusterName,
209+
existingObjs: newManagedCluster(testClusterName, true),
210+
expectError: true,
211+
},
212+
{
213+
name: "with managed control plane and no annotation",
214+
clusterName: testClusterName,
215+
existingObjs: newManagedCluster(testClusterName, false),
216+
gcTasks: []string{"load-balancer", "target-group"},
217+
expectError: false,
218+
},
219+
{
220+
name: "with awscluster and no annotation",
221+
clusterName: testClusterName,
222+
existingObjs: newUnManagedCluster(testClusterName, false),
223+
gcTasks: []string{"load-balancer", "security-group"},
224+
expectError: false,
225+
},
226+
{
227+
name: "with managed control plane and with annotation",
228+
clusterName: testClusterName,
229+
existingObjs: newManagedClusterWithAnnotations(testClusterName, map[string]string{infrav1.ExternalResourceGCTasksAnnotation: "security-group"}),
230+
gcTasks: []string{"load-balancer", "target-group"},
231+
expectError: false,
232+
},
233+
{
234+
name: "with awscluster and with annotation",
235+
clusterName: testClusterName,
236+
existingObjs: newUnManagedClusterWithAnnotations(testClusterName, map[string]string{infrav1.ExternalResourceGCTasksAnnotation: "security-group"}),
237+
gcTasks: []string{"load-balancer", "target-group"},
238+
expectError: false,
239+
},
240+
{
241+
name: "with awscluster and invalid gc tasks",
242+
clusterName: testClusterName,
243+
existingObjs: newUnManagedCluster(testClusterName, false),
244+
gcTasks: []string{"load-balancer", "INVALID"},
245+
expectError: true,
246+
},
247+
}
248+
249+
for _, tc := range testCases {
250+
t.Run(tc.name, func(t *testing.T) {
251+
g := NewWithT(t)
252+
253+
input := GCInput{
254+
ClusterName: tc.clusterName,
255+
Namespace: "default",
256+
}
257+
258+
fake := newFakeClient(scheme, tc.existingObjs...)
259+
ctx := context.TODO()
260+
261+
proc, err := New(input, WithClient(fake))
262+
g.Expect(err).NotTo(HaveOccurred())
263+
264+
resErr := proc.Configure(ctx, tc.gcTasks)
265+
if tc.expectError {
266+
g.Expect(resErr).To(HaveOccurred())
267+
return
268+
}
269+
g.Expect(resErr).NotTo(HaveOccurred())
270+
271+
cluster := tc.existingObjs[0].(*clusterv1.Cluster)
272+
ref := cluster.Spec.InfrastructureRef
273+
274+
obj, err := external.Get(ctx, fake, ref, "default")
275+
g.Expect(err).NotTo(HaveOccurred())
276+
g.Expect(obj).NotTo(BeNil())
277+
278+
expected := strings.Join(tc.gcTasks, ",")
279+
annotationVal, hasAnnotation := annotations.Get(obj, infrav1.ExternalResourceGCTasksAnnotation)
280+
281+
if expected != "" {
282+
g.Expect(hasAnnotation).To(BeTrue())
283+
g.Expect(annotationVal).To(Equal(expected))
284+
} else {
285+
g.Expect(hasAnnotation).To(BeFalse())
286+
}
287+
})
288+
}
289+
}
290+
189291
func newFakeClient(scheme *runtime.Scheme, objs ...client.Object) client.Client {
190292
return fake.NewClientBuilder().WithScheme(scheme).WithObjects(objs...).Build()
191293
}

pkg/annotations/annotations.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,17 @@ func Get(obj metav1.Object, name string) (value string, found bool) {
4242
return
4343
}
4444

45+
// Delete will delete the supplied annotation.
46+
func Delete(obj metav1.Object, name string) {
47+
annotations := obj.GetAnnotations()
48+
if len(annotations) == 0 {
49+
return
50+
}
51+
52+
delete(annotations, name)
53+
obj.SetAnnotations(annotations)
54+
}
55+
4556
// Has returns true if the supplied object has the supplied annotation.
4657
func Has(obj metav1.Object, name string) bool {
4758
annotations := obj.GetAnnotations()

0 commit comments

Comments
 (0)