Skip to content

Commit f4bdec0

Browse files
omerdemirokactions-user
authored andcommitted
feat: GCP compute node template adapter (#1432)
Closes #1428 - Implement searchable for NodeGroup to support backlinks from node templates - Add node template adapter and integration test GitOrigin-RevId: c774f6790ff49abca79a00fcb315ade8ced26923
1 parent 8fa83ec commit f4bdec0

9 files changed

+656
-26
lines changed

sources/gcp/adapters/compute-node-group.go

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66

77
"cloud.google.com/go/compute/apiv1/computepb"
88
"google.golang.org/api/iterator"
9+
"k8s.io/utils/ptr"
910

1011
"github.com/overmindtech/cli/sdp-go"
1112
"github.com/overmindtech/cli/sources"
@@ -16,7 +17,8 @@ import (
1617
var (
1718
ComputeNodeGroup = shared.NewItemType(gcpshared.GCP, gcpshared.Compute, gcpshared.NodeGroup)
1819

19-
ComputeNodeGroupLookupByName = shared.NewItemTypeLookup("name", ComputeNodeGroup)
20+
ComputeNodeGroupLookupByName = shared.NewItemTypeLookup("name", ComputeNodeGroup)
21+
ComputeNodeGroupLookupByNodeTemplateName = shared.NewItemTypeLookup("nodeTemplateName", ComputeNodeGroup)
2022
)
2123

2224
type computeNodeGroupWrapper struct {
@@ -25,7 +27,7 @@ type computeNodeGroupWrapper struct {
2527
}
2628

2729
// NewComputeNodeGroup creates a new computeNodeGroupWrapper instance
28-
func NewComputeNodeGroup(client gcpshared.ComputeNodeGroupClient, projectID, zone string) sources.ListableWrapper {
30+
func NewComputeNodeGroup(client gcpshared.ComputeNodeGroupClient, projectID, zone string) sources.SearchableListableWrapper {
2931
return &computeNodeGroupWrapper{
3032
client: client,
3133
ZoneBase: gcpshared.NewZoneBase(
@@ -51,6 +53,10 @@ func (c computeNodeGroupWrapper) TerraformMappings() []*sdp.TerraformMapping {
5153
TerraformMethod: sdp.QueryMethod_GET,
5254
TerraformQueryMap: "google_compute_node_group.name",
5355
},
56+
{
57+
TerraformMethod: sdp.QueryMethod_SEARCH,
58+
TerraformQueryMap: "google_compute_node_template.name",
59+
},
5460
}
5561
}
5662

@@ -61,6 +67,14 @@ func (c computeNodeGroupWrapper) GetLookups() sources.ItemTypeLookups {
6167
}
6268
}
6369

70+
func (c computeNodeGroupWrapper) SearchLookups() []sources.ItemTypeLookups {
71+
return []sources.ItemTypeLookups{
72+
{
73+
ComputeNodeGroupLookupByNodeTemplateName,
74+
},
75+
}
76+
}
77+
6478
// Get retrieves a compute node group by its name
6579
func (c computeNodeGroupWrapper) Get(ctx context.Context, queryParts ...string) (*sdp.Item, *sdp.QueryError) {
6680
req := &computepb.GetNodeGroupRequest{
@@ -114,6 +128,40 @@ func (c computeNodeGroupWrapper) List(ctx context.Context) ([]*sdp.Item, *sdp.Qu
114128
return items, nil
115129
}
116130

131+
// Search Currently supports a node template query.
132+
func (c computeNodeGroupWrapper) Search(ctx context.Context, queryParts ...string) ([]*sdp.Item, *sdp.QueryError) {
133+
// Supported search for now is by node template
134+
nodeTemplate := queryParts[0]
135+
136+
req := &computepb.ListNodeGroupsRequest{
137+
Project: c.ProjectID(),
138+
Zone: c.Zone(),
139+
Filter: ptr.To("nodeTemplate = " + nodeTemplate),
140+
}
141+
142+
it := c.client.List(ctx, req)
143+
144+
var items []*sdp.Item
145+
for {
146+
nodegroup, err := it.Next()
147+
if errors.Is(err, iterator.Done) {
148+
break
149+
}
150+
if err != nil {
151+
return nil, gcpshared.QueryError(err)
152+
}
153+
154+
item, sdpErr := c.gcpComputeNodeGroupToSDPItem(nodegroup)
155+
if sdpErr != nil {
156+
return nil, sdpErr
157+
}
158+
159+
items = append(items, item)
160+
}
161+
162+
return items, nil
163+
}
164+
117165
func (c computeNodeGroupWrapper) gcpComputeNodeGroupToSDPItem(nodegroup *computepb.NodeGroup) (*sdp.Item, *sdp.QueryError) {
118166
attributes, err := shared.ToAttributesWithExclude(nodegroup)
119167
if err != nil {

sources/gcp/adapters/compute-node-group_test.go

Lines changed: 75 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package adapters_test
22

33
import (
44
"context"
5+
"strings"
56
"testing"
67

78
"cloud.google.com/go/compute/apiv1/computepb"
@@ -25,10 +26,13 @@ func TestComputeNodeGroup(t *testing.T) {
2526
projectID := "test-project-id"
2627
zone := "us-central1-a"
2728

29+
testTemplateUrl := "https://www.googleapis.com/compute/v1/projects/test-project/regions/northamerica-northeast1/nodeTemplates/node-template-1"
30+
testTemplateUrl2 := "https://www.googleapis.com/compute/v1/projects/test-project/regions/northamerica-northeast1/nodeTemplates/node-template-2"
31+
2832
t.Run("Get", func(t *testing.T) {
2933
wrapper := adapters.NewComputeNodeGroup(mockClient, projectID, zone)
3034

31-
mockClient.EXPECT().Get(ctx, gomock.Any()).Return(createComputeNodeGroup("test-node-group", computepb.NodeGroup_READY), nil)
35+
mockClient.EXPECT().Get(ctx, gomock.Any()).Return(createComputeNodeGroup("test-node-group", testTemplateUrl, computepb.NodeGroup_READY), nil)
3236

3337
adapter := sources.WrapperToAdapter(wrapper)
3438

@@ -90,7 +94,7 @@ func TestComputeNodeGroup(t *testing.T) {
9094
wrapper := adapters.NewComputeNodeGroup(mockClient, projectID, zone)
9195
adapter := sources.WrapperToAdapter(wrapper)
9296

93-
mockClient.EXPECT().Get(ctx, gomock.Any()).Return(createComputeNodeGroup("test-ng", tc.input), nil)
97+
mockClient.EXPECT().Get(ctx, gomock.Any()).Return(createComputeNodeGroup("test-ng", "test-temp", tc.input), nil)
9498

9599
sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], "test-node-group", true)
96100
if qErr != nil {
@@ -112,8 +116,8 @@ func TestComputeNodeGroup(t *testing.T) {
112116
mockComputeIterator := mocks.NewMockComputeNodeGroupIterator(ctrl)
113117

114118
// add mock implementation here
115-
mockComputeIterator.EXPECT().Next().Return(createComputeNodeGroup("test-node-group-1", computepb.NodeGroup_READY), nil)
116-
mockComputeIterator.EXPECT().Next().Return(createComputeNodeGroup("test-node-group-2", computepb.NodeGroup_READY), nil)
119+
mockComputeIterator.EXPECT().Next().Return(createComputeNodeGroup("test-node-group-1", testTemplateUrl, computepb.NodeGroup_READY), nil)
120+
mockComputeIterator.EXPECT().Next().Return(createComputeNodeGroup("test-node-group-2", testTemplateUrl2, computepb.NodeGroup_READY), nil)
117121
mockComputeIterator.EXPECT().Next().Return(nil, iterator.Done)
118122

119123
// Mock the List method
@@ -133,17 +137,80 @@ func TestComputeNodeGroup(t *testing.T) {
133137
t.Fatalf("Expected no validation error, got: %v", item.Validate())
134138
}
135139

136-
if item.GetLinkedItemQueries()[0].GetQuery().GetQuery() != "node-template-1" {
137-
t.Fatalf("Expected node-template-1 as query, got: %s", item.GetTags()["env"])
140+
query := item.GetLinkedItemQueries()[0].GetQuery().GetQuery()
141+
if !strings.Contains(query, "node-template") {
142+
t.Fatalf("Expected node-template in query, got: %s", query)
143+
}
144+
}
145+
})
146+
147+
t.Run("Search", func(t *testing.T) {
148+
wrapper := adapters.NewComputeNodeGroup(mockClient, projectID, zone)
149+
adapter := sources.WrapperToAdapter(wrapper)
150+
151+
filterBy := testTemplateUrl
152+
153+
// Mock the List method
154+
mockClient.EXPECT().List(ctx, gomock.Any()).DoAndReturn(func(ctx context.Context, req *computepb.ListNodeGroupsRequest, opts ...any) *mocks.MockComputeNodeGroupIterator {
155+
fullList := []*computepb.NodeGroup{
156+
createComputeNodeGroup("test-node-group-1", testTemplateUrl, computepb.NodeGroup_READY),
157+
createComputeNodeGroup("test-node-group-2", testTemplateUrl2, computepb.NodeGroup_READY),
158+
createComputeNodeGroup("test-node-group-3", testTemplateUrl, computepb.NodeGroup_READY),
159+
createComputeNodeGroup("test-node-group-4", testTemplateUrl, computepb.NodeGroup_READY),
160+
}
161+
162+
expectedFilter := "nodeTemplate = " + filterBy
163+
if req.GetFilter() != expectedFilter {
164+
t.Fatalf("Expected filter to be %s, got: %s", expectedFilter, req.GetFilter())
165+
}
166+
167+
mockComputeIterator := mocks.NewMockComputeNodeGroupIterator(ctrl)
168+
for _, nodeGroup := range fullList {
169+
if nodeGroup.GetNodeTemplate() == filterBy {
170+
mockComputeIterator.EXPECT().Next().Return(nodeGroup, nil)
171+
}
172+
}
173+
174+
mockComputeIterator.EXPECT().Next().Return(nil, iterator.Done)
175+
176+
return mockComputeIterator
177+
})
178+
179+
// [SPEC] Search filters by the node template URL. It will list and filter out
180+
// any node groups that are not using the given URL.
181+
182+
sdpItems, err := adapter.Search(ctx, wrapper.Scopes()[0], testTemplateUrl, true)
183+
if err != nil {
184+
t.Fatalf("Expected no error, got: %v", err)
185+
}
186+
187+
// 1 of 4 are filtered out.
188+
if len(sdpItems) != 3 {
189+
t.Fatalf("Expected 3 items, got: %d", len(sdpItems))
190+
}
191+
192+
for _, item := range sdpItems {
193+
if item.Validate() != nil {
194+
t.Fatalf("Expected no validation error, got: %v", item.Validate())
195+
}
196+
197+
attributes := item.GetAttributes()
198+
nodeTemplate, err := attributes.Get("node_template")
199+
if err != nil {
200+
t.Fatalf("Failed to get node_template attribute: %v", err)
201+
}
202+
203+
if nodeTemplate != testTemplateUrl {
204+
t.Fatalf("Expected node_template to be %s, got: %s", testTemplateUrl, nodeTemplate)
138205
}
139206
}
140207
})
141208
}
142209

143-
func createComputeNodeGroup(name string, status computepb.NodeGroup_Status) *computepb.NodeGroup {
210+
func createComputeNodeGroup(name, templateUrl string, status computepb.NodeGroup_Status) *computepb.NodeGroup {
144211
return &computepb.NodeGroup{
145212
Name: ptr.To(name),
146-
NodeTemplate: ptr.To("https://www.googleapis.com/compute/v1/projects/test-project/regions/northamerica-northeast1/nodeTemplates/node-template-1"),
213+
NodeTemplate: ptr.To(templateUrl),
147214
Status: ptr.To(status.String()),
148215
}
149216
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package adapters
2+
3+
import (
4+
"context"
5+
"errors"
6+
7+
"cloud.google.com/go/compute/apiv1/computepb"
8+
"google.golang.org/api/iterator"
9+
10+
"github.com/overmindtech/cli/sdp-go"
11+
"github.com/overmindtech/cli/sources"
12+
gcpshared "github.com/overmindtech/cli/sources/gcp/shared"
13+
"github.com/overmindtech/cli/sources/shared"
14+
)
15+
16+
var (
17+
ComputeNodeTemplate = shared.NewItemType(gcpshared.GCP, gcpshared.Compute, gcpshared.NodeTemplate)
18+
19+
ComputeNodeTemplateLookupByName = shared.NewItemTypeLookup("name", ComputeNodeTemplate)
20+
)
21+
22+
type computeNodeTemplateWrapper struct {
23+
client gcpshared.ComputeNodeTemplateClient
24+
25+
*gcpshared.RegionBase
26+
}
27+
28+
// NewComputeNodeTemplate creates a new computeNodeTemplateWrapper instance.
29+
func NewComputeNodeTemplate(client gcpshared.ComputeNodeTemplateClient, projectID, region string) sources.ListableWrapper {
30+
return &computeNodeTemplateWrapper{
31+
client: client,
32+
RegionBase: gcpshared.NewRegionBase(
33+
projectID,
34+
region,
35+
sdp.AdapterCategory_ADAPTER_CATEGORY_CONFIGURATION,
36+
ComputeNodeTemplate,
37+
),
38+
}
39+
}
40+
41+
func (c computeNodeTemplateWrapper) PotentialLinks() map[shared.ItemType]bool {
42+
return shared.NewItemTypesSet(
43+
ComputeNodeGroup,
44+
)
45+
}
46+
47+
func (c computeNodeTemplateWrapper) TerraformMappings() []*sdp.TerraformMapping {
48+
return []*sdp.TerraformMapping{
49+
{
50+
TerraformMethod: sdp.QueryMethod_GET,
51+
TerraformQueryMap: "google_compute_node_template.name",
52+
},
53+
}
54+
}
55+
56+
func (c computeNodeTemplateWrapper) GetLookups() sources.ItemTypeLookups {
57+
return sources.ItemTypeLookups{
58+
ComputeNodeTemplateLookupByName,
59+
}
60+
}
61+
62+
func (c computeNodeTemplateWrapper) Get(ctx context.Context, queryParts ...string) (*sdp.Item, *sdp.QueryError) {
63+
req := &computepb.GetNodeTemplateRequest{
64+
Project: c.ProjectID(),
65+
Region: c.Region(),
66+
NodeTemplate: queryParts[0],
67+
}
68+
69+
nodeTemplate, err := c.client.Get(ctx, req)
70+
if err != nil {
71+
return nil, gcpshared.QueryError(err)
72+
}
73+
74+
var sdpErr *sdp.QueryError
75+
var item *sdp.Item
76+
item, sdpErr = c.gcpComputeNodeTemplateToSDPItem(nodeTemplate)
77+
if sdpErr != nil {
78+
return nil, sdpErr
79+
}
80+
81+
return item, nil
82+
}
83+
84+
func (c computeNodeTemplateWrapper) List(ctx context.Context) ([]*sdp.Item, *sdp.QueryError) {
85+
results := c.client.List(ctx, &computepb.ListNodeTemplatesRequest{
86+
Project: c.ProjectID(),
87+
Region: c.Region(),
88+
})
89+
90+
var items []*sdp.Item
91+
for {
92+
nodeTemplate, err := results.Next()
93+
if errors.Is(err, iterator.Done) {
94+
break
95+
}
96+
97+
if err != nil {
98+
return nil, gcpshared.QueryError(err)
99+
}
100+
101+
var sdpErr *sdp.QueryError
102+
var item *sdp.Item
103+
item, sdpErr = c.gcpComputeNodeTemplateToSDPItem(nodeTemplate)
104+
if sdpErr != nil {
105+
return nil, sdpErr
106+
}
107+
108+
items = append(items, item)
109+
}
110+
111+
return items, nil
112+
}
113+
114+
func (c computeNodeTemplateWrapper) gcpComputeNodeTemplateToSDPItem(nodeTemplate *computepb.NodeTemplate) (*sdp.Item, *sdp.QueryError) {
115+
attributes, err := shared.ToAttributesWithExclude(nodeTemplate)
116+
if err != nil {
117+
return nil, &sdp.QueryError{
118+
ErrorType: sdp.QueryError_OTHER,
119+
ErrorString: err.Error(),
120+
}
121+
}
122+
123+
sdpItem := &sdp.Item{
124+
Type: ComputeNodeTemplate.String(),
125+
UniqueAttribute: "name",
126+
Attributes: attributes,
127+
Scope: c.DefaultScope(),
128+
// No labels
129+
}
130+
131+
// Backlink to any node group using this template.
132+
// TODO: Revisit this link when working on this issue:
133+
// https://linear.app/overmind/issue/ENG-404/investigate-how-to-create-backlinks-without-the-location-information
134+
sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{
135+
Query: &sdp.Query{
136+
Type: ComputeNodeGroup.String(),
137+
Method: sdp.QueryMethod_SEARCH,
138+
Query: nodeTemplate.GetName(),
139+
Scope: "*",
140+
},
141+
142+
BlastPropagation: &sdp.BlastPropagation{
143+
In: false,
144+
Out: true,
145+
},
146+
})
147+
148+
return sdpItem, nil
149+
}

0 commit comments

Comments
 (0)