diff --git a/Makefile b/Makefile index 6aa96f3af..a4da8d2a4 100644 --- a/Makefile +++ b/Makefile @@ -48,6 +48,7 @@ help: @echo " * 'test-e2e-node' - Test cri-containerd with Kubernetes node e2e test" @echo " * 'clean' - Clean artifacts" @echo " * 'verify' - Execute the source code verification tools" + @echo " * 'proto' - Update protobuf of cri-containerd api" @echo " * 'install.tools' - Install tools used by verify" @echo " * 'install.deps' - Install dependencies of cri-containerd (containerd, runc, cni) Note: BUILDTAGS defaults to 'seccomp apparmor' for runc build" @echo " * 'uninstall' - Remove installed binaries from system locations" @@ -117,6 +118,9 @@ release: $(BUILD_DIR)/$(TARBALL) push: $(BUILD_DIR)/$(TARBALL) @BUILD_DIR=$(BUILD_DIR) TARBALL=$(TARBALL) VERSION=$(VERSION) ./hack/push.sh +proto: + @hack/update-proto.sh + .PHONY: install.deps install.deps: @@ -160,4 +164,5 @@ install.tools: .install.gitvalidation .install.gometalinter test-cri \ test-e2e-node \ uninstall \ - version + version \ + proto diff --git a/cmd/cri-containerd/cri_containerd.go b/cmd/cri-containerd/cri_containerd.go index 1793cc0bd..f1fad8e9d 100644 --- a/cmd/cri-containerd/cri_containerd.go +++ b/cmd/cri-containerd/cri_containerd.go @@ -18,15 +18,20 @@ package main import ( "flag" + "fmt" "os" + "path/filepath" "github.com/docker/docker/pkg/reexec" "github.com/golang/glog" "github.com/opencontainers/selinux/go-selinux" "github.com/spf13/cobra" + "golang.org/x/net/context" "k8s.io/kubernetes/pkg/util/interrupt" "github.com/kubernetes-incubator/cri-containerd/cmd/cri-containerd/options" + api "github.com/kubernetes-incubator/cri-containerd/pkg/api/v1" + "github.com/kubernetes-incubator/cri-containerd/pkg/client" "github.com/kubernetes-incubator/cri-containerd/pkg/server" "github.com/kubernetes-incubator/cri-containerd/pkg/version" ) @@ -72,6 +77,35 @@ func versionCommand() *cobra.Command { } } +func loadImageCommand() *cobra.Command { + c := &cobra.Command{ + Use: "load TAR", + Short: "Load an image from a tar archive.", + Args: cobra.ExactArgs(1), + } + endpoint, timeout := options.AddGRPCFlags(c.Flags()) + c.RunE = func(cmd *cobra.Command, args []string) error { + cl, err := client.NewCRIContainerdClient(*endpoint, *timeout) + if err != nil { + return fmt.Errorf("failed to create grpc client: %v", err) + } + path, err := filepath.Abs(args[0]) + if err != nil { + return fmt.Errorf("failed to get absolute path: %v", err) + } + res, err := cl.LoadImage(context.Background(), &api.LoadImageRequest{FilePath: path}) + if err != nil { + return fmt.Errorf("failed to load image: %v", err) + } + images := res.GetImages() + for _, image := range images { + fmt.Println("Loaded image:", image) + } + return nil + } + return c +} + func main() { if reexec.Init() { return @@ -81,10 +115,11 @@ func main() { o.AddFlags(cmd.Flags()) cmd.AddCommand(defaultConfigCommand()) cmd.AddCommand(versionCommand()) + cmd.AddCommand(loadImageCommand()) - cmd.Run = func(cmd *cobra.Command, args []string) { + cmd.RunE = func(cmd *cobra.Command, args []string) error { if err := o.InitFlags(cmd.Flags()); err != nil { - glog.Exitf("Failed to init CRI containerd flags: %v", err) + return fmt.Errorf("failed to init CRI containerd flags: %v", err) } validateConfig(o) @@ -93,19 +128,21 @@ func main() { glog.V(2).Infof("Run cri-containerd grpc server on socket %q", o.SocketPath) s, err := server.NewCRIContainerdService(o.Config) if err != nil { - glog.Exitf("Failed to create CRI containerd service: %v", err) + return fmt.Errorf("failed to create CRI containerd service: %v", err) } // Use interrupt handler to make sure the server is stopped properly. // Pass in non-empty final function to avoid os.Exit(1). We expect `Run` // to return itself. h := interrupt.New(func(os.Signal) {}, s.Stop) if err := h.Run(func() error { return s.Run() }); err != nil { - glog.Exitf("Failed to run cri-containerd grpc server: %v", err) + return fmt.Errorf("failed to run cri-containerd grpc server: %v", err) } + return nil } if err := cmd.Execute(); err != nil { - glog.Exitf("Failed to execute cri-containerd: %v", err) + // Error should have been reported. + os.Exit(1) } } diff --git a/cmd/cri-containerd/options/options.go b/cmd/cri-containerd/options/options.go index 6141d384d..fd5e7d0b6 100644 --- a/cmd/cri-containerd/options/options.go +++ b/cmd/cri-containerd/options/options.go @@ -19,6 +19,7 @@ package options import ( "fmt" "os" + "time" "github.com/BurntSushi/toml" "github.com/containerd/containerd" @@ -30,6 +31,8 @@ const ( configFilePathArgName = "config" // defaultConfigFilePath is the default config file path. defaultConfigFilePath = "/etc/cri-containerd/config.toml" + // connectionTimeout is the grpc connection timeout. + connectionTimeout = 10 * time.Second ) // ContainerdConfig contains config related to containerd @@ -178,6 +181,13 @@ func PrintDefaultTomlConfig() { } } +// AddGRPCFlags add flags for grpc connection. +func AddGRPCFlags(fs *pflag.FlagSet) (*string, *time.Duration) { + endpoint := fs.String("endpoint", defaultConfig().SocketPath, "cri-containerd endpoint.") + timeout := fs.Duration("timeout", connectionTimeout, "cri-containerd connection timeout.") + return endpoint, timeout +} + // defaultConfig returns default configurations of cri-containerd. func defaultConfig() Config { return Config{ diff --git a/hack/update-proto.sh b/hack/update-proto.sh new file mode 100755 index 000000000..0c2564133 --- /dev/null +++ b/hack/update-proto.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +# Copyright 2017 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -o errexit +set -o nounset +set -o pipefail + +ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"/.. +API_ROOT="${ROOT}/pkg/api/v1" + +go get k8s.io/code-generator/cmd/go-to-protobuf/protoc-gen-gogo +if ! which protoc-gen-gogo >/dev/null; then + echo "GOPATH is not in PATH" + exit 1 +fi + +function cleanup { + rm -f ${API_ROOT}/api.pb.go.bak +} + +trap cleanup EXIT + +protoc \ + --proto_path="${API_ROOT}" \ + --proto_path="${ROOT}/vendor" \ + --gogo_out=plugins=grpc:${API_ROOT} ${API_ROOT}/api.proto + +# Update boilerplate for the generated file. +echo "$(cat hack/boilerplate/boilerplate.go.txt ${API_ROOT}/api.pb.go)" > ${API_ROOT}/api.pb.go +sed -i".bak" "s/Copyright YEAR/Copyright $(date '+%Y')/g" ${API_ROOT}/api.pb.go + +gofmt -l -s -w ${API_ROOT}/api.pb.go diff --git a/hack/verify-lint.sh b/hack/verify-lint.sh index 259397946..6c7b86bf6 100755 --- a/hack/verify-lint.sh +++ b/hack/verify-lint.sh @@ -17,7 +17,7 @@ set -o errexit set -o nounset set -o pipefail -for d in $(find . -type d -a \( -iwholename './pkg*' -o -iwholename './cmd*' \)); do +for d in $(find . -type d -a \( -iwholename './pkg*' -o -iwholename './cmd*' \) -not -iwholename './pkg/api*'); do echo for directory ${d} ... gometalinter \ --exclude='error return value not checked.*(Close|Log|Print).*\(errcheck\)$' \ diff --git a/pkg/api/v1/api.pb.go b/pkg/api/v1/api.pb.go new file mode 100644 index 000000000..c44c1774c --- /dev/null +++ b/pkg/api/v1/api.pb.go @@ -0,0 +1,598 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by protoc-gen-gogo. +// source: api.proto +// DO NOT EDIT! + +/* +Package api_v1 is a generated protocol buffer package. + +It is generated from these files: + api.proto + +It has these top-level messages: + LoadImageRequest + LoadImageResponse +*/ +package api_v1 + +import proto "github.com/gogo/protobuf/proto" +import fmt "fmt" +import math "math" +import _ "github.com/gogo/protobuf/gogoproto" + +import ( + context "golang.org/x/net/context" + grpc "google.golang.org/grpc" +) + +import strings "strings" +import reflect "reflect" + +import io "io" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion2 // please upgrade the proto package + +type LoadImageRequest struct { + // FilePath is the absolute path of docker image tarball. + FilePath string `protobuf:"bytes,1,opt,name=FilePath,proto3" json:"FilePath,omitempty"` +} + +func (m *LoadImageRequest) Reset() { *m = LoadImageRequest{} } +func (*LoadImageRequest) ProtoMessage() {} +func (*LoadImageRequest) Descriptor() ([]byte, []int) { return fileDescriptorApi, []int{0} } + +func (m *LoadImageRequest) GetFilePath() string { + if m != nil { + return m.FilePath + } + return "" +} + +type LoadImageResponse struct { + // Images have been loaded. + Images []string `protobuf:"bytes,1,rep,name=Images" json:"Images,omitempty"` +} + +func (m *LoadImageResponse) Reset() { *m = LoadImageResponse{} } +func (*LoadImageResponse) ProtoMessage() {} +func (*LoadImageResponse) Descriptor() ([]byte, []int) { return fileDescriptorApi, []int{1} } + +func (m *LoadImageResponse) GetImages() []string { + if m != nil { + return m.Images + } + return nil +} + +func init() { + proto.RegisterType((*LoadImageRequest)(nil), "api.v1.LoadImageRequest") + proto.RegisterType((*LoadImageResponse)(nil), "api.v1.LoadImageResponse") +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion4 + +// Client API for CRIContainerdService service + +type CRIContainerdServiceClient interface { + // LoadImage loads a image into containerd. + LoadImage(ctx context.Context, in *LoadImageRequest, opts ...grpc.CallOption) (*LoadImageResponse, error) +} + +type cRIContainerdServiceClient struct { + cc *grpc.ClientConn +} + +func NewCRIContainerdServiceClient(cc *grpc.ClientConn) CRIContainerdServiceClient { + return &cRIContainerdServiceClient{cc} +} + +func (c *cRIContainerdServiceClient) LoadImage(ctx context.Context, in *LoadImageRequest, opts ...grpc.CallOption) (*LoadImageResponse, error) { + out := new(LoadImageResponse) + err := grpc.Invoke(ctx, "/api.v1.CRIContainerdService/LoadImage", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// Server API for CRIContainerdService service + +type CRIContainerdServiceServer interface { + // LoadImage loads a image into containerd. + LoadImage(context.Context, *LoadImageRequest) (*LoadImageResponse, error) +} + +func RegisterCRIContainerdServiceServer(s *grpc.Server, srv CRIContainerdServiceServer) { + s.RegisterService(&_CRIContainerdService_serviceDesc, srv) +} + +func _CRIContainerdService_LoadImage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(LoadImageRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CRIContainerdServiceServer).LoadImage(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/api.v1.CRIContainerdService/LoadImage", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CRIContainerdServiceServer).LoadImage(ctx, req.(*LoadImageRequest)) + } + return interceptor(ctx, in, info, handler) +} + +var _CRIContainerdService_serviceDesc = grpc.ServiceDesc{ + ServiceName: "api.v1.CRIContainerdService", + HandlerType: (*CRIContainerdServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "LoadImage", + Handler: _CRIContainerdService_LoadImage_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "api.proto", +} + +func (m *LoadImageRequest) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalTo(dAtA) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *LoadImageRequest) MarshalTo(dAtA []byte) (int, error) { + var i int + _ = i + var l int + _ = l + if len(m.FilePath) > 0 { + dAtA[i] = 0xa + i++ + i = encodeVarintApi(dAtA, i, uint64(len(m.FilePath))) + i += copy(dAtA[i:], m.FilePath) + } + return i, nil +} + +func (m *LoadImageResponse) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalTo(dAtA) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *LoadImageResponse) MarshalTo(dAtA []byte) (int, error) { + var i int + _ = i + var l int + _ = l + if len(m.Images) > 0 { + for _, s := range m.Images { + dAtA[i] = 0xa + i++ + l = len(s) + for l >= 1<<7 { + dAtA[i] = uint8(uint64(l)&0x7f | 0x80) + l >>= 7 + i++ + } + dAtA[i] = uint8(l) + i++ + i += copy(dAtA[i:], s) + } + } + return i, nil +} + +func encodeFixed64Api(dAtA []byte, offset int, v uint64) int { + dAtA[offset] = uint8(v) + dAtA[offset+1] = uint8(v >> 8) + dAtA[offset+2] = uint8(v >> 16) + dAtA[offset+3] = uint8(v >> 24) + dAtA[offset+4] = uint8(v >> 32) + dAtA[offset+5] = uint8(v >> 40) + dAtA[offset+6] = uint8(v >> 48) + dAtA[offset+7] = uint8(v >> 56) + return offset + 8 +} +func encodeFixed32Api(dAtA []byte, offset int, v uint32) int { + dAtA[offset] = uint8(v) + dAtA[offset+1] = uint8(v >> 8) + dAtA[offset+2] = uint8(v >> 16) + dAtA[offset+3] = uint8(v >> 24) + return offset + 4 +} +func encodeVarintApi(dAtA []byte, offset int, v uint64) int { + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return offset + 1 +} +func (m *LoadImageRequest) Size() (n int) { + var l int + _ = l + l = len(m.FilePath) + if l > 0 { + n += 1 + l + sovApi(uint64(l)) + } + return n +} + +func (m *LoadImageResponse) Size() (n int) { + var l int + _ = l + if len(m.Images) > 0 { + for _, s := range m.Images { + l = len(s) + n += 1 + l + sovApi(uint64(l)) + } + } + return n +} + +func sovApi(x uint64) (n int) { + for { + n++ + x >>= 7 + if x == 0 { + break + } + } + return n +} +func sozApi(x uint64) (n int) { + return sovApi(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (this *LoadImageRequest) String() string { + if this == nil { + return "nil" + } + s := strings.Join([]string{`&LoadImageRequest{`, + `FilePath:` + fmt.Sprintf("%v", this.FilePath) + `,`, + `}`, + }, "") + return s +} +func (this *LoadImageResponse) String() string { + if this == nil { + return "nil" + } + s := strings.Join([]string{`&LoadImageResponse{`, + `Images:` + fmt.Sprintf("%v", this.Images) + `,`, + `}`, + }, "") + return s +} +func valueToStringApi(v interface{}) string { + rv := reflect.ValueOf(v) + if rv.IsNil() { + return "nil" + } + pv := reflect.Indirect(rv).Interface() + return fmt.Sprintf("*%v", pv) +} +func (m *LoadImageRequest) 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 ErrIntOverflowApi + } + 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: LoadImageRequest: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: LoadImageRequest: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field FilePath", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowApi + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthApi + } + postIndex := iNdEx + intStringLen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.FilePath = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipApi(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthApi + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *LoadImageResponse) 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 ErrIntOverflowApi + } + 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: LoadImageResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: LoadImageResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Images", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowApi + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthApi + } + postIndex := iNdEx + intStringLen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Images = append(m.Images, string(dAtA[iNdEx:postIndex])) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipApi(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthApi + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipApi(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowApi + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowApi + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + return iNdEx, nil + case 1: + iNdEx += 8 + return iNdEx, nil + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowApi + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + iNdEx += length + if length < 0 { + return 0, ErrInvalidLengthApi + } + return iNdEx, nil + case 3: + for { + var innerWire uint64 + var start int = iNdEx + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowApi + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + innerWire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + innerWireType := int(innerWire & 0x7) + if innerWireType == 4 { + break + } + next, err := skipApi(dAtA[start:]) + if err != nil { + return 0, err + } + iNdEx = start + next + } + return iNdEx, nil + case 4: + return iNdEx, nil + case 5: + iNdEx += 4 + return iNdEx, nil + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + } + panic("unreachable") +} + +var ( + ErrInvalidLengthApi = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowApi = fmt.Errorf("proto: integer overflow") +) + +func init() { proto.RegisterFile("api.proto", fileDescriptorApi) } + +var fileDescriptorApi = []byte{ + // 223 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x4c, 0x2c, 0xc8, 0xd4, + 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0x03, 0x31, 0xcb, 0x0c, 0xa5, 0x74, 0xd3, 0x33, 0x4b, + 0x32, 0x4a, 0x93, 0xf4, 0x92, 0xf3, 0x73, 0xf5, 0xd3, 0xf3, 0xd3, 0xf3, 0xf5, 0xc1, 0xd2, 0x49, + 0xa5, 0x69, 0x60, 0x1e, 0x98, 0x03, 0x66, 0x41, 0xb4, 0x29, 0xe9, 0x71, 0x09, 0xf8, 0xe4, 0x27, + 0xa6, 0x78, 0xe6, 0x26, 0xa6, 0xa7, 0x06, 0xa5, 0x16, 0x96, 0xa6, 0x16, 0x97, 0x08, 0x49, 0x71, + 0x71, 0xb8, 0x65, 0xe6, 0xa4, 0x06, 0x24, 0x96, 0x64, 0x48, 0x30, 0x2a, 0x30, 0x6a, 0x70, 0x06, + 0xc1, 0xf9, 0x4a, 0xda, 0x5c, 0x82, 0x48, 0xea, 0x8b, 0x0b, 0xf2, 0xf3, 0x8a, 0x53, 0x85, 0xc4, + 0xb8, 0xd8, 0xc0, 0x02, 0xc5, 0x12, 0x8c, 0x0a, 0xcc, 0x1a, 0x9c, 0x41, 0x50, 0x9e, 0x51, 0x14, + 0x97, 0x88, 0x73, 0x90, 0xa7, 0x73, 0x7e, 0x5e, 0x49, 0x62, 0x66, 0x5e, 0x6a, 0x51, 0x4a, 0x70, + 0x6a, 0x51, 0x59, 0x66, 0x72, 0xaa, 0x90, 0x13, 0x17, 0x27, 0xdc, 0x10, 0x21, 0x09, 0x3d, 0x88, + 0xcb, 0xf5, 0xd0, 0xdd, 0x21, 0x25, 0x89, 0x45, 0x06, 0x62, 0xa3, 0x12, 0x83, 0x93, 0xcc, 0x89, + 0x87, 0x72, 0x8c, 0x37, 0x1e, 0xca, 0x31, 0x34, 0x3c, 0x92, 0x63, 0x3c, 0xf1, 0x48, 0x8e, 0xf1, + 0xc2, 0x23, 0x39, 0xc6, 0x07, 0x8f, 0xe4, 0x18, 0x27, 0x3c, 0x96, 0x63, 0x48, 0x62, 0x03, 0xfb, + 0xce, 0x18, 0x10, 0x00, 0x00, 0xff, 0xff, 0x6a, 0xfe, 0x35, 0x81, 0x21, 0x01, 0x00, 0x00, +} diff --git a/pkg/api/v1/api.proto b/pkg/api/v1/api.proto new file mode 100644 index 000000000..f5671abaa --- /dev/null +++ b/pkg/api/v1/api.proto @@ -0,0 +1,30 @@ +// To regenerate api.pb.go run `make proto`hack/update-generated-runtime.sh +syntax = 'proto3'; + +package api.v1; + +import "github.com/gogo/protobuf/gogoproto/gogo.proto"; + +option (gogoproto.goproto_stringer_all) = false; +option (gogoproto.stringer_all) = true; +option (gogoproto.goproto_getters_all) = true; +option (gogoproto.marshaler_all) = true; +option (gogoproto.sizer_all) = true; +option (gogoproto.unmarshaler_all) = true; +option (gogoproto.goproto_unrecognized_all) = false; + +// CRIContainerdService defines non-CRI APIs for cri-containerd. +service CRIContainerdService{ + // LoadImage loads a image into containerd. + rpc LoadImage(LoadImageRequest) returns (LoadImageResponse) {} +} + +message LoadImageRequest { + // FilePath is the absolute path of docker image tarball. + string FilePath = 1; +} + +message LoadImageResponse { + // Images have been loaded. + repeated string Images = 1; +} diff --git a/pkg/client/client.go b/pkg/client/client.go new file mode 100644 index 000000000..36459ff86 --- /dev/null +++ b/pkg/client/client.go @@ -0,0 +1,45 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package client + +import ( + "fmt" + "time" + + "google.golang.org/grpc" + "k8s.io/kubernetes/pkg/kubelet/util" + + api "github.com/kubernetes-incubator/cri-containerd/pkg/api/v1" +) + +// NewCRIContainerdClient creates grpc client of cri-containerd +// TODO(random-liu): Wrap grpc functions. +func NewCRIContainerdClient(endpoint string, timeout time.Duration) (api.CRIContainerdServiceClient, error) { + addr, dailer, err := util.GetAddressAndDialer(endpoint) + if err != nil { + return nil, fmt.Errorf("failed to get dialer: %v", err) + } + conn, err := grpc.Dial(addr, + grpc.WithInsecure(), + grpc.WithTimeout(timeout), + grpc.WithDialer(dailer), + ) + if err != nil { + return nil, fmt.Errorf("failed to dial: %v", err) + } + return api.NewCRIContainerdServiceClient(conn), nil +} diff --git a/pkg/containerd/importer/importer.go b/pkg/containerd/importer/importer.go new file mode 100644 index 000000000..2e52187e7 --- /dev/null +++ b/pkg/containerd/importer/importer.go @@ -0,0 +1,298 @@ +/* +Copyright 2017 The Kubernetes Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package importer + +import ( + "archive/tar" + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "strings" + "time" + + "github.com/containerd/containerd/content" + "github.com/containerd/containerd/errdefs" + "github.com/containerd/containerd/images" + "github.com/containerd/containerd/log" + "github.com/opencontainers/go-digest" + "github.com/opencontainers/image-spec/specs-go" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" + + "github.com/kubernetes-incubator/cri-containerd/pkg/util" +) + +// This code reuses the docker import code from containerd/containerd#1602. +// It has been simplified a bit and garbage collection support was added. +// If a library/helper is added to containerd in the future, we should switch to it. + +// manifestDotJSON is an entry in manifest.json. +type manifestDotJSON struct { + Config string + RepoTags []string + Layers []string + // Parent is unsupported + Parent string +} + +// isLayerTar returns true if name is like "deadbeeddeadbeef/layer.tar" +func isLayerTar(name string) bool { + slashes := len(strings.Split(name, "/")) + return slashes == 2 && strings.HasSuffix(name, "/layer.tar") +} + +// isDotJSON returns true if name is like "deadbeefdeadbeef.json" +func isDotJSON(name string) bool { + slashes := len(strings.Split(name, "/")) + return slashes == 1 && strings.HasSuffix(name, ".json") +} + +type imageConfig struct { + desc ocispec.Descriptor + img ocispec.Image +} + +// Import implements Docker Image Spec v1.1. +// An image MUST have `manifest.json`. +// `repositories` file in Docker Image Spec v1.0 is not supported (yet). +// Also, the current implementation assumes the implicit file name convention, +// which is not explicitly documented in the spec. (e.g. deadbeef/layer.tar) +// It returns a group of image references successfully loaded. +func Import(ctx context.Context, cs content.Store, is images.Store, reader io.Reader) (_ []string, retErr error) { + tr := tar.NewReader(reader) + var ( + mfsts []manifestDotJSON + layers = make(map[string]ocispec.Descriptor) // key: filename (deadbeeddeadbeef/layer.tar) + configs = make(map[string]imageConfig) // key: filename (deadbeeddeadbeef.json) + ) + // Either image is successfully imported or not, we should cleanup gc.root + // for all image layers. + defer func() { + for _, desc := range layers { + // Remove root tag from layers now that manifest refers to it + if _, err := cs.Update(ctx, content.Info{Digest: desc.Digest}, "labels.containerd.io/gc.root"); err != nil { + log.G(ctx).WithError(err).Error("Failed to remove layer %q root tag", desc.Digest) + } + } + for _, cfg := range configs { + // Remove root tag from config now that manifest refers to it + if _, err := cs.Update(ctx, content.Info{Digest: cfg.desc.Digest}, "labels.containerd.io/gc.root"); err != nil { + log.G(ctx).WithError(err).Error("Failed to remove config %q root tag", cfg.desc.Digest) + } + } + }() + for { + hdr, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, errors.Wrap(err, "get next file") + } + if hdr.Typeflag != tar.TypeReg && hdr.Typeflag != tar.TypeRegA { + continue + } + if hdr.Name == "manifest.json" { + mfsts, err = onUntarManifestJSON(tr) + if err != nil { + return nil, errors.Wrapf(err, "untar manifest %q", hdr.Name) + } + continue + } + if isLayerTar(hdr.Name) { + desc, err := onUntarLayerTar(ctx, tr, cs, hdr.Name, hdr.Size) + if err != nil { + return nil, errors.Wrapf(err, "untar layer %q", hdr.Name) + } + layers[hdr.Name] = *desc + continue + } + if isDotJSON(hdr.Name) { + c, err := onUntarDotJSON(ctx, tr, cs, hdr.Name, hdr.Size) + if err != nil { + return nil, errors.Wrapf(err, "untar config %q", hdr.Name) + } + configs[hdr.Name] = *c + continue + } + } + var refs []string + defer func() { + if retErr == nil { + return + } + // TODO(random-liu): Consider whether we should keep images already imported + // even when there is an error. + for _, ref := range refs { + if err := is.Delete(ctx, ref); err != nil { + log.G(ctx).WithError(err).Errorf("Failed to remove image %q", ref) + } + } + }() + for _, mfst := range mfsts { + config, ok := configs[mfst.Config] + if !ok { + return refs, errors.Errorf("image config %q not found", mfst.Config) + } + schema2Manifest, err := makeDockerSchema2Manifest(mfst, config, layers) + if err != nil { + return refs, errors.Wrap(err, "create docker manifest") + } + desc, err := writeDockerSchema2Manifest(ctx, cs, *schema2Manifest, config.img.Architecture, config.img.OS) + if err != nil { + return refs, errors.Wrap(err, "write docker manifest") + } + defer func() { + // Remove root tag from manifest. + if _, err := cs.Update(ctx, content.Info{Digest: desc.Digest}, "labels.containerd.io/gc.root"); err != nil { + log.G(ctx).WithError(err).Error("Failed to remove manifest root tag") + } + }() + + for _, ref := range mfst.RepoTags { + normalized, err := util.NormalizeImageRef(ref) + if err != nil { + return refs, errors.Wrapf(err, "normalize image ref %q", ref) + } + ref = normalized.String() + imgrec := images.Image{ + Name: ref, + Target: *desc, + } + if _, err := is.Create(ctx, imgrec); err != nil { + if !errdefs.IsAlreadyExists(err) { + return refs, errors.Wrapf(err, "create image ref %+v", imgrec) + } + + _, err := is.Update(ctx, imgrec) + if err != nil { + return refs, errors.Wrapf(err, "update image ref %+v", imgrec) + } + } + refs = append(refs, ref) + } + } + return refs, nil +} + +func makeDockerSchema2Manifest(mfst manifestDotJSON, config imageConfig, layers map[string]ocispec.Descriptor) (*ocispec.Manifest, error) { + manifest := ocispec.Manifest{ + Versioned: specs.Versioned{ + SchemaVersion: 2, + }, + Config: config.desc, + } + for _, f := range mfst.Layers { + desc, ok := layers[f] + if !ok { + return nil, errors.Errorf("layer %q not found", f) + } + manifest.Layers = append(manifest.Layers, desc) + } + return &manifest, nil +} + +func writeDockerSchema2Manifest(ctx context.Context, cs content.Ingester, manifest ocispec.Manifest, arch, os string) (*ocispec.Descriptor, error) { + manifestBytes, err := json.Marshal(manifest) + if err != nil { + return nil, err + } + manifestBytesR := bytes.NewReader(manifestBytes) + manifestDigest := digest.FromBytes(manifestBytes) + labels := map[string]string{} + labels["containerd.io/gc.root"] = time.Now().UTC().Format(time.RFC3339) + labels["containerd.io/gc.ref.content.0"] = manifest.Config.Digest.String() + for i, ch := range manifest.Layers { + labels[fmt.Sprintf("containerd.io/gc.ref.content.%d", i+1)] = ch.Digest.String() + } + if err := content.WriteBlob(ctx, cs, "manifest-"+manifestDigest.String(), manifestBytesR, + int64(len(manifestBytes)), manifestDigest, content.WithLabels(labels)); err != nil { + return nil, err + } + + desc := &ocispec.Descriptor{ + MediaType: images.MediaTypeDockerSchema2Manifest, + Digest: manifestDigest, + Size: int64(len(manifestBytes)), + } + if arch != "" || os != "" { + desc.Platform = &ocispec.Platform{ + Architecture: arch, + OS: os, + } + } + return desc, nil +} + +func onUntarManifestJSON(r io.Reader) ([]manifestDotJSON, error) { + // name: "manifest.json" + b, err := ioutil.ReadAll(r) + if err != nil { + return nil, err + } + var mfsts []manifestDotJSON + if err := json.Unmarshal(b, &mfsts); err != nil { + return nil, err + } + return mfsts, nil +} + +func onUntarLayerTar(ctx context.Context, r io.Reader, cs content.Ingester, name string, size int64) (*ocispec.Descriptor, error) { + // name is like "deadbeeddeadbeef/layer.tar" ( guaranteed by isLayerTar() ) + split := strings.Split(name, "/") + // note: split[0] is not expected digest here + cw, err := cs.Writer(ctx, "layer-"+split[0], size, "") + if err != nil { + return nil, err + } + defer cw.Close() + if err := content.Copy(ctx, cw, r, size, "", content.WithLabels(map[string]string{ + "containerd.io/gc.root": time.Now().UTC().Format(time.RFC3339), + })); err != nil { + return nil, err + } + return &ocispec.Descriptor{ + MediaType: images.MediaTypeDockerSchema2Layer, + Size: size, + Digest: cw.Digest(), + }, nil +} + +func onUntarDotJSON(ctx context.Context, r io.Reader, cs content.Ingester, name string, size int64) (*imageConfig, error) { + config := imageConfig{} + config.desc.MediaType = images.MediaTypeDockerSchema2Config + config.desc.Size = size + // name is like "deadbeeddeadbeef.json" ( guaranteed by is DotJSON() ) + split := strings.Split(name, ".") + cw, err := cs.Writer(ctx, "config-"+split[0], size, "") + if err != nil { + return nil, err + } + defer cw.Close() + var buf bytes.Buffer + tr := io.TeeReader(r, &buf) + if err := content.Copy(ctx, cw, tr, size, "", content.WithLabels(map[string]string{ + "containerd.io/gc.root": time.Now().UTC().Format(time.RFC3339), + })); err != nil { + return nil, err + } + config.desc.Digest = cw.Digest() + if err := json.Unmarshal(buf.Bytes(), &config.img); err != nil { + return nil, err + } + return &config, nil +} diff --git a/pkg/opts/container.go b/pkg/containerd/opts/container.go similarity index 100% rename from pkg/opts/container.go rename to pkg/containerd/opts/container.go diff --git a/pkg/server/container_create.go b/pkg/server/container_create.go index 763a5bb9b..f1ea31377 100644 --- a/pkg/server/container_create.go +++ b/pkg/server/container_create.go @@ -43,7 +43,7 @@ import ( "golang.org/x/sys/unix" "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime" - customopts "github.com/kubernetes-incubator/cri-containerd/pkg/opts" + customopts "github.com/kubernetes-incubator/cri-containerd/pkg/containerd/opts" cio "github.com/kubernetes-incubator/cri-containerd/pkg/server/io" containerstore "github.com/kubernetes-incubator/cri-containerd/pkg/store/container" "github.com/kubernetes-incubator/cri-containerd/pkg/util" diff --git a/pkg/server/helpers.go b/pkg/server/helpers.go index f89d6df14..6079df0fe 100644 --- a/pkg/server/helpers.go +++ b/pkg/server/helpers.go @@ -41,6 +41,7 @@ import ( "github.com/kubernetes-incubator/cri-containerd/pkg/store" imagestore "github.com/kubernetes-incubator/cri-containerd/pkg/store/image" + "github.com/kubernetes-incubator/cri-containerd/pkg/util" ) const ( @@ -187,35 +188,6 @@ func criContainerStateToString(state runtime.ContainerState) string { return runtime.ContainerState_name[int32(state)] } -// normalizeImageRef normalizes the image reference following the docker convention. This is added -// mainly for backward compatibility. -// The reference returned can only be either tagged or digested. For reference contains both tag -// and digest, the function returns digested reference, e.g. docker.io/library/busybox:latest@ -// sha256:7cc4b5aefd1d0cadf8d97d4350462ba51c694ebca145b08d7d41b41acc8db5aa will be returned as -// docker.io/library/busybox@sha256:7cc4b5aefd1d0cadf8d97d4350462ba51c694ebca145b08d7d41b41acc8db5aa. -func normalizeImageRef(ref string) (reference.Named, error) { - named, err := reference.ParseNormalizedNamed(ref) - if err != nil { - return nil, err - } - if _, ok := named.(reference.NamedTagged); ok { - if canonical, ok := named.(reference.Canonical); ok { - // The reference is both tagged and digested, only - // return digested. - newNamed, err := reference.WithName(canonical.Name()) - if err != nil { - return nil, err - } - newCanonical, err := reference.WithDigest(newNamed, canonical.Digest()) - if err != nil { - return nil, err - } - return newCanonical, nil - } - } - return reference.TagNameOnly(named), nil -} - // getRepoDigestAngTag returns image repoDigest and repoTag of the named image reference. func getRepoDigestAndTag(namedRef reference.Named, digest imagedigest.Digest, schema1 bool) (string, string) { var repoTag, repoDigest string @@ -237,7 +209,7 @@ func (c *criContainerdService) localResolve(ctx context.Context, ref string) (*i _, err := imagedigest.Parse(ref) if err != nil { // ref is not image id, try to resolve it locally. - normalized, err := normalizeImageRef(ref) + normalized, err := util.NormalizeImageRef(ref) if err != nil { return nil, fmt.Errorf("invalid image reference %q: %v", ref, err) } diff --git a/pkg/server/helpers_test.go b/pkg/server/helpers_test.go index 2fa5aaf92..424bc64f8 100644 --- a/pkg/server/helpers_test.go +++ b/pkg/server/helpers_test.go @@ -19,70 +19,11 @@ package server import ( "testing" - "github.com/containerd/containerd/reference" imagedigest "github.com/opencontainers/go-digest" "github.com/stretchr/testify/assert" -) -func TestNormalizeImageRef(t *testing.T) { - for _, test := range []struct { - input string - expect string - }{ - { // has nothing - input: "busybox", - expect: "docker.io/library/busybox:latest", - }, - { // only has tag - input: "busybox:latest", - expect: "docker.io/library/busybox:latest", - }, - { // only has digest - input: "busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", - expect: "docker.io/library/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", - }, - { // only has path - input: "library/busybox", - expect: "docker.io/library/busybox:latest", - }, - { // only has hostname - input: "docker.io/busybox", - expect: "docker.io/library/busybox:latest", - }, - { // has no tag - input: "docker.io/library/busybox", - expect: "docker.io/library/busybox:latest", - }, - { // has no path - input: "docker.io/busybox:latest", - expect: "docker.io/library/busybox:latest", - }, - { // has no hostname - input: "library/busybox:latest", - expect: "docker.io/library/busybox:latest", - }, - { // full reference - input: "docker.io/library/busybox:latest", - expect: "docker.io/library/busybox:latest", - }, - { // gcr reference - input: "gcr.io/library/busybox", - expect: "gcr.io/library/busybox:latest", - }, - { // both tag and digest - input: "gcr.io/library/busybox:latest@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", - expect: "gcr.io/library/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", - }, - } { - t.Logf("TestCase %q", test.input) - normalized, err := normalizeImageRef(test.input) - assert.NoError(t, err) - output := normalized.String() - assert.Equal(t, test.expect, output) - _, err = reference.Parse(output) - assert.NoError(t, err, "%q should be containerd supported reference", output) - } -} + "github.com/kubernetes-incubator/cri-containerd/pkg/util" +) // TestGetUserFromImage tests the logic of getting image uid or user name of image user. func TestGetUserFromImage(t *testing.T) { @@ -154,7 +95,7 @@ func TestGetRepoDigestAndTag(t *testing.T) { }, } { t.Logf("TestCase %q", desc) - named, err := normalizeImageRef(test.ref) + named, err := util.NormalizeImageRef(test.ref) assert.NoError(t, err) repoDigest, repoTag := getRepoDigestAndTag(named, digest, test.schema1) assert.Equal(t, test.expectedRepoDigest, repoDigest) diff --git a/pkg/server/image_load.go b/pkg/server/image_load.go new file mode 100644 index 000000000..eed7f3209 --- /dev/null +++ b/pkg/server/image_load.go @@ -0,0 +1,78 @@ +/* +Copyright 2017 The Kubernetes Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package server + +import ( + "fmt" + "golang.org/x/net/context" + "os" + "path/filepath" + + "github.com/golang/glog" + + api "github.com/kubernetes-incubator/cri-containerd/pkg/api/v1" + "github.com/kubernetes-incubator/cri-containerd/pkg/containerd/importer" + imagestore "github.com/kubernetes-incubator/cri-containerd/pkg/store/image" +) + +// LoadImage loads a image into containerd. +func (c *criContainerdService) LoadImage(ctx context.Context, r *api.LoadImageRequest) (*api.LoadImageResponse, error) { + path := r.GetFilePath() + if !filepath.IsAbs(path) { + return nil, fmt.Errorf("path %q is not an absolute path", path) + } + f, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("failed to open file: %v", err) + } + repoTags, err := importer.Import(ctx, c.client.ContentStore(), c.client.ImageService(), f) + if err != nil { + return nil, fmt.Errorf("failed to import image: %v", err) + } + for _, repoTag := range repoTags { + image, err := c.client.GetImage(ctx, repoTag) + if err != nil { + return nil, fmt.Errorf("failed to get image %q: %v", repoTag, err) + } + if err := image.Unpack(ctx, c.config.ContainerdConfig.Snapshotter); err != nil { + glog.Warningf("Failed to unpack image %q: %v", repoTag, err) + // Do not fail image importing. Unpack will be retried when container creation. + } + info, err := getImageInfo(ctx, image, c.client.ContentStore()) + if err != nil { + return nil, fmt.Errorf("failed to get image %q info: %v", repoTag, err) + } + id := info.id + + if err := c.createImageReference(ctx, id, image.Target()); err != nil { + return nil, fmt.Errorf("failed to create image reference %q: %v", id, err) + } + img := imagestore.Image{ + ID: id, + RepoTags: []string{repoTag}, + ChainID: info.chainID.String(), + Size: info.size, + Config: &info.config, + Image: image, + } + + if err := c.imageStore.Add(img); err != nil { + return nil, fmt.Errorf("failed to add image %q into store: %v", id, err) + } + glog.V(4).Infof("Imported image with id %q, repo tag %q", id, repoTag) + } + return &api.LoadImageResponse{Images: repoTags}, nil +} diff --git a/pkg/server/image_pull.go b/pkg/server/image_pull.go index a717b1f73..def337f65 100644 --- a/pkg/server/image_pull.go +++ b/pkg/server/image_pull.go @@ -32,6 +32,7 @@ import ( "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime" imagestore "github.com/kubernetes-incubator/cri-containerd/pkg/store/image" + "github.com/kubernetes-incubator/cri-containerd/pkg/util" ) // For image management: @@ -75,7 +76,7 @@ import ( // PullImage pulls an image with authentication config. func (c *criContainerdService) PullImage(ctx context.Context, r *runtime.PullImageRequest) (*runtime.PullImageResponse, error) { imageRef := r.GetImage().GetImage() - namedRef, err := normalizeImageRef(imageRef) + namedRef, err := util.NormalizeImageRef(imageRef) if err != nil { return nil, fmt.Errorf("failed to parse image reference %q: %v", imageRef, err) } diff --git a/pkg/server/instrumented_service.go b/pkg/server/instrumented_service.go index b47cbc10d..7969b17b6 100644 --- a/pkg/server/instrumented_service.go +++ b/pkg/server/instrumented_service.go @@ -20,6 +20,8 @@ import ( "github.com/golang/glog" "golang.org/x/net/context" "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime" + + api "github.com/kubernetes-incubator/cri-containerd/pkg/api/v1" ) // instrumentedService wraps service and logs each operation. @@ -316,3 +318,15 @@ func (in *instrumentedService) ListContainerStats(ctx context.Context, r *runtim }() return in.criContainerdService.ListContainerStats(ctx, r) } + +func (in *instrumentedService) LoadImage(ctx context.Context, r *api.LoadImageRequest) (res *api.LoadImageResponse, err error) { + glog.V(4).Infof("LoadImage from file %q", r.GetFilePath()) + defer func() { + if err != nil { + glog.Errorf("LoadImage failed, error: %v", err) + } else { + glog.V(4).Infof("LoadImage returns images %+v", res.GetImages()) + } + }() + return in.criContainerdService.LoadImage(ctx, r) +} diff --git a/pkg/server/sandbox_run.go b/pkg/server/sandbox_run.go index 3af3f40f8..7785487f6 100644 --- a/pkg/server/sandbox_run.go +++ b/pkg/server/sandbox_run.go @@ -33,7 +33,7 @@ import ( "golang.org/x/sys/unix" "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime" - customopts "github.com/kubernetes-incubator/cri-containerd/pkg/opts" + customopts "github.com/kubernetes-incubator/cri-containerd/pkg/containerd/opts" sandboxstore "github.com/kubernetes-incubator/cri-containerd/pkg/store/sandbox" "github.com/kubernetes-incubator/cri-containerd/pkg/util" ) diff --git a/pkg/server/service.go b/pkg/server/service.go index d9689c0dc..8caa6edfc 100644 --- a/pkg/server/service.go +++ b/pkg/server/service.go @@ -41,6 +41,7 @@ import ( "k8s.io/kubernetes/pkg/kubelet/server/streaming" "github.com/kubernetes-incubator/cri-containerd/cmd/cri-containerd/options" + api "github.com/kubernetes-incubator/cri-containerd/pkg/api/v1" osinterface "github.com/kubernetes-incubator/cri-containerd/pkg/os" "github.com/kubernetes-incubator/cri-containerd/pkg/registrar" containerstore "github.com/kubernetes-incubator/cri-containerd/pkg/store/container" @@ -62,6 +63,7 @@ type CRIContainerdService interface { Stop() runtime.RuntimeServiceServer runtime.ImageServiceServer + api.CRIContainerdServiceServer } // criContainerdService implements CRIContainerdService. @@ -167,8 +169,10 @@ func NewCRIContainerdService(config options.Config) (CRIContainerdService, error // Create the grpc server and register runtime and image services. c.server = grpc.NewServer() - runtime.RegisterRuntimeServiceServer(c.server, newInstrumentedService(c)) - runtime.RegisterImageServiceServer(c.server, newInstrumentedService(c)) + instrumented := newInstrumentedService(c) + runtime.RegisterRuntimeServiceServer(c.server, instrumented) + runtime.RegisterImageServiceServer(c.server, instrumented) + api.RegisterCRIContainerdServiceServer(c.server, instrumented) return newInstrumentedService(c), nil } diff --git a/pkg/util/image.go b/pkg/util/image.go new file mode 100644 index 000000000..0f471fc42 --- /dev/null +++ b/pkg/util/image.go @@ -0,0 +1,50 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "github.com/docker/distribution/reference" +) + +// NormalizeImageRef normalizes the image reference following the docker convention. This is added +// mainly for backward compatibility. +// The reference returned can only be either tagged or digested. For reference contains both tag +// and digest, the function returns digested reference, e.g. docker.io/library/busybox:latest@ +// sha256:7cc4b5aefd1d0cadf8d97d4350462ba51c694ebca145b08d7d41b41acc8db5aa will be returned as +// docker.io/library/busybox@sha256:7cc4b5aefd1d0cadf8d97d4350462ba51c694ebca145b08d7d41b41acc8db5aa. +func NormalizeImageRef(ref string) (reference.Named, error) { + named, err := reference.ParseNormalizedNamed(ref) + if err != nil { + return nil, err + } + if _, ok := named.(reference.NamedTagged); ok { + if canonical, ok := named.(reference.Canonical); ok { + // The reference is both tagged and digested, only + // return digested. + newNamed, err := reference.WithName(canonical.Name()) + if err != nil { + return nil, err + } + newCanonical, err := reference.WithDigest(newNamed, canonical.Digest()) + if err != nil { + return nil, err + } + return newCanonical, nil + } + } + return reference.TagNameOnly(named), nil +} diff --git a/pkg/util/image_test.go b/pkg/util/image_test.go new file mode 100644 index 000000000..b634de133 --- /dev/null +++ b/pkg/util/image_test.go @@ -0,0 +1,84 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "testing" + + "github.com/containerd/containerd/reference" + "github.com/stretchr/testify/assert" +) + +func TestNormalizeImageRef(t *testing.T) { + for _, test := range []struct { + input string + expect string + }{ + { // has nothing + input: "busybox", + expect: "docker.io/library/busybox:latest", + }, + { // only has tag + input: "busybox:latest", + expect: "docker.io/library/busybox:latest", + }, + { // only has digest + input: "busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", + expect: "docker.io/library/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", + }, + { // only has path + input: "library/busybox", + expect: "docker.io/library/busybox:latest", + }, + { // only has hostname + input: "docker.io/busybox", + expect: "docker.io/library/busybox:latest", + }, + { // has no tag + input: "docker.io/library/busybox", + expect: "docker.io/library/busybox:latest", + }, + { // has no path + input: "docker.io/busybox:latest", + expect: "docker.io/library/busybox:latest", + }, + { // has no hostname + input: "library/busybox:latest", + expect: "docker.io/library/busybox:latest", + }, + { // full reference + input: "docker.io/library/busybox:latest", + expect: "docker.io/library/busybox:latest", + }, + { // gcr reference + input: "gcr.io/library/busybox", + expect: "gcr.io/library/busybox:latest", + }, + { // both tag and digest + input: "gcr.io/library/busybox:latest@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", + expect: "gcr.io/library/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", + }, + } { + t.Logf("TestCase %q", test.input) + normalized, err := NormalizeImageRef(test.input) + assert.NoError(t, err) + output := normalized.String() + assert.Equal(t, test.expect, output) + _, err = reference.Parse(output) + assert.NoError(t, err, "%q should be containerd supported reference", output) + } +}