Skip to content

Commit 253330b

Browse files
omerdemirokactions-user
authored andcommitted
feat: GCP NodeGroup adapter (#1427)
Closes #1302 - Added NodeGroup adapter. GitOrigin-RevId: 24829366f8fe7f0533976b470151e3c750304678
1 parent fb08fd7 commit 253330b

File tree

8 files changed

+742
-2
lines changed

8 files changed

+742
-2
lines changed

sources/example/mocks/mock_external_api_client.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
package adapters
2+
3+
import (
4+
"cloud.google.com/go/compute/apiv1/computepb"
5+
"context"
6+
"errors"
7+
"google.golang.org/api/iterator"
8+
9+
"github.com/overmindtech/cli/sdp-go"
10+
"github.com/overmindtech/cli/sources"
11+
gcpshared "github.com/overmindtech/cli/sources/gcp/shared"
12+
"github.com/overmindtech/cli/sources/shared"
13+
)
14+
15+
var (
16+
ComputeNodeGroup = shared.NewItemType(gcpshared.GCP, gcpshared.Compute, gcpshared.NodeGroup)
17+
18+
ComputeNodeGroupLookupByName = shared.NewItemTypeLookup("name", ComputeNodeGroup)
19+
)
20+
21+
type computeNodeGroupWrapper struct {
22+
client gcpshared.ComputeNodeGroupClient
23+
*gcpshared.ZoneBase
24+
}
25+
26+
// NewComputeNodeGroup creates a new computeNodeGroupWrapper instance
27+
func NewComputeNodeGroup(client gcpshared.ComputeNodeGroupClient, projectID, zone string) sources.ListableWrapper {
28+
return &computeNodeGroupWrapper{
29+
client: client,
30+
ZoneBase: gcpshared.NewZoneBase(
31+
projectID,
32+
zone,
33+
sdp.AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION,
34+
ComputeNodeGroup,
35+
),
36+
}
37+
}
38+
39+
// PotentialLinks returns the potential links for the compute instance wrapper
40+
func (c computeNodeGroupWrapper) PotentialLinks() map[shared.ItemType]bool {
41+
return shared.NewItemTypesSet(
42+
ComputeNodeTemplate,
43+
)
44+
}
45+
46+
// TerraformMappings returns the Terraform mappings for the compute instance wrapper
47+
func (c computeNodeGroupWrapper) TerraformMappings() []*sdp.TerraformMapping {
48+
return []*sdp.TerraformMapping{
49+
{
50+
TerraformMethod: sdp.QueryMethod_GET,
51+
TerraformQueryMap: "google_compute_node_group.name",
52+
},
53+
}
54+
}
55+
56+
// GetLookups defines how the source can be queried for specific items.
57+
func (c computeNodeGroupWrapper) GetLookups() sources.ItemTypeLookups {
58+
return sources.ItemTypeLookups{
59+
ComputeNodeGroupLookupByName,
60+
}
61+
}
62+
63+
// Get retrieves a compute node group by its name
64+
func (c computeNodeGroupWrapper) Get(ctx context.Context, queryParts ...string) (*sdp.Item, *sdp.QueryError) {
65+
req := &computepb.GetNodeGroupRequest{
66+
Project: c.ProjectID(),
67+
Zone: c.Zone(),
68+
NodeGroup: queryParts[0],
69+
}
70+
71+
nodegroup, err := c.client.Get(ctx, req)
72+
if err != nil {
73+
return nil, gcpshared.QueryError(err)
74+
}
75+
76+
var sdpErr *sdp.QueryError
77+
var item *sdp.Item
78+
item, sdpErr = c.gcpComputeNodeGroupToSDPItem(nodegroup)
79+
if sdpErr != nil {
80+
return nil, sdpErr
81+
}
82+
83+
return item, nil
84+
}
85+
86+
// List lists compute node groups and converts them to sdp.Items.
87+
func (c computeNodeGroupWrapper) List(ctx context.Context) ([]*sdp.Item, *sdp.QueryError) {
88+
it := c.client.List(ctx, &computepb.ListNodeGroupsRequest{
89+
Project: c.ProjectID(),
90+
Zone: c.Zone(),
91+
})
92+
93+
var items []*sdp.Item
94+
for {
95+
nodegroup, err := it.Next()
96+
if errors.Is(err, iterator.Done) {
97+
break
98+
}
99+
if err != nil {
100+
return nil, gcpshared.QueryError(err)
101+
}
102+
103+
var sdpErr *sdp.QueryError
104+
var item *sdp.Item
105+
item, sdpErr = c.gcpComputeNodeGroupToSDPItem(nodegroup)
106+
if sdpErr != nil {
107+
return nil, sdpErr
108+
}
109+
110+
items = append(items, item)
111+
}
112+
113+
return items, nil
114+
}
115+
116+
func (c computeNodeGroupWrapper) gcpComputeNodeGroupToSDPItem(nodegroup *computepb.NodeGroup) (*sdp.Item, *sdp.QueryError) {
117+
attributes, err := shared.ToAttributesWithExclude(nodegroup)
118+
if err != nil {
119+
return nil, &sdp.QueryError{
120+
ErrorType: sdp.QueryError_OTHER,
121+
ErrorString: err.Error(),
122+
}
123+
}
124+
125+
sdpItem := &sdp.Item{
126+
Type: ComputeNodeGroup.String(),
127+
UniqueAttribute: "name",
128+
Attributes: attributes,
129+
Scope: c.DefaultScope(),
130+
131+
// No labels for node groups.
132+
}
133+
134+
templateUrl := nodegroup.GetNodeTemplate()
135+
if templateUrl != "" {
136+
// https://www.googleapis.com/compute/v1/projects/{project}/regions/{region}/nodeTemplates/{name}
137+
138+
region := gcpshared.ExtractRegion(templateUrl)
139+
name := gcpshared.LastPathComponent(templateUrl)
140+
141+
if region != "" && name != "" {
142+
sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{
143+
Query: &sdp.Query{
144+
Type: ComputeNodeTemplate.String(),
145+
Method: sdp.QueryMethod_GET,
146+
Query: name,
147+
Scope: gcpshared.RegionalScope(c.ProjectID(), region),
148+
},
149+
BlastPropagation: &sdp.BlastPropagation{
150+
In: true,
151+
Out: false,
152+
},
153+
})
154+
}
155+
}
156+
157+
// Translate nodegroup status to common sdp status.
158+
switch nodegroup.GetStatus() {
159+
case computepb.NodeGroup_READY.String():
160+
sdpItem.Health = sdp.Health_HEALTH_OK.Enum()
161+
case computepb.NodeGroup_INVALID.String():
162+
sdpItem.Health = sdp.Health_HEALTH_ERROR.Enum()
163+
case computepb.NodeGroup_CREATING.String():
164+
sdpItem.Health = sdp.Health_HEALTH_PENDING.Enum()
165+
case computepb.NodeGroup_DELETING.String():
166+
sdpItem.Health = sdp.Health_HEALTH_PENDING.Enum()
167+
}
168+
169+
return sdpItem, nil
170+
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package adapters_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"cloud.google.com/go/compute/apiv1/computepb"
8+
"go.uber.org/mock/gomock"
9+
"google.golang.org/api/iterator"
10+
"k8s.io/utils/ptr"
11+
12+
"github.com/overmindtech/cli/sdp-go"
13+
"github.com/overmindtech/cli/sources"
14+
"github.com/overmindtech/cli/sources/gcp/adapters"
15+
"github.com/overmindtech/cli/sources/gcp/shared/mocks"
16+
"github.com/overmindtech/cli/sources/shared"
17+
)
18+
19+
func TestComputeNodeGroup(t *testing.T) {
20+
ctx := context.Background()
21+
ctrl := gomock.NewController(t)
22+
defer ctrl.Finish()
23+
24+
mockClient := mocks.NewMockComputeNodeGroupClient(ctrl)
25+
projectID := "test-project-id"
26+
zone := "us-central1-a"
27+
28+
t.Run("Get", func(t *testing.T) {
29+
wrapper := adapters.NewComputeNodeGroup(mockClient, projectID, zone)
30+
31+
mockClient.EXPECT().Get(ctx, gomock.Any()).Return(createComputeNodeGroup("test-node-group", computepb.NodeGroup_READY), nil)
32+
33+
adapter := sources.WrapperToAdapter(wrapper)
34+
35+
sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], "test-node-group", true)
36+
if qErr != nil {
37+
t.Fatalf("Expected no error, got: %v", qErr)
38+
}
39+
40+
t.Run("StaticTests", func(t *testing.T) {
41+
queryTests := shared.QueryTests{
42+
{
43+
ExpectedType: adapters.ComputeNodeTemplate.String(),
44+
ExpectedMethod: sdp.QueryMethod_GET,
45+
ExpectedQuery: "node-template-1",
46+
ExpectedScope: "test-project-id.northamerica-northeast1",
47+
ExpectedBlastPropagation: &sdp.BlastPropagation{
48+
In: true,
49+
Out: false,
50+
},
51+
},
52+
}
53+
54+
shared.RunStaticTests(t, adapter, sdpItem, queryTests)
55+
})
56+
})
57+
58+
t.Run("HealthCheck", func(t *testing.T) {
59+
type testCase struct {
60+
name string
61+
input computepb.NodeGroup_Status
62+
expected sdp.Health
63+
}
64+
65+
testCases := []testCase{
66+
{
67+
name: "Ready status",
68+
input: computepb.NodeGroup_READY,
69+
expected: sdp.Health_HEALTH_OK,
70+
},
71+
{
72+
name: "Invalid status",
73+
input: computepb.NodeGroup_INVALID,
74+
expected: sdp.Health_HEALTH_ERROR,
75+
},
76+
{
77+
name: "Creating status",
78+
input: computepb.NodeGroup_CREATING,
79+
expected: sdp.Health_HEALTH_PENDING,
80+
},
81+
{
82+
name: "Deleting status",
83+
input: computepb.NodeGroup_DELETING,
84+
expected: sdp.Health_HEALTH_PENDING,
85+
},
86+
}
87+
88+
for _, tc := range testCases {
89+
t.Run(tc.name, func(t *testing.T) {
90+
wrapper := adapters.NewComputeNodeGroup(mockClient, projectID, zone)
91+
adapter := sources.WrapperToAdapter(wrapper)
92+
93+
mockClient.EXPECT().Get(ctx, gomock.Any()).Return(createComputeNodeGroup("test-ng", tc.input), nil)
94+
95+
sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], "test-node-group", true)
96+
if qErr != nil {
97+
t.Fatalf("Expected no error, got: %v", qErr)
98+
}
99+
100+
if sdpItem.GetHealth() != tc.expected {
101+
t.Errorf("Expected health: %v, got: %v", tc.expected, sdpItem.GetHealth())
102+
}
103+
})
104+
}
105+
})
106+
107+
t.Run("List", func(t *testing.T) {
108+
wrapper := adapters.NewComputeNodeGroup(mockClient, projectID, zone)
109+
110+
adapter := sources.WrapperToAdapter(wrapper)
111+
112+
mockComputeIterator := mocks.NewMockComputeNodeGroupIterator(ctrl)
113+
114+
// 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)
117+
mockComputeIterator.EXPECT().Next().Return(nil, iterator.Done)
118+
119+
// Mock the List method
120+
mockClient.EXPECT().List(ctx, gomock.Any()).Return(mockComputeIterator)
121+
122+
sdpItems, err := adapter.List(ctx, wrapper.Scopes()[0], true)
123+
if err != nil {
124+
t.Fatalf("Expected no error, got: %v", err)
125+
}
126+
127+
if len(sdpItems) != 2 {
128+
t.Fatalf("Expected 2 items, got: %d", len(sdpItems))
129+
}
130+
131+
for _, item := range sdpItems {
132+
if item.Validate() != nil {
133+
t.Fatalf("Expected no validation error, got: %v", item.Validate())
134+
}
135+
136+
if item.GetLinkedItemQueries()[0].GetQuery().GetQuery() != "node-template-1" {
137+
t.Fatalf("Expected node-template-1 as query, got: %s", item.GetTags()["env"])
138+
}
139+
}
140+
})
141+
}
142+
143+
func createComputeNodeGroup(name string, status computepb.NodeGroup_Status) *computepb.NodeGroup {
144+
return &computepb.NodeGroup{
145+
Name: ptr.To(name),
146+
NodeTemplate: ptr.To("https://www.googleapis.com/compute/v1/projects/test-project/regions/northamerica-northeast1/nodeTemplates/node-template-1"),
147+
Status: ptr.To(status.String()),
148+
}
149+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package adapters
2+
3+
import (
4+
gcpshared "github.com/overmindtech/cli/sources/gcp/shared"
5+
"github.com/overmindtech/cli/sources/shared"
6+
)
7+
8+
var (
9+
ComputeNodeTemplate = shared.NewItemType(gcpshared.GCP, gcpshared.Compute, gcpshared.NodeTemplate)
10+
11+
ComputeNodeTemplateLookupByName = shared.NewItemTypeLookup("name", ComputeNodeTemplate)
12+
)

0 commit comments

Comments
 (0)