Skip to content

Commit d48d877

Browse files
Merge pull request #4278 from anhdle-sso/resource-apigateway-api
feat: Direct controller ApiGatewayApi
2 parents a11517c + 9111c64 commit d48d877

File tree

15 files changed

+914
-5
lines changed

15 files changed

+914
-5
lines changed

apis/apigateway/v1alpha1/api_identity.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ func NewApiIdentity(ctx context.Context, reader client.Reader, obj *APIGatewayAP
105105

106106
func ParseApiExternal(external string) (parent *ApiParent, resourceID string, err error) {
107107
tokens := strings.Split(external, "/")
108-
if len(tokens) != 6 || tokens[0] != "projects" || tokens[2] != "locations" || tokens[4] != "apis" || tokens[3] == "global" {
108+
if len(tokens) != 6 || tokens[0] != "projects" || tokens[2] != "locations" || tokens[4] != "apis" || tokens[3] != "global" {
109109
return nil, "", fmt.Errorf("format of APIGatewayAPI external=%q was not known (use projects/{{projectID}}/locations/global/apis/{{apiID}})", external)
110110
}
111111
parent = &ApiParent{

config/tests/samples/create/harness.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -748,6 +748,8 @@ func MaybeSkip(t *testing.T, name string, resources []*unstructured.Unstructured
748748
case schema.GroupKind{Group: "alloydb.cnrm.cloud.google.com", Kind: "AlloyDBCluster"}:
749749
case schema.GroupKind{Group: "alloydb.cnrm.cloud.google.com", Kind: "AlloyDBInstance"}:
750750

751+
case schema.GroupKind{Group: "apigateway.cnrm.cloud.google.com", Kind: "APIGatewayAPI"}:
752+
751753
case schema.GroupKind{Group: "apigee.cnrm.cloud.google.com", Kind: "ApigeeEndpointAttachment"}:
752754
case schema.GroupKind{Group: "apigee.cnrm.cloud.google.com", Kind: "ApigeeEnvgroup"}:
753755
case schema.GroupKind{Group: "apigee.cnrm.cloud.google.com", Kind: "ApigeeEnvgroupAttachment"}:

mockgcp/mockapigateway/api.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,12 @@ func (s *ApiGatewayV1) CreateApi(ctx context.Context, req *pb.CreateApiRequest)
7070
obj.CreateTime = timestamppb.New(now)
7171
obj.UpdateTime = timestamppb.New(now)
7272
obj.State = pb.Api_ACTIVE
73-
obj.DisplayName = name.Api
74-
73+
if obj.DisplayName == "" {
74+
obj.DisplayName = name.Api
75+
}
76+
if obj.ManagedService == "" {
77+
obj.ManagedService = fmt.Sprintf("%s-{generatedId}.apigateway.${projectId}.cloud.goog", req.GetApiId())
78+
}
7579
if err := s.storage.Create(ctx, fqn, obj); err != nil {
7680
return nil, err
7781
}

mockgcp/mockapigateway/testdata/api/crud/_http.log

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ X-Xss-Protection: 0
6161
"@type": "type.googleapis.com/google.cloud.apigateway.v1.Api",
6262
"createTime": "2024-04-01T12:34:56.123456Z",
6363
"displayName": "test-${uniqueId}",
64+
"managedService": "test-${uniqueId}-{generatedId}.apigateway.${projectId}.cloud.goog",
6465
"name": "projects/${projectId}/locations/global/apis/test-${uniqueId}",
6566
"state": "ACTIVE",
6667
"updateTime": "2024-04-01T12:34:56.123456Z"
@@ -87,6 +88,7 @@ X-Xss-Protection: 0
8788
{
8889
"createTime": "2024-04-01T12:34:56.123456Z",
8990
"displayName": "test-${uniqueId}",
91+
"managedService": "test-${uniqueId}-{generatedId}.apigateway.${projectId}.cloud.goog",
9092
"name": "projects/${projectId}/locations/global/apis/test-${uniqueId}",
9193
"state": "ACTIVE",
9294
"updateTime": "2024-04-01T12:34:56.123456Z"
@@ -112,6 +114,7 @@ X-Xss-Protection: 0
112114
{
113115
"createTime": "2024-04-01T12:34:56.123456Z",
114116
"displayName": "test-${uniqueId}",
117+
"managedService": "test-${uniqueId}-{generatedId}.apigateway.${projectId}.cloud.goog",
115118
"name": "projects/${projectId}/locations/global/apis/test-${uniqueId}",
116119
"state": "ACTIVE",
117120
"updateTime": "2024-04-01T12:34:56.123456Z"
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
// Copyright 2024 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// +tool:controller
16+
// proto.service: google.cloud.apigateway.v1.ApiGatewayService
17+
// proto.message: google.cloud.apigateway.v1.Api
18+
// crd.type: APIGatewayAPI
19+
// crd.version: v1alpha1
20+
21+
package apigateway
22+
23+
import (
24+
"context"
25+
"fmt"
26+
"reflect"
27+
28+
gcp "cloud.google.com/go/apigateway/apiv1"
29+
pb "cloud.google.com/go/apigateway/apiv1/apigatewaypb"
30+
"google.golang.org/protobuf/types/known/fieldmaskpb"
31+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
32+
"k8s.io/apimachinery/pkg/runtime"
33+
"k8s.io/klog/v2"
34+
"sigs.k8s.io/controller-runtime/pkg/client"
35+
36+
krm "github.com/GoogleCloudPlatform/k8s-config-connector/apis/apigateway/v1alpha1"
37+
refs "github.com/GoogleCloudPlatform/k8s-config-connector/apis/refs/v1beta1"
38+
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/config"
39+
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct"
40+
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/directbase"
41+
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/registry"
42+
)
43+
44+
func init() {
45+
registry.RegisterModel(krm.APIGatewayAPIGVK, NewApiModel)
46+
}
47+
48+
func NewApiModel(ctx context.Context, config *config.ControllerConfig) (directbase.Model, error) {
49+
return &apiModel{config: *config}, nil
50+
}
51+
52+
var _ directbase.Model = &apiModel{}
53+
54+
type apiModel struct {
55+
config config.ControllerConfig
56+
}
57+
58+
func (m *apiModel) AdapterForObject(ctx context.Context, reader client.Reader, u *unstructured.Unstructured) (directbase.Adapter, error) {
59+
obj := &krm.APIGatewayAPI{}
60+
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &obj); err != nil {
61+
return nil, fmt.Errorf("error converting to %T: %w", obj, err)
62+
}
63+
64+
id, err := krm.NewApiIdentity(ctx, reader, obj)
65+
if err != nil {
66+
return nil, err
67+
}
68+
// Get apigateway GCP client
69+
gcpClient, err := newGCPClient(ctx, &m.config)
70+
if err != nil {
71+
return nil, err
72+
}
73+
apiGatewayClient, err := gcpClient.newApiGatewayClient(ctx)
74+
if err != nil {
75+
return nil, err
76+
}
77+
return &apiAdapter{
78+
gcpClient: apiGatewayClient,
79+
id: id,
80+
desired: obj,
81+
}, nil
82+
}
83+
84+
func (m *apiModel) AdapterForURL(ctx context.Context, url string) (directbase.Adapter, error) {
85+
// TODO: Support URLs
86+
return nil, nil
87+
}
88+
89+
type apiAdapter struct {
90+
gcpClient *gcp.Client
91+
id *krm.ApiIdentity
92+
desired *krm.APIGatewayAPI
93+
actual *pb.Api
94+
}
95+
96+
var _ directbase.Adapter = &apiAdapter{}
97+
98+
// Find retrieves the GCP resource.
99+
// Return true means the object is found. This triggers Adapter `Update` call.
100+
// Return false means the object is not found. This triggers Adapter `Create` call.
101+
// Return a non-nil error requeues the requests.
102+
func (a *apiAdapter) Find(ctx context.Context) (bool, error) {
103+
log := klog.FromContext(ctx)
104+
log.V(2).Info("getting apigateway api", "name", a.id)
105+
106+
req := &pb.GetApiRequest{Name: a.id.String()}
107+
actual, err := a.gcpClient.GetApi(ctx, req)
108+
if err != nil {
109+
if direct.IsNotFound(err) {
110+
return false, nil
111+
}
112+
return false, fmt.Errorf("getting apigateway api %q from gcp: %w", a.id.String(), err)
113+
}
114+
115+
a.actual = actual
116+
return true, nil
117+
}
118+
119+
// Create creates the resource in GCP based on `spec` and update the Config Connector object `status` based on the GCP response.
120+
func (a *apiAdapter) Create(ctx context.Context, createOp *directbase.CreateOperation) error {
121+
log := klog.FromContext(ctx)
122+
log.V(2).Info("creating apigateway api", "name", a.id)
123+
mapCtx := &direct.MapContext{}
124+
125+
desired := a.desired.DeepCopy()
126+
resource := APIGatewayAPISpec_ToProto(mapCtx, &desired.Spec)
127+
if mapCtx.Err() != nil {
128+
return mapCtx.Err()
129+
}
130+
131+
req := &pb.CreateApiRequest{
132+
Parent: a.id.Parent().String(),
133+
ApiId: a.id.ID(),
134+
Api: resource,
135+
}
136+
op, err := a.gcpClient.CreateApi(ctx, req)
137+
if err != nil {
138+
return fmt.Errorf("creating apigateway api %s: %w", a.id.String(), err)
139+
}
140+
created, err := op.Wait(ctx)
141+
if err != nil {
142+
return fmt.Errorf("apigateway api %s waiting creation: %w", a.id, err)
143+
}
144+
log.V(2).Info("successfully created apigateway api in gcp", "name", a.id)
145+
146+
status := &krm.APIGatewayAPIStatus{}
147+
status.ObservedState = APIGatewayAPIObservedState_FromProto(mapCtx, created)
148+
if mapCtx.Err() != nil {
149+
return mapCtx.Err()
150+
}
151+
status.ExternalRef = direct.LazyPtr(a.id.String())
152+
return createOp.UpdateStatus(ctx, status, nil)
153+
}
154+
155+
// Update updates the resource in GCP based on `spec` and update the Config Connector object `status` based on the GCP response.
156+
func (a *apiAdapter) Update(ctx context.Context, updateOp *directbase.UpdateOperation) error {
157+
log := klog.FromContext(ctx)
158+
log.V(2).Info("updating apigateway api", "name", a.id)
159+
mapCtx := &direct.MapContext{}
160+
161+
desired := a.desired.DeepCopy()
162+
resource := APIGatewayAPISpec_ToProto(mapCtx, &desired.Spec)
163+
if mapCtx.Err() != nil {
164+
return mapCtx.Err()
165+
}
166+
167+
paths := []string{}
168+
if desired.Spec.DisplayName != nil && !reflect.DeepEqual(resource.DisplayName, a.actual.DisplayName) {
169+
paths = append(paths, "display_name")
170+
}
171+
if desired.Spec.Labels != nil && !reflect.DeepEqual(resource.Labels, a.actual.Labels) {
172+
paths = append(paths, "labels")
173+
}
174+
175+
var updated *pb.Api
176+
if len(paths) == 0 {
177+
log.V(2).Info("no field needs update", "name", a.id)
178+
updated = a.actual
179+
} else {
180+
resource.Name = a.id.String() // we need to set the name so that GCP API can identify the resource
181+
req := &pb.UpdateApiRequest{
182+
Api: resource,
183+
UpdateMask: &fieldmaskpb.FieldMask{Paths: paths},
184+
}
185+
op, err := a.gcpClient.UpdateApi(ctx, req)
186+
if err != nil {
187+
return fmt.Errorf("updating apigateway api %s: %w", a.id.String(), err)
188+
}
189+
updated, err = op.Wait(ctx)
190+
if err != nil {
191+
return fmt.Errorf("apigateway api %s waiting for update: %w", a.id, err)
192+
}
193+
log.V(2).Info("successfully updated apigateway api", "name", a.id)
194+
}
195+
196+
status := &krm.APIGatewayAPIStatus{}
197+
status.ObservedState = APIGatewayAPIObservedState_FromProto(mapCtx, updated)
198+
if mapCtx.Err() != nil {
199+
return mapCtx.Err()
200+
}
201+
return updateOp.UpdateStatus(ctx, status, nil)
202+
}
203+
204+
// Export maps the GCP object to a Config Connector resource `spec`.
205+
func (a *apiAdapter) Export(ctx context.Context) (*unstructured.Unstructured, error) {
206+
if a.actual == nil {
207+
return nil, fmt.Errorf("Find() not called")
208+
}
209+
u := &unstructured.Unstructured{}
210+
211+
obj := &krm.APIGatewayAPI{}
212+
mapCtx := &direct.MapContext{}
213+
obj.Spec = direct.ValueOf(APIGatewayAPISpec_FromProto(mapCtx, a.actual))
214+
if mapCtx.Err() != nil {
215+
return nil, mapCtx.Err()
216+
}
217+
obj.Spec.ProjectRef = &refs.ProjectRef{External: a.id.Parent().ProjectID}
218+
uObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
219+
if err != nil {
220+
return nil, err
221+
}
222+
223+
u.SetName(a.actual.Name)
224+
u.SetGroupVersionKind(krm.APIGatewayAPIGVK)
225+
u.Object = uObj
226+
return u, nil
227+
}
228+
229+
// Delete the resource from GCP service when the corresponding Config Connector resource is deleted.
230+
func (a *apiAdapter) Delete(ctx context.Context, deleteOp *directbase.DeleteOperation) (bool, error) {
231+
log := klog.FromContext(ctx)
232+
log.V(2).Info("deleting apigateway api", "name", a.id)
233+
234+
req := &pb.DeleteApiRequest{Name: a.id.String()}
235+
op, err := a.gcpClient.DeleteApi(ctx, req)
236+
if err != nil {
237+
if direct.IsNotFound(err) {
238+
// Return success if not found (assume it was already deleted).
239+
log.V(2).Info("skipping delete for non-existent apigateway api, assuming it was already deleted", "name", a.id)
240+
return true, nil
241+
}
242+
return false, fmt.Errorf("deleting apigateway api %s: %w", a.id.String(), err)
243+
}
244+
log.V(2).Info("successfully deleted apigateway api", "name", a.id)
245+
246+
err = op.Wait(ctx)
247+
if err != nil {
248+
return false, fmt.Errorf("waiting delete apigateway api %s: %w", a.id, err)
249+
}
250+
return true, nil
251+
}

pkg/controller/direct/apigateway/api_fuzzer.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@ func fuzzWorkflowsWorkflow() fuzztesting.KRMFuzzer {
3131
)
3232
f.UnimplementedFields.Insert(".name")
3333

34-
f.SpecFields.Insert(".displayName")
34+
f.SpecFields.Insert(".display_name")
3535
f.SpecFields.Insert(".description")
3636
f.SpecFields.Insert(".labels")
37-
f.SpecFields.Insert(".managedService")
37+
f.SpecFields.Insert(".managed_service")
3838

3939
f.StatusFields.Insert(".state")
4040
f.StatusFields.Insert(".create_time")
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright 2024 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// +tool:controller-client
16+
// proto.service: google.cloud.apigateway.v1.ApiGatewayService
17+
18+
package apigateway
19+
20+
import (
21+
"context"
22+
"fmt"
23+
24+
api "cloud.google.com/go/apigateway/apiv1"
25+
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/config"
26+
)
27+
28+
type gcpClient struct {
29+
config *config.ControllerConfig
30+
}
31+
32+
func newGCPClient(ctx context.Context, config *config.ControllerConfig) (*gcpClient, error) {
33+
gcpClient := &gcpClient{
34+
config: config,
35+
}
36+
return gcpClient, nil
37+
}
38+
39+
func (m *gcpClient) newApiGatewayClient(ctx context.Context) (*api.Client, error) {
40+
opts, err := m.config.RESTClientOptions()
41+
if err != nil {
42+
return nil, err
43+
}
44+
client, err := api.NewRESTClient(ctx, opts...)
45+
if err != nil {
46+
return nil, fmt.Errorf("building apigateway client: %w", err)
47+
}
48+
return client, err
49+
}

pkg/controller/direct/register/register.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
_ "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/compute"
1919

2020
_ "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/alloydb"
21+
_ "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/apigateway"
2122
_ "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/apigee"
2223
_ "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/apikeys"
2324
_ "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/apphub"

0 commit comments

Comments
 (0)