diff --git a/pkg/api/registry.pb.go b/pkg/api/registry.pb.go index 06a9a9309..4430ca958 100644 --- a/pkg/api/registry.pb.go +++ b/pkg/api/registry.pb.go @@ -168,6 +168,8 @@ type Bundle struct { BundlePath string `protobuf:"bytes,6,opt,name=bundlePath" json:"bundlePath,omitempty"` ProvidedApis []*GroupVersionKind `protobuf:"bytes,7,rep,name=providedApis" json:"providedApis,omitempty"` RequiredApis []*GroupVersionKind `protobuf:"bytes,8,rep,name=requiredApis" json:"requiredApis,omitempty"` + Version string `protobuf:"bytes,9,opt,name=version" json:"version,omitempty"` + SkipRange string `protobuf:"bytes,10,opt,name=skipRange" json:"skipRange,omitempty"` } func (m *Bundle) Reset() { *m = Bundle{} } @@ -231,6 +233,20 @@ func (m *Bundle) GetRequiredApis() []*GroupVersionKind { return nil } +func (m *Bundle) GetVersion() string { + if m != nil { + return m.Version + } + return "" +} + +func (m *Bundle) GetSkipRange() string { + if m != nil { + return m.SkipRange + } + return "" +} + type ChannelEntry struct { PackageName string `protobuf:"bytes,1,opt,name=packageName" json:"packageName,omitempty"` ChannelName string `protobuf:"bytes,2,opt,name=channelName" json:"channelName,omitempty"` @@ -985,48 +1001,49 @@ var _Registry_serviceDesc = grpc.ServiceDesc{ func init() { proto.RegisterFile("registry.proto", fileDescriptor0) } var fileDescriptor0 = []byte{ - // 680 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xc4, 0x56, 0x4f, 0x6f, 0xd3, 0x30, - 0x14, 0x5f, 0xda, 0xad, 0xed, 0x5e, 0x2b, 0xb4, 0x99, 0x6d, 0x84, 0x80, 0xa6, 0xe2, 0x0b, 0x3b, - 0x55, 0x30, 0x40, 0x88, 0x03, 0x87, 0x8d, 0x41, 0x05, 0x0c, 0x34, 0x45, 0xfc, 0x39, 0x70, 0xf2, - 0x12, 0xd3, 0x86, 0x65, 0x4e, 0x66, 0x3b, 0x9b, 0xf6, 0x25, 0xb8, 0xf0, 0x3d, 0xf8, 0x8c, 0x28, - 0xb6, 0x93, 0x3a, 0x69, 0xba, 0x21, 0x21, 0xe0, 0xd6, 0xf7, 0xfc, 0xfe, 0xfc, 0xde, 0xcb, 0xef, - 0x67, 0x17, 0x6e, 0x70, 0x3a, 0x89, 0x84, 0xe4, 0x97, 0xa3, 0x94, 0x27, 0x32, 0x41, 0x6d, 0x92, - 0x46, 0xf8, 0x29, 0x74, 0x5f, 0x4c, 0x09, 0x63, 0x34, 0x46, 0x08, 0x96, 0x19, 0x39, 0xa5, 0xae, - 0x33, 0x74, 0x76, 0x56, 0x7d, 0xf5, 0x1b, 0xb9, 0xd0, 0x0d, 0xc4, 0xf9, 0xfb, 0xdc, 0xdd, 0x52, - 0xee, 0xc2, 0xc4, 0xf7, 0xa0, 0x7f, 0x44, 0x82, 0x13, 0x32, 0xa1, 0xb9, 0xd9, 0x94, 0x8c, 0x2f, - 0xa0, 0x6b, 0x42, 0x1a, 0x6b, 0xef, 0x40, 0x2f, 0xd0, 0xad, 0x85, 0xdb, 0x1a, 0xb6, 0x77, 0xfa, - 0xbb, 0x83, 0x11, 0x49, 0xa3, 0x91, 0xc1, 0xe3, 0x97, 0xa7, 0x68, 0x04, 0x28, 0xa4, 0x5f, 0x49, - 0x16, 0x4b, 0x73, 0xa6, 0x00, 0xb5, 0x55, 0xad, 0x86, 0x13, 0xcc, 0x60, 0x6d, 0xcc, 0x93, 0x2c, - 0xfd, 0x44, 0xb9, 0x88, 0x12, 0xf6, 0x36, 0x62, 0x21, 0xda, 0x80, 0x95, 0x49, 0xee, 0x33, 0x10, - 0xb4, 0x91, 0xcf, 0x77, 0xae, 0x83, 0x8a, 0xf9, 0x8c, 0x99, 0x23, 0x3e, 0x89, 0x58, 0x68, 0xba, - 0xa8, 0xdf, 0x68, 0x0b, 0x3a, 0x69, 0x9c, 0x71, 0x12, 0xbb, 0xcb, 0xca, 0x6b, 0x2c, 0xfc, 0xb3, - 0x05, 0x9d, 0xfd, 0x8c, 0x85, 0x71, 0x65, 0x61, 0x4e, 0x65, 0x61, 0x68, 0x08, 0xfd, 0x74, 0xb6, - 0x30, 0xd3, 0xce, 0x76, 0xe5, 0x11, 0xc1, 0xdc, 0x7c, 0xb6, 0xcb, 0x54, 0x7f, 0x23, 0x12, 0x66, - 0x10, 0x14, 0x66, 0x0e, 0x2d, 0x39, 0xfe, 0x46, 0x03, 0xe9, 0xae, 0x0c, 0xdb, 0x39, 0x34, 0x6d, - 0xa1, 0x6d, 0x80, 0x63, 0x85, 0xec, 0x88, 0xc8, 0xa9, 0xdb, 0x51, 0x49, 0x96, 0x07, 0x3d, 0x83, - 0x41, 0xca, 0x93, 0xf3, 0x28, 0xa4, 0xe1, 0x5e, 0x1a, 0x09, 0xb7, 0xab, 0x3e, 0xc4, 0xa6, 0xfa, - 0x10, 0xf5, 0x1d, 0xfa, 0x95, 0xd0, 0x3c, 0x95, 0xd3, 0xb3, 0x2c, 0xe2, 0x26, 0xb5, 0x77, 0x65, - 0xaa, 0x1d, 0x8a, 0xbf, 0x3b, 0x30, 0x30, 0x1f, 0xec, 0x25, 0x93, 0xfc, 0xb2, 0xbe, 0x1c, 0xe7, - 0xda, 0xe5, 0xb4, 0xe6, 0x97, 0x53, 0x8e, 0x6a, 0x6d, 0xcf, 0xf2, 0x20, 0x0f, 0x7a, 0x9c, 0xa6, - 0x31, 0x09, 0xa8, 0x30, 0xdb, 0x2b, 0x6d, 0xbc, 0x01, 0xe8, 0x30, 0x12, 0xd2, 0xd0, 0xd5, 0xa7, - 0x67, 0x19, 0x15, 0x12, 0xdf, 0x87, 0xf5, 0x31, 0xad, 0x39, 0x1b, 0x99, 0x3e, 0x85, 0xb5, 0x31, - 0x95, 0x9a, 0x02, 0x45, 0x9c, 0x0b, 0xdd, 0xf4, 0x64, 0x62, 0x33, 0xc1, 0x98, 0xbf, 0x31, 0x8a, - 0xc5, 0xa2, 0x76, 0x55, 0x76, 0x9f, 0xe1, 0x76, 0xd9, 0xe9, 0x35, 0x2b, 0xa4, 0xf2, 0xe7, 0x2d, - 0xf1, 0x13, 0x55, 0x78, 0x2f, 0x8e, 0x7d, 0xbd, 0x93, 0x53, 0xca, 0xa4, 0xb0, 0x0a, 0x37, 0xb3, - 0x1a, 0x9f, 0xc2, 0xe6, 0x98, 0x4a, 0x2b, 0xe7, 0xda, 0x14, 0x1b, 0x65, 0xeb, 0x4a, 0x94, 0xf3, - 0x02, 0xc0, 0x12, 0xb6, 0x34, 0xca, 0x23, 0xcd, 0x44, 0x5e, 0x42, 0xfc, 0x9b, 0xfa, 0xbe, 0x50, - 0xbb, 0x39, 0x24, 0x92, 0x0a, 0xf9, 0x1f, 0x1a, 0x1f, 0xe8, 0x1b, 0xae, 0xe8, 0xfc, 0x0f, 0x1a, - 0xef, 0xfe, 0x58, 0x81, 0x9e, 0x6f, 0x9e, 0x0b, 0xf4, 0x1c, 0x06, 0x96, 0x38, 0x04, 0xba, 0xa5, - 0x24, 0x3e, 0xaf, 0x17, 0x6f, 0x4d, 0x1d, 0x58, 0xcf, 0x02, 0x5e, 0x7a, 0xe0, 0xa0, 0xc7, 0x00, - 0x33, 0x15, 0xa1, 0x2d, 0x7d, 0x3f, 0xd4, 0x65, 0xe5, 0x0d, 0xec, 0x5c, 0xbc, 0x84, 0x1e, 0xc2, - 0x6a, 0x49, 0x74, 0xb4, 0x59, 0x24, 0x55, 0x24, 0xe6, 0xf5, 0x95, 0x5b, 0xfb, 0xf0, 0x12, 0x3a, - 0x80, 0x9b, 0x65, 0xc8, 0xab, 0x84, 0x17, 0xef, 0xda, 0x76, 0x35, 0xb9, 0xae, 0x9a, 0x7a, 0x95, - 0x8f, 0x70, 0x77, 0x4c, 0xa5, 0x75, 0x3b, 0x45, 0x54, 0x7c, 0x98, 0x92, 0x82, 0xe3, 0xb3, 0x72, - 0xcd, 0x5a, 0xf1, 0xd6, 0xed, 0x47, 0x4c, 0xdd, 0x6e, 0x6a, 0x0b, 0xfb, 0x4a, 0x28, 0xba, 0x8b, - 0x55, 0x4e, 0x20, 0xaf, 0xa8, 0x37, 0x2f, 0xa2, 0x3a, 0x34, 0x7f, 0x01, 0x34, 0xc3, 0x0c, 0x74, - 0xc7, 0x82, 0x56, 0xe7, 0xe9, 0x22, 0x5c, 0x5f, 0x00, 0x97, 0xdc, 0x5e, 0x5c, 0xb9, 0x1c, 0xba, - 0x59, 0x04, 0x8b, 0x8a, 0xbf, 0x53, 0x80, 0x0d, 0x7f, 0x67, 0xb3, 0x9b, 0x74, 0x31, 0x2b, 0xdb, - 0x4c, 0xf1, 0xda, 0xfc, 0xc7, 0x1d, 0xf5, 0xc7, 0xe5, 0xd1, 0xaf, 0x00, 0x00, 0x00, 0xff, 0xff, - 0xf7, 0x7c, 0x82, 0x4a, 0xca, 0x08, 0x00, 0x00, + // 701 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xc4, 0x56, 0x5d, 0x6f, 0xd3, 0x3c, + 0x14, 0x5e, 0xdb, 0xad, 0x1f, 0xa7, 0xd5, 0xab, 0xcd, 0xef, 0x36, 0x42, 0x99, 0xa6, 0xe2, 0x1b, + 0x76, 0x55, 0xc1, 0x00, 0x21, 0x2e, 0xb8, 0xd8, 0x18, 0x54, 0xc0, 0x40, 0x53, 0xc4, 0xc7, 0x05, + 0x57, 0x5e, 0x6b, 0x5a, 0xd3, 0xcc, 0xc9, 0x6c, 0x67, 0xd3, 0xfe, 0x04, 0x37, 0xfc, 0x47, 0x7e, + 0x07, 0x8a, 0xed, 0x24, 0x4e, 0x9a, 0x6e, 0x48, 0x08, 0xb8, 0xeb, 0x39, 0x3e, 0x1f, 0xcf, 0x79, + 0x72, 0x1e, 0xbb, 0xf0, 0x9f, 0xa0, 0x53, 0x26, 0x95, 0xb8, 0x1a, 0x46, 0x22, 0x54, 0x21, 0x6a, + 0x90, 0x88, 0xe1, 0x27, 0xd0, 0x7a, 0x3e, 0x23, 0x9c, 0xd3, 0x00, 0x21, 0x58, 0xe5, 0xe4, 0x8c, + 0x7a, 0xb5, 0x41, 0x6d, 0xaf, 0xe3, 0xeb, 0xdf, 0xc8, 0x83, 0xd6, 0x58, 0x5e, 0xbc, 0x4b, 0xdc, + 0x75, 0xed, 0x4e, 0x4d, 0x7c, 0x17, 0xba, 0x27, 0x64, 0x3c, 0x27, 0x53, 0x9a, 0x98, 0x55, 0xc9, + 0xf8, 0x12, 0x5a, 0x36, 0xa4, 0xb2, 0xf6, 0x1e, 0xb4, 0xc7, 0xa6, 0xb5, 0xf4, 0xea, 0x83, 0xc6, + 0x5e, 0x77, 0xbf, 0x37, 0x24, 0x11, 0x1b, 0x5a, 0x3c, 0x7e, 0x76, 0x8a, 0x86, 0x80, 0x26, 0xf4, + 0x0b, 0x89, 0x03, 0x65, 0xcf, 0x34, 0xa0, 0x86, 0xae, 0x55, 0x71, 0x82, 0x39, 0xac, 0x8f, 0x44, + 0x18, 0x47, 0x1f, 0xa9, 0x90, 0x2c, 0xe4, 0x6f, 0x18, 0x9f, 0xa0, 0x4d, 0x58, 0x9b, 0x26, 0x3e, + 0x0b, 0xc1, 0x18, 0xc9, 0x7c, 0x17, 0x26, 0x28, 0x9d, 0xcf, 0x9a, 0x09, 0xe2, 0x39, 0xe3, 0x13, + 0xdb, 0x45, 0xff, 0x46, 0xdb, 0xd0, 0x8c, 0x82, 0x58, 0x90, 0xc0, 0x5b, 0xd5, 0x5e, 0x6b, 0xe1, + 0x1f, 0x75, 0x68, 0x1e, 0xc6, 0x7c, 0x12, 0x14, 0x08, 0xab, 0x15, 0x08, 0x43, 0x03, 0xe8, 0x46, + 0x39, 0x61, 0xb6, 0x9d, 0xeb, 0x4a, 0x22, 0xc6, 0x0b, 0xf3, 0xb9, 0x2e, 0x5b, 0xfd, 0xb5, 0x0c, + 0xb9, 0x45, 0x90, 0x9a, 0x09, 0xb4, 0xf0, 0xf4, 0x2b, 0x1d, 0x2b, 0x6f, 0x6d, 0xd0, 0x48, 0xa0, + 0x19, 0x0b, 0xed, 0x02, 0x9c, 0x6a, 0x64, 0x27, 0x44, 0xcd, 0xbc, 0xa6, 0x4e, 0x72, 0x3c, 0xe8, + 0x29, 0xf4, 0x22, 0x11, 0x5e, 0xb0, 0x09, 0x9d, 0x1c, 0x44, 0x4c, 0x7a, 0x2d, 0xfd, 0x21, 0xb6, + 0xf4, 0x87, 0x28, 0x73, 0xe8, 0x17, 0x42, 0x93, 0x54, 0x41, 0xcf, 0x63, 0x26, 0x6c, 0x6a, 0xfb, + 0xda, 0x54, 0x37, 0xd4, 0xa5, 0xbd, 0x53, 0xa4, 0x7d, 0x07, 0x3a, 0x72, 0xce, 0x22, 0x9f, 0xf0, + 0x29, 0xf5, 0x40, 0x9f, 0xe5, 0x0e, 0xfc, 0xad, 0x06, 0x3d, 0xfb, 0xa1, 0x5f, 0x70, 0x25, 0xae, + 0xca, 0xa4, 0xd6, 0x6e, 0x24, 0xb5, 0xbe, 0x48, 0x6a, 0x46, 0x91, 0xc3, 0xba, 0xe3, 0x41, 0x7d, + 0x68, 0x0b, 0x1a, 0x05, 0x64, 0x4c, 0xa5, 0x65, 0x3d, 0xb3, 0xf1, 0x26, 0xa0, 0x63, 0x26, 0x95, + 0x5d, 0x73, 0x9f, 0x9e, 0xc7, 0x54, 0x2a, 0x7c, 0x0f, 0x36, 0x46, 0xb4, 0xe4, 0xac, 0x54, 0xc8, + 0x0c, 0xd6, 0x47, 0x54, 0x99, 0xd5, 0x49, 0xe3, 0x3c, 0x68, 0x45, 0xf3, 0xa9, 0xbb, 0x41, 0xd6, + 0xfc, 0x85, 0x51, 0x9c, 0xed, 0x6b, 0x14, 0xe5, 0xfa, 0x09, 0x6e, 0x67, 0x9d, 0x5e, 0xf1, 0x54, + 0x62, 0xbf, 0xdf, 0x12, 0x3f, 0xd6, 0x85, 0x0f, 0x82, 0xc0, 0x37, 0x9c, 0x9c, 0x51, 0xae, 0xa4, + 0x53, 0xb8, 0x5a, 0x0d, 0xf8, 0x0c, 0xb6, 0x46, 0x54, 0x39, 0x39, 0x37, 0xa6, 0xb8, 0x28, 0xeb, + 0xd7, 0xa2, 0x5c, 0x14, 0x0e, 0x56, 0xb0, 0x6d, 0x50, 0x9e, 0x98, 0x0d, 0x16, 0x19, 0xc4, 0x3f, + 0x79, 0x2f, 0x5c, 0x6a, 0x6e, 0x8e, 0x89, 0xa2, 0x52, 0xfd, 0x83, 0xc6, 0x47, 0xe6, 0x66, 0x4c, + 0x3b, 0xff, 0x85, 0xc6, 0xfb, 0xdf, 0xd7, 0xa0, 0xed, 0xdb, 0x67, 0x06, 0x3d, 0x83, 0x9e, 0x23, + 0x0e, 0x89, 0x6e, 0xe9, 0xab, 0x61, 0x51, 0x2f, 0xfd, 0x75, 0x7d, 0xe0, 0x3c, 0x27, 0x78, 0xe5, + 0x7e, 0x0d, 0x3d, 0x02, 0xc8, 0x55, 0x84, 0xb6, 0xcd, 0xbd, 0x52, 0x96, 0x55, 0xbf, 0xe7, 0xe6, + 0xe2, 0x15, 0xf4, 0x00, 0x3a, 0xd9, 0xa2, 0xa3, 0xad, 0x34, 0xa9, 0x20, 0xb1, 0x7e, 0x57, 0xbb, + 0x8d, 0x0f, 0xaf, 0xa0, 0x23, 0xf8, 0x3f, 0x0b, 0x79, 0x19, 0x8a, 0xf4, 0x3d, 0xdc, 0x2d, 0x26, + 0x97, 0x55, 0x53, 0xae, 0xf2, 0x01, 0x76, 0x46, 0x54, 0x39, 0xb7, 0x13, 0xa3, 0xf2, 0xfd, 0x8c, + 0xa4, 0x3b, 0x9e, 0x97, 0xab, 0xd6, 0x4a, 0x7f, 0xc3, 0x7d, 0xfc, 0xf4, 0xed, 0xa6, 0x59, 0x38, + 0xd4, 0x42, 0x31, 0x5d, 0x9c, 0x72, 0x12, 0xf5, 0xd3, 0x7a, 0x8b, 0x22, 0x2a, 0x43, 0xf3, 0x97, + 0x40, 0xb3, 0x9b, 0x81, 0xee, 0x38, 0xd0, 0xca, 0x7b, 0xba, 0x0c, 0xd7, 0x67, 0xc0, 0xd9, 0x6e, + 0x2f, 0xaf, 0x9c, 0x0d, 0x5d, 0x2d, 0x82, 0x65, 0xc5, 0xdf, 0x6a, 0xc0, 0x76, 0x7f, 0xf3, 0xd9, + 0x6d, 0xba, 0xcc, 0xcb, 0x56, 0xaf, 0x78, 0x69, 0xfe, 0xd3, 0xa6, 0xfe, 0xc3, 0xf3, 0xf0, 0x67, + 0x00, 0x00, 0x00, 0xff, 0xff, 0xe1, 0x3c, 0x83, 0x36, 0x02, 0x09, 0x00, 0x00, } diff --git a/pkg/api/registry.proto b/pkg/api/registry.proto index 08ca75841..84cf90e44 100644 --- a/pkg/api/registry.proto +++ b/pkg/api/registry.proto @@ -46,6 +46,8 @@ message Bundle{ string bundlePath = 6; repeated GroupVersionKind providedApis = 7; repeated GroupVersionKind requiredApis = 8; + string version = 9; + string skipRange = 10; } message ChannelEntry{ diff --git a/pkg/mirror/mirror.go b/pkg/mirror/mirror.go index f9b6ad427..428e68d57 100644 --- a/pkg/mirror/mirror.go +++ b/pkg/mirror/mirror.go @@ -95,14 +95,13 @@ func (b *IndexImageMirrorer) Mirror() (map[string]string, error) { var errs []error for _, img := range images { - ref, err := reference.ParseNamed(img) + ref, err := reference.ParseNormalizedNamed(img) if err != nil { errs = append(errs, fmt.Errorf("couldn't parse image for mirroring (%s), skipping mirror: %s", img, err.Error())) continue } - domain := reference.Domain(ref) - mapping[img] = b.Dest + strings.TrimPrefix(img, domain) + mapping[ref.String()] = b.Dest + strings.TrimPrefix(ref.String(), domain) } if err := b.ImageMirrorer.Mirror(mapping); err != nil { diff --git a/pkg/mirror/mirror_test.go b/pkg/mirror/mirror_test.go index bdbdd0944..12a237730 100644 --- a/pkg/mirror/mirror_test.go +++ b/pkg/mirror/mirror_test.go @@ -74,9 +74,11 @@ func TestIndexImageMirrorer_Mirror(t *testing.T) { "quay.io/coreos/prometheus-operator@sha256:0e92dd9b5789c4b13d53e1319d0a6375bcca4caaf0d698af61198061222a576d":"localhost/coreos/prometheus-operator@sha256:0e92dd9b5789c4b13d53e1319d0a6375bcca4caaf0d698af61198061222a576d", "quay.io/coreos/prometheus-operator@sha256:3daa69a8c6c2f1d35dcf1fe48a7cd8b230e55f5229a1ded438f687debade5bcf":"localhost/coreos/prometheus-operator@sha256:3daa69a8c6c2f1d35dcf1fe48a7cd8b230e55f5229a1ded438f687debade5bcf", "quay.io/coreos/prometheus-operator@sha256:5037b4e90dbb03ebdefaa547ddf6a1f748c8eeebeedf6b9d9f0913ad662b5731":"localhost/coreos/prometheus-operator@sha256:5037b4e90dbb03ebdefaa547ddf6a1f748c8eeebeedf6b9d9f0913ad662b5731", + "docker.io/strimzi/cluster-operator:0.11.0": "localhost/strimzi/cluster-operator:0.11.0", + "docker.io/strimzi/cluster-operator:0.11.1": "localhost/strimzi/cluster-operator:0.11.1", + "docker.io/strimzi/operator:0.12.1": "localhost/strimzi/operator:0.12.1", + "docker.io/strimzi/operator:0.12.2": "localhost/strimzi/operator:0.12.2", }, - // strimzi has invalid images in its manifests, so we both return a list of maps and an error - wantErr: fmt.Errorf("[couldn't parse image for mirroring (strimzi/cluster-operator:0.11.0), skipping mirror: repository name must be canonical, couldn't parse image for mirroring (strimzi/cluster-operator:0.11.1), skipping mirror: repository name must be canonical, couldn't parse image for mirroring (strimzi/operator:0.12.1), skipping mirror: repository name must be canonical, couldn't parse image for mirroring (strimzi/operator:0.12.2), skipping mirror: repository name must be canonical]"), }, } for _, tt := range tests { @@ -95,7 +97,9 @@ func TestIndexImageMirrorer_Mirror(t *testing.T) { Dest: tt.fields.Dest, } got, err := b.Mirror() - require.Equal(t, tt.wantErr.Error(), err.Error()) + if err != nil { + require.Equal(t, tt.wantErr.Error(), err.Error()) + } require.Equal(t, tt.want, got) }) } diff --git a/pkg/sqlite/migrations/000_init.go b/pkg/sqlite/migrations/000_init.go index 67df6476c..00986acae 100644 --- a/pkg/sqlite/migrations/000_init.go +++ b/pkg/sqlite/migrations/000_init.go @@ -7,6 +7,10 @@ import ( var InitMigrationKey = 0 +func init() { + registerMigration(InitMigrationKey, initMigration) +} + var initMigration = &Migration{ Id: InitMigrationKey, Up: func(ctx context.Context, tx *sql.Tx) error { @@ -72,7 +76,3 @@ var initMigration = &Migration{ return err }, } - -func init() { - migrations[InitMigrationKey] = initMigration -} diff --git a/pkg/sqlite/migrations/001_related_images.go b/pkg/sqlite/migrations/001_related_images.go index 392f72252..3b3c8c36b 100644 --- a/pkg/sqlite/migrations/001_related_images.go +++ b/pkg/sqlite/migrations/001_related_images.go @@ -14,7 +14,7 @@ import ( const RelatedImagesMigrationKey = 1 func init() { - migrations[RelatedImagesMigrationKey] = relatedImagesMigration + registerMigration(RelatedImagesMigrationKey, relatedImagesMigration) } // listBundles returns a list of operatorbundles as strings diff --git a/pkg/sqlite/migrations/002_bundle_path.go b/pkg/sqlite/migrations/002_bundle_path.go index 0ba0f09f0..b16c13d76 100644 --- a/pkg/sqlite/migrations/002_bundle_path.go +++ b/pkg/sqlite/migrations/002_bundle_path.go @@ -9,7 +9,7 @@ const BundlePathMigrationKey = 2 // Register this migration func init() { - migrations[BundlePathMigrationKey] = bundlePathMigration + registerMigration(BundlePathMigrationKey, bundlePathMigration) } var bundlePathMigration = &Migration{ @@ -23,13 +23,13 @@ var bundlePathMigration = &Migration{ return err }, Down: func(ctx context.Context, tx *sql.Tx) error { - foreingKeyOff := `PRAGMA foreign_keys = 0` + foreignKeyOff := `PRAGMA foreign_keys = 0` createTempTable := `CREATE TABLE operatorbundle_backup (name TEXT,csv TEXT,bundle TEXT)` backupTargetTable := `INSERT INTO operatorbundle_backup SELECT name,csv,bundle FROM operatorbundle` dropTargetTable := `DROP TABLE operatorbundle` renameBackUpTable := `ALTER TABLE operatorbundle_backup RENAME TO operatorbundle;` - foreingKeyOn := `PRAGMA foreign_keys = 1` - _, err := tx.ExecContext(ctx, foreingKeyOff) + foreignKeyOn := `PRAGMA foreign_keys = 1` + _, err := tx.ExecContext(ctx, foreignKeyOff) if err != nil { return err } @@ -49,7 +49,7 @@ var bundlePathMigration = &Migration{ if err != nil { return err } - _, err = tx.ExecContext(ctx, foreingKeyOn) + _, err = tx.ExecContext(ctx, foreignKeyOn) return err }, } diff --git a/pkg/sqlite/migrations/003_required_apis.go b/pkg/sqlite/migrations/003_required_apis.go index dc1b451d7..08006a030 100644 --- a/pkg/sqlite/migrations/003_required_apis.go +++ b/pkg/sqlite/migrations/003_required_apis.go @@ -13,7 +13,7 @@ const RequiredApiMigrationKey = 3 // Register this migration func init() { - migrations[RequiredApiMigrationKey] = requiredApiMigration + registerMigration(RequiredApiMigrationKey, requiredApiMigration) } var requiredApiMigration = &Migration{ diff --git a/pkg/sqlite/migrations/003_required_apis_test.go b/pkg/sqlite/migrations/003_required_apis_test.go index 8ed96fe63..42177910e 100644 --- a/pkg/sqlite/migrations/003_required_apis_test.go +++ b/pkg/sqlite/migrations/003_required_apis_test.go @@ -2,12 +2,11 @@ package migrations_test import ( "context" + "database/sql" "testing" "github.com/stretchr/testify/require" - "github.com/operator-framework/operator-registry/pkg/api" - "github.com/operator-framework/operator-registry/pkg/sqlite" "github.com/operator-framework/operator-registry/pkg/sqlite/migrations" ) @@ -35,20 +34,31 @@ func TestRequiredApisUp(t *testing.T) { require.NoError(t, tx.Commit()) // check that no required apis were extracted. - querier := sqlite.NewSQLLiteQuerierFromDb(db) - provided, required, err := querier.GetApisForEntry(context.TODO(), 1) + requiredQuery := `SELECT DISTINCT api.group_name, api.version, api.kind, api.plural FROM api + INNER JOIN api_requirer ON (api.group_name=api_requirer.group_name AND api.version=api_requirer.version AND api.kind=api_requirer.kind) + WHERE api_requirer.channel_entry_id=?` + // check that no required apis were extracted. + _, err = db.Query(requiredQuery, 1) require.Error(t, err) - require.Nil(t, provided) - require.Nil(t, required) // Up the migration with backfill err = migrator.Up(context.TODO(), migrations.Only(migrations.RequiredApiMigrationKey)) require.NoError(t, err) // check that required apis were extracted - bundle, err := querier.GetBundleForChannel(context.TODO(), "etcd", "alpha") + rows, err := db.Query(requiredQuery, 1) require.NoError(t, err) - require.Equal(t, []*api.GroupVersionKind{{Group:"etcd.database.coreos.com", Version: "v1beta2", Kind:"EtcdCluster", Plural:"etcdclusters"}}, bundle.RequiredApis) + var group sql.NullString + var version sql.NullString + var kind sql.NullString + var plural sql.NullString + rows.Next() + require.NoError(t, rows.Scan(&group, &version, &kind, &plural)) + require.Equal(t, group.String, "etcd.database.coreos.com") + require.Equal(t, version.String, "v1beta2") + require.Equal(t, kind.String, "EtcdCluster") + require.Equal(t, plural.String, "etcdclusters") + require.NoError(t, rows.Close()) } func TestRequiredApisDown(t *testing.T) { @@ -64,19 +74,29 @@ func TestRequiredApisDown(t *testing.T) { require.NoError(t, err) // check that required apis were extracted from existing bundles - querier := sqlite.NewSQLLiteQuerierFromDb(db) - provided, required, err := querier.GetApisForEntry(context.TODO(), 1) + requiredQuery := `SELECT DISTINCT api.group_name, api.version, api.kind, api.plural FROM api + INNER JOIN api_requirer ON (api.group_name=api_requirer.group_name AND api.version=api_requirer.version AND api.kind=api_requirer.kind) + WHERE api_requirer.channel_entry_id=?` + + rows, err := db.Query(requiredQuery, 1) require.NoError(t, err) - require.Equal(t, provided, []*api.GroupVersionKind{}) - require.Equal(t, []*api.GroupVersionKind{{Group:"etcd.database.coreos.com", Version: "v1beta2", Kind:"EtcdCluster", Plural:"etcdclusters"}}, required) + var group sql.NullString + var version sql.NullString + var kind sql.NullString + var plural sql.NullString + rows.Next() + require.NoError(t, rows.Scan(&group, &version, &kind, &plural)) + require.Equal(t, group.String, "etcd.database.coreos.com") + require.Equal(t, version.String, "v1beta2") + require.Equal(t, kind.String, "EtcdCluster") + require.Equal(t, plural.String, "etcdclusters") + require.NoError(t, rows.Close()) // run down migration err = migrator.Down(context.TODO(), migrations.Only(migrations.RequiredApiMigrationKey)) require.NoError(t, err) // check that no required apis were extracted. - provided, required, err = querier.GetApisForEntry(context.TODO(), 1) + _, err = db.Query(requiredQuery, 1) require.Error(t, err) - require.Nil(t, provided) - require.Nil(t, required) } diff --git a/pkg/sqlite/migrations/004_cascade_delete.go b/pkg/sqlite/migrations/004_cascade_delete.go index 3ac15390a..6cdd9bed2 100644 --- a/pkg/sqlite/migrations/004_cascade_delete.go +++ b/pkg/sqlite/migrations/004_cascade_delete.go @@ -9,7 +9,7 @@ var CascadeDeleteMigrationKey = 4 // Register this migration func init() { - migrations[CascadeDeleteMigrationKey] = cascadeDeleteMigration + registerMigration(CascadeDeleteMigrationKey, cascadeDeleteMigration) } var cascadeDeleteMigration = &Migration{ diff --git a/pkg/sqlite/migrations/005_version_skiprange.go b/pkg/sqlite/migrations/005_version_skiprange.go new file mode 100644 index 000000000..7c56236f6 --- /dev/null +++ b/pkg/sqlite/migrations/005_version_skiprange.go @@ -0,0 +1,94 @@ +package migrations + +import ( + "context" + "database/sql" + + "github.com/sirupsen/logrus" +) + +const VersionSkipRangeMigrationKey = 5 +const SkipRangeAnnotationKey = "olm.skipRange" + +// Register this migration +func init() { + registerMigration(VersionSkipRangeMigrationKey, versionSkipRangeMigration) +} + +var versionSkipRangeMigration = &Migration{ + Id: VersionSkipRangeMigrationKey, + Up: func(ctx context.Context, tx *sql.Tx) error { + sql := ` + ALTER TABLE operatorbundle + ADD COLUMN skiprange TEXT; + + ALTER TABLE operatorbundle + ADD COLUMN version TEXT; + ` + _, err := tx.ExecContext(ctx, sql) + if err != nil { + return err + } + + bundles, err := listBundles(ctx, tx) + if err != nil { + return err + } + for _, bundle := range bundles { + if err := extractVersioning(ctx, tx, bundle); err != nil { + logrus.Warnf("error backfilling related images: %v", err) + continue + } + } + return err + }, + Down: func(ctx context.Context, tx *sql.Tx) error { + foreignKeyOff := `PRAGMA foreign_keys = 0` + createTempTable := `CREATE TABLE operatorbundle_backup (name TEXT, csv TEXT, bundle TEXT, bundlepath TEXT)` + backupTargetTable := `INSERT INTO operatorbundle_backup SELECT name, csv, bundle, bundlepath FROM operatorbundle` + dropTargetTable := `DROP TABLE operatorbundle` + renameBackUpTable := `ALTER TABLE operatorbundle_backup RENAME TO operatorbundle;` + foreignKeyOn := `PRAGMA foreign_keys = 1` + _, err := tx.ExecContext(ctx, foreignKeyOff) + if err != nil { + return err + } + _, err = tx.ExecContext(ctx, createTempTable) + if err != nil { + return err + } + _, err = tx.ExecContext(ctx, backupTargetTable) + if err != nil { + return err + } + _, err = tx.ExecContext(ctx, dropTargetTable) + if err != nil { + return err + } + _, err = tx.ExecContext(ctx, renameBackUpTable) + if err != nil { + return err + } + _, err = tx.ExecContext(ctx, foreignKeyOn) + return err + }, +} + +func extractVersioning(ctx context.Context, tx *sql.Tx, name string) error { + addSql := `insert into operatorbundle(version, skiprange) values(?,?)` + csv, err := getCSV(ctx, tx, name) + if err != nil { + logrus.Warnf("error backfilling versioning: %v", err) + return err + } + skiprange, ok := csv.Annotations[SkipRangeAnnotationKey] + if !ok { + skiprange = "" + } + version, err := csv.GetVersion() + if err != nil { + version = "" + } + _, err = tx.ExecContext(ctx, addSql, version, skiprange) + return err +} diff --git a/pkg/sqlite/migrations/005_version_skiprange_test.go b/pkg/sqlite/migrations/005_version_skiprange_test.go new file mode 100644 index 000000000..e70294a44 --- /dev/null +++ b/pkg/sqlite/migrations/005_version_skiprange_test.go @@ -0,0 +1,80 @@ +package migrations_test + +import ( + "context" + "database/sql" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/operator-framework/operator-registry/pkg/sqlite/migrations" +) + +func TestVersioningUp(t *testing.T) { + db, migrator, cleanup := CreateTestDbAt(t, migrations.VersionSkipRangeMigrationKey-1) + defer cleanup() + + err := migrator.Up(context.TODO(), migrations.Only(migrations.VersionSkipRangeMigrationKey)) + require.NoError(t, err) + + // Adding row with version and skiprange column should not fail after migrating up + tx, err := db.Begin() + require.NoError(t, err) + stmt, err := tx.Prepare("insert into operatorbundle(name, csv, bundle, bundlepath, version, skiprange) values(?, ?, ?, ?, ?, ?)") + require.NoError(t, err) + defer stmt.Close() + + _, err = stmt.Exec("testName", "testCSV", "testBundle", "quay.io/test", "1.0.0", "< 1.0.0") + require.NoError(t, err) +} + +func TestVersioningDown(t *testing.T) { + db, migrator, cleanup := CreateTestDbAt(t, migrations.VersionSkipRangeMigrationKey) + defer cleanup() + + // Add a bundle without extracting required_apis + _, err := db.Exec(`PRAGMA foreign_keys = 0`) + require.NoError(t, err) + testCSV := `{"apiVersion":"operators.coreos.com/v1alpha1","kind":"ClusterServiceVersion","metadata":{"annotations":{"tectonic-visibility":"ocs"},"name":"etcdoperator.v0.6.1","namespace":"placeholder"},"spec":{"customresourcedefinitions":{"required":[{"description":"Represents a cluster of etcd nodes.","displayName":"etcd Cluster","kind":"EtcdCluster","name":"etcdclusters.etcd.database.coreos.com","resources":[{"kind":"Service","version":"v1"},{"kind":"Pod","version":"v1"}],"specDescriptors":[{"description":"The desired number of member Pods for the etcd cluster.","displayName":"Size","path":"size","x-descriptors":["urn:alm:descriptor:com.tectonic.ui:podCount"]}],"statusDescriptors":[{"description":"The status of each of the member Pods for the etcd cluster.","displayName":"Member Status","path":"members","x-descriptors":["urn:alm:descriptor:com.tectonic.ui:podStatuses"]},{"description":"The service at which the running etcd cluster can be accessed.","displayName":"Service","path":"service","x-descriptors":["urn:alm:descriptor:io.kubernetes:Service"]},{"description":"The current size of the etcd cluster.","displayName":"Cluster Size","path":"size"},{"description":"The current version of the etcd cluster.","displayName":"Current version","path":"currentVersion"},{"description":"The target version of the etcd cluster, after upgrading.","displayName":"Target version","path":"targetVersion"},{"description":"The current status of the etcd cluster.","displayName":"Status","path":"phase","x-descriptors":["urn:alm:descriptor:io.kubernetes.phase"]},{"description":"Explanation for the current status of the cluster.","displayName":"Status Details","path":"reason","x-descriptors":["urn:alm:descriptor:io.kubernetes.phase:reason"]}],"version":"v1beta2"}]},"description":"etcd is a distributed key value store that provides a reliable way to store data across a cluster of machines. It’s open-source and available on GitHub. etcd gracefully handles leader elections during network partitions and will tolerate machine failure, including the leader. Your applications can read and write data into etcd.\nA simple use-case is to store database connection details or feature flags within etcd as key value pairs. These values can be watched, allowing your app to reconfigure itself when they change. Advanced uses take advantage of the consistency guarantees to implement database leader elections or do distributed locking across a cluster of workers.\n\n_The etcd Open Cloud Service is Public Alpha. The goal before Beta is to fully implement backup features._\n\n### Reading and writing to etcd\n\nCommunicate with etcd though its command line utility etcdctl or with the API using the automatically generated Kubernetes Service.\n\n[Read the complete guide to using the etcd Open Cloud Service](https://coreos.com/tectonic/docs/latest/alm/etcd-ocs.html)\n\n### Supported Features\n**High availability**\nMultiple instances of etcd are networked together and secured. Individual failures or networking issues are transparently handled to keep your cluster up and running.\n**Automated updates**\nRolling out a new etcd version works like all Kubernetes rolling updates. Simply declare the desired version, and the etcd service starts a safe rolling update to the new version automatically.\n**Backups included**\nComing soon, the ability to schedule backups to happen on or off cluster.\n","displayName":"etcd","icon":[{"base64data":"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","mediatype":"image/png"}],"install":{"spec":{"deployments":[{"name":"etcd-operator","spec":{"replicas":1,"selector":{"matchLabels":{"name":"etcd-operator-alm-owned"}},"template":{"metadata":{"labels":{"name":"etcd-operator-alm-owned"},"name":"etcd-operator-alm-owned"},"spec":{"containers":[{"command":["etcd-operator","--create-crd=false"],"env":[{"name":"MY_POD_NAMESPACE","valueFrom":{"fieldRef":{"fieldPath":"metadata.namespace"}}},{"name":"MY_POD_NAME","valueFrom":{"fieldRef":{"fieldPath":"metadata.name"}}}],"image":"quay.io/coreos/etcd-operator@sha256:bd944a211eaf8f31da5e6d69e8541e7cada8f16a9f7a5a570b22478997819943","name":"etcd-operator"}],"serviceAccountName":"etcd-operator"}}}}],"permissions":[{"rules":[{"apiGroups":["etcd.database.coreos.com"],"resources":["etcdclusters"],"verbs":["*"]},{"apiGroups":["storage.k8s.io"],"resources":["storageclasses"],"verbs":["*"]},{"apiGroups":[""],"resources":["pods","services","endpoints","persistentvolumeclaims","events"],"verbs":["*"]},{"apiGroups":["apps"],"resources":["deployments"],"verbs":["*"]},{"apiGroups":[""],"resources":["secrets"],"verbs":["get"]}],"serviceAccountName":"etcd-operator"}]},"strategy":"deployment"},"keywords":["etcd","key value","database","coreos","open source"],"labels":{"alm-owner-etcd":"etcdoperator","alm-status-descriptors":"etcdoperator.v0.6.1","operated-by":"etcdoperator"},"links":[{"name":"Blog","url":"https://coreos.com/etcd"},{"name":"Documentation","url":"https://coreos.com/operators/etcd/docs/latest/"},{"name":"etcd Operator Source Code","url":"https://github.com/coreos/etcd-operator"}],"maintainers":[{"email":"support@coreos.com","name":"CoreOS, Inc"}],"maturity":"alpha","provider":{"name":"CoreOS, Inc"},"selector":{"matchLabels":{"alm-owner-etcd":"etcdoperator","operated-by":"etcdoperator"}},"version":"0.6.1"}}` + testBundle := `{"apiVersion":"apiextensions.k8s.io/v1beta1","kind":"CustomResourceDefinition","metadata":{"name":"etcdclusters.etcd.database.coreos.com"},"spec":{"group":"etcd.database.coreos.com","names":{"kind":"EtcdCluster","listKind":"EtcdClusterList","plural":"etcdclusters","shortNames":["etcdclus","etcd"],"singular":"etcdcluster"},"scope":"Namespaced","version":"v1beta2"}}{"apiVersion":"operators.coreos.com/v1alpha1","kind":"ClusterServiceVersion","metadata":{"annotations":{"tectonic-visibility":"ocs"},"name":"etcdoperator.v0.6.1","namespace":"placeholder"},"spec":{"customresourcedefinitions":{"required":[{"description":"Represents a cluster of etcd nodes.","displayName":"etcd Cluster","kind":"EtcdCluster","name":"etcdclusters.etcd.database.coreos.com","resources":[{"kind":"Service","version":"v1"},{"kind":"Pod","version":"v1"}],"specDescriptors":[{"description":"The desired number of member Pods for the etcd cluster.","displayName":"Size","path":"size","x-descriptors":["urn:alm:descriptor:com.tectonic.ui:podCount"]}],"statusDescriptors":[{"description":"The status of each of the member Pods for the etcd cluster.","displayName":"Member Status","path":"members","x-descriptors":["urn:alm:descriptor:com.tectonic.ui:podStatuses"]},{"description":"The service at which the running etcd cluster can be accessed.","displayName":"Service","path":"service","x-descriptors":["urn:alm:descriptor:io.kubernetes:Service"]},{"description":"The current size of the etcd cluster.","displayName":"Cluster Size","path":"size"},{"description":"The current version of the etcd cluster.","displayName":"Current version","path":"currentVersion"},{"description":"The target version of the etcd cluster, after upgrading.","displayName":"Target version","path":"targetVersion"},{"description":"The current status of the etcd cluster.","displayName":"Status","path":"phase","x-descriptors":["urn:alm:descriptor:io.kubernetes.phase"]},{"description":"Explanation for the current status of the cluster.","displayName":"Status Details","path":"reason","x-descriptors":["urn:alm:descriptor:io.kubernetes.phase:reason"]}],"version":"v1beta2"}]},"description":"etcd is a distributed key value store that provides a reliable way to store data across a cluster of machines. It’s open-source and available on GitHub. etcd gracefully handles leader elections during network partitions and will tolerate machine failure, including the leader. Your applications can read and write data into etcd.\nA simple use-case is to store database connection details or feature flags within etcd as key value pairs. These values can be watched, allowing your app to reconfigure itself when they change. Advanced uses take advantage of the consistency guarantees to implement database leader elections or do distributed locking across a cluster of workers.\n\n_The etcd Open Cloud Service is Public Alpha. The goal before Beta is to fully implement backup features._\n\n### Reading and writing to etcd\n\nCommunicate with etcd though its command line utility etcdctl or with the API using the automatically generated Kubernetes Service.\n\n[Read the complete guide to using the etcd Open Cloud Service](https://coreos.com/tectonic/docs/latest/alm/etcd-ocs.html)\n\n### Supported Features\n**High availability**\nMultiple instances of etcd are networked together and secured. Individual failures or networking issues are transparently handled to keep your cluster up and running.\n**Automated updates**\nRolling out a new etcd version works like all Kubernetes rolling updates. Simply declare the desired version, and the etcd service starts a safe rolling update to the new version automatically.\n**Backups included**\nComing soon, the ability to schedule backups to happen on or off cluster.\n","displayName":"etcd","icon":[{"base64data":"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","mediatype":"image/png"}],"install":{"spec":{"deployments":[{"name":"etcd-operator","spec":{"replicas":1,"selector":{"matchLabels":{"name":"etcd-operator-alm-owned"}},"template":{"metadata":{"labels":{"name":"etcd-operator-alm-owned"},"name":"etcd-operator-alm-owned"},"spec":{"containers":[{"command":["etcd-operator","--create-crd=false"],"env":[{"name":"MY_POD_NAMESPACE","valueFrom":{"fieldRef":{"fieldPath":"metadata.namespace"}}},{"name":"MY_POD_NAME","valueFrom":{"fieldRef":{"fieldPath":"metadata.name"}}}],"image":"quay.io/coreos/etcd-operator@sha256:bd944a211eaf8f31da5e6d69e8541e7cada8f16a9f7a5a570b22478997819943","name":"etcd-operator"}],"serviceAccountName":"etcd-operator"}}}}],"permissions":[{"rules":[{"apiGroups":["etcd.database.coreos.com"],"resources":["etcdclusters"],"verbs":["*"]},{"apiGroups":["storage.k8s.io"],"resources":["storageclasses"],"verbs":["*"]},{"apiGroups":[""],"resources":["pods","services","endpoints","persistentvolumeclaims","events"],"verbs":["*"]},{"apiGroups":["apps"],"resources":["deployments"],"verbs":["*"]},{"apiGroups":[""],"resources":["secrets"],"verbs":["get"]}],"serviceAccountName":"etcd-operator"}]},"strategy":"deployment"},"keywords":["etcd","key value","database","coreos","open source"],"labels":{"alm-owner-etcd":"etcdoperator","alm-status-descriptors":"etcdoperator.v0.6.1","operated-by":"etcdoperator"},"links":[{"name":"Blog","url":"https://coreos.com/etcd"},{"name":"Documentation","url":"https://coreos.com/operators/etcd/docs/latest/"},{"name":"etcd Operator Source Code","url":"https://github.com/coreos/etcd-operator"}],"maintainers":[{"email":"support@coreos.com","name":"CoreOS, Inc"}],"maturity":"alpha","provider":{"name":"CoreOS, Inc"},"selector":{"matchLabels":{"alm-owner-etcd":"etcdoperator","operated-by":"etcdoperator"}},"version":"0.6.1"}}` + insert := "insert into operatorbundle(name, csv, bundle, version, skiprange) values(?, ?, ?, ?, ?)" + _, err = db.Exec(insert, "etcdoperator.v0.6.1", testCSV, testBundle, "0.6.1", ">0.5.0 <0.6.1") + require.NoError(t, err) + _, err = db.Exec("insert into package(name, default_channel) values(?,?)", "etcd", "alpha") + require.NoError(t, err) + _, err = db.Exec("insert into channel(name, package_name, head_operatorbundle_name) values(?,?,?)", "alpha", "etcd", "etcdoperator.v0.6.1") + require.NoError(t, err) + _, err = db.Exec("insert into channel_entry(channel_name, package_name, operatorbundle_name, depth) values(?, ?, ?, ?)", "alpha", "etcd", "etcdoperator.v0.6.1", 0) + require.NoError(t, err) + + // check that required apis were extracted from existing bundles + rows, err := db.Query("select name, csv, bundle, version, skiprange from operatorbundle") + require.NoError(t, err) + var name sql.NullString + var csv sql.NullString + var bundle sql.NullString + var version sql.NullString + var skipRange sql.NullString + rows.Next() + require.NoError(t, rows.Scan(&name, &csv, &bundle, &version, &skipRange)) + require.Equal(t, name.String, "etcdoperator.v0.6.1") + require.Equal(t, csv.String, testCSV) + require.Equal(t, bundle.String, testBundle) + require.Equal(t, version.String, "0.6.1") + require.Equal(t, skipRange.String, ">0.5.0 <0.6.1") + require.NoError(t, rows.Close()) + + // run down migration + err = migrator.Down(context.TODO(), migrations.Only(migrations.VersionSkipRangeMigrationKey)) + require.NoError(t, err) + + // check that bundle data is fine + rows, err = db.Query("select name, csv, bundle from operatorbundle") + require.NoError(t, err) + rows.Next() + require.NoError(t, rows.Scan(&name, &csv, &bundle)) + require.Equal(t, name.String, "etcdoperator.v0.6.1") + require.Equal(t, csv.String, testCSV) + require.Equal(t, bundle.String, testBundle) + require.NoError(t, rows.Close()) +} diff --git a/pkg/sqlite/migrations/migrations.go b/pkg/sqlite/migrations/migrations.go index d2802c2ee..b9bb60fba 100644 --- a/pkg/sqlite/migrations/migrations.go +++ b/pkg/sqlite/migrations/migrations.go @@ -3,6 +3,7 @@ package migrations import ( "context" "database/sql" + "fmt" "sort" ) @@ -80,3 +81,13 @@ func Only(key int) Migrations { func All() MigrationSet { return migrations } + +func registerMigration(key int, m *Migration) { + if _, ok := migrations[key]; ok { + panic(fmt.Sprintf("already have a migration registered with id %d", key)) + } + if m.Id != key { + panic(fmt.Sprintf("migration has wrong id for key. key: %d, id: %d", key, m.Id)) + } + migrations[key] = m +} diff --git a/pkg/sqlite/migrator.go b/pkg/sqlite/migrator.go index f1c349200..da86bbc20 100644 --- a/pkg/sqlite/migrator.go +++ b/pkg/sqlite/migrator.go @@ -69,9 +69,14 @@ func (m *SQLLiteMigrator) Up(ctx context.Context, migrations migrations.Migratio if err != nil { return err } + var commitErr error defer func() { + if commitErr == nil { + return + } + logrus.WithError(commitErr).Warningf("tx commit failed") if err := tx.Rollback(); err != nil { - logrus.WithError(err).Debugf("couldn't rollback - this is expected if the transaction committed") + logrus.WithError(err).Warningf("couldn't rollback after failed commit") } }() @@ -97,10 +102,8 @@ func (m *SQLLiteMigrator) Up(ctx context.Context, migrations migrations.Migratio return err } } - if err := tx.Commit(); err != nil { - return err - } - return nil + commitErr = tx.Commit() + return commitErr } func (m *SQLLiteMigrator) Down(ctx context.Context, migrations migrations.Migrations) error { @@ -108,9 +111,14 @@ func (m *SQLLiteMigrator) Down(ctx context.Context, migrations migrations.Migrat if err != nil { return err } + var commitErr error defer func() { + if commitErr == nil { + return + } + logrus.WithError(commitErr).Warningf("tx commit failed") if err := tx.Rollback(); err != nil { - logrus.WithError(err).Debugf("couldn't rollback - this is expected if the transaction committed") + logrus.WithError(err).Warningf("couldn't rollback after failed commit") } }() if err := m.ensureMigrationTable(ctx, tx); err != nil { @@ -135,10 +143,8 @@ func (m *SQLLiteMigrator) Down(ctx context.Context, migrations migrations.Migrat return err } } - if err := tx.Commit(); err != nil { - return err - } - return nil + commitErr = tx.Commit() + return commitErr } func (m *SQLLiteMigrator) ensureMigrationTable(ctx context.Context, tx *sql.Tx) error { diff --git a/pkg/sqlite/query.go b/pkg/sqlite/query.go index 54d3d95e5..0ebc4f695 100644 --- a/pkg/sqlite/query.go +++ b/pkg/sqlite/query.go @@ -36,6 +36,8 @@ func (s *SQLQuerier) ListTables(ctx context.Context) ([]string, error) { if err != nil { return nil, err } + defer rows.Close() + tables := []string{} for rows.Next() { var tableName sql.NullString @@ -56,6 +58,8 @@ func (s *SQLQuerier) ListPackages(ctx context.Context) ([]string, error) { if err != nil { return nil, err } + defer rows.Close() + packages := []string{} for rows.Next() { var pkgName sql.NullString @@ -77,6 +81,7 @@ func (s *SQLQuerier) GetPackage(ctx context.Context, name string) (*registry.Pac if err != nil { return nil, err } + defer rows.Close() var pkgName sql.NullString var defaultChannel sql.NullString @@ -109,13 +114,14 @@ func (s *SQLQuerier) GetPackage(ctx context.Context, name string) (*registry.Pac } func (s *SQLQuerier) GetBundle(ctx context.Context, pkgName, channelName, csvName string) (*api.Bundle, error) { - query := `SELECT DISTINCT channel_entry.entry_id, operatorbundle.name, operatorbundle.bundle, operatorbundle.bundlepath + query := `SELECT DISTINCT channel_entry.entry_id, operatorbundle.name, operatorbundle.bundle, operatorbundle.bundlepath, operatorbundle.version, operatorbundle.skiprange FROM operatorbundle INNER JOIN channel_entry ON operatorbundle.name=channel_entry.operatorbundle_name WHERE channel_entry.package_name=? AND channel_entry.channel_name=? AND operatorbundle_name=? LIMIT 1` rows, err := s.db.QueryContext(ctx, query, pkgName, channelName, csvName) if err != nil { return nil, err } + defer rows.Close() if !rows.Next() { return nil, fmt.Errorf("no entry found for %s %s %s", pkgName, channelName, csvName) @@ -124,7 +130,9 @@ func (s *SQLQuerier) GetBundle(ctx context.Context, pkgName, channelName, csvNam var name sql.NullString var bundle sql.NullString var bundlePath sql.NullString - if err := rows.Scan(&entryId, &name, &bundle, &bundlePath); err != nil { + var version sql.NullString + var skipRange sql.NullString + if err := rows.Scan(&entryId, &name, &bundle, &bundlePath, &version, &skipRange); err != nil { return nil, err } @@ -139,6 +147,8 @@ func (s *SQLQuerier) GetBundle(ctx context.Context, pkgName, channelName, csvNam out.PackageName = pkgName out.ChannelName = channelName out.BundlePath = bundlePath.String + out.Version = version.String + out.SkipRange = skipRange.String provided, required, err := s.GetApisForEntry(ctx, entryId.Int64) if err != nil { @@ -151,7 +161,7 @@ func (s *SQLQuerier) GetBundle(ctx context.Context, pkgName, channelName, csvNam } func (s *SQLQuerier) GetBundleForChannel(ctx context.Context, pkgName string, channelName string) (*api.Bundle, error) { - query := `SELECT DISTINCT channel_entry.entry_id, operatorbundle.name, operatorbundle.bundle, operatorbundle.bundlepath FROM channel + query := `SELECT DISTINCT channel_entry.entry_id, operatorbundle.name, operatorbundle.bundle, operatorbundle.bundlepath, operatorbundle.version, operatorbundle.skiprange FROM channel INNER JOIN operatorbundle ON channel.head_operatorbundle_name=operatorbundle.name INNER JOIN channel_entry ON (channel_entry.channel_name = channel.name and channel_entry.package_name=channel.package_name and channel_entry.operatorbundle_name=operatorbundle.name) WHERE channel.package_name=? AND channel.name=? LIMIT 1` @@ -159,6 +169,7 @@ func (s *SQLQuerier) GetBundleForChannel(ctx context.Context, pkgName string, ch if err != nil { return nil, err } + defer rows.Close() if !rows.Next() { return nil, fmt.Errorf("no entry found for %s %s", pkgName, channelName) @@ -167,7 +178,9 @@ func (s *SQLQuerier) GetBundleForChannel(ctx context.Context, pkgName string, ch var name sql.NullString var bundle sql.NullString var bundlePath sql.NullString - if err := rows.Scan(&entryId, &name, &bundle, &bundlePath); err != nil { + var version sql.NullString + var skipRange sql.NullString + if err := rows.Scan(&entryId, &name, &bundle, &bundlePath, &version, &skipRange); err != nil { return nil, err } @@ -182,6 +195,8 @@ func (s *SQLQuerier) GetBundleForChannel(ctx context.Context, pkgName string, ch out.PackageName = pkgName out.ChannelName = channelName out.BundlePath = bundlePath.String + out.Version = version.String + out.SkipRange = skipRange.String provided, required, err := s.GetApisForEntry(ctx, entryId.Int64) if err != nil { @@ -202,6 +217,7 @@ func (s *SQLQuerier) GetChannelEntriesThatReplace(ctx context.Context, name stri if err != nil { return } + defer rows.Close() entries = []*registry.ChannelEntry{} @@ -228,7 +244,7 @@ func (s *SQLQuerier) GetChannelEntriesThatReplace(ctx context.Context, name stri } func (s *SQLQuerier) GetBundleThatReplaces(ctx context.Context, name, pkgName, channelName string) (*api.Bundle, error) { - query := `SELECT DISTINCT replaces.entry_id, operatorbundle.name, operatorbundle.bundle, operatorbundle.bundlepath + query := `SELECT DISTINCT replaces.entry_id, operatorbundle.name, operatorbundle.bundle, operatorbundle.bundlepath, operatorbundle.version, operatorbundle.skiprange FROM channel_entry LEFT OUTER JOIN channel_entry replaces ON replaces.replaces = channel_entry.entry_id INNER JOIN operatorbundle ON replaces.operatorbundle_name = operatorbundle.name @@ -237,6 +253,8 @@ func (s *SQLQuerier) GetBundleThatReplaces(ctx context.Context, name, pkgName, c if err != nil { return nil, err } + defer rows.Close() + if !rows.Next() { return nil, fmt.Errorf("no entry found for %s %s", pkgName, channelName) @@ -245,7 +263,9 @@ func (s *SQLQuerier) GetBundleThatReplaces(ctx context.Context, name, pkgName, c var outName sql.NullString var bundle sql.NullString var bundlePath sql.NullString - if err := rows.Scan(&entryId, &outName, &bundle, &bundlePath); err != nil { + var version sql.NullString + var skipRange sql.NullString + if err := rows.Scan(&entryId, &outName, &bundle, &bundlePath, &version, &skipRange); err != nil { return nil, err } @@ -260,6 +280,8 @@ func (s *SQLQuerier) GetBundleThatReplaces(ctx context.Context, name, pkgName, c out.PackageName = pkgName out.ChannelName = channelName out.BundlePath = bundlePath.String + out.Version = version.String + out.SkipRange = skipRange.String provided, required, err := s.GetApisForEntry(ctx, entryId.Int64) if err != nil { @@ -282,6 +304,7 @@ func (s *SQLQuerier) GetChannelEntriesThatProvide(ctx context.Context, group, ve if err != nil { return } + defer rows.Close() entries = []*registry.ChannelEntry{} @@ -320,6 +343,7 @@ func (s *SQLQuerier) GetLatestChannelEntriesThatProvide(ctx context.Context, gro if err != nil { return nil, err } + defer rows.Close() entries = []*registry.ChannelEntry{} @@ -348,8 +372,8 @@ func (s *SQLQuerier) GetLatestChannelEntriesThatProvide(ctx context.Context, gro } // Get the the latest bundle that provides the API in a default channel, error unless there is ONLY one -func (s *SQLQuerier) GetBundleThatProvides(ctx context.Context, group, version, kind string) (*api.Bundle, error) { - query := `SELECT DISTINCT channel_entry.entry_id, operatorbundle.bundle, operatorbundle.bundlepath, MIN(channel_entry.depth), channel_entry.operatorbundle_name, channel_entry.package_name, channel_entry.channel_name, channel_entry.replaces +func (s *SQLQuerier) GetBundleThatProvides(ctx context.Context, group, apiVersion, kind string) (*api.Bundle, error) { + query := `SELECT DISTINCT channel_entry.entry_id, operatorbundle.bundle, operatorbundle.bundlepath, MIN(channel_entry.depth), channel_entry.operatorbundle_name, channel_entry.package_name, channel_entry.channel_name, channel_entry.replaces, operatorbundle.version, operatorbundle.skiprange FROM channel_entry INNER JOIN api_provider ON channel_entry.entry_id = api_provider.channel_entry_id INNER JOIN operatorbundle ON operatorbundle.name = channel_entry.operatorbundle_name @@ -357,13 +381,14 @@ func (s *SQLQuerier) GetBundleThatProvides(ctx context.Context, group, version, WHERE api_provider.group_name = ? AND api_provider.version = ? AND api_provider.kind = ? AND package.default_channel = channel_entry.channel_name GROUP BY channel_entry.package_name, channel_entry.channel_name` - rows, err := s.db.QueryContext(ctx, query, group, version, kind) + rows, err := s.db.QueryContext(ctx, query, group, apiVersion, kind) if err != nil { return nil, err } + defer rows.Close() if !rows.Next() { - return nil, fmt.Errorf("no entry found that provides %s %s %s", group, version, kind) + return nil, fmt.Errorf("no entry found that provides %s %s %s", group, apiVersion, kind) } var entryId sql.NullInt64 var bundle sql.NullString @@ -373,12 +398,14 @@ func (s *SQLQuerier) GetBundleThatProvides(ctx context.Context, group, version, var pkgName sql.NullString var channelName sql.NullString var replaces sql.NullString - if err := rows.Scan(&entryId, &bundle, &bundlePath, &min_depth, &bundleName, &pkgName, &channelName, &replaces); err != nil { + var version sql.NullString + var skipRange sql.NullString + if err := rows.Scan(&entryId, &bundle, &bundlePath, &min_depth, &bundleName, &pkgName, &channelName, &replaces, &version, &skipRange); err != nil { return nil, err } if !bundle.Valid { - return nil, fmt.Errorf("no entry found that provides %s %s %s", group, version, kind) + return nil, fmt.Errorf("no entry found that provides %s %s %s", group, apiVersion, kind) } out := &api.Bundle{} @@ -392,6 +419,8 @@ func (s *SQLQuerier) GetBundleThatProvides(ctx context.Context, group, version, out.PackageName = pkgName.String out.ChannelName = channelName.String out.BundlePath = bundlePath.String + out.Version = version.String + out.SkipRange = skipRange.String provided, required, err := s.GetApisForEntry(ctx, entryId.Int64) if err != nil { @@ -409,6 +438,8 @@ func (s *SQLQuerier) ListImages(ctx context.Context) ([]string, error) { if err != nil { return nil, err } + defer rows.Close() + images := []string{} for rows.Next() { var imgName sql.NullString @@ -428,6 +459,7 @@ func (s *SQLQuerier) GetImagesForBundle(ctx context.Context, csvName string) ([] if err != nil { return nil, err } + defer rows.Close() images := []string{} for rows.Next() { var imgName sql.NullString @@ -450,6 +482,7 @@ func (s *SQLQuerier) GetApisForEntry(ctx context.Context, entryId int64) (provid if err != nil { return nil,nil, err } + provided = []*api.GroupVersionKind{} for providedRows.Next() { var groupName sql.NullString @@ -470,6 +503,9 @@ func (s *SQLQuerier) GetApisForEntry(ctx context.Context, entryId int64) (provid Plural: pluralName.String, }) } + if err := providedRows.Close(); err != nil { + return nil, nil, err + } requiredQuery := `SELECT DISTINCT api.group_name, api.version, api.kind, api.plural FROM api INNER JOIN api_requirer ON (api.group_name=api_requirer.group_name AND api.version=api_requirer.version AND api.kind=api_requirer.kind) @@ -499,6 +535,9 @@ func (s *SQLQuerier) GetApisForEntry(ctx context.Context, entryId int64) (provid Plural: pluralName.String, }) } + if err := requiredRows.Close(); err != nil { + return nil, nil, err + } return }