diff --git a/apis/changelogs/proto/v1alpha1/changelog.pb.go b/apis/changelogs/proto/v1alpha1/changelog.pb.go new file mode 100644 index 000000000..a4640d80f --- /dev/null +++ b/apis/changelogs/proto/v1alpha1/changelog.pb.go @@ -0,0 +1,499 @@ +// +//Copyright 2024 The Crossplane Authors. +//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. + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.33.0 +// protoc (unknown) +// source: changelogs/proto/v1alpha1/changelog.proto + +// buf:lint:ignore PACKAGE_DIRECTORY_MATCH + +package v1alpha1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + structpb "google.golang.org/protobuf/types/known/structpb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// OperationType represents the type of operation that was performed on a +// resource. +type OperationType int32 + +const ( + OperationType_OPERATION_TYPE_UNSPECIFIED OperationType = 0 + OperationType_OPERATION_TYPE_CREATE OperationType = 1 + OperationType_OPERATION_TYPE_UPDATE OperationType = 2 + OperationType_OPERATION_TYPE_DELETE OperationType = 3 +) + +// Enum value maps for OperationType. +var ( + OperationType_name = map[int32]string{ + 0: "OPERATION_TYPE_UNSPECIFIED", + 1: "OPERATION_TYPE_CREATE", + 2: "OPERATION_TYPE_UPDATE", + 3: "OPERATION_TYPE_DELETE", + } + OperationType_value = map[string]int32{ + "OPERATION_TYPE_UNSPECIFIED": 0, + "OPERATION_TYPE_CREATE": 1, + "OPERATION_TYPE_UPDATE": 2, + "OPERATION_TYPE_DELETE": 3, + } +) + +func (x OperationType) Enum() *OperationType { + p := new(OperationType) + *p = x + return p +} + +func (x OperationType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (OperationType) Descriptor() protoreflect.EnumDescriptor { + return file_changelogs_proto_v1alpha1_changelog_proto_enumTypes[0].Descriptor() +} + +func (OperationType) Type() protoreflect.EnumType { + return &file_changelogs_proto_v1alpha1_changelog_proto_enumTypes[0] +} + +func (x OperationType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use OperationType.Descriptor instead. +func (OperationType) EnumDescriptor() ([]byte, []int) { + return file_changelogs_proto_v1alpha1_changelog_proto_rawDescGZIP(), []int{0} +} + +// SendChangeLogRequest represents a request to send a single change log entry. +type SendChangeLogRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The change log entry to send as part of this request. + Entry *ChangeLogEntry `protobuf:"bytes,1,opt,name=entry,proto3" json:"entry,omitempty"` +} + +func (x *SendChangeLogRequest) Reset() { + *x = SendChangeLogRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_changelogs_proto_v1alpha1_changelog_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SendChangeLogRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SendChangeLogRequest) ProtoMessage() {} + +func (x *SendChangeLogRequest) ProtoReflect() protoreflect.Message { + mi := &file_changelogs_proto_v1alpha1_changelog_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SendChangeLogRequest.ProtoReflect.Descriptor instead. +func (*SendChangeLogRequest) Descriptor() ([]byte, []int) { + return file_changelogs_proto_v1alpha1_changelog_proto_rawDescGZIP(), []int{0} +} + +func (x *SendChangeLogRequest) GetEntry() *ChangeLogEntry { + if x != nil { + return x.Entry + } + return nil +} + +// ChangeLogEntry represents a single change log entry, with detailed information +// about the resource that was changed. +type ChangeLogEntry struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The timestamp at which the change occurred. + Timestamp *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + // The name and version of the provider that is making the change to the + // resource. + Provider string `protobuf:"bytes,2,opt,name=provider,proto3" json:"provider,omitempty"` + // The API version of the resource that was changed, e.g. Group/Version. + ApiVersion string `protobuf:"bytes,3,opt,name=api_version,json=apiVersion,proto3" json:"api_version,omitempty"` + // The kind of the resource that was changed. + Kind string `protobuf:"bytes,4,opt,name=kind,proto3" json:"kind,omitempty"` + // The name of the resource that was changed. + Name string `protobuf:"bytes,5,opt,name=name,proto3" json:"name,omitempty"` + // The external name of the resource that was changed. + ExternalName string `protobuf:"bytes,6,opt,name=external_name,json=externalName,proto3" json:"external_name,omitempty"` + // The type of operation that was performed on the resource, e.g. Create, + // Update, or Delete. + Operation OperationType `protobuf:"varint,7,opt,name=operation,proto3,enum=changelogs.proto.v1alpha1.OperationType" json:"operation,omitempty"` + // A full snapshot of the resource's state, as observed directly before the + // resource was changed. + Snapshot *structpb.Struct `protobuf:"bytes,8,opt,name=snapshot,proto3" json:"snapshot,omitempty"` + // An optional error message that describes any error encountered while + // performing the operation on the resource. + ErrorMessage *string `protobuf:"bytes,9,opt,name=error_message,json=errorMessage,proto3,oneof" json:"error_message,omitempty"` + // An optional additional details that can be provided for further context + // about the change. + AdditionalDetails map[string]string `protobuf:"bytes,10,rep,name=additional_details,json=additionalDetails,proto3" json:"additional_details,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (x *ChangeLogEntry) Reset() { + *x = ChangeLogEntry{} + if protoimpl.UnsafeEnabled { + mi := &file_changelogs_proto_v1alpha1_changelog_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ChangeLogEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChangeLogEntry) ProtoMessage() {} + +func (x *ChangeLogEntry) ProtoReflect() protoreflect.Message { + mi := &file_changelogs_proto_v1alpha1_changelog_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ChangeLogEntry.ProtoReflect.Descriptor instead. +func (*ChangeLogEntry) Descriptor() ([]byte, []int) { + return file_changelogs_proto_v1alpha1_changelog_proto_rawDescGZIP(), []int{1} +} + +func (x *ChangeLogEntry) GetTimestamp() *timestamppb.Timestamp { + if x != nil { + return x.Timestamp + } + return nil +} + +func (x *ChangeLogEntry) GetProvider() string { + if x != nil { + return x.Provider + } + return "" +} + +func (x *ChangeLogEntry) GetApiVersion() string { + if x != nil { + return x.ApiVersion + } + return "" +} + +func (x *ChangeLogEntry) GetKind() string { + if x != nil { + return x.Kind + } + return "" +} + +func (x *ChangeLogEntry) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ChangeLogEntry) GetExternalName() string { + if x != nil { + return x.ExternalName + } + return "" +} + +func (x *ChangeLogEntry) GetOperation() OperationType { + if x != nil { + return x.Operation + } + return OperationType_OPERATION_TYPE_UNSPECIFIED +} + +func (x *ChangeLogEntry) GetSnapshot() *structpb.Struct { + if x != nil { + return x.Snapshot + } + return nil +} + +func (x *ChangeLogEntry) GetErrorMessage() string { + if x != nil && x.ErrorMessage != nil { + return *x.ErrorMessage + } + return "" +} + +func (x *ChangeLogEntry) GetAdditionalDetails() map[string]string { + if x != nil { + return x.AdditionalDetails + } + return nil +} + +// SendChangeLogResponse is the response returned by the ChangeLogService after +// a change log entry is sent. Currently, this is an empty message as the only +// useful information expected to sent back at this time will be through errors. +type SendChangeLogResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *SendChangeLogResponse) Reset() { + *x = SendChangeLogResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_changelogs_proto_v1alpha1_changelog_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SendChangeLogResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SendChangeLogResponse) ProtoMessage() {} + +func (x *SendChangeLogResponse) ProtoReflect() protoreflect.Message { + mi := &file_changelogs_proto_v1alpha1_changelog_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SendChangeLogResponse.ProtoReflect.Descriptor instead. +func (*SendChangeLogResponse) Descriptor() ([]byte, []int) { + return file_changelogs_proto_v1alpha1_changelog_proto_rawDescGZIP(), []int{2} +} + +var File_changelogs_proto_v1alpha1_changelog_proto protoreflect.FileDescriptor + +var file_changelogs_proto_v1alpha1_changelog_proto_rawDesc = []byte{ + 0x0a, 0x29, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x6c, 0x6f, 0x67, 0x73, 0x2f, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2f, 0x63, 0x68, 0x61, 0x6e, + 0x67, 0x65, 0x6c, 0x6f, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x19, 0x63, 0x68, 0x61, + 0x6e, 0x67, 0x65, 0x6c, 0x6f, 0x67, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x76, 0x31, + 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x57, 0x0a, 0x14, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x68, 0x61, + 0x6e, 0x67, 0x65, 0x4c, 0x6f, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3f, 0x0a, + 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x63, + 0x68, 0x61, 0x6e, 0x67, 0x65, 0x6c, 0x6f, 0x67, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, + 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4c, + 0x6f, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x22, 0xc4, + 0x04, 0x0a, 0x0e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4c, 0x6f, 0x67, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x70, + 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, + 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x70, 0x69, 0x5f, 0x76, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x70, + 0x69, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x69, 0x6e, 0x64, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x12, 0x12, 0x0a, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x46, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x28, 0x2e, 0x63, 0x68, 0x61, 0x6e, 0x67, + 0x65, 0x6c, 0x6f, 0x67, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x76, 0x31, 0x61, 0x6c, + 0x70, 0x68, 0x61, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, + 0x70, 0x65, 0x52, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x33, 0x0a, + 0x08, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x08, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, + 0x6f, 0x74, 0x12, 0x28, 0x0a, 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0c, 0x65, 0x72, 0x72, + 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x88, 0x01, 0x01, 0x12, 0x6f, 0x0a, 0x12, + 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x64, 0x65, 0x74, 0x61, 0x69, + 0x6c, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x40, 0x2e, 0x63, 0x68, 0x61, 0x6e, 0x67, + 0x65, 0x6c, 0x6f, 0x67, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x76, 0x31, 0x61, 0x6c, + 0x70, 0x68, 0x61, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4c, 0x6f, 0x67, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x2e, 0x41, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x44, 0x65, + 0x74, 0x61, 0x69, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x11, 0x61, 0x64, 0x64, 0x69, + 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x1a, 0x44, 0x0a, + 0x16, 0x41, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x44, 0x65, 0x74, 0x61, 0x69, + 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, + 0x02, 0x38, 0x01, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x17, 0x0a, 0x15, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x68, 0x61, + 0x6e, 0x67, 0x65, 0x4c, 0x6f, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2a, 0x80, + 0x01, 0x0a, 0x0d, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, + 0x12, 0x1e, 0x0a, 0x1a, 0x4f, 0x50, 0x45, 0x52, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, + 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, + 0x12, 0x19, 0x0a, 0x15, 0x4f, 0x50, 0x45, 0x52, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, + 0x50, 0x45, 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x10, 0x01, 0x12, 0x19, 0x0a, 0x15, 0x4f, + 0x50, 0x45, 0x52, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x50, + 0x44, 0x41, 0x54, 0x45, 0x10, 0x02, 0x12, 0x19, 0x0a, 0x15, 0x4f, 0x50, 0x45, 0x52, 0x41, 0x54, + 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x10, + 0x03, 0x32, 0x88, 0x01, 0x0a, 0x10, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4c, 0x6f, 0x67, 0x53, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x74, 0x0a, 0x0d, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x68, + 0x61, 0x6e, 0x67, 0x65, 0x4c, 0x6f, 0x67, 0x12, 0x2f, 0x2e, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, + 0x6c, 0x6f, 0x67, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, + 0x68, 0x61, 0x31, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4c, 0x6f, + 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, 0x63, 0x68, 0x61, 0x6e, 0x67, + 0x65, 0x6c, 0x6f, 0x67, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x76, 0x31, 0x61, 0x6c, + 0x70, 0x68, 0x61, 0x31, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4c, + 0x6f, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x49, 0x5a, 0x47, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x72, 0x6f, 0x73, 0x73, + 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2f, 0x63, 0x72, 0x6f, 0x73, 0x73, 0x70, 0x6c, 0x61, 0x6e, 0x65, + 0x2d, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x73, 0x2f, 0x63, 0x68, + 0x61, 0x6e, 0x67, 0x65, 0x6c, 0x6f, 0x67, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, + 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_changelogs_proto_v1alpha1_changelog_proto_rawDescOnce sync.Once + file_changelogs_proto_v1alpha1_changelog_proto_rawDescData = file_changelogs_proto_v1alpha1_changelog_proto_rawDesc +) + +func file_changelogs_proto_v1alpha1_changelog_proto_rawDescGZIP() []byte { + file_changelogs_proto_v1alpha1_changelog_proto_rawDescOnce.Do(func() { + file_changelogs_proto_v1alpha1_changelog_proto_rawDescData = protoimpl.X.CompressGZIP(file_changelogs_proto_v1alpha1_changelog_proto_rawDescData) + }) + return file_changelogs_proto_v1alpha1_changelog_proto_rawDescData +} + +var file_changelogs_proto_v1alpha1_changelog_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_changelogs_proto_v1alpha1_changelog_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_changelogs_proto_v1alpha1_changelog_proto_goTypes = []interface{}{ + (OperationType)(0), // 0: changelogs.proto.v1alpha1.OperationType + (*SendChangeLogRequest)(nil), // 1: changelogs.proto.v1alpha1.SendChangeLogRequest + (*ChangeLogEntry)(nil), // 2: changelogs.proto.v1alpha1.ChangeLogEntry + (*SendChangeLogResponse)(nil), // 3: changelogs.proto.v1alpha1.SendChangeLogResponse + nil, // 4: changelogs.proto.v1alpha1.ChangeLogEntry.AdditionalDetailsEntry + (*timestamppb.Timestamp)(nil), // 5: google.protobuf.Timestamp + (*structpb.Struct)(nil), // 6: google.protobuf.Struct +} +var file_changelogs_proto_v1alpha1_changelog_proto_depIdxs = []int32{ + 2, // 0: changelogs.proto.v1alpha1.SendChangeLogRequest.entry:type_name -> changelogs.proto.v1alpha1.ChangeLogEntry + 5, // 1: changelogs.proto.v1alpha1.ChangeLogEntry.timestamp:type_name -> google.protobuf.Timestamp + 0, // 2: changelogs.proto.v1alpha1.ChangeLogEntry.operation:type_name -> changelogs.proto.v1alpha1.OperationType + 6, // 3: changelogs.proto.v1alpha1.ChangeLogEntry.snapshot:type_name -> google.protobuf.Struct + 4, // 4: changelogs.proto.v1alpha1.ChangeLogEntry.additional_details:type_name -> changelogs.proto.v1alpha1.ChangeLogEntry.AdditionalDetailsEntry + 1, // 5: changelogs.proto.v1alpha1.ChangeLogService.SendChangeLog:input_type -> changelogs.proto.v1alpha1.SendChangeLogRequest + 3, // 6: changelogs.proto.v1alpha1.ChangeLogService.SendChangeLog:output_type -> changelogs.proto.v1alpha1.SendChangeLogResponse + 6, // [6:7] is the sub-list for method output_type + 5, // [5:6] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name +} + +func init() { file_changelogs_proto_v1alpha1_changelog_proto_init() } +func file_changelogs_proto_v1alpha1_changelog_proto_init() { + if File_changelogs_proto_v1alpha1_changelog_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_changelogs_proto_v1alpha1_changelog_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SendChangeLogRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_changelogs_proto_v1alpha1_changelog_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ChangeLogEntry); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_changelogs_proto_v1alpha1_changelog_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SendChangeLogResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_changelogs_proto_v1alpha1_changelog_proto_msgTypes[1].OneofWrappers = []interface{}{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_changelogs_proto_v1alpha1_changelog_proto_rawDesc, + NumEnums: 1, + NumMessages: 4, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_changelogs_proto_v1alpha1_changelog_proto_goTypes, + DependencyIndexes: file_changelogs_proto_v1alpha1_changelog_proto_depIdxs, + EnumInfos: file_changelogs_proto_v1alpha1_changelog_proto_enumTypes, + MessageInfos: file_changelogs_proto_v1alpha1_changelog_proto_msgTypes, + }.Build() + File_changelogs_proto_v1alpha1_changelog_proto = out.File + file_changelogs_proto_v1alpha1_changelog_proto_rawDesc = nil + file_changelogs_proto_v1alpha1_changelog_proto_goTypes = nil + file_changelogs_proto_v1alpha1_changelog_proto_depIdxs = nil +} diff --git a/apis/changelogs/proto/v1alpha1/changelog.proto b/apis/changelogs/proto/v1alpha1/changelog.proto new file mode 100644 index 000000000..c7ec1a98f --- /dev/null +++ b/apis/changelogs/proto/v1alpha1/changelog.proto @@ -0,0 +1,88 @@ +/* +Copyright 2024 The Crossplane Authors. +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. +*/ + +syntax = "proto3"; + +import "google/protobuf/struct.proto"; +import "google/protobuf/timestamp.proto"; + +// buf:lint:ignore PACKAGE_DIRECTORY_MATCH +package changelogs.proto.v1alpha1; + +option go_package = "github.com/crossplane/crossplane-runtime/apis/changelogs/proto/v1alpha1"; + +// ChangeLogService is a service that provides the ability to send change log +// entries. +service ChangeLogService { + // SendChangeLog sends a change log entry to the change log service. + rpc SendChangeLog (SendChangeLogRequest) returns (SendChangeLogResponse) {} +} + +// SendChangeLogRequest represents a request to send a single change log entry. +message SendChangeLogRequest { + // The change log entry to send as part of this request. + ChangeLogEntry entry = 1; +} + +// ChangeLogEntry represents a single change log entry, with detailed information +// about the resource that was changed. +message ChangeLogEntry { + // The timestamp at which the change occurred. + google.protobuf.Timestamp timestamp = 1; + + // The name and version of the provider that is making the change to the + // resource. + string provider = 2; + + // The API version of the resource that was changed, e.g. Group/Version. + string api_version = 3; + + // The kind of the resource that was changed. + string kind = 4; + + // The name of the resource that was changed. + string name = 5; + + // The external name of the resource that was changed. + string external_name = 6; + + // The type of operation that was performed on the resource, e.g. Create, + // Update, or Delete. + OperationType operation = 7; + + // A full snapshot of the resource's state, as observed directly before the + // resource was changed. + google.protobuf.Struct snapshot = 8; + + // An optional error message that describes any error encountered while + // performing the operation on the resource. + optional string error_message = 9; + + // An optional additional details that can be provided for further context + // about the change. + map additional_details = 10; +} + +// OperationType represents the type of operation that was performed on a +// resource. +enum OperationType { + OPERATION_TYPE_UNSPECIFIED = 0; + OPERATION_TYPE_CREATE = 1; + OPERATION_TYPE_UPDATE = 2; + OPERATION_TYPE_DELETE = 3; +} + +// SendChangeLogResponse is the response returned by the ChangeLogService after +// a change log entry is sent. Currently, this is an empty message as the only +// useful information expected to sent back at this time will be through errors. +message SendChangeLogResponse {} \ No newline at end of file diff --git a/apis/changelogs/proto/v1alpha1/changelog_grpc.pb.go b/apis/changelogs/proto/v1alpha1/changelog_grpc.pb.go new file mode 100644 index 000000000..b90705e2a --- /dev/null +++ b/apis/changelogs/proto/v1alpha1/changelog_grpc.pb.go @@ -0,0 +1,125 @@ +// +//Copyright 2024 The Crossplane Authors. +//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. + +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.3.0 +// - protoc (unknown) +// source: changelogs/proto/v1alpha1/changelog.proto + +// buf:lint:ignore PACKAGE_DIRECTORY_MATCH + +package v1alpha1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +const ( + ChangeLogService_SendChangeLog_FullMethodName = "/changelogs.proto.v1alpha1.ChangeLogService/SendChangeLog" +) + +// ChangeLogServiceClient is the client API for ChangeLogService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type ChangeLogServiceClient interface { + // SendChangeLog sends a change log entry to the change log service. + SendChangeLog(ctx context.Context, in *SendChangeLogRequest, opts ...grpc.CallOption) (*SendChangeLogResponse, error) +} + +type changeLogServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewChangeLogServiceClient(cc grpc.ClientConnInterface) ChangeLogServiceClient { + return &changeLogServiceClient{cc} +} + +func (c *changeLogServiceClient) SendChangeLog(ctx context.Context, in *SendChangeLogRequest, opts ...grpc.CallOption) (*SendChangeLogResponse, error) { + out := new(SendChangeLogResponse) + err := c.cc.Invoke(ctx, ChangeLogService_SendChangeLog_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// ChangeLogServiceServer is the server API for ChangeLogService service. +// All implementations must embed UnimplementedChangeLogServiceServer +// for forward compatibility +type ChangeLogServiceServer interface { + // SendChangeLog sends a change log entry to the change log service. + SendChangeLog(context.Context, *SendChangeLogRequest) (*SendChangeLogResponse, error) + mustEmbedUnimplementedChangeLogServiceServer() +} + +// UnimplementedChangeLogServiceServer must be embedded to have forward compatible implementations. +type UnimplementedChangeLogServiceServer struct { +} + +func (UnimplementedChangeLogServiceServer) SendChangeLog(context.Context, *SendChangeLogRequest) (*SendChangeLogResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method SendChangeLog not implemented") +} +func (UnimplementedChangeLogServiceServer) mustEmbedUnimplementedChangeLogServiceServer() {} + +// UnsafeChangeLogServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ChangeLogServiceServer will +// result in compilation errors. +type UnsafeChangeLogServiceServer interface { + mustEmbedUnimplementedChangeLogServiceServer() +} + +func RegisterChangeLogServiceServer(s grpc.ServiceRegistrar, srv ChangeLogServiceServer) { + s.RegisterService(&ChangeLogService_ServiceDesc, srv) +} + +func _ChangeLogService_SendChangeLog_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SendChangeLogRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ChangeLogServiceServer).SendChangeLog(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ChangeLogService_SendChangeLog_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ChangeLogServiceServer).SendChangeLog(ctx, req.(*SendChangeLogRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// ChangeLogService_ServiceDesc is the grpc.ServiceDesc for ChangeLogService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var ChangeLogService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "changelogs.proto.v1alpha1.ChangeLogService", + HandlerType: (*ChangeLogServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "SendChangeLog", + Handler: _ChangeLogService_SendChangeLog_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "changelogs/proto/v1alpha1/changelog.proto", +} diff --git a/go.mod b/go.mod index 18cf15fb3..63943cffa 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( k8s.io/client-go v0.30.0 k8s.io/component-base v0.30.0 k8s.io/klog/v2 v2.120.1 + k8s.io/utils v0.0.0-20230726121419-3b25d923346b sigs.k8s.io/controller-runtime v0.18.2 sigs.k8s.io/controller-tools v0.14.0 sigs.k8s.io/yaml v1.4.0 @@ -80,7 +81,6 @@ require ( gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect - k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect ) diff --git a/pkg/controller/options.go b/pkg/controller/options.go index ce9fef5d8..5872c90f9 100644 --- a/pkg/controller/options.go +++ b/pkg/controller/options.go @@ -67,6 +67,9 @@ type Options struct { // MetricOptions for recording metrics. MetricOptions *MetricOptions + + // ChangeLogOptions for recording change logs. + ChangeLogOptions *ChangeLogOptions } // ForControllerRuntime extracts options for controller-runtime. @@ -97,3 +100,9 @@ type MetricOptions struct { // MRStateMetrics to use for recording state metrics. MRStateMetrics *statemetrics.MRStateMetrics } + +// ChangeLogOptions for recording changes to managed resources into the change +// logs. +type ChangeLogOptions struct { + ChangeLogger managed.ChangeLogger +} diff --git a/pkg/feature/features.go b/pkg/feature/features.go index 7a93d22e6..c4b3f7727 100644 --- a/pkg/feature/features.go +++ b/pkg/feature/features.go @@ -20,3 +20,8 @@ package feature // Management Policies. See the below design for more details. // https://github.com/crossplane/crossplane/pull/3531 const EnableBetaManagementPolicies Flag = "EnableBetaManagementPolicies" + +// EnableAlphaChangeLogs enables alpha support for capturing change logs during +// reconciliation. See the following design for more details: +// https://github.com/crossplane/crossplane/pull/5822 +const EnableAlphaChangeLogs Flag = "EnableAlphaChangeLogs" diff --git a/pkg/reconciler/managed/changelogger.go b/pkg/reconciler/managed/changelogger.go new file mode 100644 index 000000000..050ff5883 --- /dev/null +++ b/pkg/reconciler/managed/changelogger.go @@ -0,0 +1,135 @@ +/* +Copyright 2024 The Crossplane Authors. + +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 managed + +import ( + "context" + "time" + + "google.golang.org/grpc" + "google.golang.org/protobuf/types/known/timestamppb" + "k8s.io/utils/ptr" + + "github.com/crossplane/crossplane-runtime/apis/changelogs/proto/v1alpha1" + "github.com/crossplane/crossplane-runtime/pkg/errors" + "github.com/crossplane/crossplane-runtime/pkg/meta" + "github.com/crossplane/crossplane-runtime/pkg/resource" +) + +const ( + defaultSendTimeout = 10 * time.Second +) + +// ChangeLogger is an interface for recording changes made to resources to the +// change logs. +type ChangeLogger interface { + Log(ctx context.Context, managed resource.Managed, opType v1alpha1.OperationType, changeErr error, ad AdditionalDetails) error +} + +// GRPCChangeLogger processes changes to resources and helps to send them to the +// change log gRPC service. +type GRPCChangeLogger struct { + client v1alpha1.ChangeLogServiceClient + providerVersion string + sendTimeout time.Duration +} + +// NewGRPCChangeLogger creates a new gRPC based ChangeLogger initialized with +// the given client. +func NewGRPCChangeLogger(client v1alpha1.ChangeLogServiceClient, o ...GRPCChangeLoggerOption) *GRPCChangeLogger { + g := &GRPCChangeLogger{ + client: client, + sendTimeout: defaultSendTimeout, + } + + for _, clo := range o { + clo(g) + } + + return g +} + +// A GRPCChangeLoggerOption configures a GRPCChangeLoggerOption. +type GRPCChangeLoggerOption func(*GRPCChangeLogger) + +// WithProviderVersion sets the provider version to be included in the change +// log entry. +func WithProviderVersion(version string) GRPCChangeLoggerOption { + return func(g *GRPCChangeLogger) { + g.providerVersion = version + } +} + +// WithSendTimeout sets the timeout for sending and/or waiting for change log +// entries to the change log service. +func WithSendTimeout(timeout time.Duration) GRPCChangeLoggerOption { + return func(g *GRPCChangeLogger) { + g.sendTimeout = timeout + } +} + +// Log sends the given change log entry to the change log service. +func (g *GRPCChangeLogger) Log(ctx context.Context, managed resource.Managed, opType v1alpha1.OperationType, changeErr error, ad AdditionalDetails) error { + // get an error message from the error if it exists + var changeErrMessage *string + if changeErr != nil { + changeErrMessage = ptr.To(changeErr.Error()) + } + + // capture the full state of the managed resource from before we performed the change + snapshot, err := resource.AsProtobufStruct(managed) + if err != nil { + return errors.Wrap(err, "cannot snapshot managed resource") + } + + gvk := managed.GetObjectKind().GroupVersionKind() + + entry := &v1alpha1.ChangeLogEntry{ + Timestamp: timestamppb.Now(), + Provider: g.providerVersion, + ApiVersion: gvk.GroupVersion().String(), + Kind: gvk.Kind, + Name: managed.GetName(), + ExternalName: meta.GetExternalName(managed), + Operation: opType, + Snapshot: snapshot, + ErrorMessage: changeErrMessage, + AdditionalDetails: ad, + } + + // create a specific context and timeout for sending the change log entry + // that is different than the parent context that is for the entire + // reconciliation + sendCtx, sendCancel := context.WithTimeout(ctx, g.sendTimeout) + defer sendCancel() + + // send everything we've got to the change log service + _, err = g.client.SendChangeLog(sendCtx, &v1alpha1.SendChangeLogRequest{Entry: entry}, grpc.WaitForReady(true)) + return errors.Wrap(err, "cannot send change log entry") +} + +// nopChangeLogger does nothing for recording change logs, this is the default +// implementation if a provider has not enabled the change logs feature. +type nopChangeLogger struct{} + +func newNopChangeLogger() *nopChangeLogger { + return &nopChangeLogger{} +} + +func (n *nopChangeLogger) Log(_ context.Context, _ resource.Managed, _ v1alpha1.OperationType, _ error, _ AdditionalDetails) error { + return nil +} diff --git a/pkg/reconciler/managed/changelogger_test.go b/pkg/reconciler/managed/changelogger_test.go new file mode 100644 index 000000000..5b4d56279 --- /dev/null +++ b/pkg/reconciler/managed/changelogger_test.go @@ -0,0 +1,197 @@ +/* +Copyright 2024 The Crossplane Authors. + +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 managed + +import ( + "context" + "reflect" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "google.golang.org/grpc" + "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/utils/ptr" + + "github.com/crossplane/crossplane-runtime/apis/changelogs/proto/v1alpha1" + "github.com/crossplane/crossplane-runtime/pkg/errors" + "github.com/crossplane/crossplane-runtime/pkg/meta" + "github.com/crossplane/crossplane-runtime/pkg/resource" + "github.com/crossplane/crossplane-runtime/pkg/resource/fake" + "github.com/crossplane/crossplane-runtime/pkg/test" +) + +// A mock implementation of the ChangeLogServiceClient interface to help with +// testing and verifying change log entries. +type changeLogServiceClient struct { + requests []*v1alpha1.SendChangeLogRequest + sendFn func(ctx context.Context, in *v1alpha1.SendChangeLogRequest, opts ...grpc.CallOption) (*v1alpha1.SendChangeLogResponse, error) +} + +func (c *changeLogServiceClient) SendChangeLog(ctx context.Context, in *v1alpha1.SendChangeLogRequest, opts ...grpc.CallOption) (*v1alpha1.SendChangeLogResponse, error) { + c.requests = append(c.requests, in) + if c.sendFn != nil { + return c.sendFn(ctx, in, opts...) + } + return nil, nil +} + +func TestChangeLogger(t *testing.T) { + type args struct { + mr resource.Managed + ad AdditionalDetails + err error + c *changeLogServiceClient + } + + type want struct { + requests []*v1alpha1.SendChangeLogRequest + err error + } + + errBoom := errors.New("boom") + + cases := map[string]struct { + reason string + args args + want want + }{ + "ChangeLogsSuccess": { + reason: "Change log entry should be recorded successfully.", + args: args{ + mr: &fake.Managed{ObjectMeta: metav1.ObjectMeta{ + Name: "cool-managed", + Annotations: map[string]string{meta.AnnotationKeyExternalName: "cool-managed"}, + }}, + err: errBoom, + ad: AdditionalDetails{"key": "value", "key2": "value2"}, + c: &changeLogServiceClient{requests: []*v1alpha1.SendChangeLogRequest{}}, + }, + want: want{ + // a well fleshed out change log entry should be sent + requests: []*v1alpha1.SendChangeLogRequest{ + { + Entry: &v1alpha1.ChangeLogEntry{ + Timestamp: timestamppb.Now(), + Provider: "provider-cool:v9.99.999", + ApiVersion: (&fake.Managed{}).GetObjectKind().GroupVersionKind().GroupVersion().String(), + Kind: (&fake.Managed{}).GetObjectKind().GroupVersionKind().Kind, + Name: "cool-managed", + ExternalName: "cool-managed", + Operation: v1alpha1.OperationType_OPERATION_TYPE_CREATE, + Snapshot: mustObjectAsProtobufStruct(&fake.Managed{ObjectMeta: metav1.ObjectMeta{ + Name: "cool-managed", + Annotations: map[string]string{meta.AnnotationKeyExternalName: "cool-managed"}, + }}), + ErrorMessage: ptr.To("boom"), + AdditionalDetails: AdditionalDetails{"key": "value", "key2": "value2"}, + }, + }, + }, + }, + }, + "SendChangeLogsFailure": { + reason: "Error from sending change log entry should be handled and recorded.", + args: args{ + mr: &fake.Managed{}, + c: &changeLogServiceClient{ + requests: []*v1alpha1.SendChangeLogRequest{}, + // make the send change log function return an error + sendFn: func(_ context.Context, _ *v1alpha1.SendChangeLogRequest, _ ...grpc.CallOption) (*v1alpha1.SendChangeLogResponse, error) { + return &v1alpha1.SendChangeLogResponse{}, errBoom + }, + }, + }, + want: want{ + // we'll still see a change log entry, but it won't make it all + // the way to its destination and we should see an event for + // that failure + requests: []*v1alpha1.SendChangeLogRequest{ + { + Entry: &v1alpha1.ChangeLogEntry{ + // we expect less fields to be set on the change log + // entry because we're not initializing the managed + // resource with much data in this simulated failure + // test case + Timestamp: timestamppb.Now(), + Provider: "provider-cool:v9.99.999", + ApiVersion: (&fake.Managed{}).GetObjectKind().GroupVersionKind().GroupVersion().String(), + Kind: (&fake.Managed{}).GetObjectKind().GroupVersionKind().Kind, + Operation: v1alpha1.OperationType_OPERATION_TYPE_CREATE, + Snapshot: mustObjectAsProtobufStruct(&fake.Managed{}), + }, + }, + }, + err: errors.Wrap(errBoom, "cannot send change log entry"), + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + change := NewGRPCChangeLogger(tc.args.c, WithProviderVersion("provider-cool:v9.99.999")) + err := change.Log(context.Background(), tc.args.mr, v1alpha1.OperationType_OPERATION_TYPE_CREATE, tc.args.err, tc.args.ad) + + if diff := cmp.Diff(tc.want.requests, tc.args.c.requests, equateApproxTimepb(time.Second)...); diff != "" { + t.Errorf("\nReason: %s\nr.RecordChangeLog(...): -want requests, +got requests:\n%s", tc.reason, diff) + } + + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\nReason: %s\nr.RecordChangeLog(...): -want error, +got error:\n%s", tc.reason, diff) + } + }) + } +} + +func mustObjectAsProtobufStruct(o runtime.Object) *structpb.Struct { + s, err := resource.AsProtobufStruct(o) + if err != nil { + panic(err) + } + return s +} + +// A set of cmp.Option that enables usage of cmpopts.EquateApproxTime for +// timestamppb.Timestamp types. +// Source: https://github.com/golang/protobuf/issues/1347 +func equateApproxTimepb(margin time.Duration) []cmp.Option { + return cmp.Options{ + cmpopts.EquateApproxTime(margin), + protocmp.Transform(), + cmp.FilterPath( + func(p cmp.Path) bool { + if p.Last().Type() == reflect.TypeOf(protocmp.Message{}) { + a, b := p.Last().Values() + return msgIsTimestamp(a) && msgIsTimestamp(b) + } + return false + }, + cmp.Transformer("timestamppb", func(t protocmp.Message) time.Time { + return time.Unix(t["seconds"].(int64), int64(t["nanos"].(int32))).UTC() + }), + ), + } +} + +func msgIsTimestamp(x reflect.Value) bool { + return x.Interface().(protocmp.Message).Descriptor().FullName() == "google.protobuf.Timestamp" +} diff --git a/pkg/reconciler/managed/reconciler.go b/pkg/reconciler/managed/reconciler.go index b479f72e5..23b94c708 100644 --- a/pkg/reconciler/managed/reconciler.go +++ b/pkg/reconciler/managed/reconciler.go @@ -29,6 +29,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/reconcile" + "github.com/crossplane/crossplane-runtime/apis/changelogs/proto/v1alpha1" xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" "github.com/crossplane/crossplane-runtime/pkg/errors" "github.com/crossplane/crossplane-runtime/pkg/event" @@ -63,6 +64,7 @@ const ( errReconcileCreate = "create failed" errReconcileUpdate = "update failed" errReconcileDelete = "delete failed" + errRecordChangeLog = "cannot record change log entry" errExternalResourceNotExist = "external resource does not exist" ) @@ -138,6 +140,11 @@ func (fn CriticalAnnotationUpdateFn) UpdateCriticalAnnotations(ctx context.Conte // resource, for example usernames, passwords, endpoints, ports, etc. type ConnectionDetails map[string][]byte +// AdditionalDetails represent any additional details the external client wants +// to return about an operation that has been performed. These details will be +// included in the change logs. +type AdditionalDetails map[string]string + // A ConnectionPublisher manages the supplied ConnectionDetails for the // supplied Managed resource. ManagedPublishers must handle the case in which // the supplied ConnectionDetails are empty. @@ -332,7 +339,7 @@ type ExternalClient interface { // Delete the external resource upon deletion of its associated Managed // resource. Called when the managed resource has been deleted. - Delete(ctx context.Context, mg resource.Managed) error + Delete(ctx context.Context, mg resource.Managed) (ExternalDelete, error) // Disconnect from the provider and close the ExternalClient. // Called at the end of reconcile loop. An ExternalClient not requiring @@ -347,7 +354,7 @@ type ExternalClientFns struct { ObserveFn func(ctx context.Context, mg resource.Managed) (ExternalObservation, error) CreateFn func(ctx context.Context, mg resource.Managed) (ExternalCreation, error) UpdateFn func(ctx context.Context, mg resource.Managed) (ExternalUpdate, error) - DeleteFn func(ctx context.Context, mg resource.Managed) error + DeleteFn func(ctx context.Context, mg resource.Managed) (ExternalDelete, error) DisconnectFn func(ctx context.Context) error } @@ -371,7 +378,7 @@ func (e ExternalClientFns) Update(ctx context.Context, mg resource.Managed) (Ext // Delete the external resource upon deletion of its associated Managed // resource. -func (e ExternalClientFns) Delete(ctx context.Context, mg resource.Managed) error { +func (e ExternalClientFns) Delete(ctx context.Context, mg resource.Managed) (ExternalDelete, error) { return e.DeleteFn(ctx, mg) } @@ -407,7 +414,9 @@ func (c *NopClient) Update(_ context.Context, _ resource.Managed) (ExternalUpdat } // Delete does nothing. It never returns an error. -func (c *NopClient) Delete(_ context.Context, _ resource.Managed) error { return nil } +func (c *NopClient) Delete(_ context.Context, _ resource.Managed) (ExternalDelete, error) { + return ExternalDelete{}, nil +} // Disconnect does nothing. It never returns an error. func (c *NopClient) Disconnect(_ context.Context) error { return nil } @@ -466,6 +475,10 @@ type ExternalCreation struct { // unless an existing key is overwritten. Crossplane may publish these // credentials to a store (e.g. a Secret). ConnectionDetails ConnectionDetails + + // AdditionalDetails represent any additional details the external client + // wants to return about the creation operation that was performed. + AdditionalDetails AdditionalDetails } // An ExternalUpdate is the result of an update to an external resource. @@ -476,6 +489,17 @@ type ExternalUpdate struct { // unless an existing key is overwritten. Crossplane may publish these // credentials to a store (e.g. a Secret). ConnectionDetails ConnectionDetails + + // AdditionalDetails represent any additional details the external client + // wants to return about the update operation that was performed. + AdditionalDetails AdditionalDetails +} + +// An ExternalDelete is the result of a deletion of an external resource. +type ExternalDelete struct { + // AdditionalDetails represent any additional details the external client + // wants to return about the delete operation that was performed. + AdditionalDetails AdditionalDetails } // A Reconciler reconciles managed resources by creating and managing the @@ -506,6 +530,7 @@ type Reconciler struct { log logging.Logger record event.Recorder metricRecorder MetricRecorder + change ChangeLogger } type mrManaged struct { @@ -700,6 +725,14 @@ func WithReconcilerSupportedManagementPolicies(supported []sets.Set[xpv1.Managem } } +// WithChangeLogger enables support for capturing change logs during +// reconciliation. +func WithChangeLogger(c ChangeLogger) ReconcilerOption { + return func(r *Reconciler) { + r.change = c + } +} + // NewReconciler returns a Reconciler that reconciles managed resources of the // supplied ManagedKind with resources in an external system such as a cloud // provider API. It panics if asked to reconcile a managed resource kind that is @@ -730,6 +763,7 @@ func NewReconciler(m manager.Manager, of resource.ManagedKind, o ...ReconcilerOp log: logging.NewNopLogger(), record: event.NewNopRecorder(), metricRecorder: NewNopMetricRecorder(), + change: newNopChangeLogger(), } for _, ro := range o { @@ -972,11 +1006,18 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu return reconcile.Result{Requeue: true}, nil } + // deep copy the managed resource now that we've called Observe() and have + // not performed any external operations - we can use this as the + // pre-operation managed resource state in the change logs later + //nolint:forcetypeassert // managed.DeepCopyObject() will always be a resource.Managed. + managedPreOp := managed.DeepCopyObject().(resource.Managed) + if meta.WasDeleted(managed) { log = log.WithValues("deletion-timestamp", managed.GetDeletionTimestamp()) if observation.ResourceExists && policy.ShouldDelete() { - if err := external.Delete(externalCtx, managed); err != nil { + deletion, err := external.Delete(externalCtx, managed) + if err != nil { // We'll hit this condition if we can't delete our external // resource, for example if our provider credentials don't have // access to delete it. If this is the first time we encounter @@ -984,6 +1025,9 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu // status with the new error condition. If not, we want requeue // explicitly, which will trigger backoff. log.Debug("Cannot delete external resource", "error", err) + if err := r.change.Log(ctx, managedPreOp, v1alpha1.OperationType_OPERATION_TYPE_DELETE, err, deletion.AdditionalDetails); err != nil { + log.Info(errRecordChangeLog, "error", err) + } record.Event(managed, event.Warning(reasonCannotDelete, err)) managed.SetConditions(xpv1.Deleting(), xpv1.ReconcileError(errors.Wrap(err, errReconcileDelete))) return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus) @@ -997,6 +1041,9 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu // unpublish and finalize. If it still exists we'll re-enter this // block and try again. log.Debug("Successfully requested deletion of external resource") + if err := r.change.Log(ctx, managedPreOp, v1alpha1.OperationType_OPERATION_TYPE_DELETE, nil, deletion.AdditionalDetails); err != nil { + log.Info(errRecordChangeLog, "error", err) + } record.Event(managed, event.Normal(reasonDeleted, "Successfully requested deletion of external resource")) managed.SetConditions(xpv1.Deleting(), xpv1.ReconcileSuccess()) return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus) @@ -1110,6 +1157,9 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu // create failed. } + if err := r.change.Log(ctx, managedPreOp, v1alpha1.OperationType_OPERATION_TYPE_CREATE, err, creation.AdditionalDetails); err != nil { + log.Info(errRecordChangeLog, "error", err) + } managed.SetConditions(xpv1.Creating(), xpv1.ReconcileError(errors.Wrap(err, errReconcileCreate))) return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus) } @@ -1118,6 +1168,10 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu log = log.WithValues("external-name", meta.GetExternalName(managed)) record = r.record.WithAnnotations("external-name", meta.GetExternalName(managed)) + if err := r.change.Log(ctx, managedPreOp, v1alpha1.OperationType_OPERATION_TYPE_CREATE, nil, creation.AdditionalDetails); err != nil { + log.Info(errRecordChangeLog, "error", err) + } + // We handle annotations specially here because it's critical // that they are persisted to the API server. If we don't remove // add the external-create-succeeded annotation the reconciler @@ -1219,6 +1273,9 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu // requeued implicitly when we update our status with the new error // condition. If not, we requeue explicitly, which will trigger backoff. log.Debug("Cannot update external resource") + if err := r.change.Log(ctx, managedPreOp, v1alpha1.OperationType_OPERATION_TYPE_UPDATE, err, update.AdditionalDetails); err != nil { + log.Info(errRecordChangeLog, "error", err) + } record.Event(managed, event.Warning(reasonCannotUpdate, err)) managed.SetConditions(xpv1.ReconcileError(errors.Wrap(err, errReconcileUpdate))) return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus) @@ -1226,6 +1283,9 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu // record the drift after the successful update. r.metricRecorder.recordDrift(managed) + if err := r.change.Log(ctx, managedPreOp, v1alpha1.OperationType_OPERATION_TYPE_UPDATE, nil, update.AdditionalDetails); err != nil { + log.Info(errRecordChangeLog, "error", err) + } if _, err := r.managed.PublishConnection(ctx, managed, update.ConnectionDetails); err != nil { // If this is the first time we encounter this issue we'll be requeued diff --git a/pkg/reconciler/managed/reconciler_test.go b/pkg/reconciler/managed/reconciler_test.go index 3467fab79..822d50178 100644 --- a/pkg/reconciler/managed/reconciler_test.go +++ b/pkg/reconciler/managed/reconciler_test.go @@ -32,6 +32,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/reconcile" + "github.com/crossplane/crossplane-runtime/apis/changelogs/proto/v1alpha1" xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" "github.com/crossplane/crossplane-runtime/pkg/errors" "github.com/crossplane/crossplane-runtime/pkg/meta" @@ -429,8 +430,8 @@ func TestReconciler(t *testing.T) { ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true}, nil }, - DeleteFn: func(_ context.Context, _ resource.Managed) error { - return errBoom + DeleteFn: func(_ context.Context, _ resource.Managed) (ExternalDelete, error) { + return ExternalDelete{}, errBoom }, DisconnectFn: func(_ context.Context) error { return nil @@ -477,8 +478,8 @@ func TestReconciler(t *testing.T) { ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { return ExternalObservation{ResourceExists: true}, nil }, - DeleteFn: func(_ context.Context, _ resource.Managed) error { - return nil + DeleteFn: func(_ context.Context, _ resource.Managed) (ExternalDelete, error) { + return ExternalDelete{}, nil }, DisconnectFn: func(_ context.Context) error { return nil @@ -1403,7 +1404,7 @@ func TestReconciler(t *testing.T) { }, want: want{result: reconcile.Result{}}, }, - "ManagementPoliciyNotSupported": { + "ManagementPolicyNotSupported": { reason: `If an unsupported management policy is used, we should throw an error.`, args: args{ m: &fake.Manager{ @@ -1433,7 +1434,7 @@ func TestReconciler(t *testing.T) { }, want: want{result: reconcile.Result{}}, }, - "CustomManagementPoliciyNotSupported": { + "CustomManagementPolicyNotSupported": { reason: `If a custom unsupported management policy is used, we should throw an error.`, args: args{ m: &fake.Manager{ @@ -2318,3 +2319,285 @@ func TestShouldDelete(t *testing.T) { }) } } + +func TestReconcilerChangeLogs(t *testing.T) { + type args struct { + m manager.Manager + mg resource.ManagedKind + o []ReconcilerOption + c *changeLogServiceClient + } + + type want struct { + callCount int + opType v1alpha1.OperationType + errMessage string + } + + now := metav1.Now() + errBoom := errors.New("boom") + + cases := map[string]struct { + reason string + args args + want want + }{ + "CreateSuccessfulWithChangeLogs": { + reason: "Successful managed resource creation should send a create change log entry when change logs are enabled.", + args: args{ + m: &fake.Manager{ + Client: &test.MockClient{ + MockGet: test.NewMockGetFn(nil), + MockUpdate: test.NewMockUpdateFn(nil), + MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, _ client.Object, _ ...client.SubResourceUpdateOption) error { return nil }), + }, + Scheme: fake.SchemeWith(&fake.Managed{}), + }, + mg: resource.ManagedKind(fake.GVK(&fake.Managed{})), + o: []ReconcilerOption{ + WithExternalConnecter(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { + c := &ExternalClientFns{ + ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { + // resource doesn't exist, which should trigger a create operation + return ExternalObservation{ResourceExists: false, ResourceUpToDate: false}, nil + }, + CreateFn: func(_ context.Context, _ resource.Managed) (ExternalCreation, error) { + return ExternalCreation{}, nil + }, + DisconnectFn: func(_ context.Context) error { + return nil + }, + } + return c, nil + })), + }, + c: &changeLogServiceClient{}, + }, + want: want{ + callCount: 1, + opType: v1alpha1.OperationType_OPERATION_TYPE_CREATE, + errMessage: "", + }, + }, + "CreateFailureWithChangeLogs": { + reason: "Failed managed resource creation should send a create change log entry with the error when change logs are enabled.", + args: args{ + m: &fake.Manager{ + Client: &test.MockClient{ + MockGet: test.NewMockGetFn(nil), + MockUpdate: test.NewMockUpdateFn(nil), + MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, _ client.Object, _ ...client.SubResourceUpdateOption) error { return nil }), + }, + Scheme: fake.SchemeWith(&fake.Managed{}), + }, + mg: resource.ManagedKind(fake.GVK(&fake.Managed{})), + o: []ReconcilerOption{ + WithExternalConnecter(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { + c := &ExternalClientFns{ + ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { + // resource doesn't exist, which should trigger a create operation + return ExternalObservation{ResourceExists: false, ResourceUpToDate: false}, nil + }, + CreateFn: func(_ context.Context, _ resource.Managed) (ExternalCreation, error) { + // return an error from Create to simulate a failed creation + return ExternalCreation{}, errBoom + }, + DisconnectFn: func(_ context.Context) error { + return nil + }, + } + return c, nil + })), + }, + c: &changeLogServiceClient{}, + }, + want: want{ + callCount: 1, + opType: v1alpha1.OperationType_OPERATION_TYPE_CREATE, + errMessage: errBoom.Error(), + }, + }, + "UpdateSuccessfulWithChangeLogs": { + reason: "Successful managed resource update should send an update change log entry when change logs are enabled.", + args: args{ + m: &fake.Manager{ + Client: &test.MockClient{ + MockGet: test.NewMockGetFn(nil), + MockUpdate: test.NewMockUpdateFn(nil), + MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, _ client.Object, _ ...client.SubResourceUpdateOption) error { return nil }), + }, + Scheme: fake.SchemeWith(&fake.Managed{}), + }, + mg: resource.ManagedKind(fake.GVK(&fake.Managed{})), + o: []ReconcilerOption{ + WithExternalConnecter(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { + c := &ExternalClientFns{ + ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { + // resource exists but isn't up to date, which should trigger an update operation + return ExternalObservation{ResourceExists: true, ResourceUpToDate: false}, nil + }, + UpdateFn: func(_ context.Context, _ resource.Managed) (ExternalUpdate, error) { + return ExternalUpdate{}, nil + }, + DisconnectFn: func(_ context.Context) error { + return nil + }, + } + return c, nil + })), + }, + c: &changeLogServiceClient{}, + }, + want: want{ + callCount: 1, + opType: v1alpha1.OperationType_OPERATION_TYPE_UPDATE, + errMessage: "", + }, + }, + "UpdateFailureWithChangeLogs": { + reason: "Failed managed resource update should send an update change log entry with the error when change logs are enabled.", + args: args{ + m: &fake.Manager{ + Client: &test.MockClient{ + MockGet: test.NewMockGetFn(nil), + MockUpdate: test.NewMockUpdateFn(nil), + MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, _ client.Object, _ ...client.SubResourceUpdateOption) error { return nil }), + }, + Scheme: fake.SchemeWith(&fake.Managed{}), + }, + mg: resource.ManagedKind(fake.GVK(&fake.Managed{})), + o: []ReconcilerOption{ + WithExternalConnecter(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { + c := &ExternalClientFns{ + ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { + // resource exists but isn't up to date, which should trigger an update operation + return ExternalObservation{ResourceExists: true, ResourceUpToDate: false}, nil + }, + UpdateFn: func(_ context.Context, _ resource.Managed) (ExternalUpdate, error) { + // return an error from Update to simulate a failed update + return ExternalUpdate{}, errBoom + }, + DisconnectFn: func(_ context.Context) error { + return nil + }, + } + return c, nil + })), + }, + c: &changeLogServiceClient{}, + }, + want: want{ + callCount: 1, + opType: v1alpha1.OperationType_OPERATION_TYPE_UPDATE, + errMessage: errBoom.Error(), + }, + }, + "DeleteSuccessfulWithChangeLogs": { + reason: "Successful managed resource delete should send a delete change log entry when change logs are enabled.", + args: args{ + m: &fake.Manager{ + Client: &test.MockClient{ + MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { + // set a deletion timestamp, which should trigger a delete operation + mg := obj.(*fake.Managed) + mg.SetDeletionTimestamp(&now) + mg.SetDeletionPolicy(xpv1.DeletionDelete) + return nil + }), + MockUpdate: test.NewMockUpdateFn(nil), + MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, _ client.Object, _ ...client.SubResourceUpdateOption) error { return nil }), + }, + Scheme: fake.SchemeWith(&fake.Managed{}), + }, + mg: resource.ManagedKind(fake.GVK(&fake.Managed{})), + o: []ReconcilerOption{ + WithExternalConnecter(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { + c := &ExternalClientFns{ + ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { + // resource exists but we set a deletion timestamp above, which should trigger a delete operation + return ExternalObservation{ResourceExists: true}, nil + }, + DeleteFn: func(_ context.Context, _ resource.Managed) (ExternalDelete, error) { + return ExternalDelete{}, nil + }, + DisconnectFn: func(_ context.Context) error { + return nil + }, + } + return c, nil + })), + }, + c: &changeLogServiceClient{}, + }, + want: want{ + callCount: 1, + opType: v1alpha1.OperationType_OPERATION_TYPE_DELETE, + errMessage: "", + }, + }, + "DeleteFailureWithChangeLogs": { + reason: "Failed managed resource delete should send a delete change log entry with the error when change logs are enabled.", + args: args{ + m: &fake.Manager{ + Client: &test.MockClient{ + MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { + // set a deletion timestamp, which should trigger a delete operation + mg := obj.(*fake.Managed) + mg.SetDeletionTimestamp(&now) + mg.SetDeletionPolicy(xpv1.DeletionDelete) + return nil + }), + MockUpdate: test.NewMockUpdateFn(nil), + MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, _ client.Object, _ ...client.SubResourceUpdateOption) error { return nil }), + }, + Scheme: fake.SchemeWith(&fake.Managed{}), + }, + mg: resource.ManagedKind(fake.GVK(&fake.Managed{})), + o: []ReconcilerOption{ + WithExternalConnecter(ExternalConnectorFn(func(_ context.Context, _ resource.Managed) (ExternalClient, error) { + c := &ExternalClientFns{ + ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) { + // resource exists but we set a deletion timestamp above, which should trigger a delete operation + return ExternalObservation{ResourceExists: true}, nil + }, + DeleteFn: func(_ context.Context, _ resource.Managed) (ExternalDelete, error) { + // return an error from Delete to simulate a failed delete + return ExternalDelete{}, errBoom + }, + DisconnectFn: func(_ context.Context) error { + return nil + }, + } + return c, nil + })), + }, + c: &changeLogServiceClient{}, + }, + want: want{ + callCount: 1, + opType: v1alpha1.OperationType_OPERATION_TYPE_DELETE, + errMessage: errBoom.Error(), + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + tc.args.o = append(tc.args.o, WithChangeLogger(NewGRPCChangeLogger(tc.args.c, WithProviderVersion("provider-cool:v9.99.999")))) + r := NewReconciler(tc.args.m, tc.args.mg, tc.args.o...) + r.Reconcile(context.Background(), reconcile.Request{}) + + if diff := cmp.Diff(tc.want.callCount, len(tc.args.c.requests)); diff != "" { + t.Errorf("\nReason: %s\nr.Reconcile(...): -want callCount, +got callCount:\n%s", tc.reason, diff) + } + + if diff := cmp.Diff(tc.want.opType, tc.args.c.requests[0].GetEntry().GetOperation()); diff != "" { + t.Errorf("\nReason: %s\nr.Reconcile(...): -want opType, +got opType:\n%s", tc.reason, diff) + } + + if diff := cmp.Diff(tc.want.errMessage, tc.args.c.requests[0].GetEntry().GetErrorMessage()); diff != "" { + t.Errorf("\nReason: %s\nr.Reconcile(...): -want errMessage, +got errMessage:\n%s", tc.reason, diff) + } + }) + } +} diff --git a/pkg/resource/resource.go b/pkg/resource/resource.go index a6fdb7bed..4d88ba6de 100644 --- a/pkg/resource/resource.go +++ b/pkg/resource/resource.go @@ -22,12 +22,15 @@ import ( "sort" "strings" + "google.golang.org/protobuf/types/known/structpb" corev1 "k8s.io/api/core/v1" kerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + kunstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/json" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/util/retry" "sigs.k8s.io/controller-runtime/pkg/client" @@ -35,6 +38,7 @@ import ( xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" "github.com/crossplane/crossplane-runtime/pkg/errors" "github.com/crossplane/crossplane-runtime/pkg/meta" + "github.com/crossplane/crossplane-runtime/pkg/resource/unstructured" ) // SecretTypeConnection is the type of Crossplane connection secrets. @@ -46,6 +50,10 @@ const ( ExternalResourceTagKeyKind = "crossplane-kind" ExternalResourceTagKeyName = "crossplane-name" ExternalResourceTagKeyProvider = "crossplane-providerconfig" + + errMarshalJSON = "cannot marshal to JSON" + errUnmarshalJSON = "cannot unmarshal JSON data" + errStructFromUnstructured = "cannot create Struct" ) // A ManagedKind contains the type metadata for a kind of managed resource. @@ -425,3 +433,30 @@ func StableNAndSomeMore(n int, names []string) string { sort.Strings(cpy) return FirstNAndSomeMore(n, cpy) } + +// AsProtobufStruct converts the given object to a structpb.Struct for usage with gRPC +// connections. +// Copied from: +// https://github.com/crossplane/crossplane/blob/release-1.16/internal/controller/apiextensions/composite/composition_functions.go#L761 +func AsProtobufStruct(o runtime.Object) (*structpb.Struct, error) { + // If the supplied object is *Unstructured we don't need to round-trip. + if u, ok := o.(*kunstructured.Unstructured); ok { + s, err := structpb.NewStruct(u.Object) + return s, errors.Wrap(err, errStructFromUnstructured) + } + + // If the supplied object wraps *Unstructured we don't need to round-trip. + if w, ok := o.(unstructured.Wrapper); ok { + s, err := structpb.NewStruct(w.GetUnstructured().Object) + return s, errors.Wrap(err, errStructFromUnstructured) + } + + // Fall back to a JSON round-trip. + b, err := json.Marshal(o) + if err != nil { + return nil, errors.Wrap(err, errMarshalJSON) + } + + s := &structpb.Struct{} + return s, errors.Wrap(s.UnmarshalJSON(b), errUnmarshalJSON) +}