Skip to content

feat: Direct controller ApiGatewayApi #4278

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apis/apigateway/v1alpha1/api_identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ func NewApiIdentity(ctx context.Context, reader client.Reader, obj *APIGatewayAP

func ParseApiExternal(external string) (parent *ApiParent, resourceID string, err error) {
tokens := strings.Split(external, "/")
if len(tokens) != 6 || tokens[0] != "projects" || tokens[2] != "locations" || tokens[4] != "apis" || tokens[3] == "global" {
if len(tokens) != 6 || tokens[0] != "projects" || tokens[2] != "locations" || tokens[4] != "apis" || tokens[3] != "global" {
return nil, "", fmt.Errorf("format of APIGatewayAPI external=%q was not known (use projects/{{projectID}}/locations/global/apis/{{apiID}})", external)
}
parent = &ApiParent{
Expand Down
2 changes: 2 additions & 0 deletions config/tests/samples/create/harness.go
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,8 @@ func MaybeSkip(t *testing.T, name string, resources []*unstructured.Unstructured
case schema.GroupKind{Group: "alloydb.cnrm.cloud.google.com", Kind: "AlloyDBCluster"}:
case schema.GroupKind{Group: "alloydb.cnrm.cloud.google.com", Kind: "AlloyDBInstance"}:

case schema.GroupKind{Group: "apigateway.cnrm.cloud.google.com", Kind: "APIGatewayAPI"}:

case schema.GroupKind{Group: "apigee.cnrm.cloud.google.com", Kind: "ApigeeEndpointAttachment"}:
case schema.GroupKind{Group: "apigee.cnrm.cloud.google.com", Kind: "ApigeeEnvgroup"}:
case schema.GroupKind{Group: "apigee.cnrm.cloud.google.com", Kind: "ApigeeEnvgroupAttachment"}:
Expand Down
8 changes: 6 additions & 2 deletions mockgcp/mockapigateway/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,12 @@ func (s *ApiGatewayV1) CreateApi(ctx context.Context, req *pb.CreateApiRequest)
obj.CreateTime = timestamppb.New(now)
obj.UpdateTime = timestamppb.New(now)
obj.State = pb.Api_ACTIVE
obj.DisplayName = name.Api

if obj.DisplayName == "" {
obj.DisplayName = name.Api
}
if obj.ManagedService == "" {
obj.ManagedService = fmt.Sprintf("%s-{generatedId}.apigateway.${projectId}.cloud.goog", req.GetApiId())
}
if err := s.storage.Create(ctx, fqn, obj); err != nil {
return nil, err
}
Expand Down
3 changes: 3 additions & 0 deletions mockgcp/mockapigateway/testdata/api/crud/_http.log
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ X-Xss-Protection: 0
"@type": "type.googleapis.com/google.cloud.apigateway.v1.Api",
"createTime": "2024-04-01T12:34:56.123456Z",
"displayName": "test-${uniqueId}",
"managedService": "test-${uniqueId}-{generatedId}.apigateway.${projectId}.cloud.goog",
"name": "projects/${projectId}/locations/global/apis/test-${uniqueId}",
"state": "ACTIVE",
"updateTime": "2024-04-01T12:34:56.123456Z"
Expand All @@ -87,6 +88,7 @@ X-Xss-Protection: 0
{
"createTime": "2024-04-01T12:34:56.123456Z",
"displayName": "test-${uniqueId}",
"managedService": "test-${uniqueId}-{generatedId}.apigateway.${projectId}.cloud.goog",
"name": "projects/${projectId}/locations/global/apis/test-${uniqueId}",
"state": "ACTIVE",
"updateTime": "2024-04-01T12:34:56.123456Z"
Expand All @@ -112,6 +114,7 @@ X-Xss-Protection: 0
{
"createTime": "2024-04-01T12:34:56.123456Z",
"displayName": "test-${uniqueId}",
"managedService": "test-${uniqueId}-{generatedId}.apigateway.${projectId}.cloud.goog",
"name": "projects/${projectId}/locations/global/apis/test-${uniqueId}",
"state": "ACTIVE",
"updateTime": "2024-04-01T12:34:56.123456Z"
Expand Down
251 changes: 251 additions & 0 deletions pkg/controller/direct/apigateway/api_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// +tool:controller
// proto.service: google.cloud.apigateway.v1.ApiGatewayService
// proto.message: google.cloud.apigateway.v1.Api
// crd.type: APIGatewayAPI
// crd.version: v1alpha1

package apigateway

import (
"context"
"fmt"
"reflect"

gcp "cloud.google.com/go/apigateway/apiv1"
pb "cloud.google.com/go/apigateway/apiv1/apigatewaypb"
"google.golang.org/protobuf/types/known/fieldmaskpb"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/client"

krm "github.com/GoogleCloudPlatform/k8s-config-connector/apis/apigateway/v1alpha1"
refs "github.com/GoogleCloudPlatform/k8s-config-connector/apis/refs/v1beta1"
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/config"
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct"
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/directbase"
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/registry"
)

func init() {
registry.RegisterModel(krm.APIGatewayAPIGVK, NewApiModel)
}

func NewApiModel(ctx context.Context, config *config.ControllerConfig) (directbase.Model, error) {
return &apiModel{config: *config}, nil
}

var _ directbase.Model = &apiModel{}

type apiModel struct {
config config.ControllerConfig
}

func (m *apiModel) AdapterForObject(ctx context.Context, reader client.Reader, u *unstructured.Unstructured) (directbase.Adapter, error) {
obj := &krm.APIGatewayAPI{}
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &obj); err != nil {
return nil, fmt.Errorf("error converting to %T: %w", obj, err)
}

id, err := krm.NewApiIdentity(ctx, reader, obj)
if err != nil {
return nil, err
}
// Get apigateway GCP client
gcpClient, err := newGCPClient(ctx, &m.config)
if err != nil {
return nil, err
}
apiGatewayClient, err := gcpClient.newApiGatewayClient(ctx)
if err != nil {
return nil, err
}
return &apiAdapter{
gcpClient: apiGatewayClient,
id: id,
desired: obj,
}, nil
}

func (m *apiModel) AdapterForURL(ctx context.Context, url string) (directbase.Adapter, error) {
// TODO: Support URLs
return nil, nil
}

type apiAdapter struct {
gcpClient *gcp.Client
id *krm.ApiIdentity
desired *krm.APIGatewayAPI
actual *pb.Api
}

var _ directbase.Adapter = &apiAdapter{}

// Find retrieves the GCP resource.
// Return true means the object is found. This triggers Adapter `Update` call.
// Return false means the object is not found. This triggers Adapter `Create` call.
// Return a non-nil error requeues the requests.
func (a *apiAdapter) Find(ctx context.Context) (bool, error) {
log := klog.FromContext(ctx)
log.V(2).Info("getting apigateway api", "name", a.id)

req := &pb.GetApiRequest{Name: a.id.String()}
actual, err := a.gcpClient.GetApi(ctx, req)
if err != nil {
if direct.IsNotFound(err) {
return false, nil
}
return false, fmt.Errorf("getting apigateway api %q from gcp: %w", a.id.String(), err)
}

a.actual = actual
return true, nil
}

// Create creates the resource in GCP based on `spec` and update the Config Connector object `status` based on the GCP response.
func (a *apiAdapter) Create(ctx context.Context, createOp *directbase.CreateOperation) error {
log := klog.FromContext(ctx)
log.V(2).Info("creating apigateway api", "name", a.id)
mapCtx := &direct.MapContext{}

desired := a.desired.DeepCopy()
resource := APIGatewayAPISpec_ToProto(mapCtx, &desired.Spec)
if mapCtx.Err() != nil {
return mapCtx.Err()
}

req := &pb.CreateApiRequest{
Parent: a.id.Parent().String(),
ApiId: a.id.ID(),
Api: resource,
}
op, err := a.gcpClient.CreateApi(ctx, req)
if err != nil {
return fmt.Errorf("creating apigateway api %s: %w", a.id.String(), err)
}
created, err := op.Wait(ctx)
if err != nil {
return fmt.Errorf("apigateway api %s waiting creation: %w", a.id, err)
}
log.V(2).Info("successfully created apigateway api in gcp", "name", a.id)

status := &krm.APIGatewayAPIStatus{}
status.ObservedState = APIGatewayAPIObservedState_FromProto(mapCtx, created)
if mapCtx.Err() != nil {
return mapCtx.Err()
}
status.ExternalRef = direct.LazyPtr(a.id.String())
return createOp.UpdateStatus(ctx, status, nil)
}

// Update updates the resource in GCP based on `spec` and update the Config Connector object `status` based on the GCP response.
func (a *apiAdapter) Update(ctx context.Context, updateOp *directbase.UpdateOperation) error {
log := klog.FromContext(ctx)
log.V(2).Info("updating apigateway api", "name", a.id)
mapCtx := &direct.MapContext{}

desired := a.desired.DeepCopy()
resource := APIGatewayAPISpec_ToProto(mapCtx, &desired.Spec)
if mapCtx.Err() != nil {
return mapCtx.Err()
}

paths := []string{}
if desired.Spec.DisplayName != nil && !reflect.DeepEqual(resource.DisplayName, a.actual.DisplayName) {
paths = append(paths, "display_name")
}
if desired.Spec.Labels != nil && !reflect.DeepEqual(resource.Labels, a.actual.Labels) {
paths = append(paths, "labels")
}

var updated *pb.Api
if len(paths) == 0 {
log.V(2).Info("no field needs update", "name", a.id)
updated = a.actual
} else {
resource.Name = a.id.String() // we need to set the name so that GCP API can identify the resource
req := &pb.UpdateApiRequest{
Api: resource,
UpdateMask: &fieldmaskpb.FieldMask{Paths: paths},
}
op, err := a.gcpClient.UpdateApi(ctx, req)
if err != nil {
return fmt.Errorf("updating apigateway api %s: %w", a.id.String(), err)
}
updated, err = op.Wait(ctx)
if err != nil {
return fmt.Errorf("apigateway api %s waiting for update: %w", a.id, err)
}
log.V(2).Info("successfully updated apigateway api", "name", a.id)
}

status := &krm.APIGatewayAPIStatus{}
status.ObservedState = APIGatewayAPIObservedState_FromProto(mapCtx, updated)
if mapCtx.Err() != nil {
return mapCtx.Err()
}
return updateOp.UpdateStatus(ctx, status, nil)
}

// Export maps the GCP object to a Config Connector resource `spec`.
func (a *apiAdapter) Export(ctx context.Context) (*unstructured.Unstructured, error) {
if a.actual == nil {
return nil, fmt.Errorf("Find() not called")
}
u := &unstructured.Unstructured{}

obj := &krm.APIGatewayAPI{}
mapCtx := &direct.MapContext{}
obj.Spec = direct.ValueOf(APIGatewayAPISpec_FromProto(mapCtx, a.actual))
if mapCtx.Err() != nil {
return nil, mapCtx.Err()
}
obj.Spec.ProjectRef = &refs.ProjectRef{External: a.id.Parent().ProjectID}
uObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
if err != nil {
return nil, err
}

u.SetName(a.actual.Name)
u.SetGroupVersionKind(krm.APIGatewayAPIGVK)
u.Object = uObj
return u, nil
}

// Delete the resource from GCP service when the corresponding Config Connector resource is deleted.
func (a *apiAdapter) Delete(ctx context.Context, deleteOp *directbase.DeleteOperation) (bool, error) {
log := klog.FromContext(ctx)
log.V(2).Info("deleting apigateway api", "name", a.id)

req := &pb.DeleteApiRequest{Name: a.id.String()}
op, err := a.gcpClient.DeleteApi(ctx, req)
if err != nil {
if direct.IsNotFound(err) {
// Return success if not found (assume it was already deleted).
log.V(2).Info("skipping delete for non-existent apigateway api, assuming it was already deleted", "name", a.id)
return true, nil
}
return false, fmt.Errorf("deleting apigateway api %s: %w", a.id.String(), err)
}
log.V(2).Info("successfully deleted apigateway api", "name", a.id)

err = op.Wait(ctx)
if err != nil {
return false, fmt.Errorf("waiting delete apigateway api %s: %w", a.id, err)
}
return true, nil
}
4 changes: 2 additions & 2 deletions pkg/controller/direct/apigateway/api_fuzzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ func fuzzWorkflowsWorkflow() fuzztesting.KRMFuzzer {
)
f.UnimplementedFields.Insert(".name")

f.SpecFields.Insert(".displayName")
f.SpecFields.Insert(".display_name")
f.SpecFields.Insert(".description")
f.SpecFields.Insert(".labels")
f.SpecFields.Insert(".managedService")
f.SpecFields.Insert(".managed_service")

f.StatusFields.Insert(".state")
f.StatusFields.Insert(".create_time")
Expand Down
49 changes: 49 additions & 0 deletions pkg/controller/direct/apigateway/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// +tool:controller-client
// proto.service: google.cloud.apigateway.v1.ApiGatewayService

package apigateway

import (
"context"
"fmt"

api "cloud.google.com/go/apigateway/apiv1"
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/config"
)

type gcpClient struct {
config *config.ControllerConfig
}

func newGCPClient(ctx context.Context, config *config.ControllerConfig) (*gcpClient, error) {
gcpClient := &gcpClient{
config: config,
}
return gcpClient, nil
}

func (m *gcpClient) newApiGatewayClient(ctx context.Context) (*api.Client, error) {
opts, err := m.config.RESTClientOptions()
if err != nil {
return nil, err
}
client, err := api.NewRESTClient(ctx, opts...)
if err != nil {
return nil, fmt.Errorf("building apigateway client: %w", err)
}
return client, err
}
1 change: 1 addition & 0 deletions pkg/controller/direct/register/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
_ "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/compute"

_ "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/alloydb"
_ "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/apigateway"
_ "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/apigee"
_ "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/apikeys"
_ "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/apphub"
Expand Down
Loading
Loading