diff --git a/api/v1alpha1/generated.pb.go b/api/v1alpha1/generated.pb.go index aca238da8..a3c7fd4a0 100644 --- a/api/v1alpha1/generated.pb.go +++ b/api/v1alpha1/generated.pb.go @@ -1178,10 +1178,38 @@ func (m *PromotionStep) XXX_DiscardUnknown() { var xxx_messageInfo_PromotionStep proto.InternalMessageInfo +func (m *PromotionStepRetry) Reset() { *m = PromotionStepRetry{} } +func (*PromotionStepRetry) ProtoMessage() {} +func (*PromotionStepRetry) Descriptor() ([]byte, []int) { + return fileDescriptor_e26b7f7bbc391025, []int{41} +} +func (m *PromotionStepRetry) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *PromotionStepRetry) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil +} +func (m *PromotionStepRetry) XXX_Merge(src proto.Message) { + xxx_messageInfo_PromotionStepRetry.Merge(m, src) +} +func (m *PromotionStepRetry) XXX_Size() int { + return m.Size() +} +func (m *PromotionStepRetry) XXX_DiscardUnknown() { + xxx_messageInfo_PromotionStepRetry.DiscardUnknown(m) +} + +var xxx_messageInfo_PromotionStepRetry proto.InternalMessageInfo + func (m *PromotionTemplate) Reset() { *m = PromotionTemplate{} } func (*PromotionTemplate) ProtoMessage() {} func (*PromotionTemplate) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{41} + return fileDescriptor_e26b7f7bbc391025, []int{42} } func (m *PromotionTemplate) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1209,7 +1237,7 @@ var xxx_messageInfo_PromotionTemplate proto.InternalMessageInfo func (m *PromotionTemplateSpec) Reset() { *m = PromotionTemplateSpec{} } func (*PromotionTemplateSpec) ProtoMessage() {} func (*PromotionTemplateSpec) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{42} + return fileDescriptor_e26b7f7bbc391025, []int{43} } func (m *PromotionTemplateSpec) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1237,7 +1265,7 @@ var xxx_messageInfo_PromotionTemplateSpec proto.InternalMessageInfo func (m *PromotionVariable) Reset() { *m = PromotionVariable{} } func (*PromotionVariable) ProtoMessage() {} func (*PromotionVariable) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{43} + return fileDescriptor_e26b7f7bbc391025, []int{44} } func (m *PromotionVariable) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1265,7 +1293,7 @@ var xxx_messageInfo_PromotionVariable proto.InternalMessageInfo func (m *RepoSubscription) Reset() { *m = RepoSubscription{} } func (*RepoSubscription) ProtoMessage() {} func (*RepoSubscription) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{44} + return fileDescriptor_e26b7f7bbc391025, []int{45} } func (m *RepoSubscription) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1293,7 +1321,7 @@ var xxx_messageInfo_RepoSubscription proto.InternalMessageInfo func (m *Stage) Reset() { *m = Stage{} } func (*Stage) ProtoMessage() {} func (*Stage) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{45} + return fileDescriptor_e26b7f7bbc391025, []int{46} } func (m *Stage) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1321,7 +1349,7 @@ var xxx_messageInfo_Stage proto.InternalMessageInfo func (m *StageList) Reset() { *m = StageList{} } func (*StageList) ProtoMessage() {} func (*StageList) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{46} + return fileDescriptor_e26b7f7bbc391025, []int{47} } func (m *StageList) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1349,7 +1377,7 @@ var xxx_messageInfo_StageList proto.InternalMessageInfo func (m *StageSpec) Reset() { *m = StageSpec{} } func (*StageSpec) ProtoMessage() {} func (*StageSpec) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{47} + return fileDescriptor_e26b7f7bbc391025, []int{48} } func (m *StageSpec) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1377,7 +1405,7 @@ var xxx_messageInfo_StageSpec proto.InternalMessageInfo func (m *StageStatus) Reset() { *m = StageStatus{} } func (*StageStatus) ProtoMessage() {} func (*StageStatus) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{48} + return fileDescriptor_e26b7f7bbc391025, []int{49} } func (m *StageStatus) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1405,7 +1433,7 @@ var xxx_messageInfo_StageStatus proto.InternalMessageInfo func (m *Verification) Reset() { *m = Verification{} } func (*Verification) ProtoMessage() {} func (*Verification) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{49} + return fileDescriptor_e26b7f7bbc391025, []int{50} } func (m *Verification) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1433,7 +1461,7 @@ var xxx_messageInfo_Verification proto.InternalMessageInfo func (m *VerificationInfo) Reset() { *m = VerificationInfo{} } func (*VerificationInfo) ProtoMessage() {} func (*VerificationInfo) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{50} + return fileDescriptor_e26b7f7bbc391025, []int{51} } func (m *VerificationInfo) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1461,7 +1489,7 @@ var xxx_messageInfo_VerificationInfo proto.InternalMessageInfo func (m *VerifiedStage) Reset() { *m = VerifiedStage{} } func (*VerifiedStage) ProtoMessage() {} func (*VerifiedStage) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{51} + return fileDescriptor_e26b7f7bbc391025, []int{52} } func (m *VerifiedStage) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1489,7 +1517,7 @@ var xxx_messageInfo_VerifiedStage proto.InternalMessageInfo func (m *Warehouse) Reset() { *m = Warehouse{} } func (*Warehouse) ProtoMessage() {} func (*Warehouse) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{52} + return fileDescriptor_e26b7f7bbc391025, []int{53} } func (m *Warehouse) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1517,7 +1545,7 @@ var xxx_messageInfo_Warehouse proto.InternalMessageInfo func (m *WarehouseList) Reset() { *m = WarehouseList{} } func (*WarehouseList) ProtoMessage() {} func (*WarehouseList) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{53} + return fileDescriptor_e26b7f7bbc391025, []int{54} } func (m *WarehouseList) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1545,7 +1573,7 @@ var xxx_messageInfo_WarehouseList proto.InternalMessageInfo func (m *WarehouseSpec) Reset() { *m = WarehouseSpec{} } func (*WarehouseSpec) ProtoMessage() {} func (*WarehouseSpec) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{54} + return fileDescriptor_e26b7f7bbc391025, []int{55} } func (m *WarehouseSpec) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1573,7 +1601,7 @@ var xxx_messageInfo_WarehouseSpec proto.InternalMessageInfo func (m *WarehouseStatus) Reset() { *m = WarehouseStatus{} } func (*WarehouseStatus) ProtoMessage() {} func (*WarehouseStatus) Descriptor() ([]byte, []int) { - return fileDescriptor_e26b7f7bbc391025, []int{55} + return fileDescriptor_e26b7f7bbc391025, []int{56} } func (m *WarehouseStatus) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1645,6 +1673,7 @@ func init() { proto.RegisterType((*PromotionSpec)(nil), "github.com.akuity.kargo.api.v1alpha1.PromotionSpec") proto.RegisterType((*PromotionStatus)(nil), "github.com.akuity.kargo.api.v1alpha1.PromotionStatus") proto.RegisterType((*PromotionStep)(nil), "github.com.akuity.kargo.api.v1alpha1.PromotionStep") + proto.RegisterType((*PromotionStepRetry)(nil), "github.com.akuity.kargo.api.v1alpha1.PromotionStepRetry") proto.RegisterType((*PromotionTemplate)(nil), "github.com.akuity.kargo.api.v1alpha1.PromotionTemplate") proto.RegisterType((*PromotionTemplateSpec)(nil), "github.com.akuity.kargo.api.v1alpha1.PromotionTemplateSpec") proto.RegisterType((*PromotionVariable)(nil), "github.com.akuity.kargo.api.v1alpha1.PromotionVariable") @@ -1667,233 +1696,237 @@ func init() { } var fileDescriptor_e26b7f7bbc391025 = []byte{ - // 3604 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xdc, 0x5b, 0x4b, 0x6c, 0x1b, 0xc7, - 0xdd, 0xf7, 0x92, 0x12, 0x25, 0xfe, 0xa9, 0xe7, 0x58, 0x4e, 0x14, 0xe5, 0x8b, 0xe4, 0x6f, 0x13, - 0x04, 0xc9, 0x97, 0x84, 0xfa, 0xfc, 0x8a, 0x1f, 0x69, 0x5d, 0x88, 0x94, 0x1f, 0x72, 0x95, 0x58, - 0x1d, 0x3a, 0x76, 0xea, 0xd8, 0x48, 0x47, 0xe4, 0x88, 0xdc, 0x8a, 0xdc, 0x65, 0x76, 0x96, 0x4a, - 0xd4, 0x16, 0x6d, 0xd2, 0x07, 0x10, 0xa0, 0x68, 0xd1, 0x43, 0xd0, 0xa4, 0x40, 0x0b, 0x14, 0xed, - 0xb9, 0x05, 0x7a, 0xee, 0x21, 0x87, 0x5e, 0x82, 0x22, 0x28, 0x82, 0xb6, 0x87, 0x1c, 0x02, 0xa1, - 0x51, 0x80, 0x1e, 0x73, 0xeb, 0xc5, 0x40, 0x81, 0x62, 0x1e, 0xbb, 0x3b, 0xbb, 0x5c, 0x5a, 0x5c, - 0x5a, 0x32, 0x8c, 0xde, 0xc8, 0xf9, 0xcf, 0xfc, 0xfe, 0xf3, 0xf8, 0xbf, 0x67, 0x16, 0x4e, 0xd6, - 0x2d, 0xaf, 0xd1, 0x59, 0x2f, 0x56, 0x9d, 0xd6, 0x22, 0xd9, 0xec, 0x58, 0xde, 0xf6, 0xe2, 0x26, - 0x71, 0xeb, 0xce, 0x22, 0x69, 0x5b, 0x8b, 0x5b, 0xc7, 0x48, 0xb3, 0xdd, 0x20, 0xc7, 0x16, 0xeb, - 0xd4, 0xa6, 0x2e, 0xf1, 0x68, 0xad, 0xd8, 0x76, 0x1d, 0xcf, 0x41, 0x4f, 0x84, 0xa3, 0x8a, 0x72, - 0x54, 0x51, 0x8c, 0x2a, 0x92, 0xb6, 0x55, 0xf4, 0x47, 0xcd, 0x3d, 0xa7, 0x61, 0xd7, 0x9d, 0xba, - 0xb3, 0x28, 0x06, 0xaf, 0x77, 0x36, 0xc4, 0x3f, 0xf1, 0x47, 0xfc, 0x92, 0xa0, 0x73, 0x97, 0x37, - 0xcf, 0xb0, 0xa2, 0x25, 0x38, 0xd3, 0x37, 0x3d, 0x6a, 0x33, 0xcb, 0xb1, 0xd9, 0x73, 0xa4, 0x6d, - 0x31, 0xea, 0x6e, 0x51, 0x77, 0xb1, 0xbd, 0x59, 0xe7, 0x34, 0x16, 0xed, 0xb0, 0xb8, 0xd5, 0x35, - 0xbd, 0xb9, 0x93, 0x21, 0x52, 0x8b, 0x54, 0x1b, 0x96, 0x4d, 0xdd, 0xed, 0x70, 0x78, 0x8b, 0x7a, - 0x24, 0x69, 0xd4, 0x62, 0xaf, 0x51, 0x6e, 0xc7, 0xf6, 0xac, 0x16, 0xed, 0x1a, 0xf0, 0xfc, 0x5e, - 0x03, 0x58, 0xb5, 0x41, 0x5b, 0x24, 0x3e, 0xce, 0xbc, 0x05, 0x87, 0x97, 0x6c, 0xd2, 0xdc, 0x66, - 0x16, 0xc3, 0x1d, 0x7b, 0xc9, 0xad, 0x77, 0x5a, 0xd4, 0xf6, 0xd0, 0x51, 0x18, 0xb2, 0x49, 0x8b, - 0xce, 0x1a, 0x47, 0x8d, 0xa7, 0xf2, 0xa5, 0xb1, 0x0f, 0x77, 0x16, 0x0e, 0xed, 0xee, 0x2c, 0x0c, - 0xbd, 0x44, 0x5a, 0x14, 0x0b, 0x0a, 0x7a, 0x1c, 0x86, 0xb7, 0x48, 0xb3, 0x43, 0x67, 0x33, 0xa2, - 0xcb, 0xb8, 0xea, 0x32, 0x7c, 0x9d, 0x37, 0x62, 0x49, 0x33, 0x7f, 0x90, 0x8d, 0xc0, 0xbf, 0x48, - 0x3d, 0x52, 0x23, 0x1e, 0x41, 0x2d, 0xc8, 0x35, 0xc9, 0x3a, 0x6d, 0xb2, 0x59, 0xe3, 0x68, 0xf6, - 0xa9, 0xc2, 0xf1, 0x0b, 0xc5, 0x7e, 0x0e, 0xb1, 0x98, 0x00, 0x55, 0x5c, 0x15, 0x38, 0x17, 0x6c, - 0xcf, 0xdd, 0x2e, 0x4d, 0xa8, 0x49, 0xe4, 0x64, 0x23, 0x56, 0x4c, 0xd0, 0xdb, 0x06, 0x14, 0x88, - 0x6d, 0x3b, 0x1e, 0xf1, 0xf8, 0x31, 0xcd, 0x66, 0x04, 0xd3, 0x2b, 0x83, 0x33, 0x5d, 0x0a, 0xc1, - 0x24, 0xe7, 0xc3, 0x8a, 0x73, 0x41, 0xa3, 0x60, 0x9d, 0xe7, 0xdc, 0x59, 0x28, 0x68, 0x53, 0x45, - 0x53, 0x90, 0xdd, 0xa4, 0xdb, 0x72, 0x7f, 0x31, 0xff, 0x89, 0x66, 0x22, 0x1b, 0xaa, 0x76, 0xf0, - 0x5c, 0xe6, 0x8c, 0x31, 0x77, 0x1e, 0xa6, 0xe2, 0x0c, 0xd3, 0x8c, 0x37, 0x7f, 0x6a, 0xc0, 0x8c, - 0xb6, 0x0a, 0x4c, 0x37, 0xa8, 0x4b, 0xed, 0x2a, 0x45, 0x8b, 0x90, 0xe7, 0x67, 0xc9, 0xda, 0xa4, - 0xea, 0x1f, 0xf5, 0xb4, 0x5a, 0x48, 0xfe, 0x25, 0x9f, 0x80, 0xc3, 0x3e, 0x81, 0x58, 0x64, 0xee, - 0x26, 0x16, 0xed, 0x06, 0x61, 0x74, 0x36, 0x1b, 0x15, 0x8b, 0x35, 0xde, 0x88, 0x25, 0xcd, 0xfc, - 0x32, 0x3c, 0xe2, 0xcf, 0xe7, 0x1a, 0x6d, 0xb5, 0x9b, 0xc4, 0xa3, 0xe1, 0xa4, 0xf6, 0x14, 0x3d, - 0x73, 0x12, 0xc6, 0x97, 0xda, 0x6d, 0xd7, 0xd9, 0xa2, 0xb5, 0x8a, 0x47, 0xea, 0xd4, 0xfc, 0xbe, - 0x01, 0x47, 0x96, 0xdc, 0xba, 0x53, 0x5e, 0x5e, 0x6a, 0xb7, 0x2f, 0x53, 0xd2, 0xf4, 0x1a, 0x15, - 0x8f, 0x78, 0x1d, 0x86, 0xce, 0x43, 0x8e, 0x89, 0x5f, 0x0a, 0xee, 0x49, 0x5f, 0x42, 0x24, 0xfd, - 0xce, 0xce, 0xc2, 0x4c, 0xc2, 0x40, 0x8a, 0xd5, 0x28, 0xf4, 0x34, 0x8c, 0xb4, 0x28, 0x63, 0xa4, - 0xee, 0xaf, 0x79, 0x52, 0x01, 0x8c, 0xbc, 0x28, 0x9b, 0xb1, 0x4f, 0x37, 0xff, 0x9c, 0x81, 0xc9, - 0x00, 0x4b, 0xb1, 0x3f, 0x80, 0x0d, 0xee, 0xc0, 0x58, 0x43, 0x5b, 0xa1, 0xd8, 0xe7, 0xc2, 0xf1, - 0x17, 0xfa, 0x94, 0xe5, 0xa4, 0x4d, 0x2a, 0xcd, 0x28, 0x36, 0x63, 0x7a, 0x2b, 0x8e, 0xb0, 0x41, - 0x2d, 0x00, 0xb6, 0x6d, 0x57, 0x15, 0xd3, 0x21, 0xc1, 0xf4, 0x6c, 0x4a, 0xa6, 0x95, 0x00, 0xa0, - 0x84, 0x14, 0x4b, 0x08, 0xdb, 0xb0, 0xc6, 0xc0, 0xfc, 0xbd, 0x01, 0x87, 0x13, 0xc6, 0xa1, 0x2f, - 0xc5, 0xce, 0xf3, 0x89, 0xae, 0xf3, 0x44, 0x5d, 0xc3, 0xc2, 0xd3, 0x7c, 0x16, 0x46, 0x5d, 0xba, - 0x65, 0x71, 0x5b, 0xad, 0x76, 0x78, 0x4a, 0x8d, 0x1f, 0xc5, 0xaa, 0x1d, 0x07, 0x3d, 0xd0, 0x33, - 0x90, 0xf7, 0x7f, 0xf3, 0x6d, 0xce, 0x72, 0x71, 0xe6, 0x07, 0xe7, 0x77, 0x65, 0x38, 0xa4, 0x9b, - 0xdf, 0x83, 0xe1, 0x72, 0x83, 0xb8, 0x1e, 0x97, 0x18, 0x97, 0xb6, 0x9d, 0x97, 0xf1, 0xaa, 0x9a, - 0x62, 0x20, 0x31, 0x58, 0x36, 0x63, 0x9f, 0xde, 0xc7, 0x61, 0x3f, 0x0d, 0x23, 0x5b, 0xd4, 0x15, - 0xf3, 0xcd, 0x46, 0xc1, 0xae, 0xcb, 0x66, 0xec, 0xd3, 0xcd, 0xbf, 0x19, 0x30, 0x23, 0x66, 0xb0, - 0x6c, 0xb1, 0xaa, 0xb3, 0x45, 0xdd, 0x6d, 0x4c, 0x59, 0xa7, 0xb9, 0xcf, 0x13, 0x5a, 0x86, 0x29, - 0x46, 0x5b, 0x5b, 0xd4, 0x2d, 0x3b, 0x36, 0xf3, 0x5c, 0x62, 0xd9, 0x9e, 0x9a, 0xd9, 0xac, 0xea, - 0x3d, 0x55, 0x89, 0xd1, 0x71, 0xd7, 0x08, 0xf4, 0x14, 0x8c, 0xaa, 0x69, 0x73, 0x51, 0xe2, 0x1b, - 0x3b, 0xc6, 0xcf, 0x40, 0xad, 0x89, 0xe1, 0x80, 0x6a, 0xfe, 0xd3, 0x80, 0x69, 0xb1, 0xaa, 0x4a, - 0x67, 0x9d, 0x55, 0x5d, 0xab, 0xcd, 0x4d, 0xe0, 0x83, 0xb8, 0xa4, 0xf3, 0x30, 0x51, 0xf3, 0x37, - 0x7e, 0xd5, 0x6a, 0x59, 0x9e, 0xd0, 0x91, 0xe1, 0xd2, 0x43, 0x0a, 0x63, 0x62, 0x39, 0x42, 0xc5, - 0xb1, 0xde, 0xe6, 0x2f, 0xb3, 0x70, 0xd8, 0xef, 0x42, 0x6b, 0x4b, 0xae, 0x67, 0x6d, 0x90, 0xaa, - 0xc7, 0x50, 0x0d, 0xc6, 0x6a, 0x61, 0xb3, 0xa7, 0x34, 0xef, 0xff, 0x8a, 0xd2, 0xdd, 0x17, 0x75, - 0x77, 0x5f, 0x6c, 0x6f, 0xd6, 0x79, 0x03, 0x2b, 0xf2, 0xa8, 0xa2, 0xb8, 0x75, 0xac, 0x78, 0xcd, - 0x6a, 0xd1, 0x50, 0xbb, 0x35, 0x78, 0x0f, 0x47, 0x50, 0xd1, 0x0d, 0xc8, 0xd6, 0x2d, 0x4f, 0x39, - 0xe3, 0x33, 0xfd, 0xa9, 0xf5, 0x25, 0x2b, 0x2e, 0x6a, 0xa5, 0x82, 0x62, 0x95, 0xbd, 0x64, 0x79, - 0x98, 0x23, 0xa2, 0x75, 0xc8, 0x59, 0x2d, 0x52, 0xa7, 0xbe, 0xcf, 0x3d, 0xd7, 0x1f, 0xf6, 0x0a, - 0x1f, 0x13, 0x47, 0x0f, 0xbc, 0xbb, 0xa0, 0x32, 0xac, 0x90, 0x39, 0x8f, 0x2a, 0x17, 0x11, 0xa9, - 0xa4, 0x7d, 0xf3, 0x48, 0x52, 0x96, 0x90, 0x87, 0xa0, 0x32, 0xac, 0x90, 0xcd, 0x4f, 0x32, 0x30, - 0x15, 0xee, 0x5f, 0xd9, 0x69, 0xb5, 0x2c, 0x0f, 0xcd, 0x41, 0xc6, 0xaa, 0x29, 0x09, 0x04, 0x35, - 0x30, 0xb3, 0xb2, 0x8c, 0x33, 0x56, 0x0d, 0x3d, 0x09, 0xb9, 0x75, 0x97, 0xd8, 0xd5, 0x86, 0x92, - 0xbc, 0x00, 0xb8, 0x24, 0x5a, 0xb1, 0xa2, 0xa2, 0xc7, 0x20, 0xeb, 0x91, 0xba, 0x12, 0xb8, 0x60, - 0xff, 0xae, 0x91, 0x3a, 0xe6, 0xed, 0x5c, 0xd2, 0x59, 0x67, 0xfd, 0x9b, 0xb4, 0x2a, 0x4f, 0x5e, - 0x93, 0xf4, 0x8a, 0x6c, 0xc6, 0x3e, 0x9d, 0x73, 0x24, 0x1d, 0xaf, 0xe1, 0xb8, 0xb3, 0xc3, 0x51, - 0x8e, 0x4b, 0xa2, 0x15, 0x2b, 0x2a, 0xf7, 0x49, 0x55, 0x31, 0x7f, 0x8f, 0xba, 0xb3, 0xb9, 0xa8, - 0x4f, 0x2a, 0xfb, 0x04, 0x1c, 0xf6, 0x41, 0xb7, 0xa1, 0x50, 0x75, 0x29, 0xf1, 0x1c, 0x77, 0x99, - 0x78, 0x74, 0x76, 0x24, 0xb5, 0x04, 0x4e, 0xf2, 0xc0, 0xa8, 0x1c, 0x42, 0x60, 0x1d, 0xcf, 0xfc, - 0xc2, 0x80, 0xd9, 0x70, 0x6b, 0xc5, 0xd9, 0x86, 0xc1, 0x80, 0xda, 0x1e, 0xa3, 0xc7, 0xf6, 0x3c, - 0x09, 0xb9, 0x9a, 0x55, 0xa7, 0xcc, 0x8b, 0xef, 0xf2, 0xb2, 0x68, 0xc5, 0x8a, 0x8a, 0x8e, 0x03, - 0xd4, 0x2d, 0x4f, 0x19, 0x07, 0xb5, 0xd9, 0x81, 0x0b, 0xba, 0x14, 0x50, 0xb0, 0xd6, 0x0b, 0xdd, - 0x80, 0xbc, 0x98, 0xe6, 0x80, 0x6a, 0x27, 0x5c, 0x45, 0xd9, 0x07, 0xc0, 0x21, 0x96, 0xf9, 0xf1, - 0x10, 0x8c, 0x5c, 0x74, 0xa9, 0x55, 0x6f, 0x78, 0xe8, 0x1b, 0x30, 0xda, 0x52, 0x41, 0xa5, 0x58, - 0x64, 0xe1, 0xf8, 0xff, 0xf7, 0xc7, 0xe3, 0xaa, 0x38, 0x74, 0x1e, 0x90, 0x86, 0x0b, 0x09, 0xdb, - 0x70, 0x80, 0xca, 0x03, 0x32, 0xd2, 0xb4, 0x08, 0x13, 0xe7, 0xa6, 0x05, 0x64, 0x4b, 0xbc, 0x11, - 0x4b, 0x1a, 0x7a, 0x15, 0x72, 0x8e, 0x6b, 0xd5, 0x2d, 0x7b, 0x36, 0x2f, 0x26, 0x71, 0xa2, 0x3f, - 0x15, 0x52, 0xab, 0xb8, 0x2a, 0x86, 0x86, 0x9b, 0x2f, 0xff, 0x63, 0x05, 0x89, 0x6e, 0xc2, 0x88, - 0x14, 0x26, 0x5f, 0x41, 0x17, 0xfb, 0x36, 0x30, 0x52, 0x1e, 0x43, 0xa1, 0x97, 0xff, 0x19, 0xf6, - 0x01, 0x51, 0x25, 0xb0, 0x2f, 0x43, 0x02, 0xfa, 0x99, 0x14, 0xf6, 0xa5, 0xa7, 0x41, 0xa9, 0x04, - 0x06, 0x65, 0x38, 0x0d, 0xa8, 0x30, 0x19, 0xbd, 0x2c, 0x08, 0xdf, 0x62, 0x15, 0xb9, 0xe4, 0x06, - 0xd8, 0x62, 0x15, 0x36, 0x4d, 0x44, 0xc3, 0x1d, 0x3f, 0xb0, 0x31, 0xdf, 0xcd, 0xc2, 0xb4, 0xea, - 0x59, 0x76, 0x9a, 0x4d, 0x5a, 0x15, 0x6e, 0x52, 0xda, 0xa7, 0x6c, 0xa2, 0x7d, 0xb2, 0x60, 0xd8, - 0xf2, 0x68, 0xcb, 0x4f, 0xc0, 0x4a, 0xa9, 0x66, 0x13, 0xf2, 0x28, 0xae, 0x70, 0x10, 0x99, 0x03, - 0x05, 0xa7, 0xa4, 0x7a, 0x61, 0xc9, 0x01, 0xfd, 0xc8, 0x80, 0xc3, 0x5b, 0xd4, 0xb5, 0x36, 0xac, - 0xaa, 0xc8, 0x60, 0x2e, 0x5b, 0xcc, 0x73, 0xdc, 0x6d, 0xe5, 0x11, 0x9e, 0xef, 0x8f, 0xf3, 0x75, - 0x0d, 0x60, 0xc5, 0xde, 0x70, 0x4a, 0x8f, 0x2a, 0x6e, 0x87, 0xaf, 0x77, 0x43, 0xe3, 0x24, 0x7e, - 0x73, 0x6d, 0x80, 0x70, 0xb6, 0x09, 0x09, 0xd4, 0xaa, 0x9e, 0x40, 0xf5, 0x3d, 0x31, 0x7f, 0xb1, - 0xbe, 0xc9, 0xd2, 0x13, 0xaf, 0x0f, 0x0c, 0x28, 0x28, 0xfa, 0xaa, 0xc5, 0x3c, 0x74, 0xab, 0x4b, - 0xdb, 0x8b, 0xfd, 0x69, 0x3b, 0x1f, 0x2d, 0x74, 0x3d, 0x88, 0x57, 0xfd, 0x16, 0x4d, 0xd3, 0xb1, - 0x7f, 0xa4, 0x72, 0x63, 0x9f, 0x4b, 0x35, 0xff, 0xd0, 0x30, 0x88, 0x3d, 0x52, 0x67, 0x67, 0xba, - 0x30, 0x1e, 0x51, 0x72, 0x74, 0x0a, 0x86, 0x36, 0x2d, 0xdb, 0xf7, 0x7a, 0xff, 0xeb, 0xc7, 0x53, - 0x5f, 0xb5, 0xec, 0xda, 0x9d, 0x9d, 0x85, 0xe9, 0x48, 0x67, 0xde, 0x88, 0x45, 0xf7, 0xbd, 0xc3, - 0xb0, 0x73, 0xa3, 0xef, 0xff, 0x7a, 0xe1, 0xd0, 0x5b, 0x9f, 0x1e, 0x3d, 0x64, 0xbe, 0x97, 0x85, - 0xa9, 0xf8, 0xae, 0xf6, 0x51, 0x90, 0x08, 0x6d, 0xd8, 0xe8, 0x81, 0xda, 0xb0, 0xcc, 0xc1, 0xd9, - 0xb0, 0xec, 0x41, 0xd8, 0xb0, 0xa1, 0x7d, 0xb3, 0x61, 0xe6, 0x5f, 0x0c, 0x98, 0x08, 0x4e, 0xe6, - 0xf5, 0x0e, 0xf7, 0xac, 0xe1, 0xae, 0x1b, 0xfb, 0xbf, 0xeb, 0xaf, 0xc1, 0x08, 0x73, 0x3a, 0x6e, - 0x55, 0x84, 0x8f, 0x1c, 0xfd, 0x64, 0x3a, 0xa3, 0x29, 0xc7, 0x6a, 0x31, 0x93, 0x6c, 0xc0, 0x3e, - 0xaa, 0x79, 0x2b, 0x58, 0x8f, 0x22, 0xc9, 0x88, 0xc2, 0xe5, 0xf1, 0x16, 0x5f, 0xcf, 0xa8, 0x1e, - 0x51, 0xf0, 0x56, 0xac, 0xa8, 0xc8, 0x14, 0xe6, 0xdc, 0x0f, 0x6c, 0xf3, 0x25, 0x50, 0x56, 0x59, - 0x9c, 0x81, 0xa4, 0x98, 0x5f, 0x64, 0x03, 0xed, 0x51, 0xe9, 0xeb, 0x1b, 0x00, 0xd2, 0x32, 0xd1, - 0xda, 0x8a, 0xad, 0x4c, 0x6f, 0x79, 0x00, 0x47, 0xa0, 0xcc, 0x21, 0x47, 0x91, 0xb6, 0x37, 0x88, - 0x01, 0x42, 0x02, 0xd6, 0x58, 0xa1, 0x6f, 0x43, 0x81, 0xa8, 0x92, 0xc9, 0x45, 0xc7, 0x55, 0x32, - 0xbc, 0x3c, 0x08, 0xe7, 0xa5, 0x10, 0x26, 0x5e, 0xfa, 0x0a, 0x29, 0x58, 0xe7, 0x36, 0xe7, 0xc2, - 0x64, 0x6c, 0xbe, 0x09, 0xd6, 0x77, 0x25, 0x6a, 0x7d, 0x4f, 0xa4, 0x71, 0x0b, 0xaa, 0x0e, 0xa4, - 0xd7, 0xcc, 0x18, 0x4c, 0xc5, 0x67, 0xba, 0x6f, 0x4c, 0x23, 0xc5, 0x27, 0xdd, 0xde, 0xff, 0x2a, - 0x03, 0xf9, 0x40, 0xe3, 0xd3, 0x64, 0xa9, 0xd2, 0x53, 0x67, 0xf6, 0xc8, 0x24, 0xb2, 0xfd, 0x64, - 0x12, 0x43, 0xbd, 0x33, 0x09, 0xbf, 0x92, 0x95, 0xbb, 0x7b, 0x25, 0x4b, 0xcb, 0x24, 0x46, 0xfa, - 0xcf, 0x24, 0x46, 0xf7, 0xce, 0x24, 0xcc, 0xdf, 0x18, 0x80, 0xba, 0xd3, 0xc6, 0x34, 0x1b, 0x45, - 0xe2, 0x76, 0xb8, 0x4f, 0x2f, 0x1d, 0xcf, 0xdd, 0x7a, 0x9b, 0x63, 0xf3, 0x83, 0x61, 0x98, 0xbc, - 0x64, 0x0d, 0x5c, 0x70, 0xf0, 0xe0, 0x61, 0x89, 0x54, 0xa1, 0x2a, 0x46, 0xaa, 0x78, 0x2e, 0xf1, - 0x68, 0x7d, 0x5b, 0x9d, 0xef, 0x39, 0x35, 0xf4, 0xe1, 0x72, 0x72, 0xb7, 0x3b, 0xbd, 0x49, 0xb8, - 0x17, 0x74, 0xdf, 0x42, 0xf2, 0x02, 0x8c, 0x33, 0xcf, 0xb5, 0xaa, 0x9e, 0x2c, 0x69, 0xb0, 0xd9, - 0x82, 0xb0, 0x72, 0x47, 0x54, 0xf7, 0xf1, 0x8a, 0x4e, 0xc4, 0xd1, 0xbe, 0x89, 0x95, 0x92, 0xa1, - 0xd4, 0x95, 0x92, 0x45, 0xc8, 0x93, 0x66, 0xd3, 0x79, 0xe3, 0x1a, 0xa9, 0x33, 0x95, 0xaa, 0x06, - 0x52, 0xb3, 0xe4, 0x13, 0x70, 0xd8, 0x07, 0x15, 0x01, 0xac, 0xba, 0xed, 0xb8, 0x54, 0x8c, 0xc8, - 0x09, 0x73, 0x3b, 0xc1, 0x6d, 0xdd, 0x4a, 0xd0, 0x8a, 0xb5, 0x1e, 0xa8, 0x02, 0x47, 0x2c, 0x9b, - 0xd1, 0x6a, 0xc7, 0xa5, 0x95, 0x4d, 0xab, 0x7d, 0x6d, 0xb5, 0x22, 0xac, 0xc4, 0xb6, 0x90, 0xe6, - 0xd1, 0xd2, 0x63, 0x8a, 0xd9, 0x91, 0x95, 0xa4, 0x4e, 0x38, 0x79, 0x2c, 0x3a, 0x09, 0x63, 0x96, - 0x5d, 0x6d, 0x76, 0x6a, 0x74, 0x8d, 0x78, 0x0d, 0x36, 0x3b, 0x2a, 0xa6, 0x31, 0xb5, 0xbb, 0xb3, - 0x30, 0xb6, 0xa2, 0xb5, 0xe3, 0x48, 0x2f, 0x3e, 0x8a, 0xbe, 0xa9, 0x8d, 0xca, 0x87, 0xa3, 0x2e, - 0xbc, 0xa9, 0x8f, 0xd2, 0x7b, 0x25, 0xd4, 0x92, 0x20, 0x55, 0x2d, 0xe9, 0xe7, 0x59, 0xc8, 0xc9, - 0x52, 0x2e, 0x3a, 0x15, 0xab, 0x97, 0x3e, 0xd6, 0x55, 0x2f, 0x2d, 0x24, 0x95, 0xbd, 0x4d, 0xc8, - 0x59, 0x8c, 0x75, 0xa2, 0xde, 0x6d, 0x45, 0xb4, 0x60, 0x45, 0x41, 0x16, 0x00, 0xf1, 0x4b, 0xad, - 0x7e, 0xe8, 0x72, 0x2a, 0x6d, 0x45, 0x38, 0x56, 0x0d, 0x0e, 0x08, 0x0c, 0x6b, 0xe0, 0xa2, 0xc2, - 0xe3, 0xd8, 0x1b, 0x56, 0x5d, 0xe5, 0xe1, 0xe7, 0xb5, 0xa8, 0x39, 0xbc, 0x7d, 0x7b, 0x2d, 0xb8, - 0x9e, 0x0b, 0x03, 0xe8, 0x48, 0x07, 0x1e, 0x49, 0x5f, 0xa9, 0x5c, 0x7d, 0x49, 0x2e, 0xa7, 0x2c, - 0x10, 0xb1, 0x42, 0xe6, 0x3c, 0x9c, 0x8e, 0xd7, 0xee, 0x78, 0x42, 0x26, 0xf7, 0x89, 0xc7, 0x55, - 0x81, 0x88, 0x15, 0xb2, 0xf9, 0x9e, 0x01, 0x93, 0x72, 0xbb, 0xcb, 0x0d, 0x5a, 0xdd, 0xac, 0x78, - 0xb4, 0xcd, 0x03, 0xdb, 0x0e, 0xa3, 0x2c, 0x1e, 0xd8, 0xbe, 0xcc, 0x28, 0xc3, 0x82, 0xa2, 0xad, - 0x3e, 0x73, 0x50, 0xab, 0x37, 0x7f, 0x67, 0xc0, 0xb0, 0x88, 0x20, 0xd3, 0x98, 0xba, 0x68, 0x55, - 0x25, 0xd3, 0x57, 0x55, 0x65, 0x8f, 0x7a, 0x57, 0x58, 0xd0, 0x19, 0xba, 0x5b, 0x41, 0xc7, 0xfc, - 0xdc, 0x80, 0x99, 0xa4, 0x22, 0x61, 0x9a, 0xe9, 0x3f, 0x0b, 0xa3, 0xed, 0x26, 0xf1, 0x36, 0x1c, - 0xb7, 0x15, 0xbf, 0x0d, 0x58, 0x53, 0xed, 0x38, 0xe8, 0x81, 0x5c, 0x00, 0xd7, 0xcf, 0x46, 0x7c, - 0x71, 0x3f, 0x9f, 0xd6, 0xf9, 0x44, 0xab, 0x5b, 0xe1, 0x66, 0x05, 0x4d, 0x0c, 0x6b, 0x5c, 0xcc, - 0x1f, 0x0f, 0xc3, 0xb4, 0x18, 0x32, 0xa8, 0x33, 0x1a, 0xe4, 0x84, 0xda, 0xf0, 0x90, 0xc8, 0x21, - 0xba, 0xfd, 0x97, 0x3c, 0xb4, 0x33, 0x6a, 0xfc, 0x43, 0x2b, 0x89, 0xbd, 0xee, 0xf4, 0xa4, 0xe0, - 0x1e, 0xb8, 0xdd, 0x4e, 0x09, 0xfe, 0xfb, 0x9c, 0x92, 0x2e, 0x6c, 0x23, 0x7b, 0x0a, 0x5b, 0x4f, - 0x17, 0x36, 0x7a, 0x0f, 0x2e, 0xac, 0xdb, 0xad, 0xe4, 0x53, 0xb9, 0x95, 0x5f, 0x64, 0x60, 0x64, - 0xcd, 0x75, 0x44, 0xb1, 0xf9, 0xe0, 0xeb, 0x96, 0x57, 0x61, 0x88, 0xb5, 0x69, 0x55, 0xd9, 0xbc, - 0x63, 0xfd, 0x69, 0x9a, 0x9a, 0x5e, 0xa5, 0x4d, 0xab, 0xa5, 0x51, 0x6e, 0x46, 0xf9, 0x2f, 0x2c, - 0x80, 0xb4, 0x02, 0x5c, 0x36, 0x4d, 0xb0, 0xef, 0x43, 0xde, 0xbd, 0x00, 0xf7, 0x81, 0x01, 0x05, - 0xd5, 0xf3, 0x81, 0xad, 0xf4, 0xa8, 0xf9, 0xf5, 0xa8, 0xf4, 0xfc, 0x24, 0x5c, 0x01, 0xdf, 0x34, - 0xf4, 0x5d, 0x98, 0x6e, 0xbb, 0x4e, 0xcb, 0xe1, 0x9a, 0xba, 0xe6, 0x34, 0xad, 0xaa, 0x45, 0xfd, - 0x62, 0xe1, 0xa9, 0xbe, 0xf9, 0x69, 0xc3, 0xb7, 0x4b, 0x8f, 0x28, 0xbe, 0xd3, 0x6b, 0x71, 0x5c, - 0xdc, 0xcd, 0xca, 0xfc, 0xbb, 0x01, 0xe3, 0x91, 0xbd, 0x47, 0x55, 0x80, 0xaa, 0x63, 0xd7, 0x2c, - 0x2f, 0xb8, 0x90, 0x2d, 0x1c, 0x5f, 0xec, 0x6f, 0x57, 0xcb, 0xfe, 0xb8, 0x50, 0xe8, 0x82, 0x26, - 0x86, 0x35, 0x58, 0x74, 0xc2, 0x7f, 0xbf, 0x10, 0x8d, 0x97, 0xe4, 0xfb, 0x85, 0x3b, 0x3b, 0x0b, - 0x63, 0x6a, 0x4e, 0xfa, 0x7b, 0x86, 0x34, 0xaf, 0x04, 0x7e, 0x9b, 0x81, 0x7c, 0xb0, 0xfe, 0xfb, - 0xa0, 0x46, 0x2f, 0x47, 0xd4, 0xe8, 0x44, 0xca, 0x93, 0x13, 0x8a, 0x14, 0xc4, 0x24, 0x9a, 0x32, - 0xdd, 0x8e, 0x29, 0x53, 0x5a, 0x91, 0xd8, 0x43, 0x9d, 0xfe, 0x24, 0x0f, 0x5f, 0xf6, 0xbd, 0x0f, - 0x0a, 0x75, 0x2d, 0xaa, 0x50, 0x8b, 0x29, 0x57, 0xd3, 0x43, 0xa5, 0xde, 0x31, 0x60, 0x32, 0xa6, - 0x04, 0xe8, 0x71, 0x18, 0x16, 0xd5, 0x21, 0x25, 0x5f, 0xc1, 0x40, 0x55, 0x4b, 0x10, 0x34, 0xb4, - 0x06, 0x33, 0xa4, 0xe3, 0x39, 0xc1, 0xd8, 0x0b, 0x36, 0x59, 0x6f, 0x52, 0x59, 0x20, 0x18, 0x2d, - 0xfd, 0x8f, 0x1a, 0x33, 0xb3, 0x94, 0xd0, 0x07, 0x27, 0x8e, 0x34, 0x3f, 0xca, 0x00, 0x0a, 0x1a, - 0xd3, 0x54, 0x55, 0x6f, 0xc3, 0xc8, 0x86, 0xac, 0xff, 0xdc, 0x5b, 0x59, 0xbc, 0x54, 0xd0, 0x6f, - 0x06, 0x7c, 0x4c, 0xf4, 0xf5, 0xfd, 0x91, 0x23, 0xe8, 0x96, 0x21, 0x74, 0x13, 0x60, 0xc3, 0xb2, - 0x2d, 0xd6, 0x18, 0xf0, 0x02, 0x4f, 0x78, 0xf3, 0x8b, 0x01, 0x02, 0xd6, 0xd0, 0xcc, 0x77, 0x33, - 0x9a, 0x7c, 0x0a, 0x73, 0xd9, 0xd7, 0xb9, 0x3e, 0x1d, 0xdd, 0xcc, 0x7c, 0xf7, 0x95, 0x89, 0xb6, - 0x31, 0x43, 0x5b, 0xc4, 0xf5, 0xab, 0xb7, 0xa7, 0x53, 0x6e, 0xcb, 0x75, 0xe2, 0x5a, 0xfc, 0xe0, - 0xc3, 0x23, 0xbd, 0x4e, 0x5c, 0x86, 0x05, 0x24, 0x7a, 0x85, 0x4f, 0x95, 0x06, 0x39, 0x5b, 0x6a, - 0x9b, 0xe0, 0xd1, 0xb6, 0xbe, 0x3e, 0xda, 0x66, 0x58, 0x02, 0x9a, 0x6f, 0xe7, 0x34, 0x81, 0x57, - 0x56, 0xfb, 0x0a, 0xa0, 0x26, 0x61, 0xde, 0x65, 0x62, 0xd7, 0xb8, 0x78, 0xd2, 0x0d, 0x97, 0xb2, - 0x86, 0x8a, 0xd0, 0xe6, 0x14, 0x0a, 0x5a, 0xed, 0xea, 0x81, 0x13, 0x46, 0xa1, 0x53, 0x51, 0xe3, - 0xbc, 0x10, 0x37, 0xce, 0x13, 0xa1, 0xb6, 0x0d, 0x66, 0x9e, 0x75, 0x71, 0x1f, 0x3e, 0x00, 0x71, - 0xff, 0x0e, 0x4c, 0x6f, 0xc4, 0xaf, 0xd0, 0xd4, 0x85, 0xfa, 0xe9, 0x01, 0x6f, 0xe0, 0x4a, 0x47, - 0x76, 0xc3, 0x7b, 0x97, 0xb0, 0x19, 0x77, 0x33, 0x42, 0x8e, 0xff, 0x74, 0x4c, 0x64, 0x9f, 0xb2, - 0x86, 0xd1, 0xb7, 0xca, 0xc5, 0xf2, 0xd6, 0xf8, 0xa3, 0x31, 0x09, 0x89, 0x23, 0x0c, 0x62, 0x2a, - 0x98, 0xdb, 0x4f, 0x15, 0x44, 0xa7, 0xa0, 0x50, 0xed, 0xb8, 0x2e, 0xb5, 0x3d, 0x3e, 0x1d, 0x11, - 0xca, 0x66, 0xc3, 0x5a, 0x74, 0x39, 0x24, 0x61, 0xbd, 0x1f, 0x7a, 0x4d, 0xe8, 0xa9, 0x47, 0x45, - 0x8e, 0x71, 0xef, 0xb9, 0x74, 0x5e, 0xe9, 0xb8, 0x27, 0x75, 0xdc, 0xa3, 0xe6, 0x1f, 0x74, 0xd7, - 0xd5, 0x67, 0x86, 0x3f, 0x07, 0x19, 0xc2, 0xe2, 0xe5, 0xdf, 0x25, 0x86, 0x33, 0x44, 0xcf, 0xfe, - 0xb3, 0x07, 0x96, 0xfd, 0xbb, 0x10, 0xc6, 0x64, 0xfe, 0x83, 0x4c, 0x74, 0x5b, 0x45, 0x0e, 0x46, - 0x9a, 0x07, 0x86, 0x5d, 0x30, 0xbd, 0x22, 0x08, 0xf3, 0x23, 0x03, 0x8e, 0x24, 0xf6, 0x0e, 0x4c, - 0x5f, 0xe6, 0x00, 0x4d, 0x9f, 0xb1, 0xdf, 0xa6, 0xef, 0xa6, 0xb6, 0x85, 0xfe, 0x14, 0xf6, 0xeb, - 0x15, 0xf5, 0xfb, 0x19, 0x98, 0xe2, 0xd9, 0x79, 0xa4, 0x0a, 0xb0, 0xe6, 0x3f, 0xd9, 0x4a, 0xe1, - 0x36, 0x63, 0x65, 0xed, 0xd2, 0x48, 0xe4, 0xad, 0xd6, 0x2b, 0x30, 0x2c, 0x12, 0x74, 0xe5, 0xe8, - 0x4f, 0xa7, 0xb8, 0x86, 0x8c, 0xa0, 0x0a, 0x9d, 0x90, 0x95, 0x0e, 0x09, 0xc8, 0x91, 0xc5, 0x0d, - 0xa2, 0x12, 0xe1, 0xd3, 0x29, 0xee, 0x22, 0xbb, 0x91, 0x45, 0x33, 0x96, 0x80, 0xe6, 0x7b, 0x19, - 0x90, 0x2e, 0xf6, 0x3e, 0x84, 0xd2, 0x5f, 0x8b, 0x84, 0xd2, 0x7d, 0xc6, 0x88, 0x62, 0x72, 0x3d, - 0xc3, 0xe8, 0x78, 0xf8, 0x73, 0x2c, 0x0d, 0xe8, 0xdd, 0x43, 0xe8, 0x3f, 0x1a, 0x90, 0x17, 0xfd, - 0xee, 0x43, 0xf8, 0xbc, 0x16, 0x0d, 0x9f, 0x9f, 0x49, 0xb1, 0x8a, 0x1e, 0xa1, 0xf3, 0xbb, 0x59, - 0x35, 0xfb, 0x20, 0xb8, 0x6a, 0x10, 0xb7, 0xa6, 0xc2, 0x86, 0x50, 0x03, 0x79, 0x23, 0x96, 0x34, - 0xf4, 0x2d, 0x98, 0x72, 0xe5, 0xa5, 0x34, 0xad, 0x5d, 0x0c, 0x7c, 0x78, 0x36, 0xf5, 0xad, 0xb1, - 0xba, 0xd9, 0x0e, 0xcb, 0x47, 0x38, 0x86, 0x8a, 0xbb, 0xf8, 0x70, 0xbf, 0xde, 0x8e, 0xdb, 0x32, - 0xe5, 0xef, 0x4e, 0x0f, 0x68, 0x38, 0xa5, 0x5f, 0xef, 0x6a, 0xc6, 0xdd, 0x8c, 0x50, 0x03, 0xc6, - 0xf4, 0xf7, 0x2e, 0x4a, 0x96, 0x8e, 0xa7, 0x7f, 0x58, 0x23, 0x6f, 0x26, 0xf4, 0x16, 0x1c, 0x41, - 0x36, 0x77, 0x72, 0x50, 0xd0, 0x84, 0x2f, 0x96, 0x92, 0x8f, 0x1f, 0x4c, 0x4a, 0x9e, 0x1c, 0x41, - 0x16, 0x06, 0x8a, 0x20, 0x8f, 0x45, 0x23, 0xc8, 0x47, 0xe3, 0x11, 0x24, 0x88, 0xd5, 0x45, 0xa2, - 0x47, 0x06, 0x13, 0x2a, 0x94, 0xf2, 0x1f, 0x2e, 0xa5, 0x8a, 0xc9, 0xbb, 0x03, 0x36, 0xb4, 0xbb, - 0xb3, 0xe0, 0x3f, 0x3b, 0xf0, 0x1f, 0x2c, 0xc5, 0x58, 0xa0, 0xf3, 0x01, 0xd3, 0x4a, 0xa7, 0xd5, - 0x22, 0xee, 0xf6, 0xec, 0x98, 0x98, 0x70, 0x50, 0xab, 0xbb, 0x18, 0xa1, 0xe2, 0x58, 0x6f, 0xb4, - 0x06, 0x39, 0x19, 0x89, 0xa9, 0xc7, 0x30, 0xcf, 0xa6, 0x09, 0xf2, 0x64, 0x8c, 0x20, 0x7f, 0x63, - 0x85, 0xa3, 0x07, 0xd1, 0xf9, 0x3d, 0x82, 0xe8, 0x2b, 0x80, 0x9c, 0x75, 0x11, 0x8d, 0xd4, 0x2e, - 0xc9, 0xcf, 0x8d, 0xb8, 0x54, 0xe6, 0x44, 0x84, 0x16, 0x1c, 0xd8, 0xd5, 0xae, 0x1e, 0x38, 0x61, - 0x14, 0xd7, 0x6a, 0x15, 0xbe, 0x05, 0xaa, 0xa0, 0x02, 0xe6, 0x33, 0x29, 0x15, 0x2b, 0x8c, 0xcd, - 0x67, 0xb8, 0x56, 0x97, 0x63, 0xa8, 0xb8, 0x8b, 0x0f, 0x7a, 0x1d, 0xc6, 0xb9, 0x08, 0x85, 0x8c, - 0xe1, 0x1e, 0x19, 0x4f, 0xef, 0xee, 0x2c, 0x8c, 0xaf, 0xea, 0x90, 0x38, 0xca, 0xc1, 0xfc, 0x77, - 0x06, 0x22, 0xfa, 0x87, 0xde, 0x31, 0x60, 0x9a, 0xc4, 0xbe, 0x95, 0xf1, 0xc3, 0x97, 0xaf, 0xa4, - 0xfb, 0x80, 0xa9, 0xeb, 0x53, 0x9b, 0xb0, 0x22, 0x17, 0xef, 0xc2, 0x70, 0x37, 0x53, 0xf4, 0x43, - 0x03, 0x0e, 0x93, 0xee, 0x8f, 0xa1, 0x94, 0x3f, 0x3c, 0x3b, 0xf0, 0xd7, 0x54, 0xa5, 0x87, 0x77, - 0x77, 0x16, 0x92, 0x3e, 0x13, 0xc3, 0x49, 0xec, 0xd0, 0xab, 0x30, 0x44, 0xdc, 0xba, 0x9f, 0xbd, - 0xa6, 0x67, 0xeb, 0x7f, 0xe3, 0x16, 0x3a, 0xe4, 0x25, 0xb7, 0xce, 0xb0, 0x00, 0x35, 0x3f, 0xcd, - 0xc2, 0x54, 0xfc, 0xa9, 0xa1, 0x7a, 0x9d, 0x31, 0x94, 0xf8, 0x3a, 0xe3, 0x71, 0x18, 0x26, 0x55, - 0x2f, 0x78, 0x2a, 0x11, 0x3e, 0xaf, 0xe5, 0x8d, 0x58, 0xd2, 0xd0, 0x0d, 0xc8, 0x33, 0x8f, 0xb8, - 0x1e, 0x4f, 0x6a, 0x54, 0x5e, 0x99, 0xfa, 0x29, 0x71, 0xc5, 0x07, 0xc0, 0x21, 0x16, 0x3a, 0x13, - 0x35, 0x67, 0x66, 0xdc, 0x9c, 0x4d, 0xeb, 0x6b, 0x19, 0x34, 0x27, 0x6e, 0x41, 0x41, 0x3b, 0x07, - 0xe5, 0x5d, 0xce, 0xa5, 0xde, 0xf7, 0x50, 0xec, 0x26, 0xe5, 0x87, 0x72, 0x21, 0x45, 0xc7, 0x0f, - 0x93, 0x46, 0xb1, 0x5b, 0xf7, 0x94, 0x34, 0x8a, 0xed, 0xd2, 0xd0, 0xcc, 0x49, 0x18, 0x8f, 0xbc, - 0x18, 0x12, 0xe5, 0xd8, 0x1b, 0xc4, 0xa5, 0x0d, 0xa7, 0xc3, 0xe8, 0x83, 0x5a, 0x8e, 0x0d, 0x26, - 0xb8, 0xdf, 0xe5, 0xd8, 0x10, 0x78, 0xef, 0x72, 0x6c, 0xd0, 0xf7, 0x81, 0x2d, 0xc7, 0x06, 0x33, - 0xec, 0x11, 0x53, 0xfe, 0x2b, 0xa3, 0xad, 0x22, 0x1a, 0x57, 0x66, 0xee, 0x12, 0x57, 0xde, 0x82, - 0x51, 0xcb, 0xf6, 0xa8, 0xbb, 0x45, 0x9a, 0xaa, 0x8a, 0xd8, 0xe7, 0x52, 0x97, 0x3b, 0xae, 0x8a, - 0xaa, 0xfc, 0xa5, 0xae, 0x28, 0x1c, 0x1c, 0x20, 0xa2, 0x26, 0x1c, 0xf1, 0x0b, 0x35, 0x2e, 0x25, - 0x61, 0xa1, 0x58, 0x5d, 0xb6, 0x3e, 0xef, 0xdf, 0xf4, 0x5d, 0x4c, 0xea, 0x74, 0xa7, 0x17, 0x01, - 0x27, 0x83, 0x22, 0x06, 0xe3, 0x4c, 0x4b, 0xa8, 0x7c, 0x47, 0xd2, 0x67, 0x91, 0x2b, 0x9e, 0x83, - 0x6a, 0x37, 0xb4, 0x3a, 0x28, 0x8e, 0xf2, 0x30, 0xff, 0x9a, 0x85, 0xc9, 0x98, 0xa4, 0xc5, 0x02, - 0xc7, 0xfc, 0xfd, 0x0c, 0x1c, 0x73, 0x03, 0x05, 0x8e, 0xc9, 0x31, 0xcd, 0xd0, 0x40, 0x31, 0xcd, - 0x0b, 0x32, 0xae, 0x50, 0x27, 0xb7, 0xb2, 0xac, 0xde, 0xce, 0x05, 0xbb, 0xb9, 0xaa, 0x13, 0x71, - 0xb4, 0xaf, 0xf0, 0xc2, 0xb5, 0xee, 0x0f, 0xc5, 0x54, 0x50, 0x74, 0x36, 0xed, 0x8b, 0x84, 0x00, - 0x40, 0x7a, 0xe1, 0x04, 0x02, 0x4e, 0x62, 0x57, 0xba, 0xf2, 0xe1, 0x67, 0xf3, 0x87, 0x3e, 0xfe, - 0x6c, 0xfe, 0xd0, 0x27, 0x9f, 0xcd, 0x1f, 0x7a, 0x6b, 0x77, 0xde, 0xf8, 0x70, 0x77, 0xde, 0xf8, - 0x78, 0x77, 0xde, 0xf8, 0x64, 0x77, 0xde, 0xf8, 0xc7, 0xee, 0xbc, 0xf1, 0xb3, 0xcf, 0xe7, 0x0f, - 0xdd, 0x7c, 0xa2, 0x9f, 0x2f, 0xfa, 0xff, 0x13, 0x00, 0x00, 0xff, 0xff, 0xa6, 0x0e, 0x2f, 0xd4, - 0xf8, 0x3f, 0x00, 0x00, + // 3673 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xdc, 0x3c, 0x4d, 0x6f, 0x23, 0x47, + 0x76, 0xd3, 0xa4, 0x44, 0x89, 0x8f, 0xfa, 0xac, 0xd1, 0xd8, 0x5a, 0x6d, 0x2c, 0x4d, 0xda, 0x86, + 0x61, 0xc7, 0x36, 0x95, 0xf9, 0xf2, 0x7c, 0x38, 0x99, 0x40, 0xa4, 0xe6, 0x43, 0x13, 0xad, 0x47, + 0x29, 0x8e, 0x67, 0x76, 0x67, 0x67, 0xe0, 0x94, 0xc8, 0x12, 0xd9, 0x11, 0xd9, 0xcd, 0xed, 0x2a, + 0x6a, 0xad, 0x24, 0x48, 0x36, 0x5f, 0xc0, 0x02, 0xf9, 0x40, 0x0e, 0x46, 0xbc, 0x01, 0x12, 0x20, + 0x48, 0xce, 0xc9, 0x1f, 0xc8, 0xc1, 0x87, 0x5c, 0x8c, 0xc0, 0x09, 0x8c, 0x24, 0x07, 0x1f, 0x0c, + 0x21, 0x96, 0x81, 0x1c, 0x7d, 0xcb, 0x65, 0x80, 0x00, 0x41, 0x7d, 0x74, 0x77, 0x75, 0xb3, 0x39, + 0x62, 0x73, 0xa4, 0xc1, 0x60, 0x6f, 0x54, 0xbd, 0xaf, 0xaa, 0x57, 0xef, 0xbd, 0x7a, 0xef, 0x55, + 0xb5, 0xe0, 0x62, 0xd3, 0xe1, 0xad, 0xde, 0x76, 0xb9, 0xee, 0x75, 0x56, 0xc9, 0x6e, 0xcf, 0xe1, + 0xfb, 0xab, 0xbb, 0xc4, 0x6f, 0x7a, 0xab, 0xa4, 0xeb, 0xac, 0xee, 0x9d, 0x23, 0xed, 0x6e, 0x8b, + 0x9c, 0x5b, 0x6d, 0x52, 0x97, 0xfa, 0x84, 0xd3, 0x46, 0xb9, 0xeb, 0x7b, 0xdc, 0x43, 0xaf, 0x45, + 0x54, 0x65, 0x45, 0x55, 0x96, 0x54, 0x65, 0xd2, 0x75, 0xca, 0x01, 0xd5, 0xd2, 0x3b, 0x06, 0xef, + 0xa6, 0xd7, 0xf4, 0x56, 0x25, 0xf1, 0x76, 0x6f, 0x47, 0xfe, 0x25, 0xff, 0x90, 0xbf, 0x14, 0xd3, + 0xa5, 0xdb, 0xbb, 0x57, 0x58, 0xd9, 0x91, 0x92, 0xe9, 0x47, 0x9c, 0xba, 0xcc, 0xf1, 0x5c, 0xf6, + 0x0e, 0xe9, 0x3a, 0x8c, 0xfa, 0x7b, 0xd4, 0x5f, 0xed, 0xee, 0x36, 0x05, 0x8c, 0xc5, 0x11, 0x56, + 0xf7, 0xfa, 0xa6, 0xb7, 0x74, 0x31, 0xe2, 0xd4, 0x21, 0xf5, 0x96, 0xe3, 0x52, 0x7f, 0x3f, 0x22, + 0xef, 0x50, 0x4e, 0xd2, 0xa8, 0x56, 0x07, 0x51, 0xf9, 0x3d, 0x97, 0x3b, 0x1d, 0xda, 0x47, 0xf0, + 0xee, 0x51, 0x04, 0xac, 0xde, 0xa2, 0x1d, 0x92, 0xa4, 0xb3, 0x1f, 0xc1, 0xe9, 0x35, 0x97, 0xb4, + 0xf7, 0x99, 0xc3, 0x70, 0xcf, 0x5d, 0xf3, 0x9b, 0xbd, 0x0e, 0x75, 0x39, 0x3a, 0x0b, 0x63, 0x2e, + 0xe9, 0xd0, 0x45, 0xeb, 0xac, 0xf5, 0x46, 0xb1, 0x32, 0xf5, 0xd9, 0xc1, 0xca, 0xa9, 0xc3, 0x83, + 0x95, 0xb1, 0xf7, 0x49, 0x87, 0x62, 0x09, 0x41, 0xaf, 0xc2, 0xf8, 0x1e, 0x69, 0xf7, 0xe8, 0x62, + 0x4e, 0xa2, 0x4c, 0x6b, 0x94, 0xf1, 0xfb, 0x62, 0x10, 0x2b, 0x98, 0xfd, 0x47, 0xf9, 0x18, 0xfb, + 0xef, 0x51, 0x4e, 0x1a, 0x84, 0x13, 0xd4, 0x81, 0x42, 0x9b, 0x6c, 0xd3, 0x36, 0x5b, 0xb4, 0xce, + 0xe6, 0xdf, 0x28, 0x9d, 0xbf, 0x51, 0x1e, 0x66, 0x13, 0xcb, 0x29, 0xac, 0xca, 0x9b, 0x92, 0xcf, + 0x0d, 0x97, 0xfb, 0xfb, 0x95, 0x19, 0x3d, 0x89, 0x82, 0x1a, 0xc4, 0x5a, 0x08, 0xfa, 0x03, 0x0b, + 0x4a, 0xc4, 0x75, 0x3d, 0x4e, 0xb8, 0xd8, 0xa6, 0xc5, 0x9c, 0x14, 0x7a, 0x67, 0x74, 0xa1, 0x6b, + 0x11, 0x33, 0x25, 0xf9, 0xb4, 0x96, 0x5c, 0x32, 0x20, 0xd8, 0x94, 0xb9, 0x74, 0x15, 0x4a, 0xc6, + 0x54, 0xd1, 0x1c, 0xe4, 0x77, 0xe9, 0xbe, 0xd2, 0x2f, 0x16, 0x3f, 0xd1, 0x42, 0x4c, 0xa1, 0x5a, + 0x83, 0xd7, 0x72, 0x57, 0xac, 0xa5, 0xeb, 0x30, 0x97, 0x14, 0x98, 0x85, 0xde, 0xfe, 0x0b, 0x0b, + 0x16, 0x8c, 0x55, 0x60, 0xba, 0x43, 0x7d, 0xea, 0xd6, 0x29, 0x5a, 0x85, 0xa2, 0xd8, 0x4b, 0xd6, + 0x25, 0xf5, 0x60, 0xab, 0xe7, 0xf5, 0x42, 0x8a, 0xef, 0x07, 0x00, 0x1c, 0xe1, 0x84, 0x66, 0x91, + 0x7b, 0x9a, 0x59, 0x74, 0x5b, 0x84, 0xd1, 0xc5, 0x7c, 0xdc, 0x2c, 0xb6, 0xc4, 0x20, 0x56, 0x30, + 0xfb, 0x57, 0xe1, 0x3b, 0xc1, 0x7c, 0xee, 0xd1, 0x4e, 0xb7, 0x4d, 0x38, 0x8d, 0x26, 0x75, 0xa4, + 0xe9, 0xd9, 0xb3, 0x30, 0xbd, 0xd6, 0xed, 0xfa, 0xde, 0x1e, 0x6d, 0xd4, 0x38, 0x69, 0x52, 0xfb, + 0x0f, 0x2d, 0x38, 0xb3, 0xe6, 0x37, 0xbd, 0xea, 0xfa, 0x5a, 0xb7, 0x7b, 0x9b, 0x92, 0x36, 0x6f, + 0xd5, 0x38, 0xe1, 0x3d, 0x86, 0xae, 0x43, 0x81, 0xc9, 0x5f, 0x9a, 0xdd, 0xeb, 0x81, 0x85, 0x28, + 0xf8, 0x93, 0x83, 0x95, 0x85, 0x14, 0x42, 0x8a, 0x35, 0x15, 0x7a, 0x13, 0x26, 0x3a, 0x94, 0x31, + 0xd2, 0x0c, 0xd6, 0x3c, 0xab, 0x19, 0x4c, 0x7c, 0x4f, 0x0d, 0xe3, 0x00, 0x6e, 0xff, 0x6b, 0x0e, + 0x66, 0x43, 0x5e, 0x5a, 0xfc, 0x09, 0x28, 0xb8, 0x07, 0x53, 0x2d, 0x63, 0x85, 0x52, 0xcf, 0xa5, + 0xf3, 0xef, 0x0d, 0x69, 0xcb, 0x69, 0x4a, 0xaa, 0x2c, 0x68, 0x31, 0x53, 0xe6, 0x28, 0x8e, 0x89, + 0x41, 0x1d, 0x00, 0xb6, 0xef, 0xd6, 0xb5, 0xd0, 0x31, 0x29, 0xf4, 0x6a, 0x46, 0xa1, 0xb5, 0x90, + 0x41, 0x05, 0x69, 0x91, 0x10, 0x8d, 0x61, 0x43, 0x80, 0xfd, 0x4f, 0x16, 0x9c, 0x4e, 0xa1, 0x43, + 0xbf, 0x92, 0xd8, 0xcf, 0xd7, 0xfa, 0xf6, 0x13, 0xf5, 0x91, 0x45, 0xbb, 0xf9, 0x36, 0x4c, 0xfa, + 0x74, 0xcf, 0x11, 0xb1, 0x5a, 0x6b, 0x78, 0x4e, 0xd3, 0x4f, 0x62, 0x3d, 0x8e, 0x43, 0x0c, 0xf4, + 0x16, 0x14, 0x83, 0xdf, 0x42, 0xcd, 0x79, 0x61, 0xce, 0x62, 0xe3, 0x02, 0x54, 0x86, 0x23, 0xb8, + 0xfd, 0xfb, 0x30, 0x5e, 0x6d, 0x11, 0x9f, 0x0b, 0x8b, 0xf1, 0x69, 0xd7, 0xfb, 0x00, 0x6f, 0xea, + 0x29, 0x86, 0x16, 0x83, 0xd5, 0x30, 0x0e, 0xe0, 0x43, 0x6c, 0xf6, 0x9b, 0x30, 0xb1, 0x47, 0x7d, + 0x39, 0xdf, 0x7c, 0x9c, 0xd9, 0x7d, 0x35, 0x8c, 0x03, 0xb8, 0xfd, 0x9f, 0x16, 0x2c, 0xc8, 0x19, + 0xac, 0x3b, 0xac, 0xee, 0xed, 0x51, 0x7f, 0x1f, 0x53, 0xd6, 0x6b, 0x1f, 0xf3, 0x84, 0xd6, 0x61, + 0x8e, 0xd1, 0xce, 0x1e, 0xf5, 0xab, 0x9e, 0xcb, 0xb8, 0x4f, 0x1c, 0x97, 0xeb, 0x99, 0x2d, 0x6a, + 0xec, 0xb9, 0x5a, 0x02, 0x8e, 0xfb, 0x28, 0xd0, 0x1b, 0x30, 0xa9, 0xa7, 0x2d, 0x4c, 0x49, 0x28, + 0x76, 0x4a, 0xec, 0x81, 0x5e, 0x13, 0xc3, 0x21, 0xd4, 0xfe, 0x1f, 0x0b, 0xe6, 0xe5, 0xaa, 0x6a, + 0xbd, 0x6d, 0x56, 0xf7, 0x9d, 0xae, 0x08, 0x81, 0x2f, 0xe2, 0x92, 0xae, 0xc3, 0x4c, 0x23, 0x50, + 0xfc, 0xa6, 0xd3, 0x71, 0xb8, 0xf4, 0x91, 0xf1, 0xca, 0x4b, 0x9a, 0xc7, 0xcc, 0x7a, 0x0c, 0x8a, + 0x13, 0xd8, 0xf6, 0xdf, 0xe4, 0xe1, 0x74, 0x80, 0x42, 0x1b, 0x6b, 0x3e, 0x77, 0x76, 0x48, 0x9d, + 0x33, 0xd4, 0x80, 0xa9, 0x46, 0x34, 0xcc, 0xb5, 0xe7, 0xfd, 0x52, 0x59, 0x1d, 0xf7, 0x65, 0xf3, + 0xb8, 0x2f, 0x77, 0x77, 0x9b, 0x62, 0x80, 0x95, 0x45, 0x56, 0x51, 0xde, 0x3b, 0x57, 0xbe, 0xe7, + 0x74, 0x68, 0xe4, 0xdd, 0x06, 0x7b, 0x8e, 0x63, 0x5c, 0xd1, 0x03, 0xc8, 0x37, 0x1d, 0xae, 0x0f, + 0xe3, 0x2b, 0xc3, 0xb9, 0xf5, 0x2d, 0x27, 0x69, 0x6a, 0x95, 0x92, 0x16, 0x95, 0xbf, 0xe5, 0x70, + 0x2c, 0x38, 0xa2, 0x6d, 0x28, 0x38, 0x1d, 0xd2, 0xa4, 0xc1, 0x99, 0x7b, 0x6d, 0x38, 0xde, 0x1b, + 0x82, 0x26, 0xc9, 0x3d, 0x3c, 0xdd, 0x25, 0x94, 0x61, 0xcd, 0x59, 0xc8, 0xa8, 0x0b, 0x13, 0x51, + 0x4e, 0x3a, 0xb4, 0x8c, 0x34, 0x67, 0x89, 0x64, 0x48, 0x28, 0xc3, 0x9a, 0xb3, 0xfd, 0x65, 0x0e, + 0xe6, 0x22, 0xfd, 0x55, 0xbd, 0x4e, 0xc7, 0xe1, 0x68, 0x09, 0x72, 0x4e, 0x43, 0x5b, 0x20, 0x68, + 0xc2, 0xdc, 0xc6, 0x3a, 0xce, 0x39, 0x0d, 0xf4, 0x3a, 0x14, 0xb6, 0x7d, 0xe2, 0xd6, 0x5b, 0xda, + 0xf2, 0x42, 0xc6, 0x15, 0x39, 0x8a, 0x35, 0x14, 0xbd, 0x02, 0x79, 0x4e, 0x9a, 0xda, 0xe0, 0x42, + 0xfd, 0xdd, 0x23, 0x4d, 0x2c, 0xc6, 0x85, 0xa5, 0xb3, 0xde, 0xf6, 0x6f, 0xd1, 0xba, 0xda, 0x79, + 0xc3, 0xd2, 0x6b, 0x6a, 0x18, 0x07, 0x70, 0x21, 0x91, 0xf4, 0x78, 0xcb, 0xf3, 0x17, 0xc7, 0xe3, + 0x12, 0xd7, 0xe4, 0x28, 0xd6, 0x50, 0x71, 0x26, 0xd5, 0xe5, 0xfc, 0x39, 0xf5, 0x17, 0x0b, 0xf1, + 0x33, 0xa9, 0x1a, 0x00, 0x70, 0x84, 0x83, 0x1e, 0x43, 0xa9, 0xee, 0x53, 0xc2, 0x3d, 0x7f, 0x9d, + 0x70, 0xba, 0x38, 0x91, 0xd9, 0x02, 0x67, 0x45, 0x62, 0x54, 0x8d, 0x58, 0x60, 0x93, 0x9f, 0xfd, + 0xad, 0x05, 0x8b, 0x91, 0x6a, 0xe5, 0xde, 0x46, 0xc9, 0x80, 0x56, 0x8f, 0x35, 0x40, 0x3d, 0xaf, + 0x43, 0xa1, 0xe1, 0x34, 0x29, 0xe3, 0x49, 0x2d, 0xaf, 0xcb, 0x51, 0xac, 0xa1, 0xe8, 0x3c, 0x40, + 0xd3, 0xe1, 0x3a, 0x38, 0x68, 0x65, 0x87, 0x47, 0xd0, 0xad, 0x10, 0x82, 0x0d, 0x2c, 0xf4, 0x00, + 0x8a, 0x72, 0x9a, 0x23, 0xba, 0x9d, 0x3c, 0x2a, 0xaa, 0x01, 0x03, 0x1c, 0xf1, 0xb2, 0xbf, 0x18, + 0x83, 0x89, 0x9b, 0x3e, 0x75, 0x9a, 0x2d, 0x8e, 0x7e, 0x13, 0x26, 0x3b, 0x3a, 0xa9, 0x94, 0x8b, + 0x2c, 0x9d, 0xff, 0xe5, 0xe1, 0x64, 0xdc, 0x95, 0x9b, 0x2e, 0x12, 0xd2, 0x68, 0x21, 0xd1, 0x18, + 0x0e, 0xb9, 0x8a, 0x84, 0x8c, 0xb4, 0x1d, 0xc2, 0xe4, 0xbe, 0x19, 0x09, 0xd9, 0x9a, 0x18, 0xc4, + 0x0a, 0x86, 0x7e, 0x08, 0x05, 0xcf, 0x77, 0x9a, 0x8e, 0xbb, 0x58, 0x94, 0x93, 0xb8, 0x30, 0x9c, + 0x0b, 0xe9, 0x55, 0xdc, 0x95, 0xa4, 0x91, 0xf2, 0xd5, 0xdf, 0x58, 0xb3, 0x44, 0x0f, 0x61, 0x42, + 0x19, 0x53, 0xe0, 0xa0, 0xab, 0x43, 0x07, 0x18, 0x65, 0x8f, 0x91, 0xd1, 0xab, 0xbf, 0x19, 0x0e, + 0x18, 0xa2, 0x5a, 0x18, 0x5f, 0xc6, 0x24, 0xeb, 0xb7, 0x32, 0xc4, 0x97, 0x81, 0x01, 0xa5, 0x16, + 0x06, 0x94, 0xf1, 0x2c, 0x4c, 0x65, 0xc8, 0x18, 0x14, 0x41, 0x84, 0x8a, 0x75, 0xe6, 0x52, 0x18, + 0x41, 0xc5, 0x3a, 0x6d, 0x9a, 0x89, 0xa7, 0x3b, 0x41, 0x62, 0x63, 0x7f, 0x9c, 0x87, 0x79, 0x8d, + 0x59, 0xf5, 0xda, 0x6d, 0x5a, 0x97, 0xc7, 0xa4, 0x8a, 0x4f, 0xf9, 0xd4, 0xf8, 0xe4, 0xc0, 0xb8, + 0xc3, 0x69, 0x27, 0x28, 0xc0, 0x2a, 0x99, 0x66, 0x13, 0xc9, 0x28, 0x6f, 0x08, 0x26, 0xaa, 0x06, + 0x0a, 0x77, 0x49, 0x63, 0x61, 0x25, 0x01, 0xfd, 0x89, 0x05, 0xa7, 0xf7, 0xa8, 0xef, 0xec, 0x38, + 0x75, 0x59, 0xc1, 0xdc, 0x76, 0x18, 0xf7, 0xfc, 0x7d, 0x7d, 0x22, 0xbc, 0x3b, 0x9c, 0xe4, 0xfb, + 0x06, 0x83, 0x0d, 0x77, 0xc7, 0xab, 0x7c, 0x57, 0x4b, 0x3b, 0x7d, 0xbf, 0x9f, 0x35, 0x4e, 0x93, + 0xb7, 0xd4, 0x05, 0x88, 0x66, 0x9b, 0x52, 0x40, 0x6d, 0x9a, 0x05, 0xd4, 0xd0, 0x13, 0x0b, 0x16, + 0x1b, 0x84, 0x2c, 0xb3, 0xf0, 0xfa, 0xd4, 0x82, 0x92, 0x86, 0x6f, 0x3a, 0x8c, 0xa3, 0x47, 0x7d, + 0xde, 0x5e, 0x1e, 0xce, 0xdb, 0x05, 0xb5, 0xf4, 0xf5, 0x30, 0x5f, 0x0d, 0x46, 0x0c, 0x4f, 0xc7, + 0xc1, 0x96, 0x2a, 0xc5, 0xbe, 0x93, 0x69, 0xfe, 0x51, 0x60, 0x90, 0x3a, 0xd2, 0x7b, 0x67, 0xfb, + 0x30, 0x1d, 0x73, 0x72, 0x74, 0x09, 0xc6, 0x76, 0x1d, 0x37, 0x38, 0xf5, 0x7e, 0x31, 0xc8, 0xa7, + 0x7e, 0xdd, 0x71, 0x1b, 0x4f, 0x0e, 0x56, 0xe6, 0x63, 0xc8, 0x62, 0x10, 0x4b, 0xf4, 0xa3, 0xd3, + 0xb0, 0x6b, 0x93, 0x3f, 0xfb, 0xbb, 0x95, 0x53, 0x3f, 0xf9, 0xea, 0xec, 0x29, 0xfb, 0x93, 0x3c, + 0xcc, 0x25, 0xb5, 0x3a, 0x44, 0x43, 0x22, 0x8a, 0x61, 0x93, 0x27, 0x1a, 0xc3, 0x72, 0x27, 0x17, + 0xc3, 0xf2, 0x27, 0x11, 0xc3, 0xc6, 0x8e, 0x2d, 0x86, 0xd9, 0xff, 0x6e, 0xc1, 0x4c, 0xb8, 0x33, + 0x3f, 0xea, 0x89, 0x93, 0x35, 0xd2, 0xba, 0x75, 0xfc, 0x5a, 0xff, 0x10, 0x26, 0x98, 0xd7, 0xf3, + 0xeb, 0x32, 0x7d, 0x14, 0xdc, 0x2f, 0x66, 0x0b, 0x9a, 0x8a, 0xd6, 0xc8, 0x99, 0xd4, 0x00, 0x0e, + 0xb8, 0xda, 0x8f, 0xc2, 0xf5, 0x68, 0x90, 0xca, 0x28, 0x7c, 0x91, 0x6f, 0x89, 0xf5, 0x4c, 0x9a, + 0x19, 0x85, 0x18, 0xc5, 0x1a, 0x8a, 0x6c, 0x19, 0xce, 0x83, 0xc4, 0xb6, 0x58, 0x01, 0x1d, 0x95, + 0xe5, 0x1e, 0x28, 0x88, 0xfd, 0x6d, 0x3e, 0xf4, 0x1e, 0x5d, 0xbe, 0xfe, 0x18, 0x40, 0x45, 0x26, + 0xda, 0xd8, 0x70, 0x75, 0xe8, 0xad, 0x8e, 0x70, 0x10, 0xe8, 0x70, 0x28, 0xb8, 0xa8, 0xd8, 0x1b, + 0xe6, 0x00, 0x11, 0x00, 0x1b, 0xa2, 0xd0, 0xef, 0x40, 0x89, 0xe8, 0x96, 0xc9, 0x4d, 0xcf, 0xd7, + 0x36, 0xbc, 0x3e, 0x8a, 0xe4, 0xb5, 0x88, 0x4d, 0xb2, 0xf5, 0x15, 0x41, 0xb0, 0x29, 0x6d, 0xc9, + 0x87, 0xd9, 0xc4, 0x7c, 0x53, 0xa2, 0xef, 0x46, 0x3c, 0xfa, 0x5e, 0xc8, 0x72, 0x2c, 0xe8, 0x3e, + 0x90, 0xd9, 0x33, 0x63, 0x30, 0x97, 0x9c, 0xe9, 0xb1, 0x09, 0x8d, 0x35, 0x9f, 0xcc, 0x78, 0xff, + 0xb7, 0x39, 0x28, 0x86, 0x1e, 0x9f, 0xa5, 0x4a, 0x55, 0x27, 0x75, 0xee, 0x88, 0x4a, 0x22, 0x3f, + 0x4c, 0x25, 0x31, 0x36, 0xb8, 0x92, 0x08, 0x3a, 0x59, 0x85, 0xa7, 0x77, 0xb2, 0x8c, 0x4a, 0x62, + 0x62, 0xf8, 0x4a, 0x62, 0xf2, 0xe8, 0x4a, 0xc2, 0xfe, 0x7b, 0x0b, 0x50, 0x7f, 0xd9, 0x98, 0x45, + 0x51, 0x24, 0x19, 0x87, 0x87, 0x3c, 0xa5, 0x93, 0xb5, 0xdb, 0xe0, 0x70, 0x6c, 0x7f, 0x3a, 0x0e, + 0xb3, 0xb7, 0x9c, 0x91, 0x1b, 0x0e, 0x1c, 0x5e, 0x56, 0x9c, 0x6a, 0x54, 0xe7, 0x48, 0x35, 0xee, + 0x13, 0x4e, 0x9b, 0xfb, 0x7a, 0x7f, 0xaf, 0x69, 0xd2, 0x97, 0xab, 0xe9, 0x68, 0x4f, 0x06, 0x83, + 0xf0, 0x20, 0xd6, 0x43, 0x1b, 0xc9, 0x7b, 0x30, 0xcd, 0xb8, 0xef, 0xd4, 0xb9, 0x6a, 0x69, 0xb0, + 0xc5, 0x92, 0x8c, 0x72, 0x67, 0x34, 0xfa, 0x74, 0xcd, 0x04, 0xe2, 0x38, 0x6e, 0x6a, 0xa7, 0x64, + 0x2c, 0x73, 0xa7, 0x64, 0x15, 0x8a, 0xa4, 0xdd, 0xf6, 0x7e, 0x7c, 0x8f, 0x34, 0x99, 0x2e, 0x55, + 0x43, 0xab, 0x59, 0x0b, 0x00, 0x38, 0xc2, 0x41, 0x65, 0x00, 0xa7, 0xe9, 0x7a, 0x3e, 0x95, 0x14, + 0x05, 0x19, 0x6e, 0x67, 0x44, 0xac, 0xdb, 0x08, 0x47, 0xb1, 0x81, 0x81, 0x6a, 0x70, 0xc6, 0x71, + 0x19, 0xad, 0xf7, 0x7c, 0x5a, 0xdb, 0x75, 0xba, 0xf7, 0x36, 0x6b, 0x32, 0x4a, 0xec, 0x4b, 0x6b, + 0x9e, 0xac, 0xbc, 0xa2, 0x85, 0x9d, 0xd9, 0x48, 0x43, 0xc2, 0xe9, 0xb4, 0xe8, 0x22, 0x4c, 0x39, + 0x6e, 0xbd, 0xdd, 0x6b, 0xd0, 0x2d, 0xc2, 0x5b, 0x6c, 0x71, 0x52, 0x4e, 0x63, 0xee, 0xf0, 0x60, + 0x65, 0x6a, 0xc3, 0x18, 0xc7, 0x31, 0x2c, 0x41, 0x45, 0x3f, 0x32, 0xa8, 0x8a, 0x11, 0xd5, 0x8d, + 0x8f, 0x4c, 0x2a, 0x13, 0x2b, 0xa5, 0x97, 0x04, 0x99, 0x7a, 0x49, 0x7f, 0x95, 0x87, 0x82, 0x6a, + 0xe5, 0xa2, 0x4b, 0x89, 0x7e, 0xe9, 0x2b, 0x7d, 0xfd, 0xd2, 0x52, 0x5a, 0xdb, 0xdb, 0x86, 0x82, + 0xc3, 0x58, 0x2f, 0x7e, 0xba, 0x6d, 0xc8, 0x11, 0xac, 0x21, 0xc8, 0x01, 0x20, 0x41, 0xab, 0x35, + 0x48, 0x5d, 0x2e, 0x65, 0xed, 0x08, 0x27, 0xba, 0xc1, 0x21, 0x80, 0x61, 0x83, 0xb9, 0xec, 0xf0, + 0x78, 0xee, 0x8e, 0xd3, 0xd4, 0x75, 0xf8, 0x75, 0x23, 0x6b, 0x8e, 0x6e, 0xdf, 0x3e, 0x0c, 0xaf, + 0xe7, 0xa2, 0x04, 0x3a, 0x86, 0x20, 0x32, 0xe9, 0x3b, 0xb5, 0xbb, 0xef, 0xab, 0xe5, 0x54, 0x25, + 0x47, 0xac, 0x39, 0x0b, 0x19, 0x5e, 0x8f, 0x77, 0x7b, 0x5c, 0xda, 0xe4, 0x31, 0xc9, 0xb8, 0x2b, + 0x39, 0x62, 0xcd, 0xd9, 0xfe, 0xc4, 0x82, 0x59, 0xa5, 0xee, 0x6a, 0x8b, 0xd6, 0x77, 0x6b, 0x9c, + 0x76, 0x45, 0x62, 0xdb, 0x63, 0x94, 0x25, 0x13, 0xdb, 0x0f, 0x18, 0x65, 0x58, 0x42, 0x8c, 0xd5, + 0xe7, 0x4e, 0x6a, 0xf5, 0xf6, 0x3f, 0x5a, 0x30, 0x2e, 0x33, 0xc8, 0x2c, 0xa1, 0x2e, 0xde, 0x55, + 0xc9, 0x0d, 0xd5, 0x55, 0x39, 0xa2, 0xdf, 0x15, 0x35, 0x74, 0xc6, 0x9e, 0xd6, 0xd0, 0xb1, 0xbf, + 0xb1, 0x60, 0x21, 0xad, 0x49, 0x98, 0x65, 0xfa, 0x6f, 0xc3, 0x64, 0xb7, 0x4d, 0xf8, 0x8e, 0xe7, + 0x77, 0x92, 0xb7, 0x01, 0x5b, 0x7a, 0x1c, 0x87, 0x18, 0xc8, 0x07, 0xf0, 0x83, 0x6a, 0x24, 0x30, + 0xf7, 0xeb, 0x59, 0x0f, 0x9f, 0x78, 0x77, 0x2b, 0x52, 0x56, 0x38, 0xc4, 0xb0, 0x21, 0xc5, 0xfe, + 0xd3, 0x71, 0x98, 0x97, 0x24, 0xa3, 0x1e, 0x46, 0xa3, 0xec, 0x50, 0x17, 0x5e, 0x92, 0x35, 0x44, + 0xff, 0xf9, 0xa5, 0x36, 0xed, 0x8a, 0xa6, 0x7f, 0x69, 0x23, 0x15, 0xeb, 0xc9, 0x40, 0x08, 0x1e, + 0xc0, 0xb7, 0xff, 0x50, 0x82, 0x9f, 0xbf, 0x43, 0xc9, 0x34, 0xb6, 0x89, 0x23, 0x8d, 0x6d, 0xe0, + 0x11, 0x36, 0xf9, 0x0c, 0x47, 0x58, 0xff, 0xb1, 0x52, 0xcc, 0x74, 0xac, 0xfc, 0x75, 0x0e, 0x26, + 0xb6, 0x7c, 0x4f, 0x36, 0x9b, 0x4f, 0xbe, 0x6f, 0x79, 0x17, 0xc6, 0x58, 0x97, 0xd6, 0x75, 0xcc, + 0x3b, 0x37, 0x9c, 0xa7, 0xe9, 0xe9, 0xd5, 0xba, 0xb4, 0x5e, 0x99, 0x14, 0x61, 0x54, 0xfc, 0xc2, + 0x92, 0x91, 0xd1, 0x80, 0xcb, 0x67, 0x49, 0xf6, 0x03, 0x96, 0x4f, 0x6f, 0xc0, 0x7d, 0x6a, 0x41, + 0x49, 0x63, 0xbe, 0xb0, 0x9d, 0x1e, 0x3d, 0xbf, 0x01, 0x9d, 0x9e, 0x3f, 0x8f, 0x56, 0x20, 0x94, + 0x86, 0x7e, 0x0f, 0xe6, 0xbb, 0xbe, 0xd7, 0xf1, 0x84, 0xa7, 0x6e, 0x79, 0x6d, 0xa7, 0xee, 0xd0, + 0xa0, 0x59, 0x78, 0x69, 0x68, 0x79, 0x06, 0xf9, 0x7e, 0xe5, 0x3b, 0x5a, 0xee, 0xfc, 0x56, 0x92, + 0x2f, 0xee, 0x17, 0x65, 0xff, 0x97, 0x05, 0xd3, 0x31, 0xdd, 0xa3, 0x3a, 0x40, 0xdd, 0x73, 0x1b, + 0x0e, 0x0f, 0x2f, 0x64, 0x4b, 0xe7, 0x57, 0x87, 0xd3, 0x6a, 0x35, 0xa0, 0x8b, 0x8c, 0x2e, 0x1c, + 0x62, 0xd8, 0x60, 0x8b, 0x2e, 0x04, 0xef, 0x17, 0xe2, 0xf9, 0x92, 0x7a, 0xbf, 0xf0, 0xe4, 0x60, + 0x65, 0x4a, 0xcf, 0xc9, 0x7c, 0xcf, 0x90, 0xe5, 0x95, 0xc0, 0x3f, 0xe4, 0xa0, 0x18, 0xae, 0xff, + 0x39, 0xb8, 0xd1, 0x07, 0x31, 0x37, 0xba, 0x90, 0x71, 0xe7, 0xa4, 0x23, 0x85, 0x39, 0x89, 0xe1, + 0x4c, 0x8f, 0x13, 0xce, 0x94, 0xd5, 0x24, 0x8e, 0x70, 0xa7, 0x7f, 0x51, 0x9b, 0xaf, 0x70, 0x9f, + 0x83, 0x43, 0xdd, 0x8b, 0x3b, 0xd4, 0x6a, 0xc6, 0xd5, 0x0c, 0x70, 0xa9, 0x9f, 0x5a, 0x30, 0x9b, + 0x70, 0x02, 0xf4, 0x2a, 0x8c, 0xcb, 0xee, 0x90, 0xb6, 0xaf, 0x90, 0x50, 0xf7, 0x12, 0x24, 0x0c, + 0x6d, 0xc1, 0x02, 0xe9, 0x71, 0x2f, 0xa4, 0xbd, 0xe1, 0x92, 0xed, 0x36, 0x55, 0x0d, 0x82, 0xc9, + 0xca, 0x2f, 0x68, 0x9a, 0x85, 0xb5, 0x14, 0x1c, 0x9c, 0x4a, 0x69, 0x7f, 0x9e, 0x03, 0x14, 0x0e, + 0x66, 0xe9, 0xaa, 0x3e, 0x86, 0x89, 0x1d, 0xd5, 0xff, 0x79, 0xb6, 0xb6, 0x78, 0xa5, 0x64, 0xde, + 0x0c, 0x04, 0x3c, 0xd1, 0x0f, 0x8e, 0xc7, 0x8e, 0xa0, 0xdf, 0x86, 0xd0, 0x43, 0x80, 0x1d, 0xc7, + 0x75, 0x58, 0x6b, 0xc4, 0x0b, 0x3c, 0x79, 0x9a, 0xdf, 0x0c, 0x39, 0x60, 0x83, 0x9b, 0xfd, 0x71, + 0xce, 0xb0, 0x4f, 0x19, 0x2e, 0x87, 0xda, 0xd7, 0x37, 0xe3, 0xca, 0x2c, 0xf6, 0x5f, 0x99, 0x18, + 0x8a, 0x19, 0xdb, 0x23, 0x7e, 0xd0, 0xbd, 0xbd, 0x9c, 0x51, 0x2d, 0xf7, 0x89, 0xef, 0x88, 0x8d, + 0x8f, 0xb6, 0xf4, 0x3e, 0xf1, 0x19, 0x96, 0x2c, 0xd1, 0xf7, 0xc5, 0x54, 0x69, 0x58, 0xb3, 0x65, + 0x8e, 0x09, 0x9c, 0x76, 0xcd, 0xf5, 0xd1, 0x2e, 0xc3, 0x8a, 0xa1, 0xfd, 0x6f, 0x05, 0xc3, 0xe0, + 0x75, 0xd4, 0xbe, 0x03, 0xa8, 0x4d, 0x18, 0xbf, 0x4d, 0xdc, 0x86, 0x30, 0x4f, 0xba, 0xe3, 0x53, + 0xd6, 0xd2, 0x19, 0xda, 0x92, 0xe6, 0x82, 0x36, 0xfb, 0x30, 0x70, 0x0a, 0x15, 0xba, 0x14, 0x0f, + 0xce, 0x2b, 0xc9, 0xe0, 0x3c, 0x13, 0x79, 0xdb, 0x68, 0xe1, 0xd9, 0x34, 0xf7, 0xf1, 0x13, 0x30, + 0xf7, 0xdf, 0x85, 0xf9, 0x9d, 0xe4, 0x15, 0x9a, 0xbe, 0x50, 0xbf, 0x3c, 0xe2, 0x0d, 0x5c, 0xe5, + 0xcc, 0x61, 0x74, 0xef, 0x12, 0x0d, 0xe3, 0x7e, 0x41, 0xc8, 0x0b, 0x9e, 0x8e, 0xc9, 0xea, 0x53, + 0xf5, 0x30, 0x86, 0x76, 0xb9, 0x44, 0xdd, 0x9a, 0x7c, 0x34, 0xa6, 0x58, 0xe2, 0x98, 0x80, 0x84, + 0x0b, 0x16, 0x8e, 0xd3, 0x05, 0xd1, 0x25, 0x28, 0xd5, 0x7b, 0xbe, 0x4f, 0x5d, 0x2e, 0xa6, 0x23, + 0x53, 0xd9, 0x7c, 0xd4, 0x8b, 0xae, 0x46, 0x20, 0x6c, 0xe2, 0x09, 0x73, 0x34, 0xfe, 0x5c, 0xe3, + 0x9c, 0x76, 0xba, 0x5c, 0x76, 0xc1, 0xf2, 0x91, 0x39, 0x56, 0xfb, 0x30, 0x70, 0x0a, 0x15, 0xfa, + 0x50, 0xfa, 0x3c, 0xa7, 0xb2, 0x5e, 0x79, 0xf6, 0xba, 0xbc, 0xa8, 0xe3, 0x05, 0x57, 0xf1, 0x82, + 0x53, 0xfb, 0xcf, 0x62, 0x61, 0x66, 0xb8, 0x6e, 0xc1, 0x12, 0xe4, 0x08, 0x4b, 0xb6, 0x92, 0xd7, + 0x18, 0xce, 0x11, 0x86, 0x7e, 0x00, 0xe3, 0x3e, 0xe5, 0xfe, 0xbe, 0x8e, 0x86, 0x57, 0x46, 0xf0, + 0x7c, 0x2c, 0xe8, 0xd5, 0x54, 0xe5, 0x4f, 0xac, 0x38, 0x1a, 0x4d, 0x8a, 0xfc, 0x89, 0x35, 0x29, + 0x2a, 0xc6, 0x19, 0x16, 0xce, 0x45, 0x54, 0x56, 0x44, 0x6d, 0x88, 0x52, 0x4b, 0x3e, 0x3a, 0xe9, + 0xf5, 0x46, 0x31, 0x1c, 0x62, 0xd8, 0x3e, 0x44, 0xe9, 0x67, 0xf0, 0xf6, 0x14, 0x3d, 0xd6, 0x49, + 0x92, 0x95, 0xe5, 0x2d, 0x65, 0x1f, 0x9b, 0x41, 0xc9, 0x92, 0xfd, 0xb9, 0x05, 0x67, 0x52, 0xb1, + 0xc3, 0x28, 0x9f, 0x3b, 0xc1, 0x28, 0x6f, 0x1d, 0x77, 0x94, 0x7f, 0x68, 0xa8, 0x30, 0x98, 0xc2, + 0x71, 0x3d, 0x18, 0xff, 0x59, 0x0e, 0xe6, 0x30, 0xed, 0x7a, 0xb1, 0x86, 0xc7, 0x56, 0xf0, 0x3a, + 0x2d, 0x43, 0x86, 0x90, 0xe8, 0xe0, 0x57, 0x26, 0x62, 0xcf, 0xd2, 0xbe, 0x0f, 0xe3, 0xb2, 0x17, + 0xa1, 0x73, 0x9a, 0xcb, 0x19, 0x6e, 0x5c, 0x63, 0x5c, 0xa5, 0x1f, 0xa8, 0xa6, 0x8e, 0x62, 0x28, + 0x38, 0xcb, 0xcb, 0x52, 0xed, 0x06, 0x97, 0x33, 0x5c, 0xbb, 0xf6, 0x73, 0x96, 0xc3, 0x58, 0x31, + 0xb4, 0x3f, 0xc9, 0x81, 0xca, 0x26, 0x9e, 0x43, 0xd5, 0xf0, 0x1b, 0xb1, 0xaa, 0x61, 0xc8, 0x74, + 0x58, 0x4e, 0x6e, 0x60, 0xc5, 0x90, 0xcc, 0xf4, 0xce, 0x65, 0x61, 0xfa, 0xf4, 0x6a, 0xe1, 0x9f, + 0x2d, 0x28, 0x4a, 0xbc, 0xe7, 0x50, 0x29, 0x6c, 0xc5, 0x2b, 0x85, 0xb7, 0x32, 0xac, 0x62, 0x40, + 0x95, 0xf0, 0x71, 0x5e, 0xcf, 0x3e, 0xcc, 0x23, 0x5b, 0xc4, 0x6f, 0xe8, 0x0c, 0x29, 0xf2, 0x40, + 0x31, 0x88, 0x15, 0x0c, 0xfd, 0x36, 0xcc, 0xf9, 0xea, 0xfe, 0x9d, 0x36, 0x6e, 0x86, 0xe9, 0x4a, + 0x3e, 0xf3, 0x05, 0xb9, 0xbe, 0xc4, 0x8f, 0x3a, 0x65, 0x38, 0xc1, 0x15, 0xf7, 0xc9, 0x11, 0x29, + 0x4c, 0x37, 0x19, 0xcb, 0xf4, 0xd1, 0x7e, 0x79, 0xc4, 0xc0, 0xa9, 0x52, 0x98, 0xbe, 0x61, 0xdc, + 0x2f, 0x08, 0xb5, 0x60, 0xca, 0x7c, 0xda, 0xa3, 0x6d, 0xe9, 0x7c, 0xf6, 0x37, 0x44, 0xea, 0x12, + 0xc6, 0x1c, 0xc1, 0x31, 0xce, 0xf6, 0x41, 0x01, 0x4a, 0x86, 0xf1, 0x25, 0xba, 0x0f, 0xd3, 0x27, + 0xd3, 0x7d, 0x48, 0x4f, 0x96, 0x4b, 0x23, 0x25, 0xcb, 0xe7, 0xe2, 0xc9, 0xf2, 0x77, 0x93, 0xc9, + 0x32, 0xc8, 0xd5, 0xc5, 0x12, 0x65, 0x06, 0x33, 0x3a, 0x6b, 0x0c, 0xde, 0x68, 0x65, 0x2a, 0x3f, + 0xfa, 0x73, 0x53, 0x74, 0x78, 0xb0, 0x12, 0xbc, 0xb0, 0x08, 0xde, 0x66, 0x25, 0x44, 0xa0, 0xeb, + 0xa1, 0xd0, 0x5a, 0xaf, 0xd3, 0x21, 0xfe, 0xfe, 0xe2, 0x94, 0x9c, 0x70, 0xd8, 0x96, 0xbc, 0x19, + 0x83, 0xe2, 0x04, 0x36, 0xda, 0x82, 0x82, 0x4a, 0x3a, 0xf5, 0xbb, 0x9f, 0xb7, 0xb3, 0xe4, 0xb3, + 0x2a, 0xcf, 0x50, 0xbf, 0xb1, 0xe6, 0x63, 0xd6, 0x0b, 0xc5, 0x23, 0xea, 0x85, 0x3b, 0x80, 0xbc, + 0x6d, 0x99, 0xd1, 0x34, 0x6e, 0xa9, 0x2f, 0xab, 0x84, 0x55, 0x16, 0xe2, 0xe9, 0xe4, 0xdd, 0x3e, + 0x0c, 0x9c, 0x42, 0x25, 0xbc, 0x5a, 0x27, 0x99, 0xa1, 0x2b, 0xe8, 0xda, 0x20, 0x6b, 0xa2, 0x16, + 0x95, 0x21, 0x0b, 0xc2, 0xab, 0xab, 0x09, 0xae, 0xb8, 0x4f, 0x0e, 0xfa, 0x11, 0x4c, 0x0b, 0x13, + 0x8a, 0x04, 0xc3, 0x33, 0x0a, 0x9e, 0x3f, 0x3c, 0x58, 0x99, 0xde, 0x34, 0x59, 0xe2, 0xb8, 0x04, + 0xfb, 0xff, 0x72, 0x10, 0xf3, 0x3f, 0xf4, 0x53, 0x0b, 0xe6, 0x49, 0xe2, 0xb3, 0xa0, 0x20, 0x7d, + 0xf9, 0xb5, 0x6c, 0xdf, 0x6a, 0xf5, 0x7d, 0x55, 0x14, 0x35, 0x1f, 0x93, 0x28, 0x0c, 0xf7, 0x0b, + 0x45, 0x7f, 0x6c, 0xc1, 0x69, 0xd2, 0xff, 0xdd, 0x97, 0x3e, 0x0f, 0xaf, 0x8e, 0xfc, 0xe1, 0x58, + 0xe5, 0xe5, 0xc3, 0x83, 0x95, 0xb4, 0x2f, 0xe2, 0x70, 0x9a, 0x38, 0xf4, 0x43, 0x18, 0x23, 0x7e, + 0x33, 0x28, 0xd4, 0xb3, 0x8b, 0x0d, 0x3e, 0xe7, 0x8b, 0x0e, 0xe4, 0x35, 0xbf, 0xc9, 0xb0, 0x64, + 0x6a, 0x7f, 0x95, 0x87, 0xb9, 0xe4, 0xab, 0x4a, 0xfd, 0x10, 0x65, 0x2c, 0xf5, 0x21, 0xca, 0xab, + 0x30, 0x4e, 0xea, 0x3c, 0x7c, 0x15, 0x12, 0xbd, 0x24, 0x16, 0x83, 0x58, 0xc1, 0xd0, 0x03, 0x28, + 0x32, 0x4e, 0x7c, 0x2e, 0xea, 0x37, 0x5d, 0x42, 0x67, 0x7e, 0x35, 0x5d, 0x0b, 0x18, 0xe0, 0x88, + 0x17, 0xba, 0x12, 0x0f, 0x67, 0x76, 0x32, 0x9c, 0xcd, 0x9b, 0x6b, 0x19, 0xb5, 0xfc, 0xef, 0x40, + 0xc9, 0xd8, 0x07, 0x7d, 0xba, 0x5c, 0xcb, 0xac, 0xf7, 0xc8, 0xec, 0x66, 0xd5, 0x37, 0x81, 0x11, + 0xc4, 0xe4, 0x1f, 0xd5, 0xc7, 0x52, 0x5b, 0xcf, 0x54, 0x1f, 0x4b, 0x75, 0x19, 0xdc, 0xec, 0x59, + 0x98, 0x8e, 0x3d, 0x8e, 0x92, 0x9d, 0xe7, 0x07, 0xc4, 0xa7, 0x2d, 0xaf, 0xc7, 0xe8, 0x8b, 0xda, + 0x79, 0x0e, 0x27, 0x78, 0xdc, 0x9d, 0xe7, 0x88, 0xf1, 0xd1, 0x9d, 0xe7, 0x10, 0xf7, 0x85, 0xed, + 0x3c, 0x87, 0x33, 0x1c, 0x90, 0x53, 0xfe, 0x6f, 0xce, 0x58, 0x45, 0x3c, 0xaf, 0xcc, 0x3d, 0x25, + 0xaf, 0x7c, 0x04, 0x93, 0x8e, 0xcb, 0xa9, 0xbf, 0x47, 0xda, 0xba, 0x45, 0x30, 0xe4, 0x52, 0xd7, + 0x7b, 0xbe, 0xce, 0xaa, 0x82, 0xa5, 0x6e, 0x68, 0x3e, 0x38, 0xe4, 0x88, 0xda, 0x70, 0x26, 0xe8, + 0x49, 0xf9, 0x94, 0x44, 0x3d, 0x71, 0x7d, 0xaf, 0xfc, 0x6e, 0x70, 0xa9, 0x79, 0x33, 0x0d, 0xe9, + 0xc9, 0x20, 0x00, 0x4e, 0x67, 0x8a, 0x18, 0x4c, 0x33, 0xa3, 0xa0, 0x0a, 0x0e, 0x92, 0x21, 0xfb, + 0x79, 0xc9, 0x1a, 0xd4, 0xb8, 0x8c, 0x36, 0x99, 0xe2, 0xb8, 0x0c, 0xfb, 0x3f, 0xf2, 0x30, 0x9b, + 0xb0, 0xb4, 0x44, 0xe2, 0x58, 0x7c, 0x9e, 0x89, 0x63, 0x61, 0xa4, 0xc4, 0x31, 0x3d, 0xa7, 0x19, + 0x1b, 0x29, 0xa7, 0x79, 0x4f, 0xe5, 0x15, 0x7a, 0xe7, 0x36, 0xd6, 0xf5, 0x33, 0xc1, 0x50, 0x9b, + 0x9b, 0x26, 0x10, 0xc7, 0x71, 0xe5, 0x29, 0xdc, 0xe8, 0xff, 0x26, 0x4e, 0x27, 0x45, 0x57, 0xb3, + 0x3e, 0xbe, 0x08, 0x19, 0xa8, 0x53, 0x38, 0x05, 0x80, 0xd3, 0xc4, 0x55, 0xee, 0x7c, 0xf6, 0xf5, + 0xf2, 0xa9, 0x2f, 0xbe, 0x5e, 0x3e, 0xf5, 0xe5, 0xd7, 0xcb, 0xa7, 0x7e, 0x72, 0xb8, 0x6c, 0x7d, + 0x76, 0xb8, 0x6c, 0x7d, 0x71, 0xb8, 0x6c, 0x7d, 0x79, 0xb8, 0x6c, 0xfd, 0xf7, 0xe1, 0xb2, 0xf5, + 0x97, 0xdf, 0x2c, 0x9f, 0x7a, 0xf8, 0xda, 0x30, 0xff, 0xbc, 0xe0, 0xff, 0x03, 0x00, 0x00, 0xff, + 0xff, 0x90, 0x47, 0xd6, 0xce, 0xe3, 0x40, 0x00, 0x00, } func (m *AnalysisRunArgument) Marshal() (dAtA []byte, err error) { @@ -3975,6 +4008,9 @@ func (m *PromotionStatus) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + i = encodeVarintGenerated(dAtA, i, uint64(m.CurrentStepAttempt)) + i-- + dAtA[i] = 0x58 if m.State != nil { { size, err := m.State.MarshalToSizedBuffer(dAtA[:i]) @@ -4078,6 +4114,18 @@ func (m *PromotionStep) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if m.Retry != nil { + { + size, err := m.Retry.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenerated(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x22 + } if m.Config != nil { { size, err := m.Config.MarshalToSizedBuffer(dAtA[:i]) @@ -4103,6 +4151,32 @@ func (m *PromotionStep) MarshalToSizedBuffer(dAtA []byte) (int, error) { return len(dAtA) - i, nil } +func (m *PromotionStepRetry) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *PromotionStepRetry) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *PromotionStepRetry) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + i = encodeVarintGenerated(dAtA, i, uint64(m.Attempts)) + i-- + dAtA[i] = 0x8 + return len(dAtA) - i, nil +} + func (m *PromotionTemplate) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) @@ -5762,6 +5836,7 @@ func (m *PromotionStatus) Size() (n int) { l = m.State.Size() n += 1 + l + sovGenerated(uint64(l)) } + n += 1 + sovGenerated(uint64(m.CurrentStepAttempt)) return n } @@ -5779,6 +5854,20 @@ func (m *PromotionStep) Size() (n int) { l = m.Config.Size() n += 1 + l + sovGenerated(uint64(l)) } + if m.Retry != nil { + l = m.Retry.Size() + n += 1 + l + sovGenerated(uint64(l)) + } + return n +} + +func (m *PromotionStepRetry) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + n += 1 + sovGenerated(uint64(m.Attempts)) return n } @@ -6738,6 +6827,7 @@ func (this *PromotionStatus) String() string { `HealthChecks:` + repeatedStringForHealthChecks + `,`, `CurrentStep:` + fmt.Sprintf("%v", this.CurrentStep) + `,`, `State:` + strings.Replace(fmt.Sprintf("%v", this.State), "JSON", "v11.JSON", 1) + `,`, + `CurrentStepAttempt:` + fmt.Sprintf("%v", this.CurrentStepAttempt) + `,`, `}`, }, "") return s @@ -6750,6 +6840,17 @@ func (this *PromotionStep) String() string { `Uses:` + fmt.Sprintf("%v", this.Uses) + `,`, `As:` + fmt.Sprintf("%v", this.As) + `,`, `Config:` + strings.Replace(fmt.Sprintf("%v", this.Config), "JSON", "v11.JSON", 1) + `,`, + `Retry:` + strings.Replace(this.Retry.String(), "PromotionStepRetry", "PromotionStepRetry", 1) + `,`, + `}`, + }, "") + return s +} +func (this *PromotionStepRetry) String() string { + if this == nil { + return "nil" + } + s := strings.Join([]string{`&PromotionStepRetry{`, + `Attempts:` + fmt.Sprintf("%v", this.Attempts) + `,`, `}`, }, "") return s @@ -14090,6 +14191,25 @@ func (m *PromotionStatus) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 11: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field CurrentStepAttempt", wireType) + } + m.CurrentStepAttempt = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.CurrentStepAttempt |= int64(b&0x7F) << shift + if b < 0x80 { + break + } + } default: iNdEx = preIndex skippy, err := skipGenerated(dAtA[iNdEx:]) @@ -14240,6 +14360,111 @@ func (m *PromotionStep) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Retry", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.Retry == nil { + m.Retry = &PromotionStepRetry{} + } + if err := m.Retry.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipGenerated(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthGenerated + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *PromotionStepRetry) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: PromotionStepRetry: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: PromotionStepRetry: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Attempts", wireType) + } + m.Attempts = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Attempts |= int64(b&0x7F) << shift + if b < 0x80 { + break + } + } default: iNdEx = preIndex skippy, err := skipGenerated(dAtA[iNdEx:]) diff --git a/api/v1alpha1/generated.proto b/api/v1alpha1/generated.proto index 02eaa3104..08f411a1c 100644 --- a/api/v1alpha1/generated.proto +++ b/api/v1alpha1/generated.proto @@ -900,6 +900,10 @@ message PromotionStatus { // subsequent reconciliations attempts. optional int64 currentStep = 9; + // CurrentStepAttempt is the number of times the current step has been + // attempted. + optional int64 currentStepAttempt = 11; + // State stores the state of the promotion process between reconciliation // attempts. optional .k8s.io.apiextensions_apiserver.pkg.apis.apiextensions.v1.JSON state = 10; @@ -915,6 +919,9 @@ message PromotionStep { // As is the alias this step can be referred to as. optional string as = 2; + // Retry is the retry policy for this step. + optional PromotionStepRetry retry = 4; + // Config is opaque configuration for the PromotionStep that is understood // only by each PromotionStep's implementation. It is legal to utilize // expressions in defining values at any level of this block. @@ -922,6 +929,19 @@ message PromotionStep { optional .k8s.io.apiextensions_apiserver.pkg.apis.apiextensions.v1.JSON config = 3; } +// PromotionStepRetry describes the retry policy for a PromotionStep. +message PromotionStepRetry { + // Attempts is the number of times the step can be attempted before the + // PromotionStep is marked as failed. + // + // If this field is set to 1, the step will not be retried. If this + // field is set to -1, the step will be retried indefinitely. + // + // The default of this field depends on the step being executed. Refer to + // the documentation for the specific step for more information. + optional int64 attempts = 1; +} + // PromotionTemplate defines a template for a Promotion that can be used to // incorporate Freight into a Stage. message PromotionTemplate { diff --git a/api/v1alpha1/promotion_types.go b/api/v1alpha1/promotion_types.go index a5b35d489..7ae3a3651 100644 --- a/api/v1alpha1/promotion_types.go +++ b/api/v1alpha1/promotion_types.go @@ -109,6 +109,27 @@ type PromotionVariable struct { Value string `json:"value" protobuf:"bytes,2,opt,name=value"` } +// PromotionStepRetry describes the retry policy for a PromotionStep. +type PromotionStepRetry struct { + // Attempts is the number of times the step can be attempted before the + // PromotionStep is marked as failed. + // + // If this field is set to 1, the step will not be retried. If this + // field is set to -1, the step will be retried indefinitely. + // + // The default of this field depends on the step being executed. Refer to + // the documentation for the specific step for more information. + Attempts int64 `json:"attempts,omitempty" protobuf:"varint,1,opt,name=attempts"` +} + +// GetAttempts returns the Attempts field with the given fallback value. +func (r *PromotionStepRetry) GetAttempts(fallback int64) int64 { + if r == nil || r.Attempts == 0 { + return fallback + } + return r.Attempts +} + // PromotionStep describes a directive to be executed as part of a Promotion. type PromotionStep struct { // Uses identifies a runner that can execute this step. @@ -117,6 +138,8 @@ type PromotionStep struct { Uses string `json:"uses" protobuf:"bytes,1,opt,name=uses"` // As is the alias this step can be referred to as. As string `json:"as,omitempty" protobuf:"bytes,2,opt,name=as"` + // Retry is the retry policy for this step. + Retry *PromotionStepRetry `json:"retry,omitempty" protobuf:"bytes,4,opt,name=retry"` // Config is opaque configuration for the PromotionStep that is understood // only by each PromotionStep's implementation. It is legal to utilize // expressions in defining values at any level of this block. @@ -154,6 +177,9 @@ type PromotionStatus struct { // permits steps that have already run successfully to be skipped on // subsequent reconciliations attempts. CurrentStep int64 `json:"currentStep,omitempty" protobuf:"varint,9,opt,name=currentStep"` + // CurrentStepAttempt is the number of times the current step has been + // attempted. + CurrentStepAttempt int64 `json:"currentStepAttempt,omitempty" protobuf:"varint,11,opt,name=currentStepAttempt"` // State stores the state of the promotion process between reconciliation // attempts. State *apiextensionsv1.JSON `json:"state,omitempty" protobuf:"bytes,10,opt,name=state"` diff --git a/api/v1alpha1/promotion_types_test.go b/api/v1alpha1/promotion_types_test.go new file mode 100644 index 000000000..980662786 --- /dev/null +++ b/api/v1alpha1/promotion_types_test.go @@ -0,0 +1,39 @@ +package v1alpha1 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPromotionRetry_GetAttempts(t *testing.T) { + tests := []struct { + name string + retry *PromotionStepRetry + fallback int64 + want int64 + }{ + { + name: "retry is nil", + retry: nil, + fallback: 1, + want: 1, + }, + { + name: "attempts is not set", + retry: &PromotionStepRetry{}, + fallback: -1, + want: -1, + }, + { + name: "attempts is set", + retry: &PromotionStepRetry{ + Attempts: 3, + }, + want: 3, + }, + } + for _, tt := range tests { + require.Equal(t, tt.want, tt.retry.GetAttempts(tt.fallback)) + } +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index fb6807bea..efe94eef0 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -976,6 +976,11 @@ func (in *PromotionStatus) DeepCopy() *PromotionStatus { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PromotionStep) DeepCopyInto(out *PromotionStep) { *out = *in + if in.Retry != nil { + in, out := &in.Retry, &out.Retry + *out = new(PromotionStepRetry) + **out = **in + } if in.Config != nil { in, out := &in.Config, &out.Config *out = new(apiextensionsv1.JSON) @@ -993,6 +998,21 @@ func (in *PromotionStep) DeepCopy() *PromotionStep { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PromotionStepRetry) DeepCopyInto(out *PromotionStepRetry) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PromotionStepRetry. +func (in *PromotionStepRetry) DeepCopy() *PromotionStepRetry { + if in == nil { + return nil + } + out := new(PromotionStepRetry) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PromotionTemplate) DeepCopyInto(out *PromotionTemplate) { *out = *in diff --git a/charts/kargo/resources/crds/kargo.akuity.io_promotions.yaml b/charts/kargo/resources/crds/kargo.akuity.io_promotions.yaml index eb19c7814..124e00677 100644 --- a/charts/kargo/resources/crds/kargo.akuity.io_promotions.yaml +++ b/charts/kargo/resources/crds/kargo.akuity.io_promotions.yaml @@ -95,6 +95,22 @@ spec: expressions in defining values at any level of this block. See https://docs.kargo.io/references/expression-language for details. x-kubernetes-preserve-unknown-fields: true + retry: + description: Retry is the retry policy for this step. + properties: + attempts: + description: |- + Attempts is the number of times the step can be attempted before the + PromotionStep is marked as failed. + + If this field is set to 1, the step will not be retried. If this + field is set to -1, the step will be retried indefinitely. + + The default of this field depends on the step being executed. Refer to + the documentation for the specific step for more information. + format: int64 + type: integer + type: object uses: description: Uses identifies a runner that can execute this step. @@ -145,6 +161,12 @@ spec: subsequent reconciliations attempts. format: int64 type: integer + currentStepAttempt: + description: |- + CurrentStepAttempt is the number of times the current step has been + attempted. + format: int64 + type: integer finishedAt: description: FinishedAt is the time when the promotion was completed. format: date-time diff --git a/charts/kargo/resources/crds/kargo.akuity.io_stages.yaml b/charts/kargo/resources/crds/kargo.akuity.io_stages.yaml index 890cfbc95..c2d17a876 100644 --- a/charts/kargo/resources/crds/kargo.akuity.io_stages.yaml +++ b/charts/kargo/resources/crds/kargo.akuity.io_stages.yaml @@ -88,6 +88,22 @@ spec: expressions in defining values at any level of this block. See https://docs.kargo.io/references/expression-language for details. x-kubernetes-preserve-unknown-fields: true + retry: + description: Retry is the retry policy for this step. + properties: + attempts: + description: |- + Attempts is the number of times the step can be attempted before the + PromotionStep is marked as failed. + + If this field is set to 1, the step will not be retried. If this + field is set to -1, the step will be retried indefinitely. + + The default of this field depends on the step being executed. Refer to + the documentation for the specific step for more information. + format: int64 + type: integer + type: object uses: description: Uses identifies a runner that can execute this step. @@ -469,6 +485,12 @@ spec: subsequent reconciliations attempts. format: int64 type: integer + currentStepAttempt: + description: |- + CurrentStepAttempt is the number of times the current step has been + attempted. + format: int64 + type: integer finishedAt: description: FinishedAt is the time when the promotion was completed. @@ -1290,6 +1312,12 @@ spec: subsequent reconciliations attempts. format: int64 type: integer + currentStepAttempt: + description: |- + CurrentStepAttempt is the number of times the current step has been + attempted. + format: int64 + type: integer finishedAt: description: FinishedAt is the time when the promotion was completed. diff --git a/internal/controller/promotions/promotions.go b/internal/controller/promotions/promotions.go index 35c2cc146..4b89815c0 100644 --- a/internal/controller/promotions/promotions.go +++ b/internal/controller/promotions/promotions.go @@ -468,6 +468,7 @@ func (r *reconciler) promote( steps[i] = directives.PromotionStep{ Kind: step.Uses, Alias: step.As, + Retry: step.Retry, Config: step.Config.Raw, } } @@ -481,6 +482,7 @@ func (r *reconciler) promote( FreightRequests: stage.Spec.RequestedFreight, Freight: *workingPromo.Status.FreightCollection.DeepCopy(), StartFromStep: promo.Status.CurrentStep, + Attempts: promo.Status.CurrentStepAttempt, State: directives.State(workingPromo.Status.GetState()), Vars: workingPromo.Spec.Vars, } @@ -490,6 +492,7 @@ func (r *reconciler) promote( // allows individual steps to self-discover that they've run before and // examine the results of their own previous execution. promoCtx.StartFromStep = 0 + promoCtx.Attempts = 0 } else if !os.IsExist(err) { return nil, fmt.Errorf("error creating working directory: %w", err) } @@ -505,6 +508,7 @@ func (r *reconciler) promote( workingPromo.Status.Phase = res.Status workingPromo.Status.Message = res.Message workingPromo.Status.CurrentStep = res.CurrentStep + workingPromo.Status.CurrentStepAttempt = res.Attempt workingPromo.Status.State = &apiextensionsv1.JSON{Raw: res.State.ToJSON()} if res.Status == kargoapi.PromotionPhaseSucceeded { var healthChecks []kargoapi.HealthCheckStep diff --git a/internal/directives/argocd_revisions.go b/internal/directives/argocd_revisions.go index acaf966ce..13c2b26a0 100644 --- a/internal/directives/argocd_revisions.go +++ b/internal/directives/argocd_revisions.go @@ -177,7 +177,7 @@ func getCommitFromStep(sharedState State, stepAlias string) (string, error) { return "", fmt.Errorf("output from step with alias %q is not a map[string]any", stepAlias) } - commitAny, exists := stepOutputMap[commitKey] + commitAny, exists := stepOutputMap[stateKeyCommit] if !exists { return "", fmt.Errorf("no commit found in output from step with alias %q", stepAlias) diff --git a/internal/directives/argocd_updater.go b/internal/directives/argocd_updater.go index 148fc2a30..e8c721847 100644 --- a/internal/directives/argocd_updater.go +++ b/internal/directives/argocd_updater.go @@ -127,6 +127,11 @@ func (a *argocdUpdater) Name() string { return "argocd-update" } +// DefaultAttempts implements the RetryableStepRunner interface. +func (a *argocdUpdater) DefaultAttempts() int64 { + return -1 +} + // RunPromotionStep implements the PromotionStepRunner interface. func (a *argocdUpdater) RunPromotionStep( ctx context.Context, diff --git a/internal/directives/git_commiter.go b/internal/directives/git_commiter.go index 9825dfc29..9dccb0674 100644 --- a/internal/directives/git_commiter.go +++ b/internal/directives/git_commiter.go @@ -11,7 +11,8 @@ import ( "github.com/akuity/kargo/internal/controller/git" ) -const commitKey = "commit" +// stateKeyCommit is the key used to store the commit ID in the shared State. +const stateKeyCommit = "commit" func init() { builtins.RegisterPromotionStepRunner(newGitCommitter(), nil) @@ -111,7 +112,7 @@ func (g *gitCommitter) runPromotionStep( } return PromotionStepResult{ Status: kargoapi.PromotionPhaseSucceeded, - Output: map[string]any{commitKey: commitID}, + Output: map[string]any{stateKeyCommit: commitID}, }, nil } diff --git a/internal/directives/git_commiter_test.go b/internal/directives/git_commiter_test.go index ae59b1639..7a62be37c 100644 --- a/internal/directives/git_commiter_test.go +++ b/internal/directives/git_commiter_test.go @@ -225,7 +225,7 @@ func Test_gitCommitter_runPromotionStep(t *testing.T) { require.Equal(t, kargoapi.PromotionPhaseSucceeded, res.Status) expectedCommit, err := workTree.LastCommitID() require.NoError(t, err) - actualCommit, ok := res.Output[commitKey] + actualCommit, ok := res.Output[stateKeyCommit] require.True(t, ok) require.Equal(t, expectedCommit, actualCommit) lastCommitMsg, err := workTree.CommitMessage("HEAD") diff --git a/internal/directives/git_pr_opener.go b/internal/directives/git_pr_opener.go index 0b979c434..5810ac163 100644 --- a/internal/directives/git_pr_opener.go +++ b/internal/directives/git_pr_opener.go @@ -18,7 +18,8 @@ import ( _ "github.com/akuity/kargo/internal/gitprovider/gitlab" // GitLab provider registration ) -const prNumberKey = "prNumber" +// stateKeyPRNumber is the key used to store the PR number in the shared State. +const stateKeyPRNumber = "prNumber" func init() { builtins.RegisterPromotionStepRunner( @@ -83,7 +84,7 @@ func (g *gitPROpener) runPromotionStep( return PromotionStepResult{ Status: kargoapi.PromotionPhaseSucceeded, Output: map[string]any{ - prNumberKey: prNumber, + stateKeyPRNumber: prNumber, }, }, nil } @@ -161,7 +162,7 @@ func (g *gitPROpener) runPromotionStep( return PromotionStepResult{ Status: kargoapi.PromotionPhaseSucceeded, Output: map[string]any{ - prNumberKey: pr.Number, + stateKeyPRNumber: pr.Number, }, }, nil } @@ -219,7 +220,7 @@ func (g *gitPROpener) runPromotionStep( return PromotionStepResult{ Status: kargoapi.PromotionPhaseSucceeded, Output: map[string]any{ - prNumberKey: pr.Number, + stateKeyPRNumber: pr.Number, }, }, nil } @@ -243,7 +244,7 @@ func (g *gitPROpener) getPRNumber( stepCtx.Alias, ) } - prNumberAny, exists := stepOutputMap[prNumberKey] + prNumberAny, exists := stepOutputMap[stateKeyPRNumber] if !exists { return -1, nil } @@ -283,7 +284,7 @@ func (g *gitPROpener) getSourceBranch( cfg.SourceBranchFromStep, ) } - sourceBranchAny, exists := stepOutputMap[branchKey] + sourceBranchAny, exists := stepOutputMap[stateKeyBranch] if !exists { return "", fmt.Errorf( "no branch found in output from step with alias %q", diff --git a/internal/directives/git_pr_opener_test.go b/internal/directives/git_pr_opener_test.go index 00d0e511e..bb4f9074f 100644 --- a/internal/directives/git_pr_opener_test.go +++ b/internal/directives/git_pr_opener_test.go @@ -202,7 +202,7 @@ func Test_gitPROpener_runPromotionStep(t *testing.T) { CredentialsDB: &credentials.FakeDB{}, SharedState: State{ "fake-step": map[string]any{ - branchKey: testSourceBranch, + stateKeyBranch: testSourceBranch, }, }, }, @@ -216,7 +216,7 @@ func Test_gitPROpener_runPromotionStep(t *testing.T) { }, ) require.NoError(t, err) - prNumber, ok := res.Output[prNumberKey] + prNumber, ok := res.Output[stateKeyPRNumber] require.True(t, ok) require.Equal(t, testPRNumber, prNumber) diff --git a/internal/directives/git_pr_waiter.go b/internal/directives/git_pr_waiter.go index 2d29b4ff9..531a13b4a 100644 --- a/internal/directives/git_pr_waiter.go +++ b/internal/directives/git_pr_waiter.go @@ -38,6 +38,11 @@ func (g *gitPRWaiter) Name() string { return "git-wait-for-pr" } +// DefaultAttempts implements the RetryableStepRunner interface. +func (g *gitPRWaiter) DefaultAttempts() int64 { + return -1 +} + // RunPromotionStep implements the PromotionStepRunner interface. func (g *gitPRWaiter) RunPromotionStep( ctx context.Context, @@ -121,7 +126,7 @@ func (g *gitPRWaiter) runPromotionStep( } return PromotionStepResult{ Status: kargoapi.PromotionPhaseSucceeded, - Output: map[string]any{commitKey: pr.MergeCommitSHA}, + Output: map[string]any{stateKeyCommit: pr.MergeCommitSHA}, }, nil } @@ -150,7 +155,7 @@ func (g *gitPRWaiter) getPRNumber( cfg.PRNumberFromStep, ) } - prNumberAny, exists := stepOutputMap[prNumberKey] + prNumberAny, exists := stepOutputMap[stateKeyPRNumber] if !exists { return 0, fmt.Errorf( "no PR number found in output from step with alias %q", diff --git a/internal/directives/git_pusher.go b/internal/directives/git_pusher.go index 8d37f78b5..9f835e72a 100644 --- a/internal/directives/git_pusher.go +++ b/internal/directives/git_pusher.go @@ -12,7 +12,9 @@ import ( "github.com/akuity/kargo/internal/credentials" ) -const branchKey = "branch" +// stateKeyBranch is the key used to store the branch that was pushed to in the +// shared State. +const stateKeyBranch = "branch" func init() { builtins.RegisterPromotionStepRunner( @@ -134,8 +136,8 @@ func (g *gitPushPusher) runPromotionStep( return PromotionStepResult{ Status: kargoapi.PromotionPhaseSucceeded, Output: map[string]any{ - branchKey: targetBranch, - commitKey: commitID, + stateKeyBranch: targetBranch, + stateKeyCommit: commitID, }, }, nil } diff --git a/internal/directives/git_pusher_test.go b/internal/directives/git_pusher_test.go index e900dcbba..7b5618c64 100644 --- a/internal/directives/git_pusher_test.go +++ b/internal/directives/git_pusher_test.go @@ -200,12 +200,12 @@ func Test_gitPusher_runPromotionStep(t *testing.T) { }, ) require.NoError(t, err) - branchName, ok := res.Output[branchKey] + branchName, ok := res.Output[stateKeyBranch] require.True(t, ok) require.Equal(t, "kargo/promotion/fake-promotion", branchName) expectedCommit, err := workTree.LastCommitID() require.NoError(t, err) - actualCommit, ok := res.Output[commitKey] + actualCommit, ok := res.Output[stateKeyCommit] require.True(t, ok) require.Equal(t, expectedCommit, actualCommit) } diff --git a/internal/directives/promotions.go b/internal/directives/promotions.go index 789d56b60..7c965d793 100644 --- a/internal/directives/promotions.go +++ b/internal/directives/promotions.go @@ -27,6 +27,14 @@ type PromotionStepRunner interface { RunPromotionStep(context.Context, *PromotionStepContext) (PromotionStepResult, error) } +// RetryableStepRunner is an interface for PromotionStepRunners that can be +// retried in the event of a failure. +type RetryableStepRunner interface { + // DefaultAttempts returns the default number of attempts the step is + // allowed to make before failing. + DefaultAttempts() int64 +} + // PromotionContext is the context of a user-defined promotion process that is // executed by the Engine. type PromotionContext struct { @@ -50,13 +58,16 @@ type PromotionContext struct { // resolve. FreightRequests []kargoapi.FreightRequest // Freight is the collection of all Freight referenced by the Promotion. This - // collection contains both the Freight that is actively being promoted as - // well as any Freight that has been inherited from the target Stage's current + // collection contains both the Freight that is actively being promoted and + // any Freight that has been inherited from the target Stage's current // state. Freight kargoapi.FreightCollection - // SharedState is the index of the step from which the promotion should begin - // execution. + // StartFromStep is the index of the step from which the promotion should + // begin execution. StartFromStep int64 + // Attempts is the number of attempts that have been made to execute + // the current step. + Attempts int64 // State is the current state of the promotion process. State State // Vars is a list of variables definitions that can be used by the @@ -78,11 +89,25 @@ type PromotionStep struct { // step will be keyed to this alias by the Engine and made accessible to // subsequent steps. Alias string + // Retry is the retry configuration for the PromotionStep. + Retry *kargoapi.PromotionStepRetry // Config is an opaque JSON to be passed to the PromotionStepRunner executing // this step. Config []byte } +// GetMaxAttempts returns the maximum number of attempts that can be made to +// execute the step using the provided runner. If the runner is a +// RetryableStepRunner, the value of its retry configuration is used as the +// maximum default. Otherwise, the default is 1. +func (s *PromotionStep) GetMaxAttempts(runner any) int64 { + fallback := int64(1) + if retryCfg, isRetryable := runner.(RetryableStepRunner); isRetryable { + fallback = retryCfg.DefaultAttempts() + } + return s.Retry.GetAttempts(fallback) +} + // GetConfig returns the Config unmarshalled into a map. Any expr-lang // expressions are evaluated in the context of the provided arguments // prior to unmarshaling. @@ -145,6 +170,8 @@ func (s *PromotionStep) GetConfig( return config, nil } +// GetVars returns the variables defined in the PromotionStep. The variables are +// evaluated in the context of the provided PromotionContext. func (s *PromotionStep) GetVars(promoCtx PromotionContext) (map[string]any, error) { vars := make(map[string]any, len(promoCtx.Vars)) for _, v := range promoCtx.Vars { @@ -184,9 +211,11 @@ type PromotionResult struct { // health check processes. HealthCheckSteps []HealthCheckStep // If the promotion process remains in-progress, perhaps waiting for a change - // in some external state, the value of this field will indicated where to + // in some external state, the value of this field will indicate where to // resume the process in the next reconciliation. CurrentStep int64 + // Attempt tracks the current execution attempt of the current step. + Attempt int64 // State is the current state of the promotion process. State State } diff --git a/internal/directives/promotions_test.go b/internal/directives/promotions_test.go index cd86d3a8c..217440ab2 100644 --- a/internal/directives/promotions_test.go +++ b/internal/directives/promotions_test.go @@ -4,11 +4,82 @@ import ( "context" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" kargoapi "github.com/akuity/kargo/api/v1alpha1" ) +type mockRetryableRunner struct { + defaultAttempts int64 +} + +func (m mockRetryableRunner) DefaultAttempts() int64 { + return m.defaultAttempts +} + +func TestPromotionStep_GetMaxAttempts(t *testing.T) { + tests := []struct { + name string + step *PromotionStep + runner any + assertions func(t *testing.T, result int64) + }{ + { + name: "returns 1 with no retry config", + step: &PromotionStep{ + Retry: nil, + }, + assertions: func(t *testing.T, result int64) { + assert.Equal(t, int64(1), result) + }, + }, + { + name: "returns configured attempts for non-retryable runner", + step: &PromotionStep{ + Retry: &kargoapi.PromotionStepRetry{ + Attempts: 5, + }, + }, + runner: nil, + assertions: func(t *testing.T, result int64) { + assert.Equal(t, int64(5), result) + }, + }, + { + name: "returns configured attempts for retryable runner", + step: &PromotionStep{ + Retry: &kargoapi.PromotionStepRetry{ + Attempts: 5, + }, + }, + runner: mockRetryableRunner{defaultAttempts: 3}, + assertions: func(t *testing.T, result int64) { + assert.Equal(t, int64(5), result) + }, + }, + { + name: "returns default attempts when retry config returns 0", + step: &PromotionStep{ + Retry: &kargoapi.PromotionStepRetry{ + Attempts: 0, + }, + }, + runner: mockRetryableRunner{defaultAttempts: 3}, + assertions: func(t *testing.T, result int64) { + assert.Equal(t, int64(3), result) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.step.GetMaxAttempts(tt.runner) + tt.assertions(t, result) + }) + } +} + func TestPromotionStep_GetConfig(t *testing.T) { promoCtx := PromotionContext{ Project: "fake-project", diff --git a/internal/directives/simple_engine.go b/internal/directives/simple_engine.go index d504c91ea..b56a0871f 100644 --- a/internal/directives/simple_engine.go +++ b/internal/directives/simple_engine.go @@ -1,18 +1,10 @@ package directives import ( - "context" - "encoding/json" - "fmt" - "os" "regexp" - "strings" - corev1 "k8s.io/api/core/v1" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "sigs.k8s.io/controller-runtime/pkg/client" - kargoapi "github.com/akuity/kargo/api/v1alpha1" "github.com/akuity/kargo/internal/credentials" ) @@ -43,232 +35,3 @@ func NewSimpleEngine( argoCDClient: argoCDClient, } } - -// Promote implements the Engine interface. -func (e *SimpleEngine) Promote( - ctx context.Context, - promoCtx PromotionContext, - steps []PromotionStep, -) (PromotionResult, error) { - workDir := promoCtx.WorkDir - if workDir == "" { - var err error - workDir, err = os.MkdirTemp("", "run-") - if err != nil { - return PromotionResult{ - Status: kargoapi.PromotionPhaseErrored, - CurrentStep: 0, - }, - fmt.Errorf("temporary working directory creation failed: %w", err) - } - defer os.RemoveAll(workDir) - } - - var err error - promoCtx.Secrets, err = e.getSecretsMap(ctx, promoCtx.Project) - if err != nil { - return PromotionResult{ - Status: kargoapi.PromotionPhaseErrored, - CurrentStep: 0, - }, - err - } - - // Initialize the shared state that will be passed to each step. - state := promoCtx.State.DeepCopy() - if state == nil { - state = make(State) - } - var healthCheckSteps []HealthCheckStep - - for i := promoCtx.StartFromStep; i < int64(len(steps)); i++ { - step := steps[i] - select { - case <-ctx.Done(): - return PromotionResult{ - Status: kargoapi.PromotionPhaseErrored, - CurrentStep: i, - State: state, - }, ctx.Err() - default: - } - reg, err := e.registry.GetPromotionStepRunnerRegistration(step.Kind) - if err != nil { - return PromotionResult{ - Status: kargoapi.PromotionPhaseErrored, - CurrentStep: i, - State: state, - }, - fmt.Errorf("no runner registered for step kind %q: %w", step.Kind, err) - } - - stateCopy := state.DeepCopy() - - step.Alias = strings.TrimSpace(step.Alias) - if step.Alias == "" { - step.Alias = fmt.Sprintf("step-%d", i) - } else if ReservedStepAliasRegex.MatchString(step.Alias) { - // A webhook enforces this regex as well, but we're checking here to - // account for the possibility of EXISTING Stages with a promotionTemplate - // containing a step with a now-reserved alias. - return PromotionResult{ - Status: kargoapi.PromotionPhaseErrored, - CurrentStep: i, - State: state, - }, fmt.Errorf("step alias %q is forbidden", step.Alias) - } - - stepCfg, err := step.GetConfig(ctx, e.kargoClient, promoCtx, stateCopy) - if err != nil { - return PromotionResult{ - Status: kargoapi.PromotionPhaseErrored, - CurrentStep: i, - State: state, - }, - fmt.Errorf("failed to get step config: %w", err) - } - stepCtx := &PromotionStepContext{ - UIBaseURL: promoCtx.UIBaseURL, - WorkDir: workDir, - SharedState: stateCopy, - Alias: step.Alias, - Config: stepCfg, - Project: promoCtx.Project, - Stage: promoCtx.Stage, - Promotion: promoCtx.Promotion, - FreightRequests: promoCtx.FreightRequests, - Freight: promoCtx.Freight, - } - // Selectively provide these capabilities via the PromotionStepContext. - if reg.Permissions.AllowCredentialsDB { - stepCtx.CredentialsDB = e.credentialsDB - } - if reg.Permissions.AllowKargoClient { - stepCtx.KargoClient = e.kargoClient - } - if reg.Permissions.AllowArgoCDClient { - stepCtx.ArgoCDClient = e.argoCDClient - } - - result, err := reg.Runner.RunPromotionStep(ctx, stepCtx) - state[step.Alias] = result.Output - if err != nil { - return PromotionResult{ - Status: kargoapi.PromotionPhaseErrored, - CurrentStep: i, - State: state, - }, - fmt.Errorf("failed to run step %q: %w", step.Kind, err) - } - - if result.Status != kargoapi.PromotionPhaseSucceeded { - return PromotionResult{ - Status: result.Status, - Message: result.Message, - CurrentStep: i, - State: state, - }, nil - } - - if result.HealthCheckStep != nil { - healthCheckSteps = append(healthCheckSteps, *result.HealthCheckStep) - } - } - return PromotionResult{ - Status: kargoapi.PromotionPhaseSucceeded, - HealthCheckSteps: healthCheckSteps, - CurrentStep: int64(len(steps)) - 1, - State: state, - }, nil -} - -// CheckHealth implements the Engine interface. -func (e *SimpleEngine) CheckHealth( - ctx context.Context, - healthCtx HealthCheckContext, - steps []HealthCheckStep, -) kargoapi.Health { - healthStatus := kargoapi.HealthStateHealthy - healthIssues := []string{} - healthOutput := make([]State, 0, len(steps)) -stepLoop: - for _, step := range steps { - select { - case <-ctx.Done(): - healthStatus = healthStatus.Merge(kargoapi.HealthStateUnknown) - healthIssues = append(healthIssues, ctx.Err().Error()) - break stepLoop - default: - } - reg, err := e.registry.GetHealthCheckStepRunnerRegistration(step.Kind) - if err != nil { - healthStatus = healthStatus.Merge(kargoapi.HealthStateUnknown) - healthIssues = append( - healthIssues, - fmt.Sprintf("no runner registered for step kind %q: %s", step.Kind, err.Error()), - ) - continue - } - stepCtx := &HealthCheckStepContext{ - Config: step.Config.DeepCopy(), - Project: healthCtx.Project, - Stage: healthCtx.Stage, - } - // Selectively provide these capabilities via the PromotionStepContext. - if reg.Permissions.AllowCredentialsDB { - stepCtx.CredentialsDB = e.credentialsDB - } - if reg.Permissions.AllowKargoClient { - stepCtx.KargoClient = e.kargoClient - } - if reg.Permissions.AllowArgoCDClient { - stepCtx.ArgoCDClient = e.argoCDClient - } - result := reg.Runner.RunHealthCheckStep(ctx, stepCtx) - healthStatus = healthStatus.Merge(result.Status) - healthIssues = append(healthIssues, result.Issues...) - healthOutput = append(healthOutput, result.Output) - } - if len(healthOutput) == 0 { - return kargoapi.Health{ - Status: healthStatus, - Issues: healthIssues, - } - } - bytes, err := json.Marshal(healthOutput) - if err != nil { - // Leave the status alone. Whatever it was determined to be was correct. - healthIssues = append( - healthIssues, - fmt.Sprintf("failed to marshal health output: %s", err.Error()), - ) - } - return kargoapi.Health{ - Status: healthStatus, - Issues: healthIssues, - Output: &apiextensionsv1.JSON{Raw: bytes}, - } -} - -func (e *SimpleEngine) getSecretsMap( - ctx context.Context, - project string, -) (map[string]map[string]string, error) { - secrets := corev1.SecretList{} - if err := e.kargoClient.List( - ctx, - &secrets, - client.InNamespace(project), - ); err != nil { - return nil, - fmt.Errorf("error listing Secrets for Project %q: %w", project, err) - } - secretsMap := make(map[string]map[string]string, len(secrets.Items)) - for _, secret := range secrets.Items { - secretsMap[secret.Name] = make(map[string]string, len(secret.Data)) - for key, value := range secret.Data { - secretsMap[secret.Name][key] = string(value) - } - } - return secretsMap, nil -} diff --git a/internal/directives/simple_engine_health.go b/internal/directives/simple_engine_health.go new file mode 100644 index 000000000..1c785ffe4 --- /dev/null +++ b/internal/directives/simple_engine_health.go @@ -0,0 +1,115 @@ +package directives + +import ( + "context" + "encoding/json" + "fmt" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + + kargoapi "github.com/akuity/kargo/api/v1alpha1" +) + +// CheckHealth implements the Engine interface. +func (e *SimpleEngine) CheckHealth( + ctx context.Context, + healthCtx HealthCheckContext, + steps []HealthCheckStep, +) kargoapi.Health { + status, issues, output := e.executeHealthChecks(ctx, healthCtx, steps) + if len(output) == 0 { + return kargoapi.Health{ + Status: status, + Issues: issues, + } + } + + b, err := json.Marshal(output) + if err != nil { + issues = append(issues, fmt.Sprintf("failed to marshal health output: %s", err.Error())) + } + + return kargoapi.Health{ + Status: status, + Issues: issues, + Output: &apiextensionsv1.JSON{Raw: b}, + } +} + +// executeHealthChecks executes a list of HealthCheckSteps in sequence. +func (e *SimpleEngine) executeHealthChecks( + ctx context.Context, + healthCtx HealthCheckContext, + steps []HealthCheckStep, +) (kargoapi.HealthState, []string, []State) { + var ( + aggregatedStatus = kargoapi.HealthStateHealthy + aggregatedIssues []string + aggregatedOutput = make([]State, 0, len(steps)) + ) + + for _, step := range steps { + select { + case <-ctx.Done(): + aggregatedStatus = aggregatedStatus.Merge(kargoapi.HealthStateUnknown) + aggregatedIssues = append(aggregatedIssues, ctx.Err().Error()) + return aggregatedStatus, aggregatedIssues, aggregatedOutput + default: + } + + result := e.executeHealthCheck(ctx, healthCtx, step) + aggregatedStatus = aggregatedStatus.Merge(result.Status) + aggregatedIssues = append(aggregatedIssues, result.Issues...) + + if result.Output != nil { + aggregatedOutput = append(aggregatedOutput, result.Output) + } + } + + return aggregatedStatus, aggregatedIssues, aggregatedOutput +} + +// executeHealthCheck executes a single HealthCheckStep. +func (e *SimpleEngine) executeHealthCheck( + ctx context.Context, + healthCtx HealthCheckContext, + step HealthCheckStep, +) HealthCheckStepResult { + reg, err := e.registry.GetHealthCheckStepRunnerRegistration(step.Kind) + if err != nil { + return HealthCheckStepResult{ + Status: kargoapi.HealthStateUnknown, + Issues: []string{ + fmt.Sprintf("no runner registered for step kind %q: %s", step.Kind, err.Error()), + }, + } + } + + stepCtx := e.prepareHealthCheckStepContext(healthCtx, step, reg) + return reg.Runner.RunHealthCheckStep(ctx, stepCtx) +} + +// prepareHealthCheckStepContext prepares a HealthCheckStepContext for a HealthCheckStep. +func (e *SimpleEngine) prepareHealthCheckStepContext( + healthCtx HealthCheckContext, + step HealthCheckStep, + reg HealthCheckStepRunnerRegistration, +) *HealthCheckStepContext { + stepCtx := &HealthCheckStepContext{ + Config: step.Config.DeepCopy(), + Project: healthCtx.Project, + Stage: healthCtx.Stage, + } + + if reg.Permissions.AllowCredentialsDB { + stepCtx.CredentialsDB = e.credentialsDB + } + if reg.Permissions.AllowKargoClient { + stepCtx.KargoClient = e.kargoClient + } + if reg.Permissions.AllowArgoCDClient { + stepCtx.ArgoCDClient = e.argoCDClient + } + + return stepCtx +} diff --git a/internal/directives/simple_engine_health_test.go b/internal/directives/simple_engine_health_test.go new file mode 100644 index 000000000..91a70de1c --- /dev/null +++ b/internal/directives/simple_engine_health_test.go @@ -0,0 +1,332 @@ +package directives + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + kargoapi "github.com/akuity/kargo/api/v1alpha1" + "github.com/akuity/kargo/internal/credentials" +) + +func TestSimpleEngine_CheckHealth(t *testing.T) { + tests := []struct { + name string + healthCtx HealthCheckContext + steps []HealthCheckStep + assertions func(*testing.T, kargoapi.Health) + }{ + { + name: "successful health check", + steps: []HealthCheckStep{ + {Kind: "success-check"}, + }, + assertions: func(t *testing.T, health kargoapi.Health) { + assert.Equal(t, kargoapi.HealthStateHealthy, health.Status) + assert.Empty(t, health.Issues) + assert.NotNil(t, health.Output) + assert.JSONEq(t, `[{"test":"success"}]`, string(health.Output.Raw)) + }, + }, + { + name: "multiple successful health checks", + steps: []HealthCheckStep{ + {Kind: "success-check"}, + {Kind: "success-check"}, + }, + assertions: func(t *testing.T, health kargoapi.Health) { + assert.Equal(t, kargoapi.HealthStateHealthy, health.Status) + assert.Empty(t, health.Issues) + assert.NotNil(t, health.Output) + assert.JSONEq(t, `[{"test":"success"},{"test":"success"}]`, string(health.Output.Raw)) + }, + }, + { + name: "failed health check", + steps: []HealthCheckStep{ + {Kind: "error-check"}, + }, + assertions: func(t *testing.T, health kargoapi.Health) { + assert.Equal(t, kargoapi.HealthStateUnhealthy, health.Status) + assert.Contains(t, health.Issues, "health check failed") + assert.NotNil(t, health.Output) + }, + }, + { + name: "context cancellation", + steps: []HealthCheckStep{ + {Kind: "context-waiter"}, + }, + assertions: func(t *testing.T, health kargoapi.Health) { + assert.Equal(t, kargoapi.HealthStateUnknown, health.Status) + assert.Contains(t, health.Issues, context.Canceled.Error()) + assert.Nil(t, health.Output) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + testRegistry := NewStepRunnerRegistry() + testRegistry.RegisterHealthCheckStepRunner( + &mockHealthCheckStepRunner{ + name: "success-check", + runResult: HealthCheckStepResult{ + Status: kargoapi.HealthStateHealthy, + Output: State{"test": "success"}, + }, + }, + nil, + ) + testRegistry.RegisterHealthCheckStepRunner( + &mockHealthCheckStepRunner{ + name: "error-check", + runResult: HealthCheckStepResult{ + Status: kargoapi.HealthStateUnhealthy, + Issues: []string{"health check failed"}, + Output: State{"test": "error"}, + }, + }, + nil, + ) + testRegistry.RegisterHealthCheckStepRunner( + &mockHealthCheckStepRunner{ + name: "context-waiter", + runFunc: func(ctx context.Context, _ *HealthCheckStepContext) HealthCheckStepResult { + cancel() + <-ctx.Done() + return HealthCheckStepResult{ + Status: kargoapi.HealthStateUnknown, + Issues: []string{ctx.Err().Error()}, + } + }, + }, + nil, + ) + + engine := &SimpleEngine{ + registry: testRegistry, + } + + health := engine.CheckHealth(ctx, tt.healthCtx, tt.steps) + tt.assertions(t, health) + }) + } +} + +func TestSimpleEngine_executeHealthChecks(t *testing.T) { + tests := []struct { + name string + healthCtx HealthCheckContext + steps []HealthCheckStep + assertions func(*testing.T, kargoapi.HealthState, []string, []State) + }{ + { + name: "aggregate multiple healthy checks", + steps: []HealthCheckStep{ + {Kind: "success-check"}, + {Kind: "success-check"}, + }, + assertions: func(t *testing.T, status kargoapi.HealthState, issues []string, output []State) { + assert.Equal(t, kargoapi.HealthStateHealthy, status) + assert.Empty(t, issues) + assert.Len(t, output, 2) + for _, o := range output { + assert.Equal(t, "success", o["test"]) + } + }, + }, + { + name: "merge different health states", + steps: []HealthCheckStep{ + {Kind: "success-check"}, + {Kind: "error-check"}, + }, + assertions: func(t *testing.T, status kargoapi.HealthState, issues []string, output []State) { + assert.Equal(t, kargoapi.HealthStateUnhealthy, status) + assert.Contains(t, issues, "health check failed") + assert.Len(t, output, 2) + }, + }, + { + name: "context cancellation", + steps: []HealthCheckStep{ + {Kind: "context-waiter"}, + {Kind: "success-check"}, // Should not execute + }, + assertions: func(t *testing.T, status kargoapi.HealthState, issues []string, output []State) { + assert.Equal(t, kargoapi.HealthStateUnknown, status) + assert.Contains(t, issues, context.Canceled.Error()) + assert.Empty(t, output) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + testRegistry := NewStepRunnerRegistry() + testRegistry.RegisterHealthCheckStepRunner( + &mockHealthCheckStepRunner{ + name: "success-check", + runResult: HealthCheckStepResult{ + Status: kargoapi.HealthStateHealthy, + Output: State{"test": "success"}, + }, + }, + nil, + ) + testRegistry.RegisterHealthCheckStepRunner( + &mockHealthCheckStepRunner{ + name: "error-check", + runResult: HealthCheckStepResult{ + Status: kargoapi.HealthStateUnhealthy, + Issues: []string{"health check failed"}, + Output: State{"test": "error"}, + }, + }, + nil, + ) + testRegistry.RegisterHealthCheckStepRunner( + &mockHealthCheckStepRunner{ + name: "context-waiter", + runFunc: func(ctx context.Context, _ *HealthCheckStepContext) HealthCheckStepResult { + cancel() + <-ctx.Done() + return HealthCheckStepResult{ + Status: kargoapi.HealthStateUnknown, + Issues: []string{ctx.Err().Error()}, + } + }, + }, + nil, + ) + + engine := &SimpleEngine{ + registry: testRegistry, + } + + status, issues, output := engine.executeHealthChecks(ctx, tt.healthCtx, tt.steps) + tt.assertions(t, status, issues, output) + }) + } +} + +func TestSimpleEngine_executeHealthCheck(t *testing.T) { + tests := []struct { + name string + healthCtx HealthCheckContext + step HealthCheckStep + assertions func(*testing.T, HealthCheckStepResult) + }{ + { + name: "successful execution", + step: HealthCheckStep{Kind: "success-check"}, + assertions: func(t *testing.T, result HealthCheckStepResult) { + assert.Equal(t, kargoapi.HealthStateHealthy, result.Status) + assert.Empty(t, result.Issues) + }, + }, + { + name: "unregistered runner", + step: HealthCheckStep{Kind: "unknown"}, + assertions: func(t *testing.T, result HealthCheckStepResult) { + assert.Equal(t, kargoapi.HealthStateUnknown, result.Status) + assert.Contains(t, result.Issues[0], "no runner registered for step kind") + assert.Contains(t, result.Issues[0], "unknown") + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testRegistry := NewStepRunnerRegistry() + testRegistry.RegisterHealthCheckStepRunner( + &mockHealthCheckStepRunner{ + name: "success-check", + runResult: HealthCheckStepResult{ + Status: kargoapi.HealthStateHealthy, + }, + }, + nil, + ) + + engine := &SimpleEngine{ + registry: testRegistry, + } + + result := engine.executeHealthCheck(context.Background(), tt.healthCtx, tt.step) + tt.assertions(t, result) + }) + } +} + +func TestSimpleEngine_prepareHealthCheckStepContext(t *testing.T) { + tests := []struct { + name string + healthCtx HealthCheckContext + step HealthCheckStep + permissions StepRunnerPermissions + assertions func(*testing.T, *HealthCheckStepContext) + }{ + { + name: "context with all permissions", + healthCtx: HealthCheckContext{ + Project: "test-project", + Stage: "test-stage", + }, + step: HealthCheckStep{ + Config: map[string]any{ + "key": "value", + }, + }, + permissions: StepRunnerPermissions{ + AllowCredentialsDB: true, + AllowKargoClient: true, + AllowArgoCDClient: true, + }, + assertions: func(t *testing.T, ctx *HealthCheckStepContext) { + assert.Equal(t, "test-project", ctx.Project) + assert.Equal(t, "test-stage", ctx.Stage) + assert.NotNil(t, ctx.Config) + assert.NotNil(t, ctx.CredentialsDB) + assert.NotNil(t, ctx.KargoClient) + assert.NotNil(t, ctx.ArgoCDClient) + }, + }, + { + name: "context without permissions", + step: HealthCheckStep{}, + permissions: StepRunnerPermissions{}, + assertions: func(t *testing.T, ctx *HealthCheckStepContext) { + assert.Nil(t, ctx.CredentialsDB) + assert.Nil(t, ctx.KargoClient) + assert.Nil(t, ctx.ArgoCDClient) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + engine := &SimpleEngine{ + credentialsDB: &credentials.FakeDB{}, + kargoClient: fake.NewClientBuilder().Build(), + argoCDClient: fake.NewClientBuilder().Build(), + } + + reg := HealthCheckStepRunnerRegistration{ + Permissions: tt.permissions, + } + + ctx := engine.prepareHealthCheckStepContext(tt.healthCtx, tt.step, reg) + tt.assertions(t, ctx) + }) + } +} diff --git a/internal/directives/simple_engine_promote.go b/internal/directives/simple_engine_promote.go new file mode 100644 index 000000000..b62593653 --- /dev/null +++ b/internal/directives/simple_engine_promote.go @@ -0,0 +1,286 @@ +package directives + +import ( + "context" + "fmt" + "os" + "strings" + + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + kargoapi "github.com/akuity/kargo/api/v1alpha1" +) + +// Promote implements the Engine interface. +func (e *SimpleEngine) Promote( + ctx context.Context, + promoCtx PromotionContext, + steps []PromotionStep, +) (PromotionResult, error) { + workDir, err := e.setupWorkDir(promoCtx.WorkDir) + if err != nil { + return PromotionResult{Status: kargoapi.PromotionPhaseErrored}, err + } + if workDir != promoCtx.WorkDir { + defer os.RemoveAll(workDir) + } + + if promoCtx.Secrets, err = e.getProjectSecrets(ctx, promoCtx.Project); err != nil { + return PromotionResult{Status: kargoapi.PromotionPhaseErrored}, err + } + + result, err := e.executeSteps(ctx, promoCtx, steps, workDir) + if err != nil { + return result, fmt.Errorf("step execution failed: %w", err) + } + + return result, nil +} + +// executeSteps executes a list of PromotionSteps in sequence. +func (e *SimpleEngine) executeSteps( + ctx context.Context, + promoCtx PromotionContext, + steps []PromotionStep, + workDir string, +) (PromotionResult, error) { + // Initialize the state which will be passed to each step. + // This is the state that will be updated by each step, + // and returned as the final state after all steps have + // run. + state := promoCtx.State.DeepCopy() + if state == nil { + state = make(State) + } + + var ( + healthChecks []HealthCheckStep + err error + attempt = promoCtx.Attempts + ) + + // Execute each step in sequence, starting from the step index + // specified in the PromotionContext if provided. + for i := promoCtx.StartFromStep; i < int64(len(steps)); i++ { + select { + case <-ctx.Done(): + return PromotionResult{ + Status: kargoapi.PromotionPhaseErrored, + CurrentStep: i, + State: state, + }, ctx.Err() + default: + } + + // Prepare the step for execution by setting the alias. + step := steps[i] + if step.Alias, err = e.stepAlias(step.Alias, i); err != nil { + return PromotionResult{ + Status: kargoapi.PromotionPhaseErrored, + CurrentStep: i, + State: state, + }, err + } + + // Get the PromotionStepRunner for the step. + reg, err := e.registry.GetPromotionStepRunnerRegistration(step.Kind) + if err != nil { + return PromotionResult{ + Status: kargoapi.PromotionPhaseErrored, + CurrentStep: i, + State: state, + }, err + } + + // Check if the step has exceeded the maximum number of attempts. + maxAttempts := step.GetMaxAttempts(reg.Runner) + if maxAttempts > 0 && attempt >= maxAttempts { + return PromotionResult{ + Status: kargoapi.PromotionPhaseErrored, + CurrentStep: i, + State: state, + Attempt: attempt, + }, fmt.Errorf("step %q exceeded max attempts", step.Alias) + } + + // Count the attempt we are about to make. + attempt++ + + // Execute the step. + result, err := e.executeStep(ctx, promoCtx, step, reg, workDir, state) + + // If the step failed, and the maximum number of attempts has not been + // reached, we are still "Running" the step and will retry it. + if err != nil || result.Status == kargoapi.PromotionPhaseErrored || result.Status == kargoapi.PromotionPhaseFailed { + if maxAttempts < 0 || attempt < maxAttempts { + var message strings.Builder + _, _ = message.WriteString(fmt.Sprintf("step %q failed (attempt %d)", step.Alias, attempt)) + if result.Message != "" { + _, _ = message.WriteString(": ") + _, _ = message.WriteString(result.Message) + } + if err != nil { + _, _ = message.WriteString(": ") + _, _ = message.WriteString(err.Error()) + } + + // Update the result to indicate that the step is still running. + result.Status = kargoapi.PromotionPhaseRunning + result.Message = message.String() + + // Swallow the error if the step failed, as we are still + // retrying it. + err = nil + } + } + + // Update the state with the step output, regardless of the result. + state[step.Alias] = result.Output + + // If the step was not successful, return the result to wait for + // a next attempt or to fail the promotion. + if result.Status != kargoapi.PromotionPhaseSucceeded { + return PromotionResult{ + Status: result.Status, + Message: result.Message, + CurrentStep: i, + Attempt: attempt, + State: state, + }, err + } + + // If the step was successful, reset the attempts counter and add its + // health check to the list. + attempt = 0 + if healthCheck := result.HealthCheckStep; healthCheck != nil { + healthChecks = append(healthChecks, *healthCheck) + } + } + + // All steps have succeeded, return the final state. + return PromotionResult{ + Status: kargoapi.PromotionPhaseSucceeded, + HealthCheckSteps: healthChecks, + CurrentStep: int64(len(steps)) - 1, + Attempt: 0, + State: state, + }, nil +} + +// executeStep executes a single PromotionStep. +func (e *SimpleEngine) executeStep( + ctx context.Context, + promoCtx PromotionContext, + step PromotionStep, + reg PromotionStepRunnerRegistration, + workDir string, + state State, +) (PromotionStepResult, error) { + stepCtx, err := e.preparePromotionStepContext(ctx, promoCtx, step, reg.Permissions, workDir, state) + if err != nil { + return PromotionStepResult{ + Status: kargoapi.PromotionPhaseErrored, + }, err + } + + result, err := reg.Runner.RunPromotionStep(ctx, stepCtx) + if err != nil { + err = fmt.Errorf("failed to run step %q: %w", step.Kind, err) + } + return result, err +} + +// preparePromotionStepContext prepares a PromotionStepContext for a PromotionStep. +func (e *SimpleEngine) preparePromotionStepContext( + ctx context.Context, + promoCtx PromotionContext, + step PromotionStep, + permissions StepRunnerPermissions, + workDir string, + state State, +) (*PromotionStepContext, error) { + stateCopy := state.DeepCopy() + + stepCfg, err := step.GetConfig(ctx, e.kargoClient, promoCtx, stateCopy) + if err != nil { + return nil, fmt.Errorf("failed to get step config: %w", err) + } + + stepCtx := &PromotionStepContext{ + UIBaseURL: promoCtx.UIBaseURL, + WorkDir: workDir, + SharedState: stateCopy, + Alias: step.Alias, + Config: stepCfg, + Project: promoCtx.Project, + Stage: promoCtx.Stage, + Promotion: promoCtx.Promotion, + FreightRequests: promoCtx.FreightRequests, + Freight: promoCtx.Freight, + } + + if permissions.AllowCredentialsDB { + stepCtx.CredentialsDB = e.credentialsDB + } + if permissions.AllowKargoClient { + stepCtx.KargoClient = e.kargoClient + } + if permissions.AllowArgoCDClient { + stepCtx.ArgoCDClient = e.argoCDClient + } + + return stepCtx, nil +} + +// stepAlias returns the alias for a step. If the alias is empty, a default +// alias is returned based on the step index. +func (e *SimpleEngine) stepAlias(alias string, index int64) (string, error) { + if alias = strings.TrimSpace(alias); alias != "" { + // A webhook enforces this regex as well, but we're checking here to + // account for the possibility of EXISTING Stages with a promotionTemplate + // containing a step with a now-reserved alias. + if ReservedStepAliasRegex.MatchString(alias) { + return "", fmt.Errorf("step alias %q is forbidden", alias) + } + return alias, nil + } + return fmt.Sprintf("step-%d", index), nil +} + +// setupWorkDir creates a temporary working directory if one is not provided. +func (e *SimpleEngine) setupWorkDir(existingDir string) (string, error) { + if existingDir != "" { + return existingDir, nil + } + + workDir, err := os.MkdirTemp("", "run-") + if err != nil { + return "", fmt.Errorf("temporary working directory creation failed: %w", err) + } + return workDir, nil +} + +// getProjectSecrets returns a map of all Secrets in the Project. The returned +// map is keyed by Secret name and contains a map of Secret data. +func (e *SimpleEngine) getProjectSecrets( + ctx context.Context, + project string, +) (map[string]map[string]string, error) { + secrets := corev1.SecretList{} + if err := e.kargoClient.List( + ctx, + &secrets, + client.InNamespace(project), + ); err != nil { + return nil, fmt.Errorf("error listing Secrets for Project %q: %w", project, err) + } + secretsMap := make(map[string]map[string]string, len(secrets.Items)) + for _, secret := range secrets.Items { + secretsMap[secret.Name] = make(map[string]string, len(secret.Data)) + for key, value := range secret.Data { + secretsMap[secret.Name][key] = string(value) + } + } + return secretsMap, nil +} diff --git a/internal/directives/simple_engine_promote_test.go b/internal/directives/simple_engine_promote_test.go new file mode 100644 index 000000000..ed1a5cc4c --- /dev/null +++ b/internal/directives/simple_engine_promote_test.go @@ -0,0 +1,598 @@ +package directives + +import ( + "context" + "errors" + "os" + "testing" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/client/interceptor" + + kargoapi "github.com/akuity/kargo/api/v1alpha1" + "github.com/akuity/kargo/internal/credentials" +) + +func TestSimpleEngine_Promote(t *testing.T) { + tests := []struct { + name string + promoCtx PromotionContext + steps []PromotionStep + interceptor interceptor.Funcs + assertions func(*testing.T, PromotionResult, error) + }{ + { + name: "successful promotion", + promoCtx: PromotionContext{ + Project: "test-project", + State: State{"existing": "state"}, + }, + steps: []PromotionStep{ + {Kind: "success-step"}, + }, + assertions: func(t *testing.T, result PromotionResult, err error) { + assert.NoError(t, err) + assert.Equal(t, kargoapi.PromotionPhaseSucceeded, result.Status) + assert.NotNil(t, result.State) + assert.Equal(t, "state", result.State["existing"]) + assert.Equal(t, int64(0), result.CurrentStep) + }, + }, + { + name: "failed promotion", + promoCtx: PromotionContext{ + Project: "test-project", + }, + steps: []PromotionStep{ + {Kind: "error-step"}, + }, + assertions: func(t *testing.T, result PromotionResult, err error) { + assert.Error(t, err) + assert.Equal(t, kargoapi.PromotionPhaseErrored, result.Status) + assert.ErrorContains(t, err, "step execution failed") + }, + }, + { + name: "context cancellation", + promoCtx: PromotionContext{ + Project: "test-project", + }, + steps: []PromotionStep{ + {Kind: "context-waiter"}, + }, + assertions: func(t *testing.T, result PromotionResult, err error) { + assert.Error(t, err) + assert.Equal(t, kargoapi.PromotionPhaseErrored, result.Status) + assert.ErrorIs(t, err, context.Canceled) + }, + }, + { + name: "secrets retrieval failure", + promoCtx: PromotionContext{ + Project: "test-project", + }, + steps: []PromotionStep{ + {Kind: "success-step"}, + }, + interceptor: interceptor.Funcs{ + List: func(context.Context, client.WithWatch, client.ObjectList, ...client.ListOption) error { + return errors.New("something went wrong") + }, + }, + assertions: func(t *testing.T, result PromotionResult, err error) { + assert.ErrorContains(t, err, "error listing Secrets for Project") + assert.ErrorContains(t, err, "something went wrong") + assert.Equal(t, kargoapi.PromotionPhaseErrored, result.Status) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + testRegistry := NewStepRunnerRegistry() + testRegistry.RegisterPromotionStepRunner( + &mockPromotionStepRunner{ + name: "success-step", + runResult: PromotionStepResult{Status: kargoapi.PromotionPhaseSucceeded}, + }, + &StepRunnerPermissions{}, + ) + testRegistry.RegisterPromotionStepRunner( + &mockPromotionStepRunner{ + name: "error-step", + runResult: PromotionStepResult{Status: kargoapi.PromotionPhaseErrored}, + runErr: errors.New("something went wrong"), + }, + &StepRunnerPermissions{}, + ) + testRegistry.RegisterPromotionStepRunner( + &mockPromotionStepRunner{ + name: "context-waiter", + runFunc: func(ctx context.Context, _ *PromotionStepContext) (PromotionStepResult, error) { + cancel() // Cancel context immediately + <-ctx.Done() + return PromotionStepResult{Status: kargoapi.PromotionPhaseErrored}, ctx.Err() + }, + }, + &StepRunnerPermissions{}, + ) + + engine := &SimpleEngine{ + registry: testRegistry, + kargoClient: fake.NewClientBuilder(). + WithInterceptorFuncs(tt.interceptor). + Build(), + } + + result, err := engine.Promote(ctx, tt.promoCtx, tt.steps) + tt.assertions(t, result, err) + }) + } +} + +func TestSimpleEngine_executeSteps(t *testing.T) { + tests := []struct { + name string + promoCtx PromotionContext + steps []PromotionStep + assertions func(*testing.T, PromotionResult, error) + }{ + { + name: "execute all steps successfully", + steps: []PromotionStep{ + {Kind: "success-step", Alias: "step1"}, + {Kind: "success-step", Alias: "step2"}, + }, + assertions: func(t *testing.T, result PromotionResult, err error) { + assert.NoError(t, err) + assert.Equal(t, kargoapi.PromotionPhaseSucceeded, result.Status) + assert.Equal(t, int64(1), result.CurrentStep) + assert.Equal(t, int64(0), result.Attempt) + + // Verify state contains outputs from both steps + assert.Equal(t, State{ + "step1": map[string]any{ + "key": "value", + }, + "step2": map[string]any{ + "key": "value", + }, + }, result.State) + }, + }, + { + name: "start from middle step", + promoCtx: PromotionContext{ + StartFromStep: 1, + }, + steps: []PromotionStep{ + {Kind: "error-step", Alias: "step1"}, // This step should be skipped + {Kind: "success-step", Alias: "step2"}, + }, + assertions: func(t *testing.T, result PromotionResult, err error) { + assert.NoError(t, err) + assert.Equal(t, kargoapi.PromotionPhaseSucceeded, result.Status) + assert.Equal(t, int64(1), result.CurrentStep) + assert.Equal(t, int64(0), result.Attempt) + + // Verify only second step output is in state + assert.Equal(t, State{ + "step2": map[string]any{ + "key": "value", + }, + }, result.State) + }, + }, + { + name: "fail on step execution", + steps: []PromotionStep{ + {Kind: "success-step", Alias: "step1"}, + {Kind: "error-step", Alias: "step2"}, + }, + assertions: func(t *testing.T, result PromotionResult, err error) { + assert.ErrorContains(t, err, "something went wrong") + assert.Equal(t, kargoapi.PromotionPhaseErrored, result.Status) + assert.Equal(t, int64(1), result.CurrentStep) + assert.Equal(t, int64(1), result.Attempt) + + // Verify first step output is preserved in state + assert.Equal(t, State{ + "step1": map[string]any{ + "key": "value", + }, + "step2": map[string]any(nil), + }, result.State) + }, + }, + { + name: "fail on invalid step alias", + steps: []PromotionStep{ + {Kind: "success-step", Alias: "step-1"}, + }, + assertions: func(t *testing.T, result PromotionResult, err error) { + assert.ErrorContains(t, err, "is forbidden") + assert.Equal(t, kargoapi.PromotionPhaseErrored, result.Status) + assert.Equal(t, int64(0), result.CurrentStep) + }, + }, + { + name: "context cancellation", + steps: []PromotionStep{ + {Kind: "context-waiter", Alias: "step1"}, + }, + assertions: func(t *testing.T, result PromotionResult, err error) { + assert.ErrorIs(t, err, context.Canceled) + assert.Equal(t, kargoapi.PromotionPhaseErrored, result.Status) + assert.Equal(t, int64(0), result.CurrentStep) + }, + }, + { + name: "retry failed step within max attempts", + promoCtx: PromotionContext{ + Attempts: 1, + }, + steps: []PromotionStep{ + { + Kind: "error-step", + Alias: "step1", + Retry: &kargoapi.PromotionStepRetry{Attempts: 3}, + }, + }, + assertions: func(t *testing.T, result PromotionResult, err error) { + assert.NoError(t, err) + assert.Equal(t, kargoapi.PromotionPhaseRunning, result.Status) + assert.Equal(t, int64(0), result.CurrentStep) + assert.Equal(t, int64(2), result.Attempt) + assert.Contains(t, result.Message, "attempt 2") + }, + }, + { + name: "max attempts exceeded", + promoCtx: PromotionContext{ + Attempts: 3, + }, + steps: []PromotionStep{ + { + Kind: "error-step", + Alias: "step1", + Retry: &kargoapi.PromotionStepRetry{Attempts: 3}, + }, + }, + assertions: func(t *testing.T, result PromotionResult, err error) { + assert.Error(t, err) + assert.Equal(t, kargoapi.PromotionPhaseErrored, result.Status) + assert.Equal(t, int64(0), result.CurrentStep) + assert.Equal(t, int64(3), result.Attempt) + assert.Contains(t, err.Error(), "exceeded max attempts") + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + testRegistry := NewStepRunnerRegistry() + testRegistry.RegisterPromotionStepRunner( + &mockPromotionStepRunner{ + name: "success-step", + runResult: PromotionStepResult{ + Status: kargoapi.PromotionPhaseSucceeded, + Output: map[string]any{"key": "value"}, + }, + }, + &StepRunnerPermissions{}, + ) + testRegistry.RegisterPromotionStepRunner( + &mockPromotionStepRunner{ + name: "error-step", + runResult: PromotionStepResult{Status: kargoapi.PromotionPhaseErrored}, + runErr: errors.New("something went wrong"), + }, + &StepRunnerPermissions{}, + ) + testRegistry.RegisterPromotionStepRunner( + &mockPromotionStepRunner{ + name: "context-waiter", + runFunc: func(ctx context.Context, _ *PromotionStepContext) (PromotionStepResult, error) { + cancel() + <-ctx.Done() + return PromotionStepResult{Status: kargoapi.PromotionPhaseErrored}, ctx.Err() + }, + }, + &StepRunnerPermissions{}, + ) + + engine := &SimpleEngine{ + registry: testRegistry, + kargoClient: fake.NewClientBuilder().Build(), + } + + result, err := engine.executeSteps(ctx, tt.promoCtx, tt.steps, t.TempDir()) + tt.assertions(t, result, err) + }) + } +} + +func TestSimpleEngine_executeStep(t *testing.T) { + tests := []struct { + name string + promoCtx PromotionContext + step PromotionStep + reg PromotionStepRunnerRegistration + assertions func(*testing.T, PromotionStepResult, error) + }{ + { + name: "successful step execution", + reg: PromotionStepRunnerRegistration{ + Runner: &mockPromotionStepRunner{ + name: "success-step", + runResult: PromotionStepResult{ + Status: kargoapi.PromotionPhaseSucceeded, + }, + }, + }, + assertions: func(t *testing.T, result PromotionStepResult, err error) { + assert.NoError(t, err) + assert.Equal(t, kargoapi.PromotionPhaseSucceeded, result.Status) + }, + }, + { + name: "step execution failure", + step: PromotionStep{Kind: "error-step"}, + reg: PromotionStepRunnerRegistration{ + Runner: &mockPromotionStepRunner{ + name: "error-step", + runResult: PromotionStepResult{ + Status: kargoapi.PromotionPhaseErrored, + }, + runErr: errors.New("something went wrong"), + }, + }, + assertions: func(t *testing.T, result PromotionStepResult, err error) { + assert.ErrorContains(t, err, "failed to run step \"error-step\"") + assert.ErrorContains(t, err, "something went wrong") + assert.Equal(t, kargoapi.PromotionPhaseErrored, result.Status) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + engine := &SimpleEngine{ + kargoClient: fake.NewClientBuilder().Build(), + } + + result, err := engine.executeStep(context.Background(), tt.promoCtx, tt.step, tt.reg, t.TempDir(), make(State)) + tt.assertions(t, result, err) + }) + } +} + +func TestSimpleEngine_preparePromotionStepContext(t *testing.T) { + tests := []struct { + name string + promoCtx PromotionContext + step PromotionStep + permissions StepRunnerPermissions + assertions func(*testing.T, *PromotionStepContext, error) + }{ + { + name: "successful context preparation", + promoCtx: PromotionContext{ + Project: "test-project", + Stage: "test-stage", + UIBaseURL: "http://test", + }, + step: PromotionStep{Kind: "test-step"}, + permissions: StepRunnerPermissions{ + AllowCredentialsDB: true, + AllowKargoClient: true, + AllowArgoCDClient: true, + }, + assertions: func(t *testing.T, ctx *PromotionStepContext, err error) { + assert.NoError(t, err) + assert.Equal(t, "test-project", ctx.Project) + assert.Equal(t, "test-stage", ctx.Stage) + assert.Equal(t, "http://test", ctx.UIBaseURL) + assert.NotNil(t, ctx.CredentialsDB) + assert.NotNil(t, ctx.KargoClient) + assert.NotNil(t, ctx.ArgoCDClient) + }, + }, + { + name: "permissions control client access", + promoCtx: PromotionContext{}, + step: PromotionStep{Kind: "test-step"}, + permissions: StepRunnerPermissions{}, + assertions: func(t *testing.T, ctx *PromotionStepContext, err error) { + assert.NoError(t, err) + assert.Nil(t, ctx.CredentialsDB) + assert.Nil(t, ctx.KargoClient) + assert.Nil(t, ctx.ArgoCDClient) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + engine := &SimpleEngine{ + registry: NewStepRunnerRegistry(), + kargoClient: fake.NewClientBuilder().Build(), + argoCDClient: fake.NewClientBuilder().Build(), + credentialsDB: &credentials.FakeDB{}, + } + + stepCtx, err := engine.preparePromotionStepContext( + context.Background(), + tt.promoCtx, + tt.step, + tt.permissions, + t.TempDir(), + make(State), + ) + tt.assertions(t, stepCtx, err) + }) + } +} + +func TestSimpleEngine_stepAlias(t *testing.T) { + tests := []struct { + name string + alias string + index int64 + assertions func(*testing.T, string, error) + }{ + { + name: "use provided alias", + alias: "custom-step", + assertions: func(t *testing.T, alias string, err error) { + assert.NoError(t, err) + assert.Equal(t, "custom-step", alias) + }, + }, + { + name: "generate default alias", + index: 42, + assertions: func(t *testing.T, alias string, err error) { + assert.NoError(t, err) + assert.Equal(t, "step-42", alias) + }, + }, + { + name: "reject reserved alias", + alias: "step-1", + assertions: func(t *testing.T, _ string, err error) { + assert.ErrorContains(t, err, "forbidden") + }, + }, + { + name: "trim whitespace", + alias: " step ", + assertions: func(t *testing.T, alias string, err error) { + assert.NoError(t, err) + assert.Equal(t, "step", alias) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + engine := &SimpleEngine{} + alias, err := engine.stepAlias(tt.alias, tt.index) + tt.assertions(t, alias, err) + }) + } +} + +func TestSimpleEngine_setupWorkDir(t *testing.T) { + tmpDir := t.TempDir() + + tests := []struct { + name string + existingDir string + assertions func(*testing.T, string, error) + }{ + { + name: "use existing directory", + existingDir: tmpDir, + assertions: func(t *testing.T, dir string, err error) { + assert.NoError(t, err) + assert.Equal(t, tmpDir, dir) + }, + }, + { + name: "create new directory", + assertions: func(t *testing.T, dir string, err error) { + assert.NoError(t, err) + assert.DirExists(t, dir) + t.Cleanup(func() { + _ = os.RemoveAll(dir) + }) + assert.Contains(t, dir, "run-") + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + engine := &SimpleEngine{} + dir, err := engine.setupWorkDir(tt.existingDir) + tt.assertions(t, dir, err) + }) + } +} + +func TestSimpleEngine_getProjectSecrets(t *testing.T) { + tests := []struct { + name string + project string + objects []client.Object + interceptor interceptor.Funcs + assertions func(*testing.T, map[string]map[string]string, error) + }{ + { + name: "successful retrieval", + project: "test-project", + objects: []client.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret", + Namespace: "test-project", + }, + Data: map[string][]byte{ + "key1": []byte("value1"), + "key2": []byte("value2"), + }, + }, + }, + assertions: func(t *testing.T, secrets map[string]map[string]string, err error) { + assert.NoError(t, err) + assert.Equal(t, "value1", secrets["test-secret"]["key1"]) + assert.Equal(t, "value2", secrets["test-secret"]["key2"]) + }, + }, + { + name: "list error", + project: "test-project", + interceptor: interceptor.Funcs{ + List: func(context.Context, client.WithWatch, client.ObjectList, ...client.ListOption) error { + return errors.New("list error") + }, + }, + assertions: func(t *testing.T, _ map[string]map[string]string, err error) { + assert.ErrorContains(t, err, "error listing Secrets") + }, + }, + { + name: "no secrets", + project: "empty-project", + assertions: func(t *testing.T, secrets map[string]map[string]string, err error) { + assert.NoError(t, err) + assert.Empty(t, secrets) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + engine := &SimpleEngine{ + kargoClient: fake.NewClientBuilder(). + WithObjects(tt.objects...). + WithInterceptorFuncs(tt.interceptor). + Build(), + } + + secrets, err := engine.getProjectSecrets(context.Background(), tt.project) + tt.assertions(t, secrets, err) + }) + } +} diff --git a/internal/directives/simple_engine_test.go b/internal/directives/simple_engine_test.go deleted file mode 100644 index 1d130a450..000000000 --- a/internal/directives/simple_engine_test.go +++ /dev/null @@ -1,316 +0,0 @@ -package directives - -import ( - "context" - "encoding/json" - "errors" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/client/fake" - "sigs.k8s.io/controller-runtime/pkg/client/interceptor" - - kargoapi "github.com/akuity/kargo/api/v1alpha1" -) - -func TestSimpleEngine_Promote(t *testing.T) { - testHealthCheckStep := HealthCheckStep{ - Kind: "fake", - Config: Config{ - "fake-key": "fake-value", - }, - } - - errorResult := PromotionStepResult{Status: kargoapi.PromotionPhaseErrored} - successResult := PromotionStepResult{ - Status: kargoapi.PromotionPhaseSucceeded, - HealthCheckStep: &testHealthCheckStep, - } - - const successStepName = "success" - const errorStepName = "failure" - const contextWaiterStep = "waiter" - testRegistry := NewStepRunnerRegistry() - testRegistry.RegisterPromotionStepRunner( - &mockPromotionStepRunner{ - name: successStepName, - runResult: successResult, - }, - nil, - ) - testRegistry.RegisterPromotionStepRunner( - &mockPromotionStepRunner{ - name: errorStepName, - runResult: errorResult, - runErr: errors.New("something went wrong"), - }, - nil, - ) - testRegistry.RegisterPromotionStepRunner( - &mockPromotionStepRunner{ - name: contextWaiterStep, - runFunc: func( - ctx context.Context, - _ *PromotionStepContext, - ) (PromotionStepResult, error) { - <-ctx.Done() // Wait for context to be canceled - return successResult, nil - }, - }, - nil, - ) - - tests := []struct { - name string - steps []PromotionStep - ctx context.Context - client client.Client - assertions func(*testing.T, PromotionResult, error) - }{ - { - name: "failure: error getting secrets", - ctx: context.Background(), - client: fake.NewClientBuilder().WithInterceptorFuncs( - interceptor.Funcs{ - List: func(context.Context, client.WithWatch, client.ObjectList, ...client.ListOption) error { - return errors.New("something went wrong") - }, - }, - ).Build(), - assertions: func(t *testing.T, res PromotionResult, err error) { - assert.Equal(t, kargoapi.PromotionPhaseErrored, res.Status) - assert.ErrorContains(t, err, "error listing Secrets for Project") - assert.ErrorContains(t, err, "something went wrong") - }, - }, - { - name: "success: single step", - steps: []PromotionStep{{Kind: successStepName}}, - ctx: context.Background(), - client: fake.NewClientBuilder().Build(), - assertions: func(t *testing.T, res PromotionResult, err error) { - assert.Equal(t, kargoapi.PromotionPhaseSucceeded, res.Status) - assert.Equal(t, []HealthCheckStep{testHealthCheckStep}, res.HealthCheckSteps) - assert.NoError(t, err) - }, - }, - { - name: "success: multiple steps", - steps: []PromotionStep{ - {Kind: successStepName}, - {Kind: successStepName}, - }, - ctx: context.Background(), - client: fake.NewClientBuilder().Build(), - assertions: func(t *testing.T, res PromotionResult, err error) { - assert.Equal(t, kargoapi.PromotionPhaseSucceeded, res.Status) - assert.Equal( - t, - []HealthCheckStep{testHealthCheckStep, testHealthCheckStep}, - res.HealthCheckSteps, - ) - assert.NoError(t, err) - }, - }, - { - name: "failure: runner not found", - steps: []PromotionStep{{Kind: "unknown"}}, - ctx: context.Background(), - client: fake.NewClientBuilder().Build(), - assertions: func(t *testing.T, res PromotionResult, err error) { - assert.Equal(t, kargoapi.PromotionPhaseErrored, res.Status) - assert.ErrorContains(t, err, "not found") - }, - }, - { - name: "failure: runner returns error", - steps: []PromotionStep{{Kind: errorStepName}}, - ctx: context.Background(), - client: fake.NewClientBuilder().Build(), - assertions: func(t *testing.T, res PromotionResult, err error) { - assert.Equal(t, kargoapi.PromotionPhaseErrored, res.Status) - assert.ErrorContains(t, err, "something went wrong") - }, - }, - { - name: "failure: context canceled", - steps: []PromotionStep{ - {Kind: contextWaiterStep}, - {Kind: contextWaiterStep}, // This runner should not be executed - }, - ctx: func() context.Context { - ctx, cancel := context.WithCancel(context.Background()) - go func() { - time.Sleep(10 * time.Millisecond) - cancel() - }() - return ctx - }(), - client: fake.NewClientBuilder().Build(), - assertions: func(t *testing.T, res PromotionResult, err error) { - assert.Equal(t, kargoapi.PromotionPhaseErrored, res.Status) - assert.ErrorIs(t, err, context.Canceled) - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - engine := NewSimpleEngine(nil, nil, nil) - engine.registry = testRegistry - engine.kargoClient = tt.client - res, err := engine.Promote(tt.ctx, PromotionContext{}, tt.steps) - tt.assertions(t, res, err) - }) - } -} - -func TestSimpleEngine_CheckHealth(t *testing.T) { - testOutput := map[string]any{ - "fake-key": "fake-value", - } - testOutputBytes, err := json.Marshal(testOutput) - require.NoError(t, err) - - const healthyStepName = "healthy" - const unhealthyStepName = "unhealthy" - const contextWaiterStep = "waiter" - testRegistry := NewStepRunnerRegistry() - testRegistry.RegisterHealthCheckStepRunner( - &mockHealthCheckStepRunner{ - name: healthyStepName, - runResult: HealthCheckStepResult{ - Status: kargoapi.HealthStateHealthy, - Output: testOutput, - }, - }, - nil, - ) - testRegistry.RegisterHealthCheckStepRunner( - &mockHealthCheckStepRunner{ - name: unhealthyStepName, - runResult: HealthCheckStepResult{ - Status: kargoapi.HealthStateUnhealthy, - Issues: []string{"something went wrong"}, - Output: testOutput, - }, - }, - nil, - ) - testRegistry.RegisterHealthCheckStepRunner( - &mockHealthCheckStepRunner{ - name: contextWaiterStep, - runFunc: func(ctx context.Context, _ *HealthCheckStepContext) HealthCheckStepResult { - <-ctx.Done() // Wait for context to be canceled - return HealthCheckStepResult{ - Status: kargoapi.HealthStateHealthy, - Output: testOutput, - } - }, - }, - nil, - ) - - tests := []struct { - name string - steps []HealthCheckStep - ctx context.Context - assertions func(*testing.T, kargoapi.Health) - }{ - { - name: "healthy: single step", - steps: []HealthCheckStep{{Kind: healthyStepName}}, - ctx: context.Background(), - assertions: func(t *testing.T, res kargoapi.Health) { - require.Equal(t, kargoapi.HealthStateHealthy, res.Status) - require.Empty(t, res.Issues) - require.NotNil(t, res.Output) - require.NotEmpty(t, res.Output.Raw) - require.Equal(t, 1, strings.Count(string(res.Output.Raw), string(testOutputBytes))) - }, - }, - { - name: "healthy: multiple steps", - steps: []HealthCheckStep{{Kind: healthyStepName}, {Kind: healthyStepName}}, - ctx: context.Background(), - assertions: func(t *testing.T, res kargoapi.Health) { - require.Equal(t, kargoapi.HealthStateHealthy, res.Status) - require.Empty(t, res.Issues) - require.NotNil(t, res.Output) - require.NotEmpty(t, res.Output.Raw) - require.Equal(t, 2, strings.Count(string(res.Output.Raw), string(testOutputBytes))) - }, - }, - { - name: "unknown: runner not found", - steps: []HealthCheckStep{{Kind: healthyStepName}, {Kind: "unknown"}}, - ctx: context.Background(), - assertions: func(t *testing.T, res kargoapi.Health) { - // First step healthy + second step not found == unknown - require.Equal(t, kargoapi.HealthStateUnknown, res.Status) - require.Len(t, res.Issues, 1) - require.Contains(t, res.Issues[0], "no runner registered for step kind") - require.NotNil(t, res.Output) - require.NotEmpty(t, res.Output.Raw) - // We should still get the output from the first step - require.Equal(t, 1, strings.Count(string(res.Output.Raw), string(testOutputBytes))) - }, - }, - { - name: "unhealthy: a step returns unhealthy", - steps: []HealthCheckStep{{Kind: unhealthyStepName}, {Kind: healthyStepName}}, - ctx: context.Background(), - assertions: func(t *testing.T, res kargoapi.Health) { - // First step unhealthy + second step unhealthy == unhealthy - require.Equal(t, kargoapi.HealthStateUnhealthy, res.Status) - require.Len(t, res.Issues, 1) - require.Equal(t, "something went wrong", res.Issues[0]) - require.NotNil(t, res.Output) - require.NotEmpty(t, res.Output.Raw) - // We should still get the output from both steps - require.Equal(t, 2, strings.Count(string(res.Output.Raw), string(testOutputBytes))) - }, - }, - { - name: "unknown: context canceled", - steps: []HealthCheckStep{ - {Kind: contextWaiterStep}, - {Kind: contextWaiterStep}, - }, - ctx: func() context.Context { - ctx, cancel := context.WithCancel(context.Background()) - go func() { - time.Sleep(10 * time.Millisecond) - cancel() - }() - return ctx - }(), - assertions: func(t *testing.T, res kargoapi.Health) { - // First step healthy + context canceled == unknown - require.Equal(t, kargoapi.HealthStateUnknown, res.Status) - require.Len(t, res.Issues, 1) - require.Equal(t, context.Canceled.Error(), res.Issues[0]) - require.NotNil(t, res.Output) - require.NotEmpty(t, res.Output.Raw) - // We should have output from one step - require.Equal(t, 1, strings.Count(string(res.Output.Raw), string(testOutputBytes))) - }, - }, - } - - engine := NewSimpleEngine(nil, nil, nil) - engine.registry = testRegistry - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.assertions( - t, - engine.CheckHealth(tt.ctx, HealthCheckContext{}, tt.steps), - ) - }) - } -} diff --git a/ui/src/gen/schema/promotions.kargo.akuity.io_v1alpha1.json b/ui/src/gen/schema/promotions.kargo.akuity.io_v1alpha1.json index 27c269f18..afd56da14 100644 --- a/ui/src/gen/schema/promotions.kargo.akuity.io_v1alpha1.json +++ b/ui/src/gen/schema/promotions.kargo.akuity.io_v1alpha1.json @@ -40,6 +40,19 @@ "description": "Config is opaque configuration for the PromotionStep that is understood\nonly by each PromotionStep's implementation. It is legal to utilize\nexpressions in defining values at any level of this block.\nSee https://docs.kargo.io/references/expression-language for details.", "x-kubernetes-preserve-unknown-fields": true }, + "retry": { + "description": "Retry is the retry policy for this step.", + "properties": { + "attempts": { + "description": "Attempts is the number of times the step can be attempted before the\nPromotionStep is marked as failed.\n\nIf this field is set to 1, the step will not be retried. If this\nfield is set to -1, the step will be retried indefinitely.\n\nThe default of this field depends on the step being executed. Refer to\nthe documentation for the specific step for more information.", + "format": "int64", + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + } + }, + "type": "object" + }, "uses": { "description": "Uses identifies a runner that can execute this step.", "minLength": 1, @@ -94,6 +107,13 @@ "minimum": -9223372036854776000, "type": "integer" }, + "currentStepAttempt": { + "description": "CurrentStepAttempt is the number of times the current step has been\nattempted.", + "format": "int64", + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + }, "finishedAt": { "description": "FinishedAt is the time when the promotion was completed.", "format": "date-time", diff --git a/ui/src/gen/schema/stages.kargo.akuity.io_v1alpha1.json b/ui/src/gen/schema/stages.kargo.akuity.io_v1alpha1.json index c789dc920..40a55c066 100644 --- a/ui/src/gen/schema/stages.kargo.akuity.io_v1alpha1.json +++ b/ui/src/gen/schema/stages.kargo.akuity.io_v1alpha1.json @@ -35,6 +35,19 @@ "description": "Config is opaque configuration for the PromotionStep that is understood\nonly by each PromotionStep's implementation. It is legal to utilize\nexpressions in defining values at any level of this block.\nSee https://docs.kargo.io/references/expression-language for details.", "x-kubernetes-preserve-unknown-fields": true }, + "retry": { + "description": "Retry is the retry policy for this step.", + "properties": { + "attempts": { + "description": "Attempts is the number of times the step can be attempted before the\nPromotionStep is marked as failed.\n\nIf this field is set to 1, the step will not be retried. If this\nfield is set to -1, the step will be retried indefinitely.\n\nThe default of this field depends on the step being executed. Refer to\nthe documentation for the specific step for more information.", + "format": "int64", + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + } + }, + "type": "object" + }, "uses": { "description": "Uses identifies a runner that can execute this step.", "minLength": 1, @@ -412,6 +425,13 @@ "minimum": -9223372036854776000, "type": "integer" }, + "currentStepAttempt": { + "description": "CurrentStepAttempt is the number of times the current step has been\nattempted.", + "format": "int64", + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + }, "finishedAt": { "description": "FinishedAt is the time when the promotion was completed.", "format": "date-time", @@ -1201,6 +1221,13 @@ "minimum": -9223372036854776000, "type": "integer" }, + "currentStepAttempt": { + "description": "CurrentStepAttempt is the number of times the current step has been\nattempted.", + "format": "int64", + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + }, "finishedAt": { "description": "FinishedAt is the time when the promotion was completed.", "format": "date-time", diff --git a/ui/src/gen/v1alpha1/generated_pb.ts b/ui/src/gen/v1alpha1/generated_pb.ts index af35da164..c4ee4ec77 100644 --- a/ui/src/gen/v1alpha1/generated_pb.ts +++ b/ui/src/gen/v1alpha1/generated_pb.ts @@ -2767,6 +2767,14 @@ export class PromotionStatus extends Message { */ currentStep?: bigint; + /** + * CurrentStepAttempt is the number of times the current step has been + * attempted. + * + * @generated from field: optional int64 currentStepAttempt = 11; + */ + currentStepAttempt?: bigint; + /** * State stores the state of the promotion process between reconciliation * attempts. @@ -2791,6 +2799,7 @@ export class PromotionStatus extends Message { { no: 8, name: "healthChecks", kind: "message", T: HealthCheckStep, repeated: true }, { no: 6, name: "finishedAt", kind: "message", T: Time, opt: true }, { no: 9, name: "currentStep", kind: "scalar", T: 3 /* ScalarType.INT64 */, opt: true }, + { no: 11, name: "currentStepAttempt", kind: "scalar", T: 3 /* ScalarType.INT64 */, opt: true }, { no: 10, name: "state", kind: "message", T: JSON, opt: true }, ]); @@ -2833,6 +2842,13 @@ export class PromotionStep extends Message { */ as?: string; + /** + * Retry is the retry policy for this step. + * + * @generated from field: optional github.com.akuity.kargo.api.v1alpha1.PromotionStepRetry retry = 4; + */ + retry?: PromotionStepRetry; + /** * Config is opaque configuration for the PromotionStep that is understood * only by each PromotionStep's implementation. It is legal to utilize @@ -2853,6 +2869,7 @@ export class PromotionStep extends Message { static readonly fields: FieldList = proto2.util.newFieldList(() => [ { no: 1, name: "uses", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true }, { no: 2, name: "as", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true }, + { no: 4, name: "retry", kind: "message", T: PromotionStepRetry, opt: true }, { no: 3, name: "config", kind: "message", T: JSON, opt: true }, ]); @@ -2873,6 +2890,54 @@ export class PromotionStep extends Message { } } +/** + * PromotionStepRetry describes the retry policy for a PromotionStep. + * + * @generated from message github.com.akuity.kargo.api.v1alpha1.PromotionStepRetry + */ +export class PromotionStepRetry extends Message { + /** + * Attempts is the number of times the step can be attempted before the + * PromotionStep is marked as failed. + * + * If this field is set to 1, the step will not be retried. If this + * field is set to -1, the step will be retried indefinitely. + * + * The default of this field depends on the step being executed. Refer to + * the documentation for the specific step for more information. + * + * @generated from field: optional int64 attempts = 1; + */ + attempts?: bigint; + + constructor(data?: PartialMessage) { + super(); + proto2.util.initPartial(data, this); + } + + static readonly runtime: typeof proto2 = proto2; + static readonly typeName = "github.com.akuity.kargo.api.v1alpha1.PromotionStepRetry"; + static readonly fields: FieldList = proto2.util.newFieldList(() => [ + { no: 1, name: "attempts", kind: "scalar", T: 3 /* ScalarType.INT64 */, opt: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): PromotionStepRetry { + return new PromotionStepRetry().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): PromotionStepRetry { + return new PromotionStepRetry().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): PromotionStepRetry { + return new PromotionStepRetry().fromJsonString(jsonString, options); + } + + static equals(a: PromotionStepRetry | PlainMessage | undefined, b: PromotionStepRetry | PlainMessage | undefined): boolean { + return proto2.util.equals(PromotionStepRetry, a, b); + } +} + /** * PromotionTemplate defines a template for a Promotion that can be used to * incorporate Freight into a Stage.