Skip to content

Commit

Permalink
Merge pull request #1422 from justinsb/genapi_resourcemanager_tags
Browse files Browse the repository at this point in the history
genapi: Add support for TagsTagKey
  • Loading branch information
justinsb authored Apr 10, 2024
2 parents f49a68a + a4df877 commit b8aa5ac
Show file tree
Hide file tree
Showing 9 changed files with 421 additions and 8 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ replace github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp => ./mockgcp
require (
cloud.google.com/go/apikeys v1.1.5
cloud.google.com/go/profiler v0.1.0
cloud.google.com/go/resourcemanager v1.9.4
contrib.go.opencensus.io/exporter/prometheus v0.1.0
github.com/GoogleCloudPlatform/declarative-resource-client-library v1.62.0
github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp v0.0.0-00010101000000-000000000000
Expand Down
2 changes: 2 additions & 0 deletions go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 6 additions & 6 deletions pkg/controller/direct/apikeys/apikeyskey_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ func (a *adapter) Create(ctx context.Context, u *unstructured.Unstructured) erro
}

// Update implements the Adapter interface.
func (a *adapter) Update(ctx context.Context) (*unstructured.Unstructured, error) {
func (a *adapter) Update(ctx context.Context, u *unstructured.Unstructured) error {
// TODO: Skip updates if no changes
// TODO: Where/how do we want to enforce immutability?
updateMask := &fieldmaskpb.FieldMask{}
Expand All @@ -254,12 +254,12 @@ func (a *adapter) Update(ctx context.Context) (*unstructured.Unstructured, error

if len(updateMask.Paths) == 0 {
klog.Warningf("unexpected empty update mask, desired: %v, actual: %v", a.desired, a.actual)
return nil, nil
return nil
}

key := &pb.Key{}
if err := keyMapping.Map(a.desired, key); err != nil {
return nil, err
return err
}

req := &pb.UpdateKeyRequest{
Expand All @@ -271,10 +271,10 @@ func (a *adapter) Update(ctx context.Context) (*unstructured.Unstructured, error

_, err := a.gcp.UpdateKey(ctx, req)
if err != nil {
return nil, err
return err
}
// TODO: Return updated object
return nil, nil
// TODO: update status in u
return nil
}

func (a *adapter) fullyQualifiedName() string {
Expand Down
2 changes: 1 addition & 1 deletion pkg/controller/direct/directbase/directbase_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ func (r *reconcileContext) doReconcile(ctx context.Context, u *unstructured.Unst
return false, r.handleUpdateFailed(ctx, u, fmt.Errorf("error creating: %w", err))
}
} else {
if _, err = adapter.Update(ctx); err != nil {
if err := adapter.Update(ctx, u); err != nil {
if unwrappedErr, ok := lifecyclehandler.CausedByUnresolvableDeps(err); ok {
logger.Info(unwrappedErr.Error(), "resource", k8s.GetNamespacedName(u))
return r.handleUnresolvableDeps(ctx, u, unwrappedErr)
Expand Down
4 changes: 3 additions & 1 deletion pkg/controller/direct/directbase/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,11 @@ type Adapter interface {

// Create creates a new GCP object.
// This should only be called when Find has previously returned false.
// The implementation should write the updated status into `u`.
Create(ctx context.Context, u *unstructured.Unstructured) error

// Update updates an existing GCP object.
// This should only be called when Find has previously returned true.
Update(ctx context.Context) (*unstructured.Unstructured, error)
// The implementation should write the updated status into `u`.
Update(ctx context.Context, u *unstructured.Unstructured) error
}
68 changes: 68 additions & 0 deletions pkg/controller/direct/resourcemanager/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// 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.

package resourcemanager

import (
"context"
"fmt"

api "cloud.google.com/go/resourcemanager/apiv3"
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller"
"google.golang.org/api/option"
)

type gcpClient struct {
config controller.Config
}

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

func (m *gcpClient) options() ([]option.ClientOption, error) {
var opts []option.ClientOption
if m.config.UserAgent != "" {
opts = append(opts, option.WithUserAgent(m.config.UserAgent))
}
if m.config.HTTPClient != nil {
// TODO: Set UserAgent in this scenario (error is: WithHTTPClient is incompatible with gRPC dial options)
opts = append(opts, option.WithHTTPClient(m.config.HTTPClient))
}
if m.config.UserProjectOverride && m.config.BillingProject != "" {
opts = append(opts, option.WithQuotaProject(m.config.BillingProject))
}

// TODO: support endpoints?
// if m.config.Endpoint != "" {
// opts = append(opts, option.WithEndpoint(m.config.Endpoint))
// }

return opts, nil
}

func (m *gcpClient) newTagKeysClient(ctx context.Context) (*api.TagKeysClient, error) {
opts, err := m.options()
if err != nil {
return nil, err
}
client, err := api.NewTagKeysRESTClient(ctx, opts...)
if err != nil {
return nil, fmt.Errorf("building tag keys client: %w", err)
}
return client, err
}
252 changes: 252 additions & 0 deletions pkg/controller/direct/resourcemanager/tagkey_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
// 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.

package resourcemanager

import (
"context"
"fmt"
"strings"
"time"

api "cloud.google.com/go/resourcemanager/apiv3"
pb "cloud.google.com/go/resourcemanager/apiv3/resourcemanagerpb"
"google.golang.org/protobuf/types/known/fieldmaskpb"
"google.golang.org/protobuf/types/known/timestamppb"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/manager"

krm "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/clients/generated/apis/tags/v1beta1"
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller"
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/directbase"
)

// AddTagKeyController creates a new controller and adds it to the Manager.
// The Manager will set fields on the Controller and start it when the Manager is started.
func AddTagKeyController(mgr manager.Manager, config *controller.Config) error {
gvk := krm.TagsTagKeyGVK

// TODO: Share gcp client (any value in doing so)?
ctx := context.TODO()
gcpClient, err := newGCPClient(ctx, config)
if err != nil {
return err
}
m := &tagKeyModel{gcpClient: gcpClient}
return directbase.Add(mgr, gvk, m)
}

type tagKeyModel struct {
*gcpClient
}

// model implements the Model interface.
var _ directbase.Model = &tagKeyModel{}

type tagKeyAdapter struct {
resourceID string

desired *krm.TagsTagKey
actual *pb.TagKey

*gcpClient
tagKeysClient *api.TagKeysClient
}

// adapter implements the Adapter interface.
var _ directbase.Adapter = &tagKeyAdapter{}

// AdapterForObject implements the Model interface.
func (m *tagKeyModel) AdapterForObject(ctx context.Context, u *unstructured.Unstructured) (directbase.Adapter, error) {
tagKeysClient, err := m.newTagKeysClient(ctx)
if err != nil {
return nil, err
}

// TODO: Just fetch this object?
obj := &krm.TagsTagKey{}
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &obj); err != nil {
return nil, fmt.Errorf("error converting to %T: %w", obj, err)
}

resourceID := ValueOf(obj.Spec.ResourceID)
// resourceID is server-generated, no fallback
// TODO: How do we do resource acquisition - maybe by shortname?
resourceID = strings.TrimPrefix(resourceID, "tagKeys/")

return &tagKeyAdapter{
resourceID: resourceID,
desired: obj,
gcpClient: m.gcpClient,
tagKeysClient: tagKeysClient,
}, nil
}

// Find implements the Adapter interface.
func (a *tagKeyAdapter) Find(ctx context.Context) (bool, error) {
if a.resourceID == "" {
return false, nil
}

req := &pb.GetTagKeyRequest{
Name: a.fullyQualifiedName(),
}
tagKey, err := a.tagKeysClient.GetTagKey(ctx, req)
if err != nil {
if IsNotFound(err) {
return false, nil
}
return false, err
}

a.actual = tagKey

return true, nil
}

// Delete implements the Adapter interface.
func (a *tagKeyAdapter) Delete(ctx context.Context) (bool, error) {
// Already deleted
if a.resourceID == "" {
return false, nil
}

// TODO: Delete via status selfLink?
req := &pb.DeleteTagKeyRequest{
Name: a.fullyQualifiedName(),
}

op, err := a.tagKeysClient.DeleteTagKey(ctx, req)
if err != nil {
if IsNotFound(err) {
return false, nil
}
return false, fmt.Errorf("deleting tagKey %s: %w", a.fullyQualifiedName(), err)
}

if _, err := op.Wait(ctx); err != nil {
return false, fmt.Errorf("tagKey deletion failed: %w", err)
}
// TODO: Do we need to check that it was deleted?

return true, nil
}

// Create implements the Adapter interface.
func (a *tagKeyAdapter) Create(ctx context.Context, u *unstructured.Unstructured) error {
log := klog.FromContext(ctx)
log.V(2).Info("creating object", "u", u)

// TODO: Should be ref
parent := a.desired.Spec.Parent
tagKey := &pb.TagKey{
Parent: parent,
ShortName: a.desired.Spec.ShortName,
Description: ValueOf(a.desired.Spec.Description),
PurposeData: a.desired.Spec.PurposeData,
}

if s := ValueOf(a.desired.Spec.Purpose); s != "" {
purpose, ok := pb.Purpose_value[s]
if !ok {
return fmt.Errorf("unknown purpose %q", s)
}
tagKey.Purpose = pb.Purpose(purpose)
}
req := &pb.CreateTagKeyRequest{
TagKey: tagKey,
}

op, err := a.tagKeysClient.CreateTagKey(ctx, req)
if err != nil {
return fmt.Errorf("creating tagKey: %w", err)
}

created, err := op.Wait(ctx)
if err != nil {
return fmt.Errorf("tagKey creation failed: %w", err)
}

log.V(2).Info("created tagkey", "tagkey", created)

resourceID := created.Name
if err := unstructured.SetNestedField(u.Object, resourceID, "spec", "resourceID"); err != nil {
return fmt.Errorf("setting spec.resourceID: %w", err)
}

status := &krm.TagsTagKeyStatus{}
if err := tagKeyStatusToKRM(created, status); err != nil {
return err
}

return setStatus(u, status)
}

func tagKeyStatusToKRM(in *pb.TagKey, out *krm.TagsTagKeyStatus) error {
out.NamespacedName = &in.NamespacedName
// TODO: Should be metav1.Time (?)
out.CreateTime = timeToKRMString(in.GetCreateTime())
out.UpdateTime = timeToKRMString(in.GetUpdateTime())
return nil
}

func timeToKRMString(t *timestamppb.Timestamp) *string {
if t == nil {
return nil
}
s := t.AsTime().Format(time.RFC3339Nano)
return &s
}

// Update implements the Adapter interface.
func (a *tagKeyAdapter) Update(ctx context.Context, u *unstructured.Unstructured) error {
// TODO: Skip updates at the higher level if no changes?
updateMask := &fieldmaskpb.FieldMask{}
update := &pb.TagKey{}
update.Name = a.fullyQualifiedName()

// description is the only field that can be updated
if ValueOf(a.desired.Spec.Description) != a.actual.GetDescription() {
updateMask.Paths = append(updateMask.Paths, "description")
update.Description = ValueOf(a.desired.Spec.Description)
}

// TODO: Where/how do we want to enforce immutability?

if len(updateMask.Paths) != 0 {
req := &pb.UpdateTagKeyRequest{
TagKey: update,
UpdateMask: updateMask,
}

op, err := a.tagKeysClient.UpdateTagKey(ctx, req)
if err != nil {
return err
}

if _, err := op.Wait(ctx); err != nil {
return fmt.Errorf("tagKey update failed: %w", err)
}
// TODO: Do we need to check that the operation succeeeded?
}

// TODO: Return updated object status
return nil
}

func (a *tagKeyAdapter) fullyQualifiedName() string {
return fmt.Sprintf("tagKeys/%s", a.resourceID)
}
Loading

0 comments on commit b8aa5ac

Please sign in to comment.